web-dev-qa-db-ja.com

ゼロコピーのユーザー空間TCP dma_mmap_coherent()のマップされたメモリの送信

Linux 5.1をCyclone V SoCで実行しています。これは、1つのチップに2つのARMv7コアを備えたFPGAです。私の目標は、外部インターフェイスから大量のデータを収集し、TCPソケットを介してこのデータ(の一部)をストリーミングすることです。ここでの課題は、データレートが非常に高く、近づく可能性があることです。 GbEインターフェースを飽和させるために使用します。ソケットへのwrite()呼び出しを使用する実用的な実装がありますが、55MB/sで最高に達しています。理論的なGbE制限の約半分です。今、ゼロを取得しようとしています-copy TCP転送を行うと、スループットが向上しますが、壁にぶつかります。

FPGAからLinuxユーザー空間にデータを取り込むために、カーネルドライバーを作成しました。このドライバーは、FPGAのDMAブロックを使用して、大量のデータを外部インターフェイスからARMv7コアに接続されたDDR3メモリにコピーします。ドライバーは、このメモリを一連の1MBの連続したバッファーとして割り当てます_GFP_USER_でdma_alloc_coherent()を使用してプローブされ、_/dev/_内のファイルにmmap()を実装してこれらをユーザー空間アプリケーションに公開し、アプリケーションにアドレスを返すdma_mmap_coherent()事前に割り当てられたバッファ。

ここまでは順調ですね;ユーザー空間アプリケーションは有効なデータを表示しており、スループットは360MB /秒以上で十分であり、余裕があります(外部インターフェイスは、上限が実際にわかるほど高速ではありません)。

ゼロコピーを実装するには、TCPネットワーキングの場合、最初のアプローチはソケットで_SO_ZEROCOPY_を使用することでした:

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

ただし、これは_send: Bad address_になります。

少しグーグルした後、私の2番目のアプローチは、パイプとsplice()に続いて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;
}
_

ただし、結果は同じです:_vmsplice: Bad address_。

vmsplice()またはsend()への呼び出しを、buf(またはsend()が指すデータを出力するだけの関数に置き換えると、 なし_MSG_ZEROCOPY_)、すべて正常に動作しています。そのため、データにはユーザースペースからアクセスできますが、vmsplice()/send(..., MSG_ZEROCOPY)呼び出しはデータを処理できないようです。

ここで何が欠けていますか?ゼロコピーを使用する方法はありますかTCP dma_mmap_coherent()を介してカーネルドライバーから取得したユーザー空間アドレスで送信する?使用できる別の方法はありますか?

[〜#〜]更新[〜#〜]

そのため、カーネルのsendmsg() _MSG_ZEROCOPY_パスを少し詳しく調べ、最終的に失敗する呼び出しはget_user_pages_fast()です。 check_vma_flags()vmaに設定された_-EFAULT_フラグを見つけるため、この呼び出しは_VM_PFNMAP_を返します。このフラグは、ページがremap_pfn_range()またはdma_mmap_coherent()を使用してユーザー空間にマップされるときに明らかに設定されます。私の次のアプローチは、これらのページをmmapする別の方法を見つけることです。

14
rem

私の質問の更新で投稿したように、根本的な問題は、remap_pfn_range()を使用してマップされたメモリに対してzerocopyネットワークが機能しないことです(これはdma_mmap_coherent()が上手)。その理由は、このタイプのメモリ(_VM_PFNMAP_フラグが設定されている)には、必要な各ページに関連付けられた_struct page*_の形式のメタデータがないためです。

次に、解決策は、メモリに_struct page*_ s areを関連付ける方法でメモリを割り当てることです。

私がメモリを割り当てるために機能するワークフローは次のとおりです。

  1. struct page* page = alloc_pages(GFP_USER, page_order);を使用して、隣接する物理メモリのブロックを割り当てます。割り当てられる連続したページの数は、_2**page_order_で指定されます。
  2. split_page(page, page_order);を呼び出して、高次/複合ページを0次ページに分割します。これは、_struct page* page_が_2**page_order_エントリを持つ配列になったことを意味します。

次に、そのような領域をDMA(データ受信用)に送信します。

  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からコールバックを受け取ったら、領域のマップを解除して、このメモリブロックの所有権をCPUに戻す必要があります。これにより、キャッシュが処理され、古いデータを読み取っていません:

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

これで、mmap()を実装する場合、事前に割り当てたすべての0次ページに対してvm_insert_page()を繰り返し呼び出すだけで済みます。

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

_

ファイルを閉じたら、必ずページを解放してください。

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

この方法でmmap()を実装すると、_MSG_ZEROCOPY_フラグを使用して、ソケットがこのバッファをsendmsg()に使用できるようになります。

これは機能しますが、このアプローチではうまくいかないことが2つあります。

  • このメソッドでは、2のべき乗のサイズのバッファのみを割り当てることができますが、_alloc_pages_を必要なだけ呼び出して、さまざまなサイズのサブバッファで構成される任意のサイズのバッファを取得するために降順でロジックを実装できます。これには、これらのバッファーをmmap()で結び付け、sgではなくスキャッターギャザー(single)呼び出しでバッファーをDMAするためのロジックが必要になります。
  • split_page()はそのドキュメントで述べています:
_ * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.
_

これらの問題は、カーネルに任意の量の連続した物理ページを割り当てるためのインターフェースがあった場合、簡単に解決されます。なぜそれが存在しないのかはわかりませんが、なぜこれが利用できないのか/それを実装する方法を掘り下げるほど重要な上記の問題は見つかりません:-)

8
rem

たぶん、これはalloc_pagesが2のべき乗のページ番号を必要とする理由を理解するのに役立ちます。

頻繁に使用されるページ割り当てプロセスを最適化(および外部の断片化を減らす)するために、LinuxカーネルはCPUごとのページキャッシュとバディアロケーターを開発してメモリを割り当てました(メモリ割り当てよりも小さいメモリ割り当てを提供する別のアロケーター、スラブがありますページ)。

CPUごとのページキャッシュは1ページの割り当てリクエストを処理しますが、buddy-allocatorはそれぞれ2 ^ {0-10}の物理ページを含む11のリストを保持します。これらのリストは、ページを割り当てたり解放したりするときにうまく機能します。もちろん、前提は、2の累乗のサイズのバッファを要求していることです。

2
medivh