一 前言
上一篇介紹了通過AF-PACKET的V1 版本進行網絡包的捕獲,比較新的Linux內核是支持V3版本的,相對于前兩個版本(V2和V1比較相似,V2版本的時間精度從微秒提升到納秒。)V3版本,具有以下的提升:
- CPU使用率降低約15-20%
- 數據包捕獲率提高約20%
- 數據包的密度提升2倍(不知道什么意思, 如 ~2x increase in packet density)
- 端口聚合分析
- 非靜態數據幀大小,可以保存整個數據包。
所以這次就學習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;
}
代碼說明:
- 使用的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)值。接收哈希通常用于負載均衡和流量分類等場景,它可以提供一種快速的方式來決定數據包應該如何被處理或路由。
- v3版本來說,數據包幀的最小大小,不能低于這個大小。
- mmap申請內核和用戶空間共享的內存,注意設置MAP_LOCKED標識的意思,這個內存是不可以交換到磁盤上的,會被鎖在物理內存中。
- 如果需要讓內核設置包的時間,可以通過下面代碼設置:
int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req));
- 轉換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地址,而不是嘗試查找主機名。
- 主循環:
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);
}
代碼里面數據結構示意圖如下:
三 SOCKET的fanout
雖然V3版本相當于前面的版本來說,性能有所提升,但是在現在的多核環境下,如果只簡單的用V3版本,仍然,不能達到捕獲超大流量數據包情況下而不丟包。
AF-PACKET支持多核抓包,從內核3.1版本開始,AF-PACKET 可以將多個不同進程或線程的socket加入到一個fanout組中,并行一同抓包。內核會利用一定的算法對packet進行分組進入不同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