【轉載】AF_XDP技術詳解

原文信息
作者:rexrock
出處:https://rexrock.github.io/post/af_xdp1/

AF_XDP是一個協議族(例如AF_NET),主要用於高性能報文處理。

前文XDP技術簡介中提到過,通過XDP_REDIRECT我們可以將報文重定向到其他設備發送出去或者重定向到其他的CPU繼續進行處理。而AF_XDP則利用 bpf_redirect_map()函數,實現將報文重定向到用戶態一塊指定的內存中,接下來我們看一下這到底是如何做到的。

我們使用普通的 socket() 系統調用創建一個AF_XDP套接字(XSK)。每個XSK都有兩個ring:RX RING 和 TX RING。套接字可以在 RX RING 上接收數據包,並且可以在 TX RING 環上發送數據包。這些環分別通過setockopts()的 XDP_RX_RING 和 XDP_TX_RING 進行註冊和調整大小。每個 socket 必須至少有一個這樣的環。RX或TX描述符環指向存儲區域(稱爲UMEM)中的數據緩衝區。RX和TX可以共享同一UMEM,因此不必在RX和TX之間複製數據包。

UMEM也有兩個 ring:FILL RING 和 COMPLETION RING。應用程序使用 FILL RING 向內核發送可以承載報文的 addr (該 addr 指向UMEM中某個chunk),以供內核填充RX數據包數據。每當收到數據包,對這些 chunks 的引用就會出現在RX環中。另一方面,COMPLETION RING包含內核已完全傳輸的 chunks 地址,可以由用戶空間再次用於 TX 或 RX。

enter description here

關於ring,熟悉dpdk的同學應該都不陌生,這裏只做簡單介紹。ring就是一個固定長度的數組,並且同時擁有一個生產者和一個消費者,生產者向數組中逐個填寫數據,消費者從數組中逐個讀取生產者填充的數據,生產者和消費者都用數組的下標表示,不斷累加,像一個環一樣不斷重複生產然後消費的動作,因此得名ring。
enter description here

此外需要注意的事,AF_XDP socket不再通過 send()/recv()等函數實現報文收發,而實通過直接操作ring來實現報文收發。

  1. FILL RING

fill_ring 的生產者是用戶態程序,消費者是內核態中的XDP程序;

用戶態程序通過 fill_ring 將可以用來承載報文的 UMEM frames 傳到內核,然後內核消耗 fill_ring 中的元素(後文統一稱爲 desc),並將報文拷貝到desc中指定地址(該地址即UMEM frame的地址);

  1. COMPLETION RING

completion_ring 的生產者是XDP程序,消費者是用戶態程序;

當內核完成XDP報文的發送,會通過 completion_ring 來通知用戶態程序,哪些報文已經成功發送,然後用戶態程序消耗 completion_ring 中 desc(只是更新consumer計數相當於確認);

  1. RX RING

rx_ring的生產者是XDP程序,消費者是用戶態程序;

XDP程序消耗 fill_ring,獲取可以承載報文的 desc並將報文拷貝到desc中指定的地址,然後將desc填充到 rx_ring 中,並通過socket IO機制通知用戶態程序從 rx_ring 中接收報文;

  1. TX RING

tx_ring的生產者是用戶態程序,消費者是XDP程序;

用戶態程序將要發送的報文拷貝 tx_ring 中 desc指定的地址中,然後 XDP程序 消耗 tx_ring 中的desc,將報文發送出去,並通過 completion_ring 將成功發送的報文的desc告訴用戶態程序;

1. 用戶態程序

1.1 創建AF_XDP的socket

xsk_fd = socket(AF_XDP, SOCK_RAW, 0);

這一步沒什麼好展開的。

1.2 爲UMEM申請內存

上文提到UMEM是一塊包含固定大小chunk的內存,我們可以通過malloc/mmap/hugepages申請。下文大部分代碼出自kernel samples。

    bufs = mmap(NULL, NUM_FRAMES * opt_xsk_frame_size,
                         PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS | opt_mmap_flags, -1, 0);
    if (bufs == MAP_FAILED) {
        printf("ERROR: mmap failed\n");
        exit(EXIT_FAILURE);
    }

1.3 向AF_XDP socket註冊UMEM

        struct xdp_umem_reg mr;
        memset(&mr, 0, sizeof(mr));
        mr.addr = (uintptr_t)umem_area; // umem_area即上面通過mmap申請到內存起始地址
        mr.len = size;
        mr.chunk_size = umem->config.frame_size;
        mr.headroom = umem->config.frame_headroom;
        mr.flags = umem->config.flags;

        err = setsockopt(umem->fd, SOL_XDP, XDP_UMEM_REG, &mr, sizeof(mr));
        if (err) {
                err = -errno;
                goto out_socket;
        }

其中xdp_umem_reg結構定義在 usr/include/linux/if_xdp.h中:

struct xdp_umem_reg {
        __u64 addr; /* Start of packet data area */
        __u64 len; /* Length of packet data area */
        __u32 chunk_size;
        __u32 headroom;
        __u32 flags;
};

成員解析:

  • addr就是UMEM內存的起始地址;
  • len是整個UMEM內存的總長度;
  • chunk_size就是每個chunk的大小;
  • headroom,如果設置了,那麼報文數據將不是從每個chunk的起始地址開始存儲,而是要預留出headroom大小的內存,再開始存儲報文數據,headroom在隧道網絡中非常常見,方便封裝外層頭部;
  • flags, UMEM還有一些更復雜的用法,通過flag設置,後面再進一步展開;

1.4 創建FILL RING 和 COMPLETION RING

我們通過 setsockopt() 設置 FILL/COMPLETION/RX/TX ring的大小(在我看來這個過程相當於創建,不設置大小的ring是沒有辦法使用的)。

FILL RING 和 COMPLETION RING是UMEM必須,RX和TX則是 AF_XDP socket二選一的,例如AF_XDP socket只收包那麼只需要設置RX RING的大小即可。

        err = setsockopt(umem->fd, SOL_XDP, XDP_UMEM_FILL_RING,
                         &umem->config.fill_size,
                         sizeof(umem->config.fill_size));
        if (err) {
                err = -errno;
                goto out_socket;
        }
        err = setsockopt(umem->fd, SOL_XDP, XDP_UMEM_COMPLETION_RING,
                         &umem->config.comp_size,
                         sizeof(umem->config.comp_size));
        if (err) {
                err = -errno;
                goto out_socket;
        }

上述操作相當於創建了 FILL RING 和 和 COMPLETION RING,創建ring的過程主要是初始化 producer 和 consumer 的下標,以及創建ring數組。

問題來了:

上文提到,用戶態程序是 FILL RING 的生產者和 CONPLETION RING 的消費者,上面2個 ring 的創建是在內核中創建了 ring 並初始化了其相關成員。那麼用戶態程序如何操作這兩個位於內核中的 ring 呢?所以接下來我們需要將整個 ring 映射到用戶態空間。

1.5 將FILL RING 映射到用戶態

第一步是獲取內核中ring結構各成員的偏移,因爲從5.4版本開始後,ring結構中除了 producer、consumer、desc外,又新增了一個flag成員。所以用戶態程序需要先獲取 ring 結構中各成員的準確便宜,才能在mmap() 之後準確識別內存中各成員位置。

        err = xsk_get_mmap_offsets(umem->fd, &off);
        if (err) {
                err = -errno;
                goto out_socket;
        }

xsk_get_mmap_offsets() 函數主要是通過getsockopt函數實現這一功能:

        err = getsockopt(fd, SOL_XDP, XDP_MMAP_OFFSETS, off, &optlen);
        if (err)
                return err;

一切就緒,開始將內核中的 FILL RING 映射到用戶態程序中:

        map = mmap(NULL, off.fr.desc + umem->config.fill_size * sizeof(__u64),
                   PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, umem->fd,
                   XDP_UMEM_PGOFF_FILL_RING);
        if (map == MAP_FAILED) {
                err = -errno;
                goto out_socket;
        }

        umem->fill = fill;
        fill->mask = umem->config.fill_size - 1;
        fill->size = umem->config.fill_size;
        fill->producer = map + off.fr.producer;
        fill->consumer = map + off.fr.consumer;
        fill->flags = map + off.fr.flags;
        fill->ring = map + off.fr.desc;
        fill->cached_cons = umem->config.fill_size;

上面代碼需要關注的一點是 mmap() 函數中指定內存的長度——off.fr.desc + umem->config.fill_size * sizeof(__u64),umem->config.fill_size * sizeof(__u64)沒什麼好說的,就是ring數組的長度,而 off.fr.desc 則是ring結構體的長度,我們先看下內核中ring結構的定義:

struct xdp_ring_offset {
        __u64 producer;
        __u64 consumer;
        __u64 desc;
};

這是沒有flag的定義,無傷大雅。這裏desc的地址其實就是ring數組的起始地址了。而off.fr.desc是desc相對 ring 結構體起始地址的偏移,相當於結構體長度。我們用一張圖來看下ring所在內存的結構分佈:

enter description here

後面一堆賦值代碼沒什麼好講的,umem->fill 是用戶態程序自定義的一個結構體,其成員 producer、consumer、flags、ring都是指針,分別指向實際ring結構中的對應成員,umem->fill中的其他成員主要在後面報文收發時用到,起輔助作用。

1.6 將COMPLETION RING 映射到用戶態

跟上面 FILL RING 的映射一樣,只貼代碼好了:

        map = mmap(NULL, off.cr.desc + umem->config.comp_size * sizeof(__u64),
                   PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, umem->fd,
                   XDP_UMEM_PGOFF_COMPLETION_RING);
        if (map == MAP_FAILED) {
                err = -errno;
                goto out_mmap;
        }

        umem->comp = comp;
        comp->mask = umem->config.comp_size - 1;
        comp->size = umem->config.comp_size;
        comp->producer = map + off.cr.producer;
        comp->consumer = map + off.cr.consumer;
        comp->flags = map + off.cr.flags;
        comp->ring = map + off.cr.desc;

1.7 創建RX RING和TX RING然後mmap

這裏和 FILL RING 以及 COMPLETION RING的做法基本完全一致,只貼代碼:

        if (rx) {
                err = setsockopt(xsk->fd, SOL_XDP, XDP_RX_RING,
                                 &xsk->config.rx_size,
                                 sizeof(xsk->config.rx_size));
                if (err) {
                        err = -errno;
                        goto out_socket;
                }
        }
        if (tx) {
                err = setsockopt(xsk->fd, SOL_XDP, XDP_TX_RING,
                                 &xsk->config.tx_size,
                                 sizeof(xsk->config.tx_size));
                if (err) {
                        err = -errno;
                        goto out_socket;
                }
        }

        err = xsk_get_mmap_offsets(xsk->fd, &off);
        if (err) {
                err = -errno;
                goto out_socket;
        }

        if (rx) {
                rx_map = mmap(NULL, off.rx.desc +
                              xsk->config.rx_size * sizeof(struct xdp_desc),
                              PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
                              xsk->fd, XDP_PGOFF_RX_RING);
                if (rx_map == MAP_FAILED) {
                        err = -errno;
                        goto out_socket;
                }

                rx->mask = xsk->config.rx_size - 1;
                rx->size = xsk->config.rx_size;
                rx->producer = rx_map + off.rx.producer;
                rx->consumer = rx_map + off.rx.consumer;
                rx->flags = rx_map + off.rx.flags;
                rx->ring = rx_map + off.rx.desc;
        }
        xsk->rx = rx;

        if (tx) {
                tx_map = mmap(NULL, off.tx.desc +
                              xsk->config.tx_size * sizeof(struct xdp_desc),
                              PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
                              xsk->fd, XDP_PGOFF_TX_RING);
                if (tx_map == MAP_FAILED) {
                        err = -errno;
                        goto out_mmap_rx;
                }

                tx->mask = xsk->config.tx_size - 1;
                tx->size = xsk->config.tx_size;
                tx->producer = tx_map + off.tx.producer;
                tx->consumer = tx_map + off.tx.consumer;
                tx->flags = tx_map + off.tx.flags;
                tx->ring = tx_map + off.tx.desc;
                tx->cached_cons = xsk->config.tx_size;
        }
        xsk->tx = tx;

1.8 調用bind()將AF_XDP socket綁定的指定設備的某一隊列

        sxdp.sxdp_family = PF_XDP;
        sxdp.sxdp_ifindex = xsk->ifindex;
        sxdp.sxdp_queue_id = xsk->queue_id;
        sxdp.sxdp_flags = xsk->config.bind_flags;

        err = bind(xsk->fd, (struct sockaddr *)&sxdp, sizeof(sxdp));
        if (err) {
                err = -errno;
                goto out_mmap_tx;
        }

2. 內核態程序

相比用戶態程序的一堆操作,內核態XDP程序看起來要簡單的多。

XDP技術簡介我們曾介紹過,XDP程序利用 bpf_reditrct() 函數可以將報文重定向到其他設備發送出去或者重定向到其他CPU繼續處理,後來又發展出了bpf_redirect_map()函數,可以將重定向的目的地保存在map中。AF_XDP 正是利用了 bpf_redirect_map() 函數以及 BPF_MAP_TYPE_XSKMAP 類型的 map 實現將報文重定向到用戶態程序。

2.1 創建BPF_MAP_TYPE_XSKMAP類型的map

該類型map的key是網口設備的queue_id,value則是該queue上綁定的AF_XDP socket fd,所以通常需要爲每個網口設備各自創建獨立的map,並在用戶態將對應的queue_id->xsk_fd存儲到map中。

static int xsk_create_bpf_maps(struct xsk_socket *xsk)
{
        int max_queues;
        int fd;

        max_queues = xsk_get_max_queues(xsk);
        if (max_queues < 0)
                return max_queues;

        fd = bpf_create_map_name(BPF_MAP_TYPE_XSKMAP, "xsks_map",
                                 sizeof(int), sizeof(int), max_queues, 0);
        if (fd < 0)
                return fd;

        xsk->xsks_map_fd = fd;

        return 0;
}

bpf_create_map_name參數詳解:

  • BPF_MAP_TYPE_XSKMAP,map類型
  • "xsks_map",map的名字
  • sizeof(int),分別指定key和vlue的size
  • max_queues,map大小
  • 0, map_flags

2.2 XDP程序代碼

        /* This is the C-program:
         * SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
         * {
         *     int index = ctx->rx_queue_index;
         *
         *     // A set entry here means that the correspnding queue_id
         *     // has an active AF_XDP socket bound to it.
         *     if (bpf_map_lookup_elem(&xsks_map, &index))
         *         return bpf_redirect_map(&xsks_map, index, 0);
         *
         *     return XDP_PASS;
         * }
         */

是不是非常的簡單,真正的redirect操作只有一行代碼。

2.3 XDP程序的加載

static int xsk_load_xdp_prog(struct xsk_socket *xsk)
{
        static const int log_buf_size = 16 * 1024;
        char log_buf[log_buf_size];
        int err, prog_fd;

        /* This is the C-program:
         * SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
         * {
         *     int index = ctx->rx_queue_index;
         *
         *     // A set entry here means that the correspnding queue_id
         *     // has an active AF_XDP socket bound to it.
         *     if (bpf_map_lookup_elem(&xsks_map, &index))
         *         return bpf_redirect_map(&xsks_map, index, 0);
         *
         *     return XDP_PASS;
         * }
         */
        struct bpf_insn prog[] = {
                /* r1 = *(u32 *)(r1 + 16) */
                BPF_LDX_MEM(BPF_W, BPF_REG_1, BPF_REG_1, 16),
                /* *(u32 *)(r10 - 4) = r1 */
                BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -4),
                BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
                BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
                BPF_LD_MAP_FD(BPF_REG_1, xsk->xsks_map_fd),
                BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
                BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
                BPF_MOV32_IMM(BPF_REG_0, 2),
                /* if r1 == 0 goto +5 */
                BPF_JMP_IMM(BPF_JEQ, BPF_REG_1, 0, 5),
                /* r2 = *(u32 *)(r10 - 4) */
				                BPF_LD_MAP_FD(BPF_REG_1, xsk->xsks_map_fd),
                BPF_LDX_MEM(BPF_W, BPF_REG_2, BPF_REG_10, -4),
                BPF_MOV32_IMM(BPF_REG_3, 0),
                BPF_EMIT_CALL(BPF_FUNC_redirect_map),
                /* The jumps are to this instruction */
                BPF_EXIT_INSN(),
        };
        size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn);

        prog_fd = bpf_load_program(BPF_PROG_TYPE_XDP, prog, insns_cnt,
                                   "LGPL-2.1 or BSD-2-Clause", 0, log_buf,
                                   log_buf_size);
        if (prog_fd < 0) {
                pr_warning("BPF log buffer:\n%s", log_buf);
                return prog_fd;
        }

        err = bpf_set_link_xdp_fd(xsk->ifindex, prog_fd, xsk->config.xdp_flags);
        if (err) {
                close(prog_fd);
                return err;
        }

        xsk->prog_fd = prog_fd;
        return 0;
}

XDP程序的load

調用函數 bpf_load_program() 之前的代碼不用關心。通常 eBPF 程序使用 C 語言的一個子集(restricted C)編寫,然後通過 LLVM 編譯成字節碼注入到內核執行。由於本例中XDP程序代碼比較簡單,功力深厚的作者直接將其編寫爲 eBPF(JIT)可識別的字節碼,然後直接調用 bpf_load_program() 函數將字節碼程序加載到內核中。

XDP程序的attach

XDP程序加載成功會返回對應的fd(後面統稱爲prog_fd),但是此時XDP程序還不會被執行(所有的eBPF都需要經過load和attach兩步才能被觸發執行,load只是將程序加載到內核中,attach將程序添加到hook點後,程序才能真正被觸發執行)。我們調用函數 bpf_set_link_xdp_fd() 函數將XDP程序attach到指定網口設備的驅動中的hook點。

注意: AF_XDP socket是跟指定網口設備的隊列綁定,而XDP程序則是跟指定的網口設備綁定(attach)。

3. 回到用戶態,讓程序run起來

經過前面兩步,AF_XDP socket、UMEM、FILL/COMPLETION/RX/TX RING 都創建設置好了,XSKMAP 和XDP PROG 也都加載好了。但是要想讓XDP程序把報文傳到用戶態程序,我們還得再進行兩補操作。

3.1 將AF_XDP socket存儲到XSKMAP中

前面介紹XSKMAP的時候,大家應該都想到這一步了,所以只貼代碼不說話:

static int xsk_set_bpf_maps(struct xsk_socket *xsk)
{
        return bpf_map_update_elem(xsk->xsks_map_fd, &xsk->queue_id,
                                   &xsk->fd, 0);
}

3.2 標題先賣個關子

前面我們介紹過4種ring,分別對應收發包兩個場景(收包:FILL/RX ring,發包:TX/COMPLETION RING),我畫個圖分別描述一下收發包場景。

3.2.1 先看收包

收包
收包過程是由XDP程序觸發的,但是XDP程序收包,需要依賴用戶態程序填充FILL RING,將可以承載報文的desc告訴XDP程序。所以在用戶態程序初始化階段,我們需要先填充FILL RING,直接看代碼:

        ret = xsk_ring_prod__reserve(&xsk->umem->fq,
                                     XSK_RING_PROD__DEFAULT_NUM_DESCS,
                                     &idx);
        if (ret != XSK_RING_PROD__DEFAULT_NUM_DESCS)
                exit_with_error(-ret);
        for (i = 0; i < XSK_RING_PROD__DEFAULT_NUM_DESCS; i++)
                *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx++) =
                        i * opt_xsk_frame_size;
        xsk_ring_prod__submit(&xsk->umem->fq,
                              XSK_RING_PROD__DEFAULT_NUM_DESCS);

三個經過封裝的函數,看起來不明覺厲,咱們一個一個看:

1. xsk_ring_prod__reserve

static inline size_t xsk_ring_prod__reserve(struct xsk_ring_prod *prod,
                                            size_t nb, __u32 *idx)
{
        if (xsk_prod_nb_free(prod, nb) < nb)
                return 0;

        *idx = prod->cached_prod;
        prod->cached_prod += nb;

        return nb;
}

這個函數前面先判斷一下:我現在想生產nb個數據,ring裏有沒有足夠的地方放啊?沒有的話直接退出,等會再試試。

vhostuser裏再這塊有個BUG,前端程序想發包發現ring裏空間不夠了,而後端驅動處理又由於有有問題的判斷,導致報文已發的報文一直不被處理,結果造成死鎖,以後別的文章中再介紹吧。

如果有足夠的空間,那麼會將生產者當前下標(cached_prog)賦值給idx,因爲退出函數後會根據從這個idx指向的位置開始生產desc,最後cached_prod + nb。

爲什麼要有個cached_prog呢?

因爲生產數據這個過程需要分幾步完成,所以這個東西應該爲了多線程同步吧。

2. xsk_ring_prod__fill_addr

static inline __u64 *xsk_ring_prod__fill_addr(struct xsk_ring_prod *fill,
                                              __u32 idx)
{
        __u64 *addrs = (__u64 *)fill->ring;

        return &addrs[idx & fill->mask];
}

看這段代碼前,我們先看下ring中元素xdp_desc的成員結構:

struct xdp_desc {
        __u64 addr;
        __u32 len;
        __u32 options;
};

成員解析

  • addr指向UMEM中某個幀的具體位置,並且不是真正的虛擬內存地址,而是相對UMEM內存起始地址的偏移。
  • len則是指報文的具體的長度,當XDP程序向desc填充報文的時候需要設置len,但是用戶態程序向FILL RING中填充desc則不用關心len。

所以上面xsk_ring_prod__fill_addr的功能就好理解了,返回的ring中下標爲idx處的desc中addr的指針;並且在函數返回後對addr進行了賦值,再看下這塊代碼,可以看到賦值給addr是個偏移量:

        for (i = 0; i < XSK_RING_PROD__DEFAULT_NUM_DESCS; i++)
                *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx++) =
                        i * opt_xsk_frame_size;
  1. xsk_ring_prod__submit
static inline void xsk_ring_prod__submit(struct xsk_ring_prod *prod, size_t nb)
{
        /* Make sure everything has been written to the ring before indicating
         * this to the kernel by writing the producer pointer.
         */
        libbpf_smp_wmb();

        *prod->producer += nb;
}

數據填充完畢,更新生產者下標。

說明:下標永遠指向下一個可填充數據位置。

3.2.2 再看發包

發包

發包真的沒啥好說的。初始化的時候不用管,想發包的時候直接就發啦。

4. 收包流程解析

收包
AF_XDP socket畢竟也是socket,所以select/poll/epoll這些函數都能用的,怎麼用這裏不介紹了。

我們只看具體從一個AF_XDP socket收包的過程:

static void rx_drop(struct xsk_socket_info *xsk, struct pollfd *fds)
{
        unsigned int rcvd, i;
        u32 idx_rx = 0, idx_fq = 0;
        int ret;

        rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx_rx);
        if (!rcvd) {
                if (xsk_ring_prod__needs_wakeup(&xsk->umem->fq))
                        ret = poll(fds, num_socks, opt_timeout);
                return;
        }

        ret = xsk_ring_prod__reserve(&xsk->umem->fq, rcvd, &idx_fq);
        while (ret != rcvd) {
                if (ret < 0)
                        exit_with_error(-ret);
                if (xsk_ring_prod__needs_wakeup(&xsk->umem->fq))
                        ret = poll(fds, num_socks, opt_timeout);
                ret = xsk_ring_prod__reserve(&xsk->umem->fq, rcvd, &idx_fq);
        }

        for (i = 0; i < rcvd; i++) {
                u64 addr = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->addr;
                u32 len = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx++)->len;
                u64 orig = xsk_umem__extract_addr(addr);

                addr = xsk_umem__add_offset_to_addr(addr);
                char *pkt = xsk_umem__get_data(xsk->umem->buffer, addr);

                hex_dump(pkt, len, addr);
                *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = orig;
        }

        xsk_ring_prod__submit(&xsk->umem->fq, rcvd);
        xsk_ring_cons__release(&xsk->rx, rcvd);
        xsk->rx_npkts += rcvd;
}

該函數並沒有對報文做什麼複雜處理,只是hex_dump了一下,整個收發包分五個步驟:

1. xsk_ring_cons__peek()

開始對RX RING進行消費,返回消費者下標和消費個數,並累加cached_cons;

2. xsk_ring_prod__reserve

開始對FILL RING進行生產,返回生產者下標和生產個數,並累加cached_prod;

3. 報文處理

處理從RX RING中收到的報文,並回填到FILL RING中;

        for (i = 0; i < rcvd; i++) {
                u64 addr = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->addr;
                u32 len = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx++)->len;
                u64 orig = xsk_umem__extract_addr(addr);

                addr = xsk_umem__add_offset_to_addr(addr);
                char *pkt = xsk_umem__get_data(xsk->umem->buffer, addr);

                hex_dump(pkt, len, addr);
                *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = orig;
        }

從desc中讀取addr,並通過 xsk_umem__get_data() 函數得到報文真正的虛擬地址,然後 hex_dump()下。

static inline void *xsk_umem__get_data(void *umem_area, __u64 addr)
{
        return &((char *)umem_area)[addr];
}

然後將處理完報文所在的 UMEM 幀回填到FILL RING中:

*xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = orig;

4. xsk_ring_prod__submit(&xsk->umem->fq, rcvd)

完成對RX RING的消費,更新消費者下標;

5. xsk_ring_cons__release(&xsk->rx, rcvd)

完成對FILL RING的生產,更新生產者下標;

5. 結語

關於AF_XDP的使用及背後原理暫且分析到這,目前AF_XDP已經在ovs、dpdk、cilium中應用,相應的文檔下面有鏈接。如有錯誤紕漏,歡迎大家拍磚。

相關代碼均出自kernel:

samples/bpf/xdpsock_user.c
tools/lib/bpf/xsk.c
tools/lib/bpf/xsk.h
net/xdp/xsk.c
net/xdp/xsk.h
usr/include/linux/if_xdp.h

相關參考文檔如下:

Kernel document for AF_XDP

Man for bpf

Openvswitch and XDP

DPDK and XDP

性能對比

編譯內核源碼中的示例代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章