Jak jsem zveřejnil v aktualizaci v mé otázce, základním problémem je, že síť zerocopy nefunguje pro paměť, která byla namapována pomocí remap_pfn_range()
(což dma_mmap_coherent()
používá se také pod kapotou). Důvodem je, že tento typ paměti (s VM_PFNMAP
flag set) nemá metadata ve tvaru struct page*
přidružené ke každé stránce, kterou potřebuje.
Řešením je pak alokovat paměť způsobem struct page*
s jsou spojené s pamětí.
Pracovní postup, který nyní funguje pro alokaci paměti, je:
- Použijte
struct page* page = alloc_pages(GFP_USER, page_order);
k alokaci bloku souvislé fyzické paměti, kde počet souvislých stránek, které budou přiděleny, je dán2**page_order
. - Rozdělte stránku vyššího řádu/složené stránky na stránky nulového řádu voláním
split_page(page, page_order);
. To nyní znamená, žestruct page* page
se stalo polem s2**page_order
záznamy.
Nyní k odeslání takové oblasti do DMA (pro příjem dat):
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);
Když dostaneme zpětné volání z DMA, že přenos skončil, musíme zrušit mapování regionu, abychom převedli vlastnictví tohoto bloku paměti zpět na CPU, které se stará o mezipaměti, aby se ujistil, že nečteme zastaralá data:
dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);
Nyní, když chceme implementovat mmap()
, vše, co opravdu musíme udělat, je zavolat vm_insert_page()
opakovaně pro všechny stránky s nulovou objednávkou, které jsme předem přidělili:
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 zavření souboru nezapomeňte uvolnit stránky:
for (i = 0; i < 2**page_order; ++i) {
__free_page(&dev->shm[i].pages[i]);
}
Implementace mmap()
tímto způsobem nyní umožňuje soketu používat tuto vyrovnávací paměť pro sendmsg()
s MSG_ZEROCOPY
vlajka.
Ačkoli to funguje, jsou dvě věci, které mi s tímto přístupem nesedí:
- Touto metodou můžete přidělit pouze vyrovnávací paměti o velikosti 2, ačkoli můžete implementovat logiku pro volání
alloc_pages
tolikrát, kolikrát je potřeba, s klesajícími objednávkami, abyste získali libovolnou velikost vyrovnávací paměti složené z dílčích vyrovnávacích pamětí různých velikostí. To pak bude vyžadovat nějakou logiku, aby se tyto vyrovnávací paměti spojily dohromady vmmap()
a k jejich DMA pomocí scatter-gather (sg
) spíše nežsingle
. split_page()
ve své dokumentaci říká:
* Note: this is probably too low level an operation for use in drivers.
* Please consult with lkml before using this in your driver.
Tyto problémy by se daly snadno vyřešit, kdyby v jádře existovalo nějaké rozhraní pro alokaci libovolného množství souvislých fyzických stránek. Nevím, proč tomu tak není, ale výše uvedené problémy nepovažuji za tak důležité, abych se zabýval tím, proč to není k dispozici / jak to implementovat :-)
Možná vám to pomůže pochopit, proč alloc_pages vyžaduje číslo stránky s mocninou 2.
Pro optimalizaci procesu alokace stránek (a snížení externí fragmentace), která se často vyskytuje, vyvinulo linuxové jádro mezipaměť stránek pro jednotlivé procesory a alokátor kamarádů pro alokaci paměti (existuje další alokátor, slab, který slouží k alokaci paměti, která je menší než stránka).
Mezipaměť stránek na procesor obsluhuje jednostránkový požadavek na přidělení, zatímco buddy-allocator uchovává 11 seznamů, z nichž každý obsahuje 2^{0-10} fyzických stránek. Tyto seznamy fungují dobře, když alokujete a uvolníte stránky, a samozřejmě předpokladem je, že požadujete vyrovnávací paměť o síle 2.