Come ho pubblicato in un aggiornamento nella mia domanda, il problema di fondo è che la rete zerocopy non funziona per la memoria che è stata mappata utilizzando remap_pfn_range()
(che dma_mmap_coherent()
capita di usare anche sotto il cofano). Il motivo è che questo tipo di memoria (con il VM_PFNMAP
flag set) non ha metadati sotto forma di struct page*
associato a ciascuna pagina, di cui ha bisogno.
La soluzione quindi è allocare la memoria in un modo che struct page*
s sono associato alla memoria.
Il flusso di lavoro che ora funziona per me per allocare la memoria è:
- Usa
struct page* page = alloc_pages(GFP_USER, page_order);
per allocare un blocco di memoria fisica contigua, dove il numero di pagine contigue che verranno allocate è dato da2**page_order
. - Dividi la pagina di ordine elevato/composta in pagine di ordine 0 chiamando
split_page(page, page_order);
. Questo ora significa chestruct page* page
è diventato un array con2**page_order
voci.
Ora per inviare tale regione al DMA (per la ricezione dei dati):
dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
dmaengine_submit(dma_desc);
Quando riceviamo una richiamata 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 obsoleti:
dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);
Ora, quando vogliamo implementare mmap()
, tutto quello che dobbiamo fare è chiamare vm_insert_page()
ripetutamente per tutte le pagine di ordine 0 che abbiamo preassegnato:
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]);
}
Implementazione di mmap()
in questo modo ora consente a un socket di utilizzare questo buffer per sendmsg()
con il MSG_ZEROCOPY
bandiera.
Anche se funziona, ci sono due cose che non mi stanno bene con questo approccio:
- Puoi allocare solo buffer di dimensioni power-of-2 con questo metodo, sebbene tu possa implementare la logica per chiamare
alloc_pages
tutte le volte che è necessario con ordini decrescenti per ottenere un buffer di qualsiasi dimensione composto da sub-buffer di varie dimensioni. Ciò richiederà quindi una logica per legare insieme questi buffer nelmmap()
e a DMA loro con scatter-gather (sg
) chiama invece disingle
. 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 andare a scavare nel motivo per cui non è disponibile / come implementarlo :-)
Forse questo ti aiuterà a capire perché alloc_pages richiede un numero di pagina con potenza di 2.
Per ottimizzare il processo di allocazione della pagina (e diminuire le frammentazioni esterne), che è frequentemente impegnato, il kernel Linux ha sviluppato cache di pagina per CPU e buddy-allocator per allocare memoria (c'è un altro allocatore, slab, per servire allocazioni di memoria che sono più piccole di un pagina).
La cache di pagina per CPU serve la richiesta di allocazione di una pagina, mentre buddy-allocator conserva 11 elenchi, ciascuno contenente rispettivamente 2^{0-10} pagine fisiche. Questi elenchi funzionano bene quando allocano e liberano pagine e, naturalmente, la premessa è che stai richiedendo un buffer di dimensioni power-of-2.