Zerocopy receive

投稿者: | 2023年4月9日

概要

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

リソース設定(メモリ) ~DPDK入門 第4回~ | NTTテクノクロスブログ (ntt-tx.co.jp)

(Download PDF) Understanding DPDK (dokumen.tips)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です