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


14

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

FPGAからLinuxユーザー空間にデータを取り込むために、カーネルドライバーを作成しました。このドライバーは、FPGAのDMAブロックを使用して、外部インターフェイスからARMv7コアに接続されたDDR3メモリに大量のデータをコピーします。ドライバは、使用してプローブするときの連続1MBのバッファの束としてこのメモリを割り当てdma_alloc_coherent()GFP_USER、そして実装することで、ユーザ空間アプリケーションにこれらを公開mmap()中のファイルに/dev/して使用してアプリケーションにアドレスを返す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()。で設定されたフラグを見つける-EFAULTためcheck_vma_flags()、この呼び出しは戻ります。このフラグは、またはを使用してページがユーザー空間にマッピングされるときに設定されるようです。私の次のアプローチは、これらのページへの別の方法を見つけることです。VM_PFNMAPvmaremap_pfn_range()dma_mmap_coherent()mmap

回答:


8

私は私の質問での更新で掲載され、根本的な問題はzerocopyネットワークが使用してマッピングされているメモリのための作業をしないということであるremap_pfn_range()(これdma_mmap_coherent()もとボンネットの下に使用するために起こります)。その理由は、このタイプのメモリ(VM_PFNMAPフラグが設定されている)にはstruct page*、必要な各ページに関連付けられた形式のメタデータがないためです。

次に、解決策は、struct page*s メモリに関連付けられるようにメモリを割り当てることです。

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

  1. struct page* page = alloc_pages(GFP_USER, page_order);連続した物理メモリのブロックを割り当てるために使用します。割り当てられる連続したページの数はによって与えられ2**page_orderます。
  2. を呼び出して、高次/複合ページを0次ページに分割しますsplit_page(page, page_order);。これはstruct page* page2**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()場合、vm_insert_page()事前に割り当てたすべての0次のページを繰り返し呼び出すだけで済みます。

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()この方法を実装すると、ソケットでこのバッファをフラグsendmsg()付きで使用できるようになりますMSG_ZEROCOPY

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

  • このメソッドでは2のべき乗のサイズのバッファしか割り当てることができませんがalloc_pages、さまざまなサイズのサブバッファで構成される任意のサイズのバッファを取得するために降順で必要な回数だけ呼び出すロジックを実装できます。この場合、これらのバッファをで結び付け、mmap()DMA sgではなくscatter-gather()呼び出しでDMAを実行するためのロジックが必要になりsingleます。
  • 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.

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


2

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.