深入解析MMAP模式下的原始套接字
一、前言
对于原始套接字大家都不陌生,即PF_PACKET
类型的套接字,我们平时使用的抓包程序就是基于这种套接字实现的。下图中就是我们平时使用到的套接字的分类,可以看到原始套接字是一种底层的链路层的套接字,它直接接收网卡驱动收上来的包。这里要做一下概念上的区分,PF_INET
中的SOCK_RAW
一般情况下也被称为原始套接字,只不过这种套接字是工作在IP层的,即网络模型中的三层,并非我们今天关注的对象。
原始套接字可能会接收到或者发送出去大量的报文,因此效率对其是非常重要的,而报文在内核态与用户态之间的拷贝是很费资源的。为此,Linux
内核在很早就引入了原始套接字的MMAP
机制用于解决这一问题。
二、基本原理
简单来说,MMAP
方式的原始套接字的原理,就是用户申请了一块共享内存,内核在收到报文后就将很多个报文一起放到共享内存实现的一个RingBuff
里,然后通知用户程序,用户程序再从缓冲区中把报文取出来。这种方式的好处有两个:(1)减少了系统调用的次数;(2)避免了内核态和用户态的内存拷贝。我们知道,这两个环境都是比较费资源的。
2.1 常规socket
常规情况下,我们创建一个用于抓取网络报文的套接字的步骤为:首先使用socket
系统调用创建一个PF_PACKET
类型的原始套接字。
int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
如果我们是要抓取所有网口上的报文,那么这个套接口就可以直接使用了,我们可以创建一个缓冲区,并使用read
系统调用进行报文数据的读取:
unsigned char buf[2048] = {}
read(fd, buf, sizeof(buf));
2.2 MMAP模式的socket
如果是使用共享内存的方式,那么步骤要稍微繁琐一些了。套接字创建的过程与上面相同,额外我们还要分配一块共享内存作为环形缓冲区。下面的代码用于接收缓冲区的分配,其中req
用于指定缓冲区的一些信息。
struct struct tpacket_req req = {
.tp_block_size= 4096
.tp_frame_size= 2048
.tp_block_nr = 4
.tp_frame_nr = 8
};
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *) &req, sizeof(req));
这个创建的缓冲区并不是一个传统的一块内存,而是有着固定的结构,因为这块内存会用于内核和用户程序之间的交互,两者需要配合使用。tp_block_size
用于指定要分配的内存块的尺寸,这里设置为4096,即1页;tp_block_nr
代表要分配的块的数量,即要分配多少个页,这里分配了4个页。这里的内存分配是以块大小作为单位的,每个块都是连续的物理内存,因此块大小最好要是页的整数倍。frame
用于设置帧的大小,可以理解为单个报文缓冲区的大小,这里设置为2048,因为单个以太网报文一般不会超过1500字节。注意:块的大小要是帧的整数倍,即帧不能大于块。tp_frame_nr
代表帧的数量,可以看出这是个多余的参数,因为可以根据前面的参数算出来。
通过上面的设置,接收的RingBuff
就分配出来了,但是还不能直接使用,还要将其映射到用户态才行:
unsigned cahr *rx_ring = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
2.3 帧格式
每个帧都由两部分构成:struct tpacket
和data
,其中tpacket
代表着帧的头部,存储着当前帧的信息,包括状态等;data
存储着报文的数据。目前内核中有三个版本的原始套接字,每个版本在帧格式以及数据交互上都有一些出入,首先我们来看一下V1版本帧头的定义:
struct tpacket_hdr {
unsigned long tp_status; //当前帧的状态
unsigned int tp_len; //报文长度
unsigned int tp_snaplen; //接收到的报文的长度
unsigned short tp_mac; //以太网报文头部在当前帧的偏移量
unsigned short tp_net; //报文网络层(三层,IP)的偏移量
unsigned int tp_sec;
unsigned int tp_usec;
};
首先说一下帧的状态,对于发送和接收缓冲区,其状态是不一样的,这里以接收缓冲区为例讲解一下其常见的几种状态。
TP_STATUS_KERNEL
:帧的状态,代表着当前帧是空闲的,里面没有数据,内核可以将报文数据放到里面;TP_STATUS_USER
:当前帧里面是有报文数据的,用户可以将其取走,记得取走之后将状态重置为TP_STATUS_KERNEL
,好让内核继续往里放数据;TP_STATUS_COPY
:代表由于报文比帧要大,当前报文数据被截断了;TP_STATUS_CSUM_VALID
:这个报文的校验码已经检查通过了;- […]
默认版本是V1,各个版本之间的差异主要包括:
-
V1->V2
:相对来说,V1与V2之间版本差异不算大,主要差异表现在:- 支持64位内核使用32位用户态程序。由于在V1版本中的一些结构体中使用了
unsigned long
,导致如果内核是64位,而用户态程序是32位,那么会产生数据交互异常; - V2版本的帧的头部存储了
VLAN
信息;
V2版本的帧头如下:
struct tpacket2_hdr { __u32 tp_status; __u32 tp_len; __u32 tp_snaplen; __u16 tp_mac; __u16 tp_net; __u32 tp_sec; __u32 tp_nsec; __u16 tp_vlan_tci; __u16 tp_vlan_tpid; __u8 tp_padding[4]; };
- 支持64位内核使用32位用户态程序。由于在V1版本中的一些结构体中使用了
-
V2->V3
:V2与V3之间的差异还是有点大的,主要表现在:- 使用“柔性帧”,即每个块中不再指定帧的大小,而是根据报文的大小动态的设置帧大小。这种机制直接使得块布局发生了变化,增加了一个块“头”,里面存储了当前块的信息,包括帧的数量、第一个帧的偏移量等。帧结构中也引入了一个
tp_next_offset
成员,用于定位当前块中下一个帧的位置。通过这种方式,块的空间利用率得到了一定的提升。 - 鉴于上面的机制,V3版本的读操作都是以块作为单位来进行的。
V3版本的块头如下:
struct block_desc { uint32_t version; uint32_t offset_to_priv; struct tpacket_hdr_v1 h1; }; struct tpacket_hdr_v1 { __u32 block_status; //当前块的状态,与帧状态类似 __u32 num_pkts; //帧的数量 __u32 offset_to_first_pkt; //第一个帧的位置 __u32 blk_len; //块中有效数据长度 __aligned_u64 seq_num; struct tpacket_bd_ts ts_first_pkt, ts_last_pkt; };
V3版本的帧头格式如下:
struct tpacket3_hdr { __u32 tp_next_offset; //下一个帧的位置 __u32 tp_sec; __u32 tp_nsec; __u32 tp_snaplen; __u32 tp_len; __u32 tp_status; __u16 tp_mac; __u16 tp_net; /* pkt_hdr variants */ union { struct tpacket_hdr_variant1 hv1; }; __u8 tp_padding[8]; };
- 使用“柔性帧”,即每个块中不再指定帧的大小,而是根据报文的大小动态的设置帧大小。这种机制直接使得块布局发生了变化,增加了一个块“头”,里面存储了当前块的信息,包括帧的数量、第一个帧的偏移量等。帧结构中也引入了一个
2.4 报文接收示例
下面为内核文档里给的官方示例,示例的版本是V3,我们就以这个示例为例来讲一下原始套接字MMAP
下的收包过程吧。这个示例中,函数的调用过程为:main()->setup_socket()->walk_block()->display()
,其中walk_block
用于遍历ringBuf
中所有的块和帧,display
用于将帧中的信息提取出来,包括报文数据。
#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);
}
/* 设置PACKET版本,有v1、v2和v3三个版本,默认是v1 */
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;
/* 创建ringBuf */
err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &ring->req,
sizeof(ring->req));
if (err < 0) {
perror("setsockopt");
exit(1);
}
/* 将ringBuf映射到用户态 */
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);
}
/* 使用iovec向量的方式来访问缓冲区,为每个块创建一个向量,存储到ring->rd中 */
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;
/* 将这个原始套接字绑定到某个网口(netdev) */
err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
if (err < 0) {
perror("bind");
exit(1);
}
return fd;
}
/* 显示报文数据 */
static void display(struct tpacket3_hdr *ppd)
{
/* 帧头部的地址加上MAC偏移量,就是以太网报文的地址 */
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;
/* 将源IP地址转换成主机名字 */
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);
/* 初始化poll参数 */
memset(&pfd, 0, sizeof(pfd));
pfd.fd = fd;
pfd.events = POLLIN | POLLERR;
pfd.revents = 0;
/* 进入poll的循环收包环节 */
while (likely(!sigint)) {
pbd = (struct block_desc *) ring.rd[block_num].iov_base;
/* 检查当前块头的状态,判断是否有数据,没有的话就进行poll */
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;
}
三、内核实现
讲到这里,大家对原始套接字MMAP
方式的机制应该了解的差不多了,下面我们还是以收包为例简单看一下内核这一块的代码流程。针对与原始套接字是否开启了ringBuf
模式,报文处理函数依次为tpacket_rcv()
和packet_rcv()
,所以这里我们就讲解以下tpacket_rcv()
这个函数。
packet_ring_buffer
在讲解实现之前,我们先来看一下内核中用来管理ringBuf
的结构体struct packet_ring_buffer
。
struct packet_ring_buffer {
struct pgv *pg_vec; //一个指针数组,其中存储了当前ringBuf所有的缓存块
unsigned int head; //当前可用的帧的位置
unsigned int frames_per_block; //每个块中帧的数量
unsigned int frame_size; //帧尺寸
unsigned int frame_max; //帧最大值
unsigned int pg_vec_order; //块的阶数
unsigned int pg_vec_pages; //多少个页
unsigned int pg_vec_len; //块的数量
unsigned int __percpu *pending_refcnt;
union {
unsigned long *rx_owner_map;
struct tpacket_kbdq_core prb_bdqc;
};
};
对于V1和V2版本,从当前ringBuf
中获取下一个可用帧的函数为packet_lookup_frame()
,该函数实现比较简单:
static void *packet_lookup_frame(const struct packet_sock *po,
const struct packet_ring_buffer *rb,
unsigned int position,
int status)
{
unsigned int pg_vec_pos, frame_offset;
union tpacket_uhdr h;
pg_vec_pos = position / rb->frames_per_block; //根据position(即head)算出可用帧所在块
frame_offset = position % rb->frames_per_block; //计算可用帧是块中的第几个帧
//计算出可用帧的指针
h.raw = rb->pg_vec[pg_vec_pos].buffer +
(frame_offset * rb->frame_size);
//检查状态
if (status != __packet_get_status(po, h.raw))
return NULL;
return h.raw;
}
对于V3版本,实现就比较复杂了,因为其是以块为单位进行操作的,如何在将多个报文放入到同一个块的时候保证其状态与用户进程的一致性将会是个问题。为此,内核引入了struct tpacket_kbdq_core
结构体,用于描述所有的块的状态:
struct tpacket_kbdq_core {
struct pgv *pkbdq; //一个指针数组,存储了所有的内存块的描述符
unsigned int feature_req_word;
unsigned int hdrlen;
unsigned char reset_pending_on_curr_blk; //队列是否被冻结
unsigned char delete_blk_timer;
unsigned short kactive_blk_num; //当前活动的块的索引
unsigned short blk_sizeof_priv; //块中私有数据长度,存储数据时会跳过这么长
/* 上一个活动的块,即当前已经就绪、等待用户拷贝的块 */
unsigned short last_kactive_blk_num;
char *pkblk_start; //当前块的开始地址
char *pkblk_end; //当前块的结束地址
int kblk_size; //块尺寸
unsigned int max_frame_len; //最大帧的长度
unsigned int knum_blocks; //块的数量
uint64_t knxt_seq_num; //下一个要使用的序列号
char *prev; //当前块中上一个帧的地址
char *nxt_offset; //当前块中下一个可用的内存的地址
struct sk_buff *skb;
rwlock_t blk_fill_in_prog_lock;
/* Default is set to 8ms */
#define DEFAULT_PRB_RETIRE_TOV (8)
unsigned short retire_blk_tov;
unsigned short version;
unsigned long tov_in_jiffies; //定时器超时时间
/* timer to retire an outstanding block */
struct timer_list retire_blk_timer; //定时器,下面会讲到
};
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
struct sk_buff *skb,
unsigned int len
)
{
struct tpacket_kbdq_core *pkc;
struct tpacket_block_desc *pbd;
char *curr, *end;
//获取描述队列
pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
//获取当前块描述信息(头信息)
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
/* 队列被冻结了,用户空间可以进行该操作 */
if (prb_queue_frozen(pkc)) {
/*
* 当前块仍然没有被用户态释放,那么保持冻结状态
*/
if (prb_curr_blk_in_use(pbd)) {
/* Can't record this packet */
return NULL;
} else {
/*
* 当前块已经可用,那么打开当前块,解冻队列。打开内存块所做的操作会
* 将块描述符里面的信息进行初始化,包括块长度、序列号、帧数量等。
* 同时,还会打开当前队列。
*/
prb_open_block(pkc, pbd);
}
}
smp_mb();
//获取当前块中的下一个可用的地址
curr = pkc->nxt_offset;
pkc->skb = skb;
//当前块的结束地址
end = (char *)pbd + pkc->kblk_size;
/* 此时说明当前块的剩余空间可以容纳报文数据,返回这个块 */
if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) {
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
/* 这个块不够用,关闭它。关闭它意味着会将其状态设置为用户可读,
* 并会将当前队列的状态移动到下一个可用的块,然后通知当前socket
* 有数据到达。
*/
prb_retire_current_block(pkc, po, 0);
/* 获取当前可用的块,这个函数里面会调用prb_open_block() */
curr = (char *)prb_dispatch_next_block(pkc, po);
if (curr) {
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
/*
* No free blocks are available.user_space hasn't caught up yet.
* Queue was just frozen and now this packet will get dropped.
*/
return NULL;
}
tpacket_rcv
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
struct sock *sk;
struct packet_sock *po;
struct sockaddr_ll *sll;
union tpacket_uhdr h; //一个联合体,里面是V1、2、3的帧头指针
u8 *skb_head = skb->data; //报文数据
int skb_len = skb->len; //报文长度
unsigned int snaplen, res;
unsigned long status = TP_STATUS_USER;
unsigned short macoff, hdrlen;
unsigned int netoff;
struct sk_buff *copy_skb = NULL;
struct timespec64 ts;
__u32 ts_status;
bool is_drop_n_account = false;
unsigned int slot_id = 0;
bool do_vnet = false;
/* 各种数据对其检查.
*/
BUILD_BUG_ON(TPACKET_ALIGN(sizeof(*h.h2)) != 32);
BUILD_BUG_ON(TPACKET_ALIGN(sizeof(*h.h3)) != 48);
if (skb->pkt_type == PACKET_LOOPBACK)
goto drop;
sk = pt->af_packet_priv; //从packet_type上获取当前原始套接字
po = pkt_sk(sk); //转换为packet_sock类型
if (!net_eq(dev_net(dev), sock_net(sk)))
goto drop;
if (dev_has_header(dev)) {
if (sk->sk_type != SOCK_DGRAM)
skb_push(skb, skb->data - skb_mac_header(skb));
else if (skb->pkt_type == PACKET_OUTGOING) {
/* Special case: outgoing packets have ll header at head */
skb_pull(skb, skb_network_offset(skb));
}
}
snaplen = skb->len;
/* 运行当前套接字上的eBPF程序进行过滤 */
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
/* 没内存了,丢掉 */
if (__packet_rcv_has_room(po, skb) == ROOM_NONE) {
atomic_inc(&po->tp_drops);
goto drop_n_restore;
}
/* 根据校验码是否计算过设置对应的标志 */
if (skb->ip_summed == CHECKSUM_PARTIAL)
status |= TP_STATUS_CSUMNOTREADY;
else if (skb->pkt_type != PACKET_OUTGOING &&
(skb->ip_summed == CHECKSUM_COMPLETE ||
skb_csum_unnecessary(skb)))
status |= TP_STATUS_CSUM_VALID;
if (snaplen > res)
snaplen = res;
//计算对应的链路层和网络层报文偏移
if (sk->sk_type == SOCK_DGRAM) {
macoff = netoff = TPACKET_ALIGN(po->tp_hdrlen) + 16 +
po->tp_reserve;
} else {
unsigned int maclen = skb_network_offset(skb);
netoff = TPACKET_ALIGN(po->tp_hdrlen +
(maclen < 16 ? 16 : maclen)) +
po->tp_reserve;
if (po->has_vnet_hdr) {
netoff += sizeof(struct virtio_net_hdr);
do_vnet = true;
}
macoff = netoff - maclen;
}
if (netoff > USHRT_MAX) {
atomic_inc(&po->tp_drops);
goto drop_n_restore;
}
if (po->tp_version <= TPACKET_V2) {
//对于V1和V2版本,如果报文太大,比当前帧尺寸要大,放不下,需要对报文进行截断
if (macoff + snaplen > po->rx_ring.frame_size) {
if (po->copy_thresh &&
atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf) {
if (skb_shared(skb)) {
copy_skb = skb_clone(skb, GFP_ATOMIC);
} else {
copy_skb = skb_get(skb);
skb_head = skb->data;
}
if (copy_skb)
skb_set_owner_r(copy_skb, sk);
}
snaplen = po->rx_ring.frame_size - macoff;
if ((int)snaplen < 0) {
snaplen = 0;
do_vnet = false;
}
}
} else if (unlikely(macoff + snaplen >
GET_PBDQC_FROM_RB(&po->rx_ring)->max_frame_len)) {
u32 nval;
//V3版本,当前报文大于块所能容纳的,需要截断
nval = GET_PBDQC_FROM_RB(&po->rx_ring)->max_frame_len - macoff;
pr_err_once("tpacket_rcv: packet too big, clamped from %u to %u. macoff=%u\n",
snaplen, nval, macoff);
snaplen = nval;
if (unlikely((int)snaplen < 0)) {
snaplen = 0;
macoff = GET_PBDQC_FROM_RB(&po->rx_ring)->max_frame_len;
do_vnet = false;
}
}
//进行加锁
spin_lock(&sk->sk_receive_queue.lock);
//获取当前ringBuf可用的帧,即状态为TP_STATUS_KERNEL,长度为macoff+snaplen
h.raw = packet_current_rx_frame(po, skb,
TP_STATUS_KERNEL, (macoff+snaplen));
if (!h.raw)
goto drop_n_account;
if (po->tp_version <= TPACKET_V2) {
slot_id = po->rx_ring.head;
//这个一个采用bit位进行加锁的操作,如果对应的bit位被置位,
//说明当前帧被加锁了,正在被内核使用。并发收报是有可能出现这种情况的。
//因为当前的操作是在sk->sk_receive_queue.lock加锁保护中,因此
//这个检测不需要是原子的。
if (test_bit(slot_id, po->rx_ring.rx_owner_map))
goto drop_n_account;
//将对应的块所在的位置位,上锁
__set_bit(slot_id, po->rx_ring.rx_owner_map);
}
//enn...不知道vnet是干啥的,先不管
if (do_vnet &&
virtio_net_hdr_from_skb(skb, h.raw + macoff -
sizeof(struct virtio_net_hdr),
vio_le(), true, 0)) {
if (po->tp_version == TPACKET_V3)
prb_clear_blk_fill_status(&po->rx_ring);
goto drop_n_account;
}
if (po->tp_version <= TPACKET_V2) {
//对于V1和V2,移动队列head到下一个帧
packet_increment_rx_head(po, &po->rx_ring);
/*
* 如果之前存在丢包问题,将该状态传递上去
*/
if (atomic_read(&po->tp_drops))
status |= TP_STATUS_LOSING;
}
//收包统计+1
po->stats.stats1.tp_packets++;
/* 如果存在copy_skb,也就是报文被截断了,将该报文加到接收队列,并设置对应标志。
* 这个意思说的是,如果报文被截断了,那么将被截断的数据放到ringBuf,然后
* 对报文做一份拷贝放到传统的socket接收队列,这样socket就可以通过传统的方式
* 获取到完整的报文了。
*/
if (copy_skb) {
status |= TP_STATUS_COPY;
__skb_queue_tail(&sk->sk_receive_queue, copy_skb);
}
spin_unlock(&sk->sk_receive_queue.lock);
//将报文数据拷贝到对应的帧里面
skb_copy_bits(skb, 0, h.raw + macoff, snaplen);
if (!(ts_status = tpacket_get_timestamp(skb, &ts, po->tp_tstamp)))
ktime_get_real_ts64(&ts);
status |= ts_status;
//下面根据具体的版本,设置对应的帧头数据
switch (po->tp_version) {
case TPACKET_V1:
h.h1->tp_len = skb->len;
h.h1->tp_snaplen = snaplen;
h.h1->tp_mac = macoff;
h.h1->tp_net = netoff;
h.h1->tp_sec = ts.tv_sec;
h.h1->tp_usec = ts.tv_nsec / NSEC_PER_USEC;
hdrlen = sizeof(*h.h1);
break;
case TPACKET_V2:
h.h2->tp_len = skb->len;
h.h2->tp_snaplen = snaplen;
h.h2->tp_mac = macoff;
h.h2->tp_net = netoff;
h.h2->tp_sec = ts.tv_sec;
h.h2->tp_nsec = ts.tv_nsec;
if (skb_vlan_tag_present(skb)) {
h.h2->tp_vlan_tci = skb_vlan_tag_get(skb);
h.h2->tp_vlan_tpid = ntohs(skb->vlan_proto);
status |= TP_STATUS_VLAN_VALID | TP_STATUS_VLAN_TPID_VALID;
} else {
h.h2->tp_vlan_tci = 0;
h.h2->tp_vlan_tpid = 0;
}
memset(h.h2->tp_padding, 0, sizeof(h.h2->tp_padding));
hdrlen = sizeof(*h.h2);
break;
case TPACKET_V3:
/* tp_nxt_offset,vlan are already populated above.
* So DONT clear those fields here
*/
h.h3->tp_status |= status;
h.h3->tp_len = skb->len;
h.h3->tp_snaplen = snaplen;
h.h3->tp_mac = macoff;
h.h3->tp_net = netoff;
h.h3->tp_sec = ts.tv_sec;
h.h3->tp_nsec = ts.tv_nsec;
memset(h.h3->tp_padding, 0, sizeof(h.h3->tp_padding));
hdrlen = sizeof(*h.h3);
break;
default:
BUG();
}
//将对应的sockaddr_ll信息也放到帧数据中,在帧头的后面(16字节对齐了的)
sll = h.raw + TPACKET_ALIGN(hdrlen);
sll->sll_halen = dev_parse_header(skb, sll->sll_addr);
sll->sll_family = AF_PACKET;
sll->sll_hatype = dev->type;
sll->sll_protocol = skb->protocol;
sll->sll_pkttype = skb->pkt_type;
if (unlikely(po->origdev))
sll->sll_ifindex = orig_dev->ifindex;
else
sll->sll_ifindex = dev->ifindex;
smp_mb();
[...]
if (po->tp_version <= TPACKET_V2) {
//对于V1、V2版本,将状态设置到帧上,并清除对于的加锁标志位
spin_lock(&sk->sk_receive_queue.lock);
__packet_set_status(po, h.raw, status);
__clear_bit(slot_id, po->rx_ring.rx_owner_map);
spin_unlock(&sk->sk_receive_queue.lock);
//通知应用程序,数据已经就绪了。
sk->sk_data_ready(sk);
} else if (po->tp_version == TPACKET_V3) {
//V3版本的去锁操作
prb_clear_blk_fill_status(&po->rx_ring);
}
drop_n_restore:
if (skb_head != skb->data && skb_shared(skb)) {
skb->data = skb_head;
skb->len = skb_len;
}
drop:
if (!is_drop_n_account)
consume_skb(skb);
else
kfree_skb(skb);
return 0;
drop_n_account:
spin_unlock(&sk->sk_receive_queue.lock);
atomic_inc(&po->tp_drops);
is_drop_n_account = true;
sk->sk_data_ready(sk);
kfree_skb(copy_skb);
goto drop_n_restore;
}
定时器
从前面的代码我们基本上了解了收包过程中,以及ringBuf
是如何使用的。但是还有一个疑点,就是当我们将数据放到缓冲区中后,什么时候唤醒进程去读取里面的数据呢?很显然,V1和V2版本都会在最后通知应用程序数据已经就绪了。但是由于V3版本不是以报文作为单位,而是内存块,所以此时不能通知应用程序块就绪了,因为当前块可能还可以继续放报文。但是又不能等到块满了再通知应用程序,因为不知道下一个报文什么时候到来,可能要等很久。为解决这一问题,内核使用了定时器的方式,即为每个PACKET
套接字设置了一个定时器,定时器超时后就会将当前块设置为就绪状态,并通知应用程序。
V3版本在初始化过程中,会调用下面的函数初始化块描述队列上的定时器,可以看出超时处理函数为prb_retire_rx_blk_timer_expired
。
static void prb_setup_retire_blk_timer(struct packet_sock *po)
{
struct tpacket_kbdq_core *pkc;
pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
timer_setup(&pkc->retire_blk_timer, prb_retire_rx_blk_timer_expired,
0);
pkc->retire_blk_timer.expires = jiffies;
}
定时器刷新函数如下,可以看出超时时间为tov_in_jiffies
,在刷新定时器的时候会刷新上一个活动块。定时器的刷新一般发生在通知应用程序有数据到达之后,也就是说在关闭某个内存块的时候。
static void _prb_refresh_rx_retire_blk_timer(struct tpacket_kbdq_core *pkc)
{
mod_timer(&pkc->retire_blk_timer,
jiffies + pkc->tov_in_jiffies);
pkc->last_kactive_blk_num = pkc->kactive_blk_num;
}
下面我们来看一下定时器超时后会发生什么,想想就知道其实也没啥,无非就是关闭当前内存块:
static void prb_retire_rx_blk_timer_expired(struct timer_list *t)
{
struct packet_sock *po =
from_timer(po, t, rx_ring.prb_bdqc.retire_blk_timer);
struct tpacket_kbdq_core *pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
unsigned int frozen;
struct tpacket_block_desc *pbd;
spin_lock(&po->sk.sk_receive_queue.lock);
frozen = prb_queue_frozen(pkc);
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
/* 定时器被删除,可能是当前套接口没有报文了 */
if (unlikely(pkc->delete_blk_timer))
goto out;
/* 一种防止竟态条件的手段
*/
if (BLOCK_NUM_PKTS(pbd)) {
/* Waiting for skb_copy_bits to finish... */
write_lock(&pkc->blk_fill_in_prog_lock);
write_unlock(&pkc->blk_fill_in_prog_lock);
}
//意味着当前块还没通知应用程序数据就绪
if (pkc->last_kactive_blk_num == pkc->kactive_blk_num) {
/* 没有被冻结的情况 */
if (!frozen) {
if (!BLOCK_NUM_PKTS(pbd)) {
/* 当前块没有数据,刷新定时器。 */
goto refresh_timer;
}
/* 关闭当前块,并通知应用程序数据就绪。此时会将当前活动块移动到下一个。 */
prb_retire_current_block(pkc, po, TP_STATUS_BLK_TMO);
/* 检查新的块是否可用,可用的话就刷新定时器;否则,该函数会将队列锁定,
* 因为新的块不可用说明当前已经没有空间了。
*/
if (!prb_dispatch_next_block(pkc, po))
goto refresh_timer;
else
goto out;
} else {
/* 如果是因为没有空间了导致被冻结,直接刷新定时器 */
if (prb_curr_blk_in_use(pbd)) {
goto refresh_timer;
} else {
/* 当前队列被冻结,但是用户已经释放了当前块,因此可以使用它了。
* 这里会重新打开该内存块,打开的时候会刷新定时器。
*/
prb_open_block(pkc, pbd);
goto out;
}
}
}
refresh_timer:
//刷新定时器
_prb_refresh_rx_retire_blk_timer(pkc);
out:
spin_unlock(&po->sk.sk_receive_queue.lock);
}
到此,内原始核套接口ringBuf
方式收包的原理基本上就讲完了。