深入解析MMAP模式下的原始套接字

深入解析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 tpacketdata,其中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];
    };
    
  • 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方式收包的原理基本上就讲完了。

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Linux下,mmap函数可以用来将文件或设备的一部分物理内存映射到进程的虚拟地址空间中,从而实现进程和文件或设备的直交互。使用mmap函数可以提高文件或设备的读写效率,避免了频繁的系统调用和缓冲区的拷贝。 mmap函数的原型为: ```c void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); ``` 各个参数的含义如下: - addr:映射区域的首地址,一般设为NULL,由系统自动分配。 - length:映射区域的长度,单位是节。 - prot:映射区域的保护模式,可以是PROT_READ、PROT_WRITE或PROT_EXEC的组合。 - flags:映射区域的标志,可以是MAP_SHARED、MAP_PRIVATE、MAP_FIXED等的组合。 - fd:需要映射的文件描述符。 - offset:文件偏移量,表示从文件的哪个位置开始映射。 mmap函数返回映射区域的首地址或者MAP_FAILED,表示映射失败。 使用mmap函数时,需要先打开文件或设备,并获取相应的文件描述符。然后,调用mmap函数将文件或设备的一部分物理内存映射到进程的虚拟地址空间中。最后,使用指针来访问映射区域的数据,进行读写操作。使用完映射区域后,需要调用munmap函数解除映射关系。 需要注意的是,使用mmap函数进行读写操作时,需要考虑到内存对齐和边界问题,否则可能会出现读写错误。同时,对于设备文件的映射,还需要考虑到设备驱动程序的特殊要求,比如缓冲区的大小和对齐方式等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值