TCP з нульовою копією простору користувача, що надсилається, надсилає карту пам'яті dma_mmap_coherent ()


14

Я запускаю Linux 5.1 на Cyclone V SoC, який є FPGA з двома ядрами ARMv7 в одній мікросхемі. Моя мета - зібрати велику кількість даних із зовнішнього інтерфейсу та передавати (частину) ці дані через сокет TCP. Проблема тут полягає в тому, що швидкість передачі даних дуже висока і може наблизитися до насичення інтерфейсу GbE. У мене працює реалізація, яка просто використовує write()дзвінки в сокет, але вона перевищує 55 Мб / с; приблизно половина теоретичної межі GbE. Зараз я намагаюся змусити TCP-передачу з нульовою копією працювати на збільшення пропускної здатності, але я натискаю на стіну.

Щоб отримати дані з FPGA в просторі користувача Linux, я написав драйвер ядра. Цей драйвер використовує блок DMA у FPGA для копіювання великої кількості даних із зовнішнього інтерфейсу в пам'ять DDR3, приєднану до ядер ARMv7. У цьому драйвері виділяє пам'ять як набір послідовних буферів 1Мб при зондуванні використання dma_alloc_coherent()з GFP_USER, і виставляють їх в призначеному для користувача додатку, впроваджуючи mmap()на файл в /dev/і повертаючи адресу з додатком , використовуючи dma_mmap_coherent()на визначених буферах.

Все йде нормально; програма для користувальницького простору бачить дійсні дані, а пропускна здатність більш ніж достатня при> 360 Мб / с, щоб залишити місце (зовнішній інтерфейс недостатньо швидкий, щоб реально побачити верхню межу).

Щоб реалізувати мережу TCP з нульовою копією, моїм першим підходом було використання SO_ZEROCOPYсокета:

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

Однак це призводить до send: Bad address.

Трохи погуглившись, другий мій підхід полягав у використанні труби, а splice()потім vmsplice():

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}

Тим НЕ менше, результат той же: vmsplice: Bad address.

Зауважте, що якщо я заміню виклик на vmsplice()або send()функцію, яка просто друкує дані, на які вказує buf(або send() без MSG_ZEROCOPY ), все працює нормально; Таким чином, дані є доступними для простору користувачів, але vmsplice()/ send(..., MSG_ZEROCOPY)дзвінки, здається, не можуть їх обробити.

Що я тут пропускаю? Чи можливий спосіб передачі TCP з нульовою копією з адресою простору користувача, отриманою від драйвера ядра через dma_mmap_coherent()? Чи є інший підхід, який я міг би використати?

ОНОВЛЕННЯ

Таким чином , я пірнув трохи глибше в sendmsg() MSG_ZEROCOPYшлях в ядрі, і виклик , який в кінцевому рахунку зазнає невдачі це get_user_pages_fast(). Цей виклик повертається, -EFAULTоскільки check_vma_flags()знаходить VM_PFNMAPпрапор, встановлений у vma. Цей прапор, мабуть, встановлюється, коли сторінки відображаються в просторі користувача за допомогою remap_pfn_range()або dma_mmap_coherent(). Мій наступний підхід - знайти інший шлях до mmapцих сторінок.

Відповіді:


8

Як я публікував у своєму запиті оновлення, основна проблема полягає в тому, що мережа zerocopy не працює для пам'яті, яка була відображена за допомогою remap_pfn_range()(що dma_mmap_coherent()трапляється і під кришкою). Причина полягає в тому, що цей тип пам'яті (із встановленим VM_PFNMAPпрапором) не має метаданих у формі, struct page*пов’язаних із кожною сторінкою, яка їй потрібна.

Рішення є те, щоб виділити пам'ять таким чином , що struct page*їй буде асоційована з пам'яттю.

Робочий процес, який зараз працює для мене, щоб розподілити пам'ять:

  1. Використовуйте struct page* page = alloc_pages(GFP_USER, page_order);для виділення блоку суміжної фізичної пам'яті, де кількість сусідніх сторінок, які будуть виділені, задається 2**page_order.
  2. Розділіть сторінку високого замовлення / складову на сторінки 0 замовлення, зателефонувавши split_page(page, page_order);. Це означає, що struct page* pageце стало масив із 2**page_orderзаписами.

Тепер надіслати такий регіон до DMA (для отримання даних):

  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. dmaengine_submit(dma_desc);

Коли ми отримуємо зворотний дзвінок від DMA, що передача завершена, нам потрібно скасувати карту, щоб передати право власності на цей блок пам'яті назад до центрального процесора, який піклується про кеші, щоб переконатися, що ми не читаємо застарілих даних:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

Тепер, коли ми хочемо реалізувати mmap(), все, що нам потрібно зробити, - це vm_insert_page()повторно телефонувати на всі сторінки 0-замовлення, які ми попередньо виділили:

static int my_mmap(struct file *file, struct vm_area_struct *vma) {
    int res;
...
    for (i = 0; i < 2**page_order; ++i) {
        if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
            break;
        }
    }
    vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
    return res;
}

Коли файл закритий, не забудьте звільнити сторінки:

for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

Реалізація mmap()таким чином дозволяє тепер сокет використовувати цей буфер для sendmsg()з MSG_ZEROCOPYпрапором.

Хоча це працює, є дві речі, які не відповідають мені при такому підході:

  • За допомогою цього методу ви можете виділити лише буфери потужності 2, хоча ви можете застосувати логіку для виклику alloc_pagesстільки разів, скільки потрібно, за допомогою зменшення замовлень, щоб отримати будь-який розмір буфера, що складається з підбуферів різної величини. Тоді для цього знадобиться певна логіка, щоб зв'язати ці буфери разом у викликах mmap()DMA та їх викликом, а не розмовляти-збирати ( sg) single.
  • split_page() говорить у своїй документації:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Ці питання легко вирішити, якби в ядрі був якийсь інтерфейс для виділення довільної кількості суміжних фізичних сторінок. Я не знаю, чому цього немає, але я не вважаю вищезазначені питання настільки важливими, щоби розібратися, чому це недоступно / як це здійснити :-)


2

Можливо, це допоможе вам зрозуміти, для чого потрібен номер сторінки alloc_pages.

Щоб оптимізувати процес розподілу сторінки (та зменшити зовнішні фрагментації), якими часто займається, ядро ​​Linux розробило кеш сторінок per-cpu та асигнатор buddy для виділення пам'яті (є інший розподільник, плита, для обслуговування розподілу пам'яті, менший ніж сторінки).

Кеш сторінок кеш-сервісу обслуговує запит на розподіл однієї сторінки, тоді як приятель-розподільник зберігає 11 списків, кожен з яких містить 2 ^ {0-10} фізичні сторінки відповідно. Ці списки спрацьовують добре при розподілі та вільних сторінках, і, звичайно, умова полягає в тому, що ви вимагаєте буфера розміру 2-х розмірів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.