Gửi bộ nhớ TCP không gian người dùng sao chép bộ nhớ ánh xạ dma_mmap_coherent ()


14

Tôi đang chạy Linux 5.1 trên Cyclone V SoC, đây là một GPU có hai lõi ARMv7 trong một chip. Mục tiêu của tôi là thu thập nhiều dữ liệu từ giao diện bên ngoài và truyền phát (một phần) dữ liệu này qua ổ cắm TCP. Thách thức ở đây là tốc độ dữ liệu rất cao và có thể tiến gần đến việc bão hòa giao diện GbE. Tôi có một triển khai hoạt động chỉ sử dụng write()các cuộc gọi đến ổ cắm, nhưng nó đạt tốc độ 55MB / s; khoảng một nửa giới hạn GbE lý thuyết. Bây giờ tôi đang cố gắng để truyền TCP không sao chép để hoạt động để tăng thông lượng, nhưng tôi đang va vào tường.

Để đưa dữ liệu ra khỏi FPGA vào không gian người dùng Linux, tôi đã viết một trình điều khiển hạt nhân. Trình điều khiển này sử dụng một khối DMA trong FPGA để sao chép một lượng lớn dữ liệu từ giao diện bên ngoài vào bộ nhớ DDR3 được gắn vào lõi ARMv7. Các giao đất điều khiển bộ nhớ này là một loạt các bộ đệm 1MB tiếp giáp khi sử dụng thăm dò dma_alloc_coherent()với GFP_USER, và cho thấy những cho các ứng dụng userspace bằng cách thực hiện mmap()trên một tập tin trong /dev/và trả lại một địa chỉ để các ứng dụng sử dụng dma_mmap_coherent()trên bộ đệm preallocated.

Càng xa càng tốt; ứng dụng không gian người dùng đang nhìn thấy dữ liệu hợp lệ và thông lượng là quá đủ với> 360 MB / giây có chỗ trống (giao diện bên ngoài không đủ nhanh để thực sự thấy giới hạn trên là gì).

Để thực hiện kết nối mạng TCP không sao chép, cách tiếp cận đầu tiên của tôi là sử dụng SO_ZEROCOPYtrên ổ cắm:

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

Tuy nhiên, điều này dẫn đến send: Bad address.

Sau khi googling một chút, cách tiếp cận thứ hai của tôi là sử dụng một đường ống và splice()theo sau 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;
}

Tuy nhiên, kết quả là như nhau : vmsplice: Bad address.

Lưu ý rằng nếu tôi thay thế cuộc gọi đến vmsplice()hoặc send()đến một chức năng chỉ in dữ liệu được trỏ đến buf(hoặc send() không có MSG_ZEROCOPY ), mọi thứ đều hoạt động tốt; vì vậy dữ liệu có thể truy cập vào không gian người dùng, nhưng các cuộc gọi vmsplice()/ send(..., MSG_ZEROCOPY)dường như không thể xử lý nó.

Tôi đang thiếu gì ở đây? Có cách nào sử dụng gửi TCP không sao chép với địa chỉ không gian người dùng thu được từ trình điều khiển hạt nhân thông qua dma_mmap_coherent()không? Có cách tiếp cận nào khác tôi có thể sử dụng?

CẬP NHẬT

Vì vậy, tôi tìm hiểu sâu hơn một chút về sendmsg() MSG_ZEROCOPYđường dẫn trong kernel và cuộc gọi cuối cùng thất bại là get_user_pages_fast(). Cuộc gọi này trả về -EFAULTcheck_vma_flags()tìm thấy VM_PFNMAPcờ được đặt trong vma. Cờ này rõ ràng được đặt khi các trang được ánh xạ vào không gian người dùng bằng cách sử dụng remap_pfn_range()hoặc dma_mmap_coherent(). Cách tiếp cận tiếp theo của tôi là tìm một cách khác cho mmapcác trang này.

Câu trả lời:


8

Như tôi đã đăng trong một bản cập nhật trong câu hỏi của mình, vấn đề tiềm ẩn là mạng zerocopy không hoạt động đối với bộ nhớ đã được ánh xạ bằng cách sử dụng remap_pfn_range()(điều này cũng dma_mmap_coherent()xảy ra khi sử dụng dưới mui xe). Lý do là loại bộ nhớ này (với bộ VM_PFNMAPcờ) không có siêu dữ liệu ở dạng struct page*liên kết với mỗi trang mà nó cần.

Giải pháp sau đó là phân bổ bộ nhớ theo cách mà struct page*s được liên kết với bộ nhớ.

Quy trình làm việc hiện tại để tôi phân bổ bộ nhớ là:

  1. Sử dụng struct page* page = alloc_pages(GFP_USER, page_order);để phân bổ một khối bộ nhớ vật lý liền kề, trong đó số lượng các trang liền kề sẽ được phân bổ được đưa ra bởi 2**page_order.
  2. Tách trang thứ tự cao / ghép thành các trang 0 thứ tự bằng cách gọi split_page(page, page_order);. Điều này bây giờ có nghĩa là struct page* pageđã trở thành một mảng với 2**page_ordercác mục.

Bây giờ để gửi một khu vực như vậy đến DMA (để nhận dữ liệu):

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

Khi chúng tôi nhận được một cuộc gọi lại từ DMA mà quá trình chuyển đã kết thúc, chúng tôi cần hủy ánh xạ vùng để chuyển quyền sở hữu khối bộ nhớ này trở lại CPU, đảm bảo lưu trữ bộ nhớ cache để đảm bảo chúng tôi không đọc dữ liệu cũ:

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

Bây giờ, khi chúng tôi muốn triển khai mmap(), tất cả những gì chúng tôi thực sự phải làm là gọi vm_insert_page()liên tục cho tất cả các trang có thứ tự 0 mà chúng tôi đã phân bổ trước:

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

Khi tệp được đóng, đừng quên giải phóng các trang:

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

Thực hiện mmap()theo cách này bây giờ cho phép một ổ cắm sử dụng bộ đệm này sendmsg()với MSG_ZEROCOPYcờ.

Mặc dù điều này hoạt động, có hai điều không phù hợp với tôi với phương pháp này:

  • Bạn chỉ có thể phân bổ bộ đệm có kích thước 2 kích thước bằng phương pháp này, mặc dù bạn có thể triển khai logic để gọi alloc_pagesnhiều lần nếu cần với các đơn hàng giảm kích thước để có bất kỳ bộ đệm kích thước nào được tạo thành từ bộ đệm phụ có kích thước khác nhau. Điều này sau đó sẽ yêu cầu một số logic để liên kết các bộ đệm này với nhau mmap()và với DMA chúng bằng sgcác lệnh gọi phân tán ( ) chứ không phải single.
  • split_page() nói trong tài liệu của nó:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Các vấn đề này sẽ được giải quyết dễ dàng nếu có một số giao diện trong kernel để phân bổ số lượng trang vật lý liền kề tùy ý. Tôi không biết tại sao không có, nhưng tôi không thấy các vấn đề trên quan trọng đến mức đi sâu vào lý do tại sao điều này không có sẵn / làm thế nào để thực hiện nó :-)


2

Có lẽ điều này sẽ giúp bạn hiểu lý do tại sao alloc_pages yêu cầu số trang có sức mạnh là 2.

Để tối ưu hóa quy trình phân bổ trang (và giảm các phân đoạn bên ngoài) thường được tham gia, nhân Linux đã phát triển bộ đệm trang per-cpu và cấp phát bạn bè để phân bổ bộ nhớ (có một cấp phát khác, bản mỏng, để phục vụ phân bổ bộ nhớ nhỏ hơn một trang).

Bộ đệm trang Per-cpu phục vụ yêu cầu phân bổ một trang, trong khi bộ cấp phát bạn bè giữ 11 danh sách, mỗi danh sách chứa 2 ^ {0-10} trang vật lý tương ứng. Các danh sách này hoạt động tốt khi phân bổ và các trang miễn phí, và tất nhiên, tiền đề là bạn đang yêu cầu bộ đệm có kích cỡ 2.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.