Dma_mmap_coherent () eşlenen belleğin sıfır kopya kullanıcı alanı TCP gönderimi


14

Linux 5.1'i bir çipte iki ARMv7 çekirdeği olan bir FPGA olan Cyclone V SoC'de çalıştırıyorum. Amacım bir dış arabirimden çok sayıda veri toplamak ve bu verileri bir TCP soketi aracılığıyla dışarı aktarmak. Buradaki zorluk, veri hızının çok yüksek olması ve GbE arayüzünü doyurmaya yaklaşmasıdır. Ben sadece write()soket çağrıları kullanan bir çalışma uygulaması var , ama 55MB / s başında tops; teorik GbE sınırının yaklaşık yarısı. Şimdi verimi artırmak için çalışmak için sıfır kopya TCP iletimini almaya çalışıyorum, ama bir duvara çarpıyorum.

Veriyi FPGA'dan Linux kullanıcı alanına almak için bir çekirdek sürücüsü yazdım. Bu sürücü, harici bir arayüzden büyük miktarda veriyi ARMv7 çekirdeklerine bağlı DDR3 belleğine kopyalamak için FPGA'da bir DMA bloğu kullanır. Sürücüsü ayırır bitişik 1MB tampon bir grup olarak bu bellek probed kullanırken dma_alloc_coherent()ile GFP_USERve uygulamak suretiyle userspace uygulamasına bu ortaya mmap()bir dosya üzerinde /dev/ve kullanma uygulamasına bir adres dönen dma_mmap_coherent()preallocation'u tampon üzerinde.

Çok uzak çok iyi; kullanıcı alanı uygulaması geçerli verileri görüyor ve verim> 360MB / s'de yedek odaya sahip olandan fazlasıyla yeterli (harici arayüz üst sınırın ne olduğunu gerçekten görecek kadar hızlı değil).

Sıfır kopya TCP ağını uygulamak için ilk yaklaşımım SO_ZEROCOPYsoket üzerinde kullanmaktı :

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

Ancak, bu sonuçlanır send: Bad address.

Biraz googling yaptıktan sonra, ikinci yaklaşımım bir boru kullanmak ve splice()bunu takip etmekti 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;
}

Ancak, sonuç aynıdır: vmsplice: Bad address.

Not ben çağrısına değiştirirseniz vmsplice()veya send()sadece işaret ettiği verileri yazdırır bir işleve buf(veya send() olmadan MSG_ZEROCOPY ), her şey iyi çalışıyor; bu nedenle verilere kullanıcı alanı erişilebilir, ancak vmsplice()/ send(..., MSG_ZEROCOPY)çağrıları işleyemiyor gibi görünüyor.

Burada ne eksik? Çekirdek sürücüsünden alınan kullanıcı alanı adresiyle sıfır kopya TCP gönderimi kullanmanın herhangi bir yolu var mı dma_mmap_coherent()? Kullanabileceğim başka bir yaklaşım var mı?

GÜNCELLEME

Bu yüzden sendmsg() MSG_ZEROCOPYçekirdekteki yola biraz daha derine iniyorum ve sonunda başarısız olan çağrı get_user_pages_fast(). Bu çağrı döndürür -EFAULTçünkü check_vma_flags()bulur VM_PFNMAPbayrak kümesi vma. Bu işaret, sayfalar remap_pfn_range()veya kullanılarak kullanıcı alanına eşlendiğinde görünür dma_mmap_coherent(). Bir sonraki yaklaşımım mmapbu sayfalara başka bir yol bulmak .

Yanıtlar:


8

Soruma bir güncellemede yayınladığım gibi, altta yatan sorun, sıfır kopya ağının, eşleştirilen remap_pfn_range()( dma_mmap_coherent()başlık altında da kullanılıyor) bellek için çalışmadığıdır . Bunun nedeni, bu bellek türünün ( VM_PFNMAPbayrak ayarlı olarak) struct page*ihtiyaç duyduğu her bir sayfayla ilişkilendirilmiş meta verileri içermemesidir .

Çözelti daha sonra bir şekilde bellek tahsis etmektir struct page*s olan hafıza ile bağlantılı.

Şimdi belleği ayırmak için benim için çalışan iş akışı:

  1. struct page* page = alloc_pages(GFP_USER, page_order);Ayrılacak bitişik sayfa sayısının verildiği bitişik fiziksel bellek bloğunu ayırmak için kullanın 2**page_order.
  2. Üst sıra / bileşik sayfayı arayarak 0 sıra sayfalara bölün split_page(page, page_order);. Bu, artık girişleri olan struct page* pagebir dizi haline geldiği anlamına gelir 2**page_order.

Şimdi böyle bir bölgeyi DMA'ya göndermek için (veri alımı için):

  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'dan aktarımın tamamlandığı bir geri arama aldığımızda, bu bellek bloğunun sahipliğini CPU'ya geri aktarmak için bölgeyi eşleştirmemiz gerekir; bu, eski verileri okumadığımızdan emin olmak için önbelleklerle ilgilenir:

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

Şimdi, uygulamak istediğimizde mmap(), gerçekten yapmamız gereken tek şey, vm_insert_page()önceden tahsis ettiğimiz 0 dereceli sayfaların hepsini tekrar tekrar aramaktır:

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

Dosya kapalıyken sayfaları boşaltmayı unutmayın:

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

mmap()Bu şekilde uygulamak artık bir soketin bayrakla sendmsg()birlikte bu arabelleği kullanmasına izin verir MSG_ZEROCOPY.

Bu işe yarıyor olsa da, bu yaklaşımla benimle iyi oturmayan iki şey var:

  • alloc_pagesDeğişken boyutlardaki alt tamponlardan oluşan herhangi bir boyut arabelleğini almak için azalan siparişlerle gerektiği kadar çok çağrı yapmak için mantığı uygulayabilmenize rağmen, bu yöntemle yalnızca 2 boyutlu arabellekleri ayırabilirsiniz . Bu daha sonra bu arabellekleri mmap()DMA'da birleştirmek ve bunun sgyerine scatter-gather ( ) çağrılarıyla birleştirmek için bir mantık gerektirir single.
  • split_page() belgelerinde diyor:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Çekirdekte gelişigüzel miktarda bitişik fiziksel sayfa tahsis etmek için bazı arayüzler varsa, bu sorunlar kolayca çözülebilirdi. Neden olmadığını bilmiyorum, ama yukarıdaki sorunları neden bu kadar mevcut değil / nasıl uygulanır içine kazma gitmek kadar önemli bulamıyorum :-)


2

Belki bu tahsis_sayfalarının neden 2 sayfalık bir güç numarası gerektirdiğini anlamanıza yardımcı olacaktır.

Sıkça kullanılan sayfa ayırma işlemini optimize etmek (ve harici parçalanmaları azaltmak) için, Linux çekirdeği, bellek ayırmak için işlemci başına önbellek ve arkadaş-ayırıcı geliştirdi (başka bir ayırıcı, levha var. sayfa).

İşlemci başına sayfa önbelleği tek sayfalık ayırma isteğini yerine getirirken, arkadaş-ayırıcı her biri sırasıyla 2 ^ {0-10} fiziksel sayfa içeren 11 liste tutar. Bu listeler, sayfa ayırırken ve serbest bırakıldığında iyi performans gösterir ve elbette, öncül, 2 boyutlu bir arabellek istemenizdir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.