Przesyłanie TCP w zerowej przestrzeni przez użytkownika pamięci zmapowanej dma_mmap_coherent ()

14

Używam Linuksa 5.1 na SoC Cyclone V, który jest FPGA z dwoma rdzeniami ARMv7 w jednym układzie. Moim celem jest zebranie dużej ilości danych z zewnętrznego interfejsu i przesłanie (części) tych danych przez gniazdo TCP. Wyzwanie polega na tym, że szybkość przesyłania danych jest bardzo wysoka i może zbliżyć się do nasycenia interfejsu GbE. Mam działającą implementację, która po prostu wykorzystuje write()wywołania do gniazda, ale osiąga maksymalną prędkość 55 MB / s; mniej więcej połowa teoretycznego limitu GbE. Próbuję teraz uruchomić transmisję TCP bez kopiowania, aby zwiększyć przepustowość, ale uderzam o ścianę.

Aby przenieść dane z FPGA do przestrzeni użytkownika Linuksa, napisałem sterownik jądra. Ten sterownik używa bloku DMA w układzie FPGA do kopiowania dużej ilości danych z zewnętrznego interfejsu do pamięci DDR3 podłączonej do rdzeni ARMv7. W tej pamięci sterownik przydziela jak pęczek sąsiadujących buforów 1MB gdy badane przy użyciu dma_alloc_coherent()z GFP_USER, i naraża je do stosowania w przestrzeni użytkownika poprzez wdrożenie mmap()na pliku w /dev/i powrocie adresu do aplikacji za pomocą dma_mmap_coherent()na zdefiniowanej przez bufory.

Na razie w porządku; aplikacja działająca w przestrzeni użytkownika widzi prawidłowe dane, a przepustowość jest większa niż wystarczająca przy> 360 MB / s, z wolną przestrzenią (zewnętrzny interfejs nie jest wystarczająco szybki, aby naprawdę zobaczyć, jaka jest górna granica).

Aby zaimplementować sieć TCP z zerową kopią, moim pierwszym podejściem było użycie SO_ZEROCOPYgniazda:

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

To jednak powoduje send: Bad address.

Po pewnym czasie googlowania, moim drugim podejściem było użycie fajki, a splice()następnie 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;
}

Jednak wynik jest taki sam: vmsplice: Bad address.

Zauważ, że jeśli zastąpię wywołanie funkcji vmsplice()lub send()funkcji, która po prostu drukuje dane wskazane przez buf(lub send() bez MSG_ZEROCOPY ), wszystko działa dobrze; więc dane są dostępne dla przestrzeni użytkownika, ale połączenia vmsplice()/ send(..., MSG_ZEROCOPY)wydają się nie być w stanie ich obsłużyć.

Czego tu brakuje? Czy jest jakiś sposób na użycie wysyłania TCP bez kopiowania z adresem przestrzeni użytkownika uzyskanym przez sterownik jądra dma_mmap_coherent()? Czy mogę zastosować inne podejście?

AKTUALIZACJA

Więc zagłębiłem się nieco głębiej w sendmsg() MSG_ZEROCOPYścieżkę jądra, a wywołanie, które ostatecznie kończy się niepowodzeniem, jest get_user_pages_fast(). To wywołanie zwraca, -EFAULTponieważ check_vma_flags()znajduje VM_PFNMAPflagę ustawioną w vma. Ta flaga jest najwyraźniej ustawiona, gdy strony są mapowane w przestrzeń użytkownika za pomocą remap_pfn_range()lub dma_mmap_coherent(). Moje następne podejście polega na znalezieniu innego sposobu na mmapte strony.

rem
źródło

Odpowiedzi:

8

Jak napisałem w aktualizacji mojego pytania, podstawowy problem polega na tym, że sieć zerocopy nie działa dla pamięci, która została zmapowana remap_pfn_range()(która dma_mmap_coherent()zdarza się również używać pod maską). Powodem jest to, że ten typ pamięci (z VM_PFNMAPustawioną flagą) nie ma metadanych w postaci struct page*powiązanych z każdą stroną, której potrzebuje.

Następnie roztwór jest rozmieszczenie pamięci w taki sposób, że struct page*a jest związany z pamięcią.

Przepływ pracy, który teraz działa dla mnie w celu przydzielenia pamięci, jest następujący:

  1. Służy struct page* page = alloc_pages(GFP_USER, page_order);do przydzielania bloku ciągłej pamięci fizycznej, gdzie liczba ciągłych stron, które zostaną przydzielone, jest określona przez 2**page_order.
  2. Dzieląc stronę najwyższego rzędu / złożoną na strony zerowego rzędu, dzwoniąc split_page(page, page_order);. Oznacza to teraz, że struct page* pagestał się tablicą z 2**page_orderwpisami.

Teraz, aby przesłać taki region do DMA (do odbioru danych):

  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);

Kiedy otrzymamy od DMA wywołanie zwrotne, że transfer został zakończony, musimy usunąć mapowanie regionu, aby przenieść własność tego bloku pamięci z powrotem do procesora, który zajmuje się pamięcią podręczną, aby upewnić się, że nie odczytujemy nieaktualnych danych:

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

Teraz, gdy chcemy wdrożyć mmap(), wszystko, co naprawdę musimy zrobić, to wywoływać vm_insert_page()wielokrotnie dla wszystkich stron zamówienia 0, które wstępnie przydzieliliśmy:

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;
}

Po zamknięciu pliku nie zapomnij zwolnić stron:

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

Implementacja w mmap()ten sposób pozwala teraz gniazdu na używanie tego bufora sendmsg()z MSG_ZEROCOPYflagą.

Chociaż to działa, są dwie rzeczy, które nie pasują do mnie przy takim podejściu:

  • Za pomocą tej metody można alokować bufory potęgi 2-wielkości, chociaż można zaimplementować logikę wywoływania alloc_pagestyle razy, ile potrzeba, przy malejących zamówieniach, aby uzyskać dowolny bufor wielkości złożony z sub-buforów o różnych rozmiarach. Będzie to wymagało pewnej logiki do powiązania tych buforów ze sobą mmap()i do DMA za pomocą sgwywołań scatter-gather ( ) zamiast single.
  • split_page() mówi w swojej dokumentacji:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Problemy te można łatwo rozwiązać, jeśli w jądrze znajduje się interfejs do przydzielania dowolnej liczby ciągłych stron fizycznych. Nie wiem, dlaczego tak nie jest, ale nie uważam powyższych problemów za tak ważne, aby zagłębić się w to, dlaczego nie jest to dostępne / jak je wdrożyć :-)

rem
źródło
2

Może to pomoże ci zrozumieć, dlaczego alokacja stron wymaga numeru strony o sile 2.

Aby zoptymalizować proces alokacji stron (i zmniejszyć fragmentację zewnętrzną), która jest często zaangażowana, jądro Linuksa opracowało pamięć podręczną stron na procesor i moduł alokacji znajomych w celu alokacji pamięci (istnieje inny alokator, płyta, która obsługuje alokacje pamięci mniejsze niż strona).

Pamięć podręczna stron na procesor obsługuje żądanie przydziału jednej strony, podczas gdy program buddy-alokator przechowuje 11 list, z których każda zawiera odpowiednio 2 ^ {0-10} stron fizycznych. Listy te działają dobrze podczas przydzielania i darmowych stron, i oczywiście założeniem jest, że prosisz o bufor wielkości 2.

medivh
źródło