PF_PACKET环形接收缓存

套接口PF_PACKET目前有两种工作模式,以(SOCK_PACKET)类别运行的模式;和以(SOCK_DGRAM/SOCK_RAW)类别运行的模式。前者为传统的方式,在内核和用户层拷贝数据包,并且兼容老内核的数据包抓取接口(参考以下介绍);后者为前者的替代类型,而且可以通过设置共享内存的方式,在内核与用户层交换数据,节省内存拷贝的消耗。以下内容主要介绍后一种模式的共享内存方式。


PACKET套接口创建

内核函数packet_create处理PF_PACKET套接口的创建工作。其参数sock->type决定了采用哪一种工作模式,如果参数type为SOCK_PACKET即第一种模式,type为SOCK_DGRAM或者SOCK_RAW即为第二种模式。两种模式内核会赋予不同的操作函数集合和数据包接收函数,例如后者使用packet_ops函数集,而前者使用packet_ops_spkt函数集。接收函数一个为packet_rcv,一个为packet_rcv_spkt函数。

    sock->ops = &packet_ops;
    if (sock->type == SOCK_PACKET)
        sock->ops = &packet_ops_spkt;
		
    po->prot_hook.func = packet_rcv;
    if (sock->type == SOCK_PACKET)
        po->prot_hook.func = packet_rcv_spkt;

对应的用户态socket系统调用如下,前两个为第一种工作模式。第一个socket系统调用的domain和type组合内核已经废弃,但是为了向后兼容,内核检测到之后,将首个参数PF_INET替换为PF_PACKET,这两个socket系统调用其实完全一致。
    socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL)) :
    socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_ALL));

后一种模式有如下两种系统调用,SOCK_DGRAM套接口在往用户层上送数据包时,会剥掉物理头部数据(MAC header),而SOCK_RAW套接口上送完整的数据包。
    socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) :
    socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

SOCK_PACKET与SOCK_RAW工作模式相近,都是与用户层交互完整的数据包(包括物理头部数据),区别在于前者使用结构sockaddr_pkt表示套接口地址,后者使用结构sockaddr_ll表示套接口地址。


套接口接收选项

类型为SOCK_DGRAM/SOCK_RAW的PF_PACKET套接口,除了普通的在内核与用户层间拷贝数据包的方式外,还可通过setsockopt系统调用设置环形接收buffer,通过mmap与应用层共享这部分内存。这样就可省去拷贝操作,但是数据包的套接口地址信息就不能通过recvfrom/recvmsg调用送到用户层,内核需将这部分信息和数据包拼接在一起,另外,数据包的一些信息如时间戳、VLAN等和环形buffer管理信息也需要在内核与用户态交互,所以还需要一个结构,为此内核定义了TPACKET_HAEDER结构存储这些信息。如果通过setsockopt系统调用使能了PACKET_VNET_HDR选项,还有一个virtio_net_hdr结构,如下数据帧空间buffer中一个数据包相关的所有信息块如下:

目前TPACKET_HEADER有三个版本,每个版本的长度略有不同,用户层可使用setsockopt(PACKET_VERSION)设置需要的版本,另外也可通过getsockopt(PACKET_HDRLEN)获取到每个版本对应的头部长度,设置环形接收buffer需要此长度值。

    enum tpacket_versions {
        TPACKET_V1,
        TPACKET_V2,
        TPACKET_V3
    };

    int val = TPACKET_V3;
    setsockopt(sk, SOL_PACKET, PACKET_VERSION, &val, sizeof(val))
    getsockopt(sk, SOL_PACKET, PACKET_HDRLEN, &val, &len)


对于版本1和2,不论接收还是发送的环形buffer,需要配置4个参数:分别为内存块的大小和数量、每个数据包的大小和数据包总数。版本3暂不讨论。

struct tpacket_req {
    unsigned int    tp_block_size;  /* Minimal size of contiguous block */
    unsigned int    tp_block_nr;    /* Number of blocks */
    unsigned int    tp_frame_size;  /* Size of frame */
    unsigned int    tp_frame_nr;    /* Total number of frames */
};  

用户层通过setsockopt(PACKET_RX_RING/PACKET_TX_RING)设置环形buffer参数,内核函数packet_set_ring进行处理,并对这4个字段的合法性检查,来看一下其中的要求和关联。

1)内存块大小tp_block_size必须按照页面大小对其,即必须是页面大小的整数倍;每个内存块至少要能够容纳一个数据包;另外,tp_block_size的大小要求是页面大小的2的指数倍(2,4,8倍);
        if (unlikely((int)req->tp_block_size <= 0))   goto out;
        if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))   goto out;
        rb->frames_per_block = req->tp_block_size/req->tp_frame_size;
        if (unlikely(rb->frames_per_block <= 0))
            goto out;
        order = get_order(req->tp_block_size);

2)内存块数量tp_block_nr不能太大,所有内存块不能占用超过UINT_MAX大小的内存;

        if (unlikely(req->tp_block_size > UINT_MAX / req->tp_block_nr))  goto out;

3)数据包大小tp_frame_size必须是16字节(TPACKET_ALIGNMENT)对其;不能太小,必须大于TPACKET头部信息的长度;
        if (unlikely(req->tp_frame_size < po->tp_hdrlen + po->tp_reserve))    goto out;
        if (unlikely(req->tp_frame_size & (TPACKET_ALIGNMENT - 1)))    goto out;

4)内存块数量tp_block_nr乘以每个内存块容纳的数据帧数目,应该等于数据包的总数tp_frame_nr。

        if (unlikely((rb->frames_per_block * req->tp_block_nr) != req->tp_frame_nr))  goto out;

合法性检查通过后,内核根据tp_block_size和tp_block_nr分配相应的存储页面,并将相关信息保持在packet_sock套接口的成员rx_ring(packet_ring_buffer)结构体中。最后,更改数据包接收函数为tpacket_rcv,其处理环形buffer接收数据包功能。

    po->prot_hook.func = (po->rx_ring.pg_vec) ? tpacket_rcv : packet_rcv;
    register_prot_hook(sk);

            
用户层要访问内核的接收环形buffer,需要通过mmap将其映射到用户空间;

    mmapbuf = mmap(0, mmapbuflen, PROT_READ|PROT_WRITE, MAP_SHARED, sk, 0);

 

接收数据帧

内核函数tpacket_rcv负责数据帧的接收工作。对于SOCK_DGRAM类型的套接口,当其接收到的是本机发出的数据帧的时候,跳过物理头部,将skb的data指针指到网络头。对于SOCK_RAW而言,需要将物理头部上送用户层,将skb的data指针外推到MAC头部。data为上送到用户层的数据的起始位置。

    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));
    }

接下来需要确定新接收到的数据帧应当放入共享环形buffer的哪个位置?由函数packet_lookup_frame计算得到。参数position为保存在环形buffer中的可用帧空间的头索引(rx_ring.head),根据此索引,计算得到页面索引(内存块索引)和帧偏移,即得到可用来保存数据帧的地址(h.raw)。

static void *packet_lookup_frame(struct packet_sock *po, ...)
{
    pg_vec_pos = position / rb->frames_per_block;
    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;
}

函数packet_increment_head用来增加可用帧空间头索引head,对于我们的环形buffer,在头索引head到达最大值后,从0开始下一次循环。

    buff->head = buff->head != buff->frame_max ? buff->head+1 : 0;

接下来就可以拷贝数据包到找到的帧空间了(skb_copy_bits),拷贝snaplen长度的数据到帧空间中macoff偏移开始的空间。macoff之前的空间还要保存两个类型的结构体,分别是tpacket_hdr(根据TPACKET_VERSION选择不同版本的头部结构)和sockaddr_ll结构体,依次填充这两项信息。至此数据帧接收完成。

    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);

最后,关注一下内核与用户层在操作环形buffer时的同步实现,参见tpacket_hdr字段中的tp_status字段,此字段的第一个bit位来实现功能,当前为0时(TP_STATUS_KERNEL)标识内核在使用此段数据帧空间,反之,为1时(TP_STATUS_USER)标识用户层面在使用此段空间。前面介绍的内核使用packet_lookup_frame函数查找可用的数据帧空间,找到之后使用函数__packet_get_status来判断一下此段空间是否可用,tp_status等于TP_STATUS_KERNEL可正常使用,否则,说明用户层还没有处理此段空间内的数据帧,通常在环形buffer已满的情况下出现。

内核在填充完数据帧空间之后,将tp_status的同步位设置为TP_STATUS_USER,同时调用sk->sk_data_ready(sk)通知用户层数据已准备好。

 

内核版本

Linux-4.15

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值