Salin nol ruang-pengguna TCP kirim memori yang dipetakan dma_mmap_coherent ()

14

Saya menjalankan Linux 5.1 pada Cyclone V SoC, yang merupakan FPGA dengan dua core ARMv7 dalam satu chip. Tujuan saya adalah mengumpulkan banyak data dari antarmuka eksternal dan mengalirkan (bagian dari) data ini melalui soket TCP. Tantangannya di sini adalah kecepatan data sangat tinggi dan bisa mendekati saturasi antarmuka GbE. Saya memiliki implementasi yang berfungsi yang hanya menggunakan write()panggilan ke soket, tetapi mencapai 55MB / s; kira-kira setengah dari batas GbE teoritis. Saya sekarang mencoba untuk mendapatkan nol-copy transmisi TCP untuk meningkatkan throughput, tapi saya memukul dinding.

Untuk mendapatkan data dari FPGA ke dalam ruang pengguna Linux, saya telah menulis driver kernel. Driver ini menggunakan blok DMA di FPGA untuk menyalin sejumlah besar data dari antarmuka eksternal ke memori DDR3 yang melekat pada inti ARMv7. Pengandar mengalokasikan memori ini sebagai sekelompok buffer 1MB yang berdekatan ketika diselidiki dma_alloc_coherent()dengan GFP_USER, dan memaparkan ini ke aplikasi userspace dengan mengimplementasikan mmap()pada file di /dev/dan mengembalikan alamat ke aplikasi menggunakan dma_mmap_coherent()pada buffer preallocated.

Sejauh ini bagus; aplikasi ruang pengguna melihat data yang valid dan throughput lebih dari cukup di> 360MB / s dengan ruang kosong (antarmuka eksternal tidak cukup cepat untuk benar-benar melihat apa batas atas).

Untuk menerapkan zero-copy TCP networking, pendekatan pertama saya adalah menggunakan SO_ZEROCOPYsoket:

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

Namun, ini menghasilkan send: Bad address.

Setelah googling sebentar, pendekatan kedua saya adalah menggunakan pipa dan splice()diikuti oleh 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;
}

Namun, hasilnya adalah sama: vmsplice: Bad address.

Perhatikan bahwa jika saya mengganti panggilan ke vmsplice()atau send()ke fungsi yang hanya mencetak data yang ditunjukkan oleh buf(atau send() tanpa MSG_ZEROCOPY ), semuanya berfungsi dengan baik; sehingga data dapat diakses oleh userspace, tetapi vmsplice()/ send(..., MSG_ZEROCOPY)panggilan tampaknya tidak dapat menanganinya.

Apa yang kulewatkan di sini? Apakah ada cara menggunakan pengiriman nol-salinan TCP dengan alamat ruang pengguna yang diperoleh dari driver kernel melalui dma_mmap_coherent()? Apakah ada pendekatan lain yang bisa saya gunakan?

MEMPERBARUI

Jadi saya terjun sedikit lebih dalam ke sendmsg() MSG_ZEROCOPYjalur di kernel, dan panggilan yang akhirnya gagal adalah get_user_pages_fast(). Panggilan ini kembali -EFAULTkarena check_vma_flags()menemukan VM_PFNMAPbendera yang diatur di vma. Bendera ini tampaknya disetel saat halaman dipetakan ke dalam ruang pengguna menggunakan remap_pfn_range()atau dma_mmap_coherent(). Pendekatan saya berikutnya adalah menemukan cara lain untuk mmaphalaman-halaman ini.

rem
sumber

Jawaban:

8

Ketika saya memposting dalam pembaruan dalam pertanyaan saya, masalah mendasarnya adalah bahwa jaringan zerocopy tidak berfungsi untuk memori yang telah dipetakan menggunakan remap_pfn_range()(yang juga dma_mmap_coherent()terjadi di bawah tenda). Alasannya adalah bahwa jenis memori ini (dengan VM_PFNMAPset bendera) tidak memiliki metadata dalam bentuk yang struct page*terkait dengan setiap halaman, yang dibutuhkannya.

Solusinya kemudian adalah untuk mengalokasikan memori dengan cara yang struct page*s yang terkait dengan memori.

Alur kerja yang sekarang berfungsi bagi saya untuk mengalokasikan memori adalah:

  1. Gunakan struct page* page = alloc_pages(GFP_USER, page_order);untuk mengalokasikan blok memori fisik yang berdekatan, di mana jumlah halaman yang berdekatan yang akan dialokasikan diberikan oleh 2**page_order.
  2. Bagi halaman orde tinggi / halaman menjadi halaman 0 pesanan dengan menelepon split_page(page, page_order);. Ini sekarang berarti struct page* pagetelah menjadi array dengan 2**page_orderentri.

Sekarang untuk mengirim wilayah seperti itu ke DMA (untuk penerimaan data):

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

Saat kami menerima panggilan balik dari DMA bahwa transfer telah selesai, kami perlu menghapus peta wilayah untuk mentransfer kepemilikan blok memori ini kembali ke CPU, yang menangani cache untuk memastikan kami tidak membaca data basi:

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

Sekarang, ketika kita ingin mengimplementasikan mmap(), yang harus kita lakukan adalah memanggil vm_insert_page()berulang kali untuk semua halaman 0-order yang kita alokasikan sebelumnya:

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

Ketika file ditutup, jangan lupa untuk membebaskan halaman:

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

Menerapkan mmap()cara ini sekarang memungkinkan soket untuk menggunakan buffer ini sendmsg()dengan MSG_ZEROCOPYbendera.

Meskipun ini berhasil, ada dua hal yang tidak cocok dengan saya dengan pendekatan ini:

  • Anda hanya dapat mengalokasikan buffer berukuran 2 dengan metode ini, meskipun Anda dapat menerapkan logika untuk memanggil alloc_pagessebanyak yang diperlukan dengan mengurangi pesanan untuk mendapatkan buffer ukuran yang terdiri dari sub-buffer dengan ukuran yang berbeda-beda. Ini kemudian akan memerlukan beberapa logika untuk mengikat buffer ini bersama-sama di mmap()dan untuk DMA mereka dengan sgpanggilan sebar-berkumpul ( ) daripada single.
  • split_page() mengatakan dalam dokumentasinya:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Masalah-masalah ini akan dengan mudah diselesaikan jika ada beberapa antarmuka dalam kernel untuk mengalokasikan jumlah halaman fisik yang berdekatan secara sewenang-wenang. Saya tidak tahu mengapa tidak ada, tetapi saya tidak menemukan masalah di atas sangat penting untuk menggali mengapa ini tidak tersedia / bagaimana menerapkannya :-)

rem
sumber
2

Mungkin ini akan membantu Anda untuk memahami mengapa alokasi_ halaman memerlukan nomor halaman kekuatan-of-2.

Untuk mengoptimalkan proses alokasi halaman (dan mengurangi fragmentasi eksternal), yang sering digunakan, kernel Linux mengembangkan cache halaman per-CPU dan buddy-dialokasikan untuk mengalokasikan memori (ada pengalokasi lain, slab, untuk melayani alokasi memori yang lebih kecil dari halaman).

Cache halaman per-cpu melayani permintaan alokasi satu halaman, sedangkan buddy-dialokasikan menyimpan 11 daftar, masing-masing berisi 2 ^ {0-10} halaman fisik masing-masing. Daftar ini berfungsi dengan baik ketika mengalokasikan dan membebaskan halaman, dan tentu saja, premisnya adalah Anda meminta penyangga berukuran 2 ukuran.

medivh
sumber