Zero-copia spazio utente invio TCP della memoria mappata dma_mmap_coherent ()


14

Sto eseguendo Linux 5.1 su un SoC Cyclone V, che è un FPGA con due core ARMv7 in un chip. Il mio obiettivo è quello di raccogliere molti dati da un'interfaccia esterna e trasmettere (parte di) questi dati attraverso un socket TCP. La sfida qui è che la velocità dei dati è molto alta e potrebbe avvicinarsi a saturare l'interfaccia GbE. Ho un'implementazione funzionante che utilizza solo le write()chiamate al socket, ma supera a 55 MB / s; circa la metà del limite teorico GbE. Ora sto cercando di far funzionare la trasmissione TCP zero-copia per aumentare il throughput, ma sto colpendo un muro.

Per ottenere i dati dall'FPGA nello spazio utente di Linux, ho scritto un driver del kernel. Questo driver utilizza un blocco DMA nell'FPGA per copiare una grande quantità di dati da un'interfaccia esterna nella memoria DDR3 collegata ai core ARMv7. Il driver alloca questa memoria come un gruppo di buffer contigui da 1 MB quando viene sondato usando dma_alloc_coherent()con GFP_USER, ed espone questi all'applicazione dello spazio utente implementando mmap()un file /dev/e restituendo un indirizzo all'applicazione usando dma_mmap_coherent()sui buffer preallocati.

Fin qui tutto bene; l'applicazione per lo spazio utente sta visualizzando dati validi e la velocità effettiva è più che sufficiente a> 360 MB / s con spazio libero (l'interfaccia esterna non è abbastanza veloce per vedere quale sia il limite superiore).

Per implementare la rete TCP zero-copia, il mio primo approccio è stato quello di utilizzare SO_ZEROCOPYsul socket:

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

Tuttavia, questo si traduce in send: Bad address.

Dopo aver cercato su Google per un po ', il mio secondo approccio era usare una pipe e splice()seguito da 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;
}

Tuttavia, il risultato è lo stesso: vmsplice: Bad address.

Nota che se sostituisco la chiamata a vmsplice()o send()a una funzione che stampa solo i dati puntati da buf(o send() senza MSG_ZEROCOPY ), tutto funziona perfettamente; quindi i dati sono accessibili allo spazio utenti, ma le chiamate vmsplice()/ send(..., MSG_ZEROCOPY)sembrano incapaci di gestirli.

Cosa mi sto perdendo qui? Esiste un modo per utilizzare l'invio TCP zero-copy con un indirizzo spazio utente ottenuto da un driver del kernel dma_mmap_coherent()? C'è un altro approccio che potrei usare?

AGGIORNARE

Quindi mi sono tuffato un po 'più a fondo nel sendmsg() MSG_ZEROCOPYpercorso nel kernel e la chiamata che alla fine fallisce è get_user_pages_fast(). Questa chiamata ritorna -EFAULTperché check_vma_flags()trova il VM_PFNMAPflag impostato in vma. Questo flag è apparentemente impostato quando le pagine sono mappate nello spazio utente usando remap_pfn_range()o dma_mmap_coherent(). Il mio prossimo approccio è trovare un altro modo per mmapqueste pagine.

Risposte:


8

Come ho postato in un aggiornamento nella mia domanda, il problema di fondo è che la rete zerocopy non funziona per la memoria che è stata mappata usando remap_pfn_range()(che dma_mmap_coherent()si usa anche sotto il cofano). Il motivo è che questo tipo di memoria (con il VM_PFNMAPflag impostato) non ha metadati nella forma struct page*associata a ciascuna pagina, di cui ha bisogno.

La soluzione è quindi di allocare la memoria in modo che struct page*s sono associati con la memoria.

Il flusso di lavoro che ora funziona per me allocare la memoria è:

  1. Utilizzare struct page* page = alloc_pages(GFP_USER, page_order);per allocare un blocco di memoria fisica contigua, in cui è indicato il numero di pagine contigue che verranno allocate 2**page_order.
  2. Dividi la pagina di ordine superiore / composta in pagine di ordine 0 chiamando split_page(page, page_order);. Questo ora significa che struct page* pageè diventato un array con 2**page_ordervoci.

Ora per inviare tale regione al DMA (per la ricezione dei dati):

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

Quando riceviamo un callback dal DMA che il trasferimento è terminato, dobbiamo annullare la mappatura della regione per trasferire la proprietà di questo blocco di memoria alla CPU, che si occupa delle cache per assicurarsi che non stiamo leggendo dati non aggiornati:

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

Ora, quando vogliamo implementare mmap(), tutto ciò che dobbiamo fare è chiamare vm_insert_page()ripetutamente per tutte le pagine di ordine 0 che abbiamo pre-allocato:

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

Quando il file è chiuso, non dimenticare di liberare le pagine:

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

L'implementazione in mmap()questo modo ora consente a un socket di utilizzare questo buffer per sendmsg()con il MSG_ZEROCOPYflag.

Sebbene funzioni, ci sono due cose che non mi stanno bene con questo approccio:

  • Con questo metodo è possibile allocare solo buffer di potenza di 2 dimensioni, sebbene sia possibile implementare la logica per chiamare alloc_pagestutte le volte necessarie con ordini decrescenti per ottenere buffer di qualsiasi dimensione composto da sub-buffer di dimensioni variabili. Ciò richiederà quindi un po 'di logica per legare questi buffer insieme mmap()e per DMA con sgchiamate scatter-gather ( ) anziché single.
  • split_page() dice nella sua documentazione:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Questi problemi sarebbero facilmente risolti se ci fosse un'interfaccia nel kernel per allocare una quantità arbitraria di pagine fisiche contigue. Non so perché non ci sia, ma non trovo i problemi di cui sopra così importanti da approfondire perché non è disponibile / come implementarlo :-)


2

Forse questo ti aiuterà a capire perché alloc_pages richiede un numero di pagina di potenza di 2.

Per ottimizzare il processo di allocazione della pagina (e ridurre le frammentazioni esterne), che è frequentemente coinvolto, il kernel Linux ha sviluppato cache di pagina per CPU e allocatore di buddy per allocare memoria (esiste un altro allocatore, slab, per servire allocazioni di memoria più piccole di un pagina).

La cache della pagina per CPU serve la richiesta di allocazione di una pagina, mentre l'allocatore di amici mantiene 11 elenchi, ciascuno contenente rispettivamente 2 ^ {0-10} pagine fisiche. Questi elenchi funzionano bene quando si allocano e liberano pagine e, naturalmente, la premessa è che si richiede un buffer di dimensioni pari a 2.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.