Linux下AF-PACKET的V3版本

一 前言

上一篇介紹了通過AF-PACKET的V1 版本進行網絡包的捕獲,比較新的Linux內核是支持V3版本的,相對于前兩個版本(V2和V1比較相似,V2版本的時間精度從微秒提升到納秒。)V3版本,具有以下的提升:

  1. CPU使用率降低約15-20%
  2. 數據包捕獲率提高約20%
  3. 數據包的密度提升2倍(不知道什么意思, 如 ~2x increase in packet density)
  4. 端口聚合分析
  5. 非靜態數據幀大小,可以保存整個數據包。

所以這次就學習V3版本的用法,和其他能提示AF-PACKET抓包性能的均衡策略和方法。

二 V3版本的實戰

V3的版本結構每次遍歷和以前的不同是按照block遍歷,當然下一層再按照frame遍歷。
V3的時間戳精確度到納秒。

struct tpacket_req3 {
    unsigned int    tp_block_size;      // 每個連續內存塊的最小尺寸(必須是 PAGE_SIZE * 2^n )
    unsigned int    tp_block_nr;        // 內存塊數量
    unsigned int    tp_frame_size;      // 每個幀的大小(雖然V3中的幀長是可變的,但創建時還是會傳入一個最大的允許值)
    unsigned int    tp_frame_nr;        // 幀的總個數(必須等于 每個內存塊中的幀數量*內存塊數量)
    unsigned int    tp_retire_blk_tov;  // 內存塊的壽命(ms),超時后即使內存塊沒有被數據填入也會被內核停用,0意味著不設超時
    unsigned int    tp_sizeof_priv;     // 每個內存塊中私有空間大小,0意味著不設私有空間
    unsigned int    tp_feature_req_word;// 標志位集合(目前就支持1個標志 TP_FT_REQ_FILL_RXHASH)
}

// TPACKET_V3環形緩沖區每個幀的頭部結構
struct tpacket3_hdr {
    __u32       tp_next_offset; // 指向同一個內存塊中的下一個幀
    __u32       tp_sec;         // 時間戳(s)
    __u32       tp_nsec;        // 時間戳(ns)
    __u32       tp_snaplen;     // 捕獲到的幀實際長度
    __u32       tp_len;         // 幀的理論長度
    __u32       tp_status;      // 幀的狀態
    __u16       tp_mac;         // 以太網MAC字段距離幀頭的偏移量
    __u16       tp_net;
    union {
        struct tpacket_hdr_variant1 hv1;    // 包含vlan信息的子結構
    };
    __u8        tp_padding[8];
}

下面是內核文檔中的收包例子,代碼如下:


/* Written from scratch, but kernel-to-user space API usage
 * dissected from lolpcap:
 *  Copyright 2011, Chetan Loke <loke.chetan@gmail.com>
 *  License: GPL, version 2.0
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#include <unistd.h>
#include <signal.h>
#include <inttypes.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#ifndef likely
# define likely(x)      __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
# define unlikely(x)        __builtin_expect(!!(x), 0)
#endif

struct block_desc {
    uint32_t version;
    uint32_t offset_to_priv;
    struct tpacket_hdr_v1 h1;
};

struct ring {
    struct iovec *rd;
    uint8_t *map;
    struct tpacket_req3 req;
};

static unsigned long packets_total = 0, bytes_total = 0;
static sig_atomic_t sigint = 0;

static void sighandler(int num)
{
    sigint = 1;
}

static int setup_socket(struct ring *ring, char *netdev)
{
    int err, i, fd, v = TPACKET_V3;
    struct sockaddr_ll ll;
    unsigned int blocksiz = 1 << 22, framesiz = 1 << 11;
    unsigned int blocknum = 64;

    fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (fd < 0) {
        perror("socket");
        exit(1);
    }

    err = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
    if (err < 0) {
        perror("setsockopt");
        exit(1);
    }

    memset(&ring->req, 0, sizeof(ring->req));
    ring->req.tp_block_size = blocksiz;
    ring->req.tp_frame_size = framesiz;
    ring->req.tp_block_nr = blocknum;
    ring->req.tp_frame_nr = (blocksiz * blocknum) / framesiz;
    ring->req.tp_retire_blk_tov = 60;
    ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

    err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &ring->req,
                     sizeof(ring->req));
    if (err < 0) {
        perror("setsockopt");
        exit(1);
    }

    ring->map = mmap(NULL, ring->req.tp_block_size * ring->req.tp_block_nr,
                     PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0);
    if (ring->map == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    ring->rd = malloc(ring->req.tp_block_nr * sizeof(*ring->rd));
    assert(ring->rd);
    for (i = 0; i < ring->req.tp_block_nr; ++i) {
        ring->rd[i].iov_base = ring->map + (i * ring->req.tp_block_size);
        ring->rd[i].iov_len = ring->req.tp_block_size;
    }

    memset(&ll, 0, sizeof(ll));
    ll.sll_family = PF_PACKET;
    ll.sll_protocol = htons(ETH_P_ALL);
    ll.sll_ifindex = if_nametoindex(netdev);
    ll.sll_hatype = 0;
    ll.sll_pkttype = 0;
    ll.sll_halen = 0;

    err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
    if (err < 0) {
        perror("bind");
        exit(1);
    }

    return fd;
}

static void display(struct tpacket3_hdr *ppd)
{
    struct ethhdr *eth = (struct ethhdr *) ((uint8_t *) ppd + ppd->tp_mac);
    struct iphdr *ip = (struct iphdr *) ((uint8_t *) eth + ETH_HLEN);

    if (eth->h_proto == htons(ETH_P_IP)) {
        struct sockaddr_in ss, sd;
        char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

        memset(&ss, 0, sizeof(ss));
        ss.sin_family = PF_INET;
        ss.sin_addr.s_addr = ip->saddr;
        getnameinfo((struct sockaddr *) &ss, sizeof(ss),
                    sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

        memset(&sd, 0, sizeof(sd));
        sd.sin_family = PF_INET;
        sd.sin_addr.s_addr = ip->daddr;
        getnameinfo((struct sockaddr *) &sd, sizeof(sd),
                    dbuff, sizeof(dbuff), NULL, 0, NI_NUMERICHOST);

        printf("%s -> %s, ", sbuff, dbuff);
    }

    printf("rxhash: 0x%x\n", ppd->hv1.tp_rxhash);
}

static void walk_block(struct block_desc *pbd, const int block_num)
{
    int num_pkts = pbd->h1.num_pkts, i;
    unsigned long bytes = 0;
    struct tpacket3_hdr *ppd;

    ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                   pbd->h1.offset_to_first_pkt);
    for (i = 0; i < num_pkts; ++i) {
        bytes += ppd->tp_snaplen;
        display(ppd);

        ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                       ppd->tp_next_offset);
    }

    packets_total += num_pkts;
    bytes_total += bytes;
}

static void flush_block(struct block_desc *pbd)
{
    pbd->h1.block_status = TP_STATUS_KERNEL;
}

static void teardown_socket(struct ring *ring, int fd)
{
    munmap(ring->map, ring->req.tp_block_size * ring->req.tp_block_nr);
    free(ring->rd);
    close(fd);
}

int main(int argc, char **argp)
{
    int fd, err;
    socklen_t len;
    struct ring ring;
    struct pollfd pfd;
    unsigned int block_num = 0, blocks = 64;
    struct block_desc *pbd;
    struct tpacket_stats_v3 stats;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s INTERFACE\n", argp[0]);
        return EXIT_FAILURE;
    }

    signal(SIGINT, sighandler);

    memset(&ring, 0, sizeof(ring));
    fd = setup_socket(&ring, argp[argc - 1]);
    assert(fd > 0);

    memset(&pfd, 0, sizeof(pfd));
    pfd.fd = fd;
    pfd.events = POLLIN | POLLERR;
    pfd.revents = 0;

    while (likely(!sigint)) {
        pbd = (struct block_desc *) ring.rd[block_num].iov_base;

        if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
            poll(&pfd, 1, -1);
            continue;
        }

        walk_block(pbd, block_num);
        flush_block(pbd);
        block_num = (block_num + 1) % blocks;
    }

    len = sizeof(stats);
    err = getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
    if (err < 0) {
        perror("getsockopt");
        exit(1);
    }

    fflush(stdout);
    printf("\nReceived %u packets, %lu bytes, %u dropped, freeze_q_cnt: %u\n",
           stats.tp_packets, bytes_total, stats.tp_drops,
           stats.tp_freeze_q_cnt);

    teardown_socket(&ring, fd);
    return 0;
}

代碼說明:

  1. 使用的tpacket_req3 時候會有兩個以前沒有的變量賦值如下:
    ring->req.tp_retire_blk_tov = 60;
    ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

tp_retrie_blk_tov 即超時值,單位是毫秒。當這個超時被觸發時,內核會將該塊的狀態從 TP_STATUS_USER 改為 TP_STATUS_KERNEL,即超時觸發后,會將緩存區釋放給內核用來裝數據了。
tp_feature_req_word 即功能值,tp_feature_req_word 被設置為 TP_FT_REQ_FILL_RXHASH。這個標志請求內核在每個數據包的頭部填充接收哈希(RX hash)值。接收哈希通常用于負載均衡和流量分類等場景,它可以提供一種快速的方式來決定數據包應該如何被處理或路由。

  1. v3版本來說,數據包幀的最小大小,不能低于這個大小。
  2. mmap申請內核和用戶空間共享的內存,注意設置MAP_LOCKED標識的意思,這個內存是不可以交換到磁盤上的,會被鎖在物理內存中。
  3. 如果需要讓內核設置包的時間,可以通過下面代碼設置:
int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req));
  1. 轉換ip代碼:
struct sockaddr_in ss, sd;
char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

memset(&ss, 0, sizeof(ss));
ss.sin_family = PF_INET;
ss.sin_addr.s_addr = ip->saddr;
getnameinfo((struct sockaddr *) &ss, sizeof(ss),sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

這段代碼是將獲取的包的ip地址,其中:NI_NUMERICHOST: 直接返回主機的IP地址,而不是嘗試查找主機名。

  1. 主循環:
    while (likely(!sigint)) {
        pbd = (struct block_desc *) ring.rd[block_num].iov_base;

        if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
            poll(&pfd, 1, -1);
            continue;
        }

        walk_block(pbd, block_num);
        flush_block(pbd);
        block_num = (block_num + 1) % blocks;
    }

在沒有中斷的情況下,獲取block頭部信息,根據頭的block狀態,如果不是TP_STATUS_USER情況下,繼續進入poll等待。如果已經有數據了,則走walk循環,然后更改block狀態,block指針后移,做循環隊列,這個和原來v1版本的frame做循環。
代碼如下:

 // 調整block執行第一個packet包、下一個包是通過tp_next_offset 來指向的。   
 ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                   pbd->h1.offset_to_first_pkt);
    for (i = 0; i < num_pkts; ++i) {
        bytes += ppd->tp_snaplen;
        display(ppd);

        ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                       ppd->tp_next_offset);
    }

代碼里面數據結構示意圖如下:


image.png

三 SOCKET的fanout

雖然V3版本相當于前面的版本來說,性能有所提升,但是在現在的多核環境下,如果只簡單的用V3版本,仍然,不能達到捕獲超大流量數據包情況下而不丟包。

AF-PACKET支持多核抓包,從內核3.1版本開始,AF-PACKET 可以將多個不同進程或線程的socket加入到一個fanout組中,并行一同抓包。內核會利用一定的算法對packet進行分組進入不同socket中,從而讓包在多個線程或進程中進行處理,達到負載均衡的目的。

多socket收包

一個fanout組中最多可以支持65536個socket,通過setsockopt系統調用并指定PACKET_FANOUT選項來加入扇出組,當關閉一個組內socket的時候,就會從扇出組內扇出,如果組內的所有socket都退出了,組也會被內核銷毀。 這里面的fanout翻譯成扇出組,想想挺形象,像扇子一樣,扇子的頭是一個,打開后分散了,扇出組也是這個目的,上圖中NIC是有多隊列的,所以本身就是分散進入的數據。

扇出有不同的算法,說明如下:

PACKET_FANOUT_HASH: 默認的扇出算法,通過計算ip地址和可選的tcp端口的哈希函數,將包發到不同的socket,且能保持同一個流的packet發到同一個socket,便于后續線程繼續處理。
PACKET_FANOUT_LB: 簡單的采用輪訓的負載均衡算法,但是不能保證同一個流發送到同一個socket。
PACKET_FANOUT_RND :模式通過使用偽隨機數生成器來選擇目標套接字。同樣,這種模式只允許進行無狀態處理。
PACKET_FANOUT_CPU :模式根據接收到數據包的 CPU 來選擇數據包套接字。
PACKET_FANOUT_ROLLOVER: 模式持續將所有數據發送到一個套接字,直到該套接字出現擁塞。然后,它轉移到組內的下一個套接字,直到該套接字也耗盡,依此類推。
PACKET_FANOUT_QM : 模式選擇與接收到數據包的硬件隊列編號相匹配的數據包套接字。即RSS隊列和socket對應,這種默認如果設置了RSS的均衡算法為對稱哈希算法,則也能保證一個流的所有packet發送到同一個socket中去。Linux 3.14以后可用,以前版本不支持。

看下內核測試代碼里面的例子如下:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>

static const char *device_name;
static int fanout_type;
static int fanout_id;

#ifndef PACKET_FANOUT
# define PACKET_FANOUT          18
# define PACKET_FANOUT_HASH     0
# define PACKET_FANOUT_LB       1
#endif

static int setup_socket(void)
{
    int err, fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
    struct sockaddr_ll ll;
    struct ifreq ifr;
    int fanout_arg;

    if (fd < 0) {
        perror("socket");
        return EXIT_FAILURE;
    }
    memset(&ifr, 0, sizeof(ifr));
    strcpy(ifr.ifr_name, device_name);
    err = ioctl(fd, SIOCGIFINDEX, &ifr);
    if (err < 0) {
        perror("SIOCGIFINDEX");
        return EXIT_FAILURE;
    }
    memset(&ll, 0, sizeof(ll));
    ll.sll_family = AF_PACKET;
    ll.sll_ifindex = ifr.ifr_ifindex;
    err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
    if (err < 0) {
        perror("bind");
        return EXIT_FAILURE;
    }
    fanout_arg = (fanout_id | (fanout_type << 16));
    err = setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
             &fanout_arg, sizeof(fanout_arg));
    if (err) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }
    return fd;
}

static void fanout_thread(void)
{
    int fd = setup_socket();
    int limit = 10000;

    if (fd < 0)
        exit(fd);
    while (limit-- > 0) {
        char buf[1600];
        int err;
        err = read(fd, buf, sizeof(buf));
        if (err < 0) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        if ((limit % 10) == 0)
            fprintf(stdout, "(%d) \n", getpid());
    }

    fprintf(stdout, "%d: Received 10000 packets\n", getpid());

    close(fd);
    exit(0);
}

int main(int argc, char **argp)
{
    int fd, err;
    int i;
    if (argc != 3) {
        fprintf(stderr, "Usage: %s INTERFACE {hash|lb}\n", argp[0]);
        return EXIT_FAILURE;
    }
    if (!strcmp(argp[2], "hash"))
        fanout_type = PACKET_FANOUT_HASH;
    else if (!strcmp(argp[2], "lb"))
        fanout_type = PACKET_FANOUT_LB;
    else {
        fprintf(stderr, "Unknown fanout type [%s]\n", argp[2]);
        exit(EXIT_FAILURE);
    }

    device_name = argp[1];
    fanout_id = getpid() & 0xffff;

    for (i = 0; i < 4; i++) {
        pid_t pid = fork();

        switch (pid) {
        case 0:
            fanout_thread();
        case -1:
            perror("fork");
            exit(EXIT_FAILURE);
        }
    }
    for (i = 0; i < 4; i++) {
        int status;
        wait(&status);
    }
    return 0;
}

代碼采用簡單的方式演示了fanout代碼,關鍵代碼在于:

// fanout_arg 高16位存的是fanout的類型,低16位存的是fanout的id,即是fanout的組id。
fanout_arg = (fanout_id | (fanout_type << 16));
err = setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
             &fanout_arg, sizeof(fanout_arg));

四 參考

https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容