目次
概要
Kernel4.14で追加されたMSG_ZEROCOPY機能は、データのゼロコピー送信のみに対応しており、受信側については対応していなかった。
4.18Kernelで受信に対応した。Zero copy receiveはアプリがパケットを保持する一連のバッファを割り当てる。一般的に、Kernelは受信パケットのサイズを事前に知ることができないため、特定のバッファに到着する次のパケットの意図された受信者を事前に知ることはできません。したがって、ゼロコピーリシーブの実装では、これらのパケットバッファをパケットが入ってきて、オープンされたソケットに関連付けられた後にユーザースペースのメモリにマップする必要があります。
これには、満たす必要がある制約があります。プロセスのアドレス空間にメモリをマップすることは、ページ単位で行われます。ページの一部をマップすることはできません。そのため、入力ネットワークデータは、受信バッファに到着したときにページに合わせてアラインメントされ、ページサイズにする必要があります。そうしないと、ユーザースペースにマップすることはできません。アラインメントは、受信プロセスが興味を持っているデータではなく、プロトコルヘッダで始まるインターフェースから出てくるパケットに少しトリッキーになることがあります。データをアラインメントすることは可能ですが、ネットワークインタフェースからヘッダを別のバッファに分割できるネットワークインタフェースを使用する必要があります。
システムのページサイズの倍数でデ
メモ
Re: How to test TCP zero-copy receive with real NICs? – Eric Dumazet (kernel.org)
NICによっては制約があるらしい。何が悪い?
Header data splitが必要?ConnectX-5だとLinuxでは使えない?
メモリ空間
使い方
linux/tcp_mmap.c at master · torvalds/linux · GitHub
zerocopy receiveを使用するには、アプリケーションはTCPソケット上でmmap()コールを発行し、プロセス空間内に、カーネルからデータバッファをマッピングするためのアドレス空間(マッピング領域)を確保する。mmapだけでは、マッピング領域は受信バッファにマッピングされておらず、実際にマッピングするためにはTCP_ZEROCOPY_RECEIVEオプションを指定してgetsockopt()をコールする。データが利用可能になると、アプリケーションはTCP_ZEROCOPY_RECEIVEオプションを指定してgetsockopt()を呼び出し、カーネルにプロセス空間内のアドレス情報を渡す。addressにはマッピング領域のプロセス空間内のアドレスが、lengthにはマッピング猟奇の長さが格納される。getsockoptではstruct tcp_zerocopy_receiveにより、マッピング領域のプロセス空間内の(address)とマッピング領域のサイズ(length)をカーネルに通知します。カーネルからはマップされたデータ長(length)とマップ前にrecvmsg/readで読みだす必要があるデータ長( recv_skip_hint)が応答される。
struct tcp_zerocopy_receive {
__u64 address; /* Input, address of mmap carved space */
__u32 length; /* Input/Output, length of address space , amount of data mapped */
__u32 recv_skip_hint; /* Output, bytes to read via recvmsg before reading mmap region */
};
プロセス空間は4KB単位で管理されるのに対し、カーネル内の受信バッファ上の受信パケット単位にsk_buffで管理される。マッピング領域に受信パケットを割り当てるためには、
データが消費されると、mmapアドレス空間は、別のTCP_ZEROCOPY_RECEIVEコールによって再利用のために解放することができます。カーネルは、次のデータバッファをマッピングするためにアドレス空間を再利用します。 munmap()は、アドレス空間のマッピングを解除して解放するために使用することができます。再利用には mmap() 呼び出しは必要ないことに注意してください。
このAPIは、バッファのアライメントに関する条件が満たされた場合にのみ、性能向上を示します。つまり、これは汎用的なAPIではありません。
ユーザ空間からのgetsockoptの引数には、struct tcp_zerocopy_receiveを使う。これとは別にsocketのFDを指定する。
最新カーネルの実装
最新カーネルではstruct tcp_zerocopy_receiveの引数に、コピー用領域のアドレス/サイズ(copybuf_address, copybuf_len)やCMSGタイムスタンプ制御用のフィールド(msg_control, msg_controllen, msg_flags)が追加されている。
コピー用領域を事前に確保しておくことで、ページアラインがあっていないデータのコピーと、mmapを同時に行うことができるようになる。
ゼロコピー処理の本体はipv4/tcp.cのtcp_mmap(ソケットのFDに対し、mmapされた場合の処理関数)と、tcp_zerocopy_receive(getsockoptでTCP_ZEROCOPY_RECEIVEが指定されて場合の処理関数)となる。
struct tcp_zerocopy_receive {
__u64 address; /* in: address of mapping */
__u32 length; /* in/out: number of bytes to map/mapped */
__u32 recv_skip_hint; /* out: amount of bytes to skip */
__u32 inq; /* out: amount of bytes in read queue */
__s32 err; /* out: socket error */
__u64 copybuf_address; /* in: copybuf address (small reads) */
__s32 copybuf_len; /* in/out: copybuf bytes avail/used or error */
__u32 flags; /* in: flags */
__u64 msg_control; /* ancillary data */
__u64 msg_controllen;
__u32 msg_flags;
__u32 reserved; /* set to 0 for now */
};
if (sflg) {
int fdlisten = socket(cfg_family, SOCK_STREAM, 0);★ソケット作成。
if (fdlisten == -1) {
perror("socket");
exit(1);
}
apply_rcvsnd_buf(fdlisten);
setsockopt(fdlisten, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
setup_sockaddr(cfg_family, host, &listenaddr);
if (mss &&
setsockopt(fdlisten, IPPROTO_TCP, TCP_MAXSEG,
&mss, sizeof(mss)) == -1) {
perror("setsockopt TCP_MAXSEG");
exit(1);
}
if (bind(fdlisten, (const struct sockaddr *)&listenaddr, cfg_alen) == -1) {
perror("bind");
exit(1);
}
if (listen(fdlisten, 128) == -1) {
perror("listen");
exit(1);
}
do_accept(fdlisten);
}
static void do_accept(int fdlisten)
{
pthread_attr_t attr;
int rcvlowat;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
rcvlowat = chunk_size;
if (setsockopt(fdlisten, SOL_SOCKET, SO_RCVLOWAT,
&rcvlowat, sizeof(rcvlowat)) == -1) {
perror("setsockopt SO_RCVLOWAT");
}
apply_rcvsnd_buf(fdlisten);
while (1) {
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
pthread_t th;
int fd, res;
fd = accept(fdlisten, (struct sockaddr *)&addr, &addrlen);
if (fd == -1) {
perror("accept");
continue;
}
res = pthread_create(&th, &attr, child_thread,
(void *)(unsigned long)fd);
if (res) {
errno = res;
perror("pthread_create");
close(fd);
}
}
}
void *child_thread(void *arg)
{
unsigned long total_mmap = 0, total = 0;
struct tcp_zerocopy_receive zc;
unsigned long delta_usec;
int flags = MAP_SHARED;
struct timeval t0, t1;
char *buffer = NULL;
void *raddr = NULL;
void *addr = NULL;
double throughput;
struct rusage ru;
size_t buffer_sz;
int lu, fd;
fd = (int)(unsigned long)arg;★fdはaccpetした新ソケット。
gettimeofday(&t0, NULL);
fcntl(fd, F_SETFL, O_NDELAY);
buffer = mmap_large_buffer(chunk_size, &buffer_sz);★ユーザスペースにchunk_sizeの領域を作成。コピー先に使用。
if (buffer == (void *)-1) {
perror("mmap");
goto error;
}
if (zflg) {
raddr = mmap(NULL, chunk_size + map_align, PROT_READ, flags, fd, 0);
★第一引数:NULLを指定した場合カーネルがマッピングを作成するアドレスを選択する
★第二引数:マップするサイズ。
★第三引数:ReadOnly
★第四引数:MAP_SHAREDが指定されている。このマッピングを共有する。 マッピングに対する更新はこのファイルをマッピングしている他のプロセス から見える。更新はマッピング元のファイルを通じて伝えられる。
★第五引数:ソケットのFD
★第六引数:ソケット内のオフセット
if (raddr == (void *)-1) {
perror("mmap");
zflg = 0;
} else {
addr = ALIGN_PTR_UP(raddr, map_align);★mmapした領域をアライン。
}
}
while (1) {
struct pollfd pfd = { .fd = fd, .events = POLLIN, };
int sub;
poll(&pfd, 1, 10000);
if (zflg) {
socklen_t zc_len = sizeof(zc);
int res;
memset(&zc, 0, sizeof(zc));
zc.address = (__u64)((unsigned long)addr);★ゼロコピー先のメモリアドレス(ページアライン)
zc.length = chunk_size;★ゼロコピーするサイズ。
res = getsockopt(fd, IPPROTO_TCP, TCP_ZEROCOPY_RECEIVE,
&zc, &zc_len);★受信データをユーザ空間にマッピングする。
if (res == -1)
break;
if (zc.length) {★zc.lenghtはカーネルが応答したマップ猟奇のデータサイズ。
assert(zc.length <= chunk_size);
total_mmap += zc.length;
if (xflg)
hash_zone(addr, zc.length);
/* It is more efficient to unmap the pages right now,
* instead of doing this in next TCP_ZEROCOPY_RECEIVE.
*/
madvise(addr, zc.length, MADV_DONTNEED);
★MADV_DONTNEEDをmadvisseした場合、カーネルに該当範囲をunmapできることをhintとして出せる。
total += zc.length;
}
if (zc.recv_skip_hint) {
assert(zc.recv_skip_hint <= chunk_size);
lu = read(fd, buffer, zc.recv_skip_hint);★mapしたデータでなくリードする。
if (lu > 0) {
if (xflg)
hash_zone(buffer, lu);
total += lu;
}
}
continue;
}
sub = 0;
while (sub < chunk_size) {
lu = read(fd, buffer + sub, chunk_size - sub);
if (lu == 0)
goto end;
if (lu < 0)
break;
if (xflg)
hash_zone(buffer + sub, lu);
total += lu;
sub += lu;
}
}
end:
gettimeofday(&t1, NULL);
delta_usec = (t1.tv_sec - t0.tv_sec) * 1000000 + t1.tv_usec - t0.tv_usec;
throughput = 0;
if (delta_usec)
throughput = total * 8.0 / (double)delta_usec / 1000.0;
getrusage(RUSAGE_THREAD, &ru);
if (total > 1024*1024) {
unsigned long total_usec;
unsigned long mb = total >> 20;
total_usec = 1000000*ru.ru_utime.tv_sec + ru.ru_utime.tv_usec +
1000000*ru.ru_stime.tv_sec + ru.ru_stime.tv_usec;
printf("received %lg MB (%lg %% mmap'ed) in %lg s, %lg Gbit\n"
" cpu usage user:%lg sys:%lg, %lg usec per MB, %lu c-switches\n",
total / (1024.0 * 1024.0),
100.0*total_mmap/total,
(double)delta_usec / 1000000.0,
throughput,
(double)ru.ru_utime.tv_sec + (double)ru.ru_utime.tv_usec / 1000000.0,
(double)ru.ru_stime.tv_sec + (double)ru.ru_stime.tv_usec / 1000000.0,
(double)total_usec/mb,
ru.ru_nvcsw);
}
error:
munmap(buffer, buffer_sz);
close(fd);
if (zflg)
munmap(raddr, chunk_size + map_align);
pthread_exit(0);
}
static void *mmap_large_buffer(size_t need, size_t *allocated)
{
void *buffer;
size_t sz;
/* Attempt to use huge pages if possible. */
sz = ALIGN_UP(need, map_align);
buffer = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
if (buffer == (void *)-1) {
sz = need;
buffer = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
-1, 0);
if (buffer != (void *)-1)
fprintf(stderr, "MAP_HUGETLB attempt failed, look at /sys/kernel/mm/hugepages for optimal performance\n");
}
*allocated = sz;
return buffer;
}
コア実装
初期のパッチでzerocopy receiveのコア実装である、tcp_mmapの実装がまとまっている。コア実装ではmmap時にプロセス空間に割り当てられるtcpの仮想メモリの実装がわかる。
[PATCH net-next 4/5] tcp: implement mmap() for zero copy receive [LWN.net]
カーネル実装
tcp_mmap
tcp_mmapはプロセスの仮想アドレス空間にtcp処理用ハンドラ(tcp_vm_ops)を登録する。カーネルはソケットFDにmmapが呼ばれた場合、mmap対象の仮想アドレス空間(vma)のハンドラにtcp_vm_opsを登録する。カーネルはVMAにtcp_vm_opsが登録されているかどうかで、VMAのゼロコピー可否を判定する。tcp_vm_opsの実体は空関数となる。
int tcp_mmap(struct file *file, struct socket *sock,
struct vm_area_struct *vma)
{
if (vma->vm_flags & (VM_WRITE | VM_EXEC))
return -EPERM;
vma->vm_flags &= ~(VM_MAYWRITE | VM_MAYEXEC);
/* Instruct vm_insert_page() to not mmap_read_lock(mm) */
vma->vm_flags |= VM_MIXEDMAP;
vma->vm_ops = &tcp_vm_ops;
return 0;
}
EXPORT_SYMBOL(tcp_mmap);
tcp_zerocopy_receive
tcp_zerocopy_receiveは、ユーザから受信したstruct tcp_zerocopy_receiveをもとに、ゼロコピー処理を行う関数である。カーネルはソケットが受信済のソケットバッファ(skb)を調べ、受信データのうちページアラインされた物理ページ領域をユーザ空間VMAにマップする。ユーザが複製用の楊力(copybuf)を指定している場合は、ページアラインされていない猟奇
static int tcp_zerocopy_receive(struct sock *sk,
struct tcp_zerocopy_receive *zc)
{
u32 length = 0, offset, vma_len, avail_len, copylen = 0;
unsigned long address = (unsigned long)zc->address;
struct page *pages[TCP_ZEROCOPY_PAGE_BATCH_SIZE];
s32 copybuf_len = zc->copybuf_len;
struct tcp_sock *tp = tcp_sk(sk);
const skb_frag_t *frags = NULL;
unsigned int pages_to_map = 0;
struct vm_area_struct *vma;
struct sk_buff *skb = NULL;
u32 seq = tp->copied_seq;// copied_seqはHead of yet unread data.
u32 total_bytes_to_map;
// get available receive bytes on socket
// ①ソケットの受信データ量を調べる。
int inq = tcp_inq(sk);
int ret;
struct scm_timestamping_internal tss;
printk("tcp_zerocopy_receive_mod: Entering inq=%d\n", inq);
zc->copybuf_len = 0;
zc->msg_flags = 0;
if (address & (PAGE_SIZE - 1) || address != zc->address)
return -EINVAL;
if (sk->sk_state == TCP_LISTEN)
return -ENOTCONN;
#ifdef NOT_FOR_ZCOPY
// TODO: RPS対応。
//sock_rps_record_flow(sk);
// ②アプリが確保したアプリバッファが受信データ量より大きい場合、
// アプリバッファにデータをcopyする。
if (inq && inq <= copybuf_len)
return receive_fallback_to_copy(sk, zc, inq, tss);
#endif //NOT_FOR_ZCOPY
// ③PAGE_SIZEより受信データ量が小さい場合は、zcopyせずに修了。
if (inq < PAGE_SIZE) {
zc->length = 0;
zc->recv_skip_hint = inq;
printk("tcp_zerocopy_receive_mod(3): Finished tcp_zerocopy_receive_mod. inq is less than PAGE_SIZE. inq=%d, zc->length=%d, zc->recv_skip_hint=%d\n", inq, zc->length, zc->recv_skip_hint);
if (!inq && sock_flag(sk, SOCK_DONE))
return -EIO;
return 0;
}
mmap_read_lock(current->mm);
// ⑤指定されたアドレスのvm_area_structを調べる。
vma = vma_lookup(current->mm, address);
printk("tcp_zerocopy_receive_mod(5): Looked up VMA. vma=%p, zc->length=%d, zc->recv_skip_hint=%d\n", vma, zc->length, zc->recv_skip_hint);
#ifdef NOT_FOR_ZCOPY
// ⑥指定されたアドレスがzcopy空間にmmapされているかチェックする(tcp_mmapされているかを調べる)。
// TODO: アプリ側でチェックさせる?
if (!vma || vma->vm_ops != &tcp_vm_ops) {
mmap_read_unlock(current->mm);
return -EINVAL;
}
#endif //NOT_FOR_ZCOPY
// ⑦マップ対象のサイズを調べる。min(指定されたサイズ、mmapした領域の使用可能なサイズ、受信データ量)
vma_len = min_t(unsigned long, zc->length, vma->vm_end - address);
avail_len = min_t(u32, vma_len, inq);
total_bytes_to_map = avail_len & ~(PAGE_SIZE - 1);
printk("tcp_zerocopy_receive_mod(7): Calculated total_bytes_to_map=%d, avail_len, zc->length=%d, zc->recv_skip_hint=%d\n", total_bytes_to_map, avail_len, zc->length, zc->recv_skip_hint);
if (total_bytes_to_map) {
if (!(zc->flags & TCP_RECEIVE_ZEROCOPY_FLAG_TLB_CLEAN_HINT))
// remove user pages in a given range
zap_page_range(vma, address, total_bytes_to_map);
zc->length = total_bytes_to_map;
zc->recv_skip_hint = 0;
} else {
zc->length = avail_len;
zc->recv_skip_hint = avail_len;
}
printk("tcp_zerocopy_receive_mod: Entering while loop total_bytes_to_map=%d, zc->length=%d, zc->recv_skip_hint=%d\n", total_bytes_to_map, zc->length, zc->recv_skip_hint);
ret = 0;
while (length + PAGE_SIZE <= zc->length) {
int mappable_offset;
struct page *page;
if (zc->recv_skip_hint < PAGE_SIZE) {
u32 offset_frag;
if (skb) {
// この条件分岐はニ回目以降のloop。
// 640行目でrecv_skip_hint(SKB内の未処理データ量がページサイズ以下の場合、この条件分布に入る。
if (zc->recv_skip_hint > 0)
break;
skb = skb->next;
offset = seq - TCP_SKB_CB(skb)->seq;
printk("tcp_zerocopy_receive_mod: Calculated offset. offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", offset, zc->length, zc->recv_skip_hint);
} else {
// この条件分岐は一回目のloop。
// seq(ソケット内の未処理データの先頭シーケンス番号)を含むskbを調べる。offsetにはskb内のseqのoffsetが含まれる。
skb = tcp_recv_skb(sk, seq, &offset);
printk("tcp_zerocopy_receive_mod(9): Finished tcp_recv_skb. seq=%d, skb=%p,offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", seq, skb, offset, zc->length, zc->recv_skip_hint);
}
#ifdef NOT_FOR_ZCOPY
// 受信タイムスタンプ。デバッグ用?
if (TCP_SKB_CB(skb)->has_rxtstamp) {
tcp_update_recv_tstamps(skb, tss);
zc->msg_flags |= TCP_CMSG_TS;
}
#endif //NOT_FOR_ZCOPY
// recv_skip_hintにSKBサイズからoffset(ソケット内の未処理データのオフセット)を引いたi
// 値(SKB内の未処理データ量(remaining_data))を格納。
zc->recv_skip_hint = skb->len - offset;
// static skb_frag_t *skb_advance_to_frag(struct sk_buff *skb, u32 offset_skb, u32 *offset_frag)
// ⑩skb内のfragmentリストをたどり、offset_skbで指定された位置に
// あるfragmentを返す。offset_fragにはskb内の該当fragmentのオフセット
// が返される。
frags = skb_advance_to_frag(skb, offset, &offset_frag);
printk("tcp_zerocopy_receive_mod(10): Finished skb_advance_to_frag. skb=%p, offset=%d, offset_frag=%d,zc->length=%d, zc->recv_skip_hint=%d\n", skb, offset, offset_frag, zc->length, zc->recv_skip_hint);
if (!frags || offset_frag)
// もし、fragが見つからない(=offsetがskbのサイズよりも大きい場合)、この処理に入る。
break;
}
// recv_skip_hint以降のアライン領域のオフセットを取得
// SKBのデータがskb_fragsで管理。skb_fragsはfragmentの配列構造で、
// fragmentごとに物理ページと物理ページ内のoffset/長さを管理。
// ここでは、物理ページ全体を際しているPageを氏rベル。
mappable_offset = find_next_mappable_frag(frags,
zc->recv_skip_hint);
if (mappable_offset) {
// ⑪mapできるfragmentまでのSKB内のoffsetをrecv_skip_hintに格納し、loopを先頭からやり直す。
zc->recv_skip_hint = mappable_offset;
printk("tcp_zerocopy_receive_mod(11): Checked mappable_offset mappable_offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", mappable_offset, zc->length, zc->recv_skip_hint);
break;
}
printk("tcp_zerocopy_receive_mod: Finished find_nex_mappable_frag mappable_offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", mappable_offset, zc->length, zc->recv_skip_hint);
// この時点でfragsはmapできる状態になっている。
// skb_frag_page - retrieve the page referred to by a paged fragment
// * @frag: the paged fragment
page = skb_frag_page(frags);
prefetchw(page);
pages[pages_to_map++] = page;
length += PAGE_SIZE;
zc->recv_skip_hint -= PAGE_SIZE;
frags++;
if (pages_to_map == TCP_ZEROCOPY_PAGE_BATCH_SIZE ||
zc->recv_skip_hint < PAGE_SIZE) {
// map対象のページがTCP_ZEROCOPY_PAGE_BATCH_SIZEまでたまったら、mmap領域に物理メモリを割り当てる
// Either full batch, or we're about to go to next skb
// (and we cannot unroll failed ops across skbs).
//
ret = tcp_zerocopy_vm_insert_batch(vma, pages,
pages_to_map,
&address, &length,
&seq, zc,
total_bytes_to_map);
if (ret)
goto out;
pages_to_map = 0;
}
}
printk("tcp_zerocopy_receive_mod: Finished loop pages_to_map=%d, zc->length=%d, zc->recv_skip_hint=%d\n", pages_to_map, zc->length, zc->recv_skip_hint);
if (pages_to_map) {
ret = tcp_zerocopy_vm_insert_batch(vma, pages, pages_to_map,
&address, &length, &seq,
zc, total_bytes_to_map);
}
out:
mmap_read_unlock(current->mm);
// Try to copy straggler data.
if (!ret)
// 元の実装では残ったデータをcopy_bufferにコピーさせる。
copylen = tcp_zc_handle_leftover(zc, sk, skb, &seq, copybuf_len, &tss);
if (length + copylen) {
// mapまたはbufferにコピーした場合、この分岐に入る
// 受信バッファ領域を計算しなおす。
WRITE_ONCE(tp->copied_seq, seq);
tcp_rcv_space_adjust(sk);
// Clean up data we have read: This will do ACK frames.
tcp_recv_skb(sk, seq, &offset);
tcp_cleanup_rbuf(sk, length + copylen);
ret = 0;
if (length == zc->length)
zc->recv_skip_hint = 0;
} else {
if (!zc->recv_skip_hint && sock_flag(sk, SOCK_DONE))
ret = -EIO;
}
zc->length = length;
return ret;
}
Hugepageとの関係
HugePageとは単語の意味通り「巨大」な「ページ」のことで、メモリ管理として1ページのサイズを大幅に拡張することを可能とした機能。DPDKではNICを直接操作して、HugePageの領域に転送。
制約
- ネットワークスタックとアプリが受信データの格納領域を共有するため、メモリ更新した場合プログラムがバギーになる
- MSG_ZEROCOPYは通常10 KB以上で効果がある
For receiving data, in order to take advantage of the zero copy receive code, the user must have a NIC that is configured for an MTU greater than the architecture page size. (E.g., for i386 it would be 4KB.) Additionally, in order for zero copy receive to work, packet payloads must be at least a page in size and page aligned. Achieving page aligned payloads requires a NIC that can split an incoming packet into multiple buffers. It also generally requires some sort of intelligence on the NIC to make sure that the payload starts in its own buffer. This is called “header splitting”. Currently the only NICs with support for header splitting are Alteon Tigon 2 based boards running slightly modified firmware. The FreeBSD ti(4) driver includes modified firmware for Tigon 2 boards only. Header splitting code can be written, however, for any NIC that allows putting received packets into multiple buffers and that has enough programmability to determine that the header should go into one buffer and the payload into another. You can also do a form of header splitting that does not require any NIC modifications if your NIC is at least capable of splitting packets into multiple buffers. This requires that you optimize the NIC driver for your most common packet header size. If that size (ethernet + IP + TCP headers) is generally 66 bytes, for instance, you would set the first buffer in a set for a particular packet to be 66 bytes long, and then subsequent buffers would be a page in size. For packets that have headers that are exactly 66 bytes long, your payload will be page aligned. The other requirement for zero copy receive to work is that the buffer that is the destination for the data read from a socket must be at least a page in size and page aligned. Obviously the requirements for receive side zero copy are impossible to meet without NIC hardware that is programmable enough to do header splitting of some sort. Since most NICs are not that programmable, or their manufacturers will not share the source code to their firmware, this approach to zero copy receive is not widely useful. There are other approaches, such as RDMA and TCP Offload, that may potentially help alleviate the CPU overhead associated with copying data out of the kernel. Most known techniques require some sort of support at the NIC level to work, and describing such techniques is beyond the scope of this manual page.
Virtio-netでzerocopyが働かない理由
virtio_netではmeageable_rx_buffferが働きバッファサイズがPAGE_SIZEを超えるため?
zerocopy_receiveはPAGE_SIZEのバッファしかサポートしない。→本当?
VIRTIO_NET_F_CSUM, VIRTIO_NET_F_GUEST_CSUM, \
VIRTIO_NET_F_MAC, \
VIRTIO_NET_F_HOST_TSO4, VIRTIO_NET_F_HOST_UFO, VIRTIO_NET_F_HOST_TSO6, \
VIRTIO_NET_F_HOST_ECN, VIRTIO_NET_F_GUEST_TSO4, VIRTIO_NET_F_GUEST_TSO6, \
VIRTIO_NET_F_GUEST_ECN, VIRTIO_NET_F_GUEST_UFO, \
VIRTIO_NET_F_HOST_USO, VIRTIO_NET_F_GUEST_USO4, VIRTIO_NET_F_GUEST_USO6, \
VIRTIO_NET_F_MRG_RXBUF, VIRTIO_NET_F_STATUS, VIRTIO_NET_F_CTRL_VQ, \
VIRTIO_NET_F_CTRL_RX, VIRTIO_NET_F_CTRL_VLAN, \
VIRTIO_NET_F_GUEST_ANNOUNCE, VIRTIO_NET_F_MQ, \
VIRTIO_NET_F_CTRL_MAC_ADDR, \
VIRTIO_NET_F_MTU, VIRTIO_NET_F_CTRL_GUEST_OFFLOADS, \
VIRTIO_NET_F_SPEED_DUPLEX, VIRTIO_NET_F_STANDBY, \
VIRTIO_NET_F_RSS, VIRTIO_NET_F_HASH_REPORT, VIRTIO_NET_F_NOTF_COAL
VirtualBoxでzerocopyが働かない理由
ローカル通信ではIP層をすっ飛ばす。TCPでマージしちゃうため?→多分違う
ハードウェアオフロードで受信パケットをマージしているため。→違う
NICのオフロード設定の変更方法(Linux) | アカスブログ (ac-as.net)
送受信するサーバが一緒だとおっきなサイズのパケットを送るらしい。送受信を分ければvirtualboxだと1ページはゼロコピーできる。
<送信側(ubuntu3)>
sudo ifconfig enp0s3 mtu 9000
sudo ./a.out -4 -s -z
<受信側(ubuntu2)>
sudo ifconfig enp0s3 mtu 9000 ★送受信両方
res=0, zc.length=0,zc.recv_skip_hint=4030.
res=0, zc.length=4096,zc.recv_skip_hint=4852.
res=0, zc.length=4096,zc.recv_skip_hint=4852.
res=0, zc.length=4096,zc.recv_skip_hint=4852.
res=0, zc.length=4096,zc.recv_skip_hint=4852.
sudo ./tcp_mmap -4 -H 192.168.56.101
できた!
情報強化
static struct sk_buff *tcp_recv_skb(struct sock *sk, u32 seq, u32 *off)
{
struct sk_buff *skb;
u32 offset;
printk("tcp_recv_skb(1): Entering seq=%u", seq);
while ((skb = skb_peek(&sk->sk_receive_queue)) != NULL) {
// TCP_SKB_CB(skb)->seqはskbのヘッダ解析用のキャッシュ領域のデータを元に、
// skbのシーケンス番号を返す。
// シーケンス番号はソケットがTCPデータ(セグメント)を送信したバイト数
// offsetには、指定されたseqに対するskbの先頭からのバイト数が入る。
printk("tcp_recv_skb(2): Looping. offset=%u,skb->len=%u, seq=%u", offset, skb->len, seq);
offset = seq - TCP_SKB_CB(skb)->seq;
if (unlikely(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_SYN)) {
pr_err_once("%s: found a SYN, please report !\n", __func__);
offset--;
}
if (offset < skb->len || (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)) {
printk("tcp_recv_skb(3): Exiting. offset=%u,skb->len=%u, seq=%u", offset, skb->len, seq);
*off = offset;
return skb;
}
/* This looks weird, but this can happen if TCP collapsing
* splitted a fat GRO packet, while we released socket lock
* in skb_splice_bits()
*/
// SKBを解放する。ここにくるのは、fat GROパケットによる例外ケースのみ。
sk_eat_skb(sk, skb);
}
return NULL;
}
// 既に読み込み済のskbのバイト数に達するまでfragmentリストをたどる。
// もし、読み込み済のskbより多くのfragmentが残っていれば、offset_frag=0を返す。
// offset_frag=0の場合、残っているfragmentに対してゼロコピーできると判断する。
// offset_skb: [in]引数のskbが何バイト読まれているか?
// offset_frag: [out]引数のskbが何バイト読まれているか?
static skb_frag_t *skb_advance_to_frag(struct sk_buff *skb, u32 offset_skb,
u32 *offset_frag)
{
skb_frag_t *frag;
printk("skb_advance_to_frag(1): Entering\n");
offset_skb -= skb_headlen(skb);
if ((int)offset_skb < 0 || skb_has_frag_list(skb)){
printk("skb_advance_to_frag(2): Failed to advance frag. offset_skb=%u, skb_has_frag_list(skb)=%d\n", offset_skb, skb_has_frag_list(skb));
return NULL;
}
frag = skb_shinfo(skb)->frags;// fragmentの配列?
while (offset_skb) {
printk("skb_advance_to_frag(3): Looping. skbfrag_size(frag)=%u, offset_skb=%u\n", skb_frag_size(frag), *offset_frag);
if (skb_frag_size(frag) > offset_skb) {
*offset_frag = offset_skb;
printk("skb_advance_to_frag(4): Exiting *offset_frag=%u\n", *offset_frag);
return frag;
}
offset_skb -= skb_frag_size(frag);
++frag;
}
*offset_frag = 0;
printk("skb_advance_to_frag(5): Exiting *offset_frag=%u\n", *offset_frag);
return frag;
}
static int tcp_zerocopy_receive_mod(struct sock *sk,
struct tcp_zerocopy_receive *zc)
{
u32 length = 0, offset, vma_len, avail_len, copylen = 0;
unsigned long address = (unsigned long)zc->address;
struct page *pages[TCP_ZEROCOPY_PAGE_BATCH_SIZE];
s32 copybuf_len = zc->copybuf_len;
struct tcp_sock *tp = tcp_sk(sk);
const skb_frag_t *frags = NULL;
unsigned int pages_to_map = 0;
struct vm_area_struct *vma;
struct sk_buff *skb = NULL;
u32 seq = tp->copied_seq;// copied_seqはHead of yet unread data.
u32 total_bytes_to_map;
// get available receive bytes on socket
// ①ソケットの受信データ量を調べる。
int inq = tcp_inq(sk);
int ret;
struct scm_timestamping_internal tss;
printk("tcp_zerocopy_receive_mod: Entering inq=%d\n", inq);
zc->copybuf_len = 0;
zc->msg_flags = 0;
if (address & (PAGE_SIZE - 1) || address != zc->address)
return -EINVAL;
if (sk->sk_state == TCP_LISTEN)
return -ENOTCONN;
#ifdef NOT_FOR_ZCOPY
// TODO: RPS対応。
//sock_rps_record_flow(sk);
// ②アプリが確保したアプリバッファが受信データ量より大きい場合、
// アプリバッファにデータをcopyする。
if (inq && inq <= copybuf_len)
return receive_fallback_to_copy(sk, zc, inq, tss);
#endif //NOT_FOR_ZCOPY
// ③PAGE_SIZEより受信データ量が小さい場合は、zcopyせずに修了。
if (inq < PAGE_SIZE) {
zc->length = 0;
zc->recv_skip_hint = inq;
printk("tcp_zerocopy_receive_mod(3): Finished tcp_zerocopy_receive_mod. inq is less than PAGE_SIZE. inq=%d, zc->length=%d, zc->recv_skip_hint=%d\n", inq, zc->length, zc->recv_skip_hint);
if (!inq && sock_flag(sk, SOCK_DONE))
return -EIO;
return 0;
}
mmap_read_lock(current->mm);
// ⑤指定されたアドレスのvm_area_structを調べる。
vma = vma_lookup(current->mm, address);
printk("tcp_zerocopy_receive_mod(5): Looked up VMA. vma=%p, zc->length=%d, zc->recv_skip_hint=%d\n", vma, zc->length, zc->recv_skip_hint);
#ifdef NOT_FOR_ZCOPY
// ⑥指定されたアドレスがzcopy空間にmmapされているかチェックする(tcp_mmapされているかを調べる)。
// TODO: アプリ側でチェックさせる?
if (!vma || vma->vm_ops != &tcp_vm_ops) {
mmap_read_unlock(current->mm);
return -EINVAL;
}
#endif //NOT_FOR_ZCOPY
// ⑦マップ対象のサイズを調べる。min(指定されたサイズ、mmapした領域の使用可能なサイズ、受信データ量)
vma_len = min_t(unsigned long, zc->length, vma->vm_end - address);
avail_len = min_t(u32, vma_len, inq);
total_bytes_to_map = avail_len & ~(PAGE_SIZE - 1);
printk("tcp_zerocopy_receive_mod(7): Calculated total_bytes_to_map=%d, avail_len=%u, zc->length=%d, zc->recv_skip_hint=%d\n", total_bytes_to_map, avail_len, zc->length, zc->recv_skip_hint);
if (total_bytes_to_map) {
if (!(zc->flags & TCP_RECEIVE_ZEROCOPY_FLAG_TLB_CLEAN_HINT))
// remove user pages in a given range
zap_page_range(vma, address, total_bytes_to_map);
zc->length = total_bytes_to_map;
zc->recv_skip_hint = 0;
} else {
zc->length = avail_len;
zc->recv_skip_hint = avail_len;
}
printk("tcp_zerocopy_receive_mod: Entering while loop total_bytes_to_map=%d, zc->length=%d, zc->recv_skip_hint=%d\n", total_bytes_to_map, zc->length, zc->recv_skip_hint);
ret = 0;
while (length + PAGE_SIZE <= zc->length) {
int mappable_offset;
struct page *page;
if (zc->recv_skip_hint < PAGE_SIZE) {
u32 offset_frag;
if (skb) {
// この条件分岐はニ回目以降のloop。
// 640行目でrecv_skip_hint(SKB内の未処理データ量がページサイズ以下の場合、この条件分布に入る。
if (zc->recv_skip_hint > 0)
break;
skb = skb->next;
offset = seq - TCP_SKB_CB(skb)->seq;
printk("tcp_zerocopy_receive_mod: Calculated offset. offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", offset, zc->length, zc->recv_skip_hint);
} else {
// この条件分岐は一回目のloop。
// seq(ソケット内の未処理データの先頭シーケンス番号)を含むskbを調べる。offsetにはskb内のseqのoffsetが含まれる。
skb = tcp_recv_skb(sk, seq, &offset);
printk("tcp_zerocopy_receive_mod(9): Finished tcp_recv_skb. seq=%d, skb=%p,offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", seq, skb, offset, zc->length, zc->recv_skip_hint);
}
#ifdef NOT_FOR_ZCOPY
// 受信タイムスタンプ。デバッグ用?
if (TCP_SKB_CB(skb)->has_rxtstamp) {
tcp_update_recv_tstamps(skb, tss);
zc->msg_flags |= TCP_CMSG_TS;
}
#endif //NOT_FOR_ZCOPY
// recv_skip_hintにSKBサイズからoffset(ソケット内の未処理データのオフセット)を引いたi
// 値(SKB内の未処理データ量(remaining_data))を格納。
zc->recv_skip_hint = skb->len - offset;
// static skb_frag_t *skb_advance_to_frag(struct sk_buff *skb, u32 offset_skb, u32 *offset_frag)
// ⑩skb内のfragmentリストをたどり、offset_skbで指定された位置に
// あるfragmentを返す。offset_fragにはskb内の該当fragmentのオフセット
// が返される。
frags = skb_advance_to_frag(skb, offset, &offset_frag);
printk("tcp_zerocopy_receive_mod(10): Finished skb_advance_to_frag. skb=%p, offset=%d, offset_frag=%d,zc->length=%d, zc->recv_skip_hint=%d\n", skb, offset, offset_frag, zc->length, zc->recv_skip_hint);
if (!frags || offset_frag)
// もし、fragが見つからない(=offsetがskbのサイズよりも大きい場合)、この処理に入る。
break;
}
// recv_skip_hint以降のアライン領域のオフセットを取得
// SKBのデータがskb_fragsで管理。skb_fragsはfragmentの配列構造で、
// fragmentごとに物理ページと物理ページ内のoffset/長さを管理。
// ここでは、物理ページ全体を際しているPageを氏rベル。
mappable_offset = find_next_mappable_frag(frags,
zc->recv_skip_hint);
if (mappable_offset) {
// ⑪mapできるfragmentまでのSKB内のoffsetをrecv_skip_hintに格納し、loopを先頭からやり直す。
zc->recv_skip_hint = mappable_offset;
printk("tcp_zerocopy_receive_mod(11): Checked mappable_offset mappable_offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", mappable_offset, zc->length, zc->recv_skip_hint);
break;
}
printk("tcp_zerocopy_receive_mod: Finished find_nex_mappable_frag mappable_offset=%d, zc->length=%d, zc->recv_skip_hint=%d\n", mappable_offset, zc->length, zc->recv_skip_hint);
// この時点でfragsはmapできる状態になっている。
// skb_frag_page - retrieve the page referred to by a paged fragment
// * @frag: the paged fragment
page = skb_frag_page(frags);
prefetchw(page);
pages[pages_to_map++] = page;
length += PAGE_SIZE;
zc->recv_skip_hint -= PAGE_SIZE;
frags++;
if (pages_to_map == TCP_ZEROCOPY_PAGE_BATCH_SIZE ||
zc->recv_skip_hint < PAGE_SIZE) {
// map対象のページがTCP_ZEROCOPY_PAGE_BATCH_SIZEまでたまったら、mmap領域に物理メモリを割り当てる
// Either full batch, or we're about to go to next skb
// (and we cannot unroll failed ops across skbs).
//
ret = tcp_zerocopy_vm_insert_batch(vma, pages,
pages_to_map,
&address, &length,
&seq, zc,
total_bytes_to_map);
if (ret)
goto out;
pages_to_map = 0;
}
}
printk("tcp_zerocopy_receive_mod: Finished loop pages_to_map=%d, zc->length=%d, zc->recv_skip_hint=%d\n", pages_to_map, zc->length, zc->recv_skip_hint);
if (pages_to_map) {
ret = tcp_zerocopy_vm_insert_batch(vma, pages, pages_to_map,
&address, &length, &seq,
zc, total_bytes_to_map);
}
out:
mmap_read_unlock(current->mm);
// Try to copy straggler data.
if (!ret)
// 元の実装では残ったデータをcopy_bufferにコピーさせる。
copylen = tcp_zc_handle_leftover(zc, sk, skb, &seq, copybuf_len, &tss);
if (length + copylen) {
// mapまたはbufferにコピーした場合、この分岐に入る。
// 受信バッファ領域を計算しなおす。
WRITE_ONCE(tp->copied_seq, seq);
tcp_rcv_space_adjust(sk);
// Clean up data we have read: This will do ACK frames.
tcp_recv_skb(sk, seq, &offset);
tcp_cleanup_rbuf(sk, length + copylen);
ret = 0;
if (length == zc->length)
zc->recv_skip_hint = 0;
} else {
if (!zc->recv_skip_hint && sock_flag(sk, SOCK_DONE))
ret = -EIO;
}
zc->length = length;
printk("tcp_zerocopy_receive_mod: Finished. zc->length=%d, zc->recv_skip_hint=%d\n", zc->length, zc->recv_skip_hint);
return ret;
}
mtu=9000のケース
ioctl finished.length=4096, recv_skip_hint=4852
ioctl finished.length=4096, recv_skip_hint=4852
ioctl finished.length=4096, recv_skip_hint=4852
[ 2449.095859] tcp_zerocopy_receive_mod: Entering inq=12627
[ 2449.095862] tcp_zerocopy_receive_mod(5): Looked up VMA. vma=0000000095474e2e, zc->length=524288, zc->recv_skip_hint=0
[ 2449.095864] tcp_zerocopy_receive_mod(7): Calculated total_bytes_to_map=12288, avail_len=12627, zc->length=524288, zc->recv_skip_hint=0
[ 2449.095865] tcp_zerocopy_receive_mod: Entering while loop total_bytes_to_map=12288, zc->length=12288, zc->recv_skip_hint=0
[ 2449.095867] tcp_recv_skb(1): Entering seq=1090029030
[ 2449.095867] tcp_recv_skb(2): Looping. offset=0,skb->len=25605, seq=1090029030
[ 2449.095868] tcp_recv_skb(3): Exiting. offset=12978,skb->len=25605, seq=1090029030
[ 2449.095869] tcp_zerocopy_receive_mod(9): Finished tcp_recv_skb. seq=1090029030, skb=0000000099c963a8,offset=12978, zc->length=12288, zc->recv_skip_hint=0
[ 2449.095871] skb_advance_to_frag(1): Entering
[ 2449.095872] skb_advance_to_frag(3): Looping. skbfrag_size(frag)=4030, offset_skb=12978
[ 2449.095873] skb_advance_to_frag(3): Looping. skbfrag_size(frag)=4096, offset_skb=8948
[ 2449.095873] skb_advance_to_frag(3): Looping. skbfrag_size(frag)=822, offset_skb=4852
[ 2449.095874] skb_advance_to_frag(3): Looping. skbfrag_size(frag)=4030, offset_skb=4030
[ 2449.095875] skb_advance_to_frag(5): Exiting *offset_frag=0
[ 2449.095876] tcp_zerocopy_receive_mod(10): Finished skb_advance_to_frag. skb=0000000099c963a8, offset=12978, offset_frag=0,zc->length=12288, zc->recv_skip_hint=12627
[ 2449.095877] tcp_zerocopy_receive_mod: Finished find_nex_mappable_frag mappable_offset=0, zc->length=12288, zc->recv_skip_hint=12627
[ 2449.095879] tcp_zerocopy_receive_mod(11): Checked mappable_offset mappable_offset=8531, zc->length=12288, zc->recv_skip_hint=8531
[ 2449.095881] tcp_zerocopy_receive_mod: Finished loop pages_to_map=1, zc->length=12288, zc->recv_skip_hint=8531
[ 2449.095883] tcp_recv_skb(1): Entering seq=1090033126
[ 2449.095884] tcp_recv_skb(2): Looping. offset=217060864,skb->len=25605, seq=1090033126
[ 2449.095885] tcp_recv_skb(3): Exiting. offset=17074,skb->len=25605, seq=1090033126
[ 2449.095886] tcp_zerocopy_receive_mod: Finished. zc->length=4096, zc->recv_skip_hint=8531
[ 2449.095887] Processing
mtu=1500のケース
IPフラグメント(skb_has_frag_list(skb)=1)が起こると✕。
[ 2659.758788] Processing
[ 2659.758902] tcp_zerocopy_receive_mod: Entering inq=28304
[ 2659.758904] tcp_zerocopy_receive_mod(5): Looked up VMA. vma=000000008b2e66e9, zc->length=524288, zc->recv_skip_hint=0
[ 2659.758905] tcp_zerocopy_receive_mod(7): Calculated total_bytes_to_map=24576, avail_len=28304, zc->length=524288, zc->recv_skip_hint=0
[ 2659.758907] tcp_zerocopy_receive_mod: Entering while loop total_bytes_to_map=24576, zc->length=24576, zc->recv_skip_hint=0
[ 2659.758908] tcp_recv_skb(1): Entering seq=3974627946
[ 2659.758909] tcp_recv_skb(2): Looping. offset=0,skb->len=28304, seq=3974627946
[ 2659.758910] tcp_recv_skb(3): Exiting. offset=0,skb->len=28304, seq=3974627946
[ 2659.758911] tcp_zerocopy_receive_mod(9): Finished tcp_recv_skb. seq=-320339350, skb=00000000d6ecbcb2,offset=0, zc->length=24576, zc->recv_skip_hint=0
[ 2659.758913] skb_advance_to_frag(1): Entering
[ 2659.758913] skb_advance_to_frag(2): Failed to advance frag. offset_skb=0, skb_has_frag_list(skb)=1
[ 2659.758914] tcp_zerocopy_receive_mod(10): Finished skb_advance_to_frag. skb=00000000d6ecbcb2, offset=0, offset_frag=0,zc->length=24576, zc->recv_skip_hint=28304
[ 2659.758916] tcp_zerocopy_receive_mod: Finished loop pages_to_map=0, zc->length=24576, zc->recv_skip_hint=28304
[ 2659.758917] tcp_zerocopy_receive_mod: Finished. zc->length=0, zc->recv_skip_hint=28304
気付き
- zerocopy receiveは、NICからDMA転送した受信バッファをプロセス空間にmmapする。
- mmapは4ページ単位。受信バッファはIPパケットごとに確保するため、データが4KB以上じゃないとmmapできない。
- IPパケットの構造体はsk_buff
わからないこと
- 4KB以上だとmmapできる?
- mmapしたメモリを、BF2のRMDA転送元にできる?
- なぜ実機確認したときに4KBできない?
今後調べること
参考文献
Zero-copy Sendmsg/Receiveについて調べてみた – Qiita
Zero-copy TCP receive [LWN.net]
MSG_ZEROCOPY — The Linux Kernel documentation
Implementing TCP RX zero copy.pdf (netdevconf.info)
Zero Copy Networking in UEK6 (oracle.com)
internal22-282-受信処理アルゴリズム – Linux Kernel Documents Wiki – Linux Kernel Documents – OSDN