套接口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

PF_PACKET环形接收缓存相关推荐

  1. linux设置TCP接收缓存,TCP缓存设置及自调节

    工作的原因,同事在单条流的性能测试中出现性能值低的问题,最后的问题点确认为缓存设置不合理.为什么要设置缓存?如何设置缓存? 缓存和带宽时延积 读缓存的上限应该由TCP接收窗口的最大值确定,过大或过小的 ...

  2. Zynq 【SDK裸机开发之PS】——串口接收缓存

    最近项目上在使用Zynq开发,也是第一次使用,期间会遇到各种各样的问题,属于Zynq本身问题的我会更新到我的另一篇博客<Zynq开发调试踩坑指南>中,这个版块将会陆续记录我自身在程序开发中 ...

  3. c++tcp接收文件缓存多大合适_网易面经:深剖TCP协议的流量控制和拥塞控制,你懂了吗?...

    1.自我介绍+项目 2.RPC框架和普通http有什么区别和优势? 基于Tcp封装还是http封装的 3.rpc是长连接吗?如果要传输一个特别大的文件 底层还是基于流吗? Nio是一个什么IO模型? ...

  4. c++tcp接收文件缓存多大合适_linux高性能网络编程之tcp连接的内存使用

    当服务器的并发TCP连接数以十万计时,我们就会对一个TCP连接在操作系统内核上消耗的内存多少感兴趣.socket编程方法提供了SO_SNDBUF.SO_RCVBUF这样的接口来设置连接的读写缓存,li ...

  5. Linux网络-数据包的接收流程(基于RTL8139网卡驱动程序)

    本文将介绍Linux系统中,基于RTL8139网卡驱动程序,是如何一步一步将接收到的数据包传送到内核的网络协议栈的. 下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理: +-- ...

  6. 一个严谨的STM32串口DMA发送接收(1.5Mbps波特率)机制

    文章目录 1 前言 2 串口有必要使用DMA吗 3 实现方式 4 STM32串口使用DMA 5 串口DMA接收 5.1 基本流程 5.2 相关配置 5.3 接收处理 5.3 .1 接收数据大小 5.3 ...

  7. 32 ART DMA 接收未知长度的数据和发送

    STM32实现USART+DMA接收未知长度的数据和发送 STM32学习笔记三 竹天笑 前言:开始学USART+DMA的时候看到帖子<STM32 UART DMA实现未知数据长度接收>,觉 ...

  8. 04-1-数据处理思想和程序架构: 关于环形队列

    资料源码:https://gitee.com/yang456/OpenProgrammingModuleForMCU.git 点击加入群聊[单片机,物联网,上位机]: 说明1:知识从未如此性感. 烂程 ...

  9. 【计算机网络】数据链路层 : 选择重传协议 SR ( 帧分类 | “发送方“ 确认帧、超时事件 | “接受方“ 接收帧机制 | 滑动窗口长度 | 计算示例 )★

    文章目录 一. 选择重传协议 ( SR ) 引入 二. 选择重传协议 ( SR ) 帧分类 三. 发送方 事件 ( 确认帧.超时事件 ) 四. 接收方 事件 ( 接收帧 ) 五. 滑动窗口长度 五. ...

最新文章

  1. onchange事件只生效一次的问题
  2. 关于libnmap 的一些应用
  3. Cocos游戏引擎3D特效全新升级 更流畅更炫酷
  4. 怎么调整磁盘分区的大小
  5. 拼接符 防注入正则校验_Apache Kylin 命令注入漏洞调试分析(CVE-2020-1956)
  6. Qt工作笔记-在ListWidget中多线程检索数据
  7. Redis主从、哨兵模式的搭建
  8. java main方法static_在java中为什么要把main方法定义为一个static方法?
  9. xen虚拟化部署遇到的问题(持续更新)
  10. .[算法]图论专题之最短路径
  11. SQL注入盲注——布尔注入
  12. 汽车传感器:车载雷达与摄像头
  13. 里诺全系列注册机+暗桩patch
  14. win2003系统的序列号
  15. mysql cmake ncursor_在移植的过程中主要会用到boost库、cmake工具以及wt库的编译。...
  16. VB中On Error Resume Next 什么意思,一般在什么情况下使用
  17. mysql pid无法写入_ERROR /usr/libexec/mysqld:写入文件'/var/run/mysqld/mysqld.pid'时出错(错误代码:28)...
  18. 剑灵32位登录服务器维护,(32位系统可稳定运行剑灵)剑灵客户端发生错误的解决办法...
  19. Android 源码 PackageManagerService 启动流程分析
  20. Ubuntu ROS Arduino Gazebo学习镜像iso说明(indigo版)

热门文章

  1. 微信小程序导入其他字体会不会影响运行_微信小程序是否可以引用特殊字体
  2. 这个教人怎么赚钱的社群,第7年了
  3. 无人机航测案例-湘南某县增减挂钩拆旧地块航拍任务
  4. 如何布置环境?自己这边打版了,后台怎么打版才能创建活动?
  5. 绿氢、蓝氢、灰氢,原来氢也可以这么出彩
  6. 搜索引擎(三)-- PageRank和HITS算法
  7. python爬虫王者荣耀高清皮肤大图背景故事通用爬虫
  8. ASP标准MD5加密签名函数代码
  9. 【pyecharts数据可视化】python爬取去哪儿网景点数据,做交互式数据可视化
  10. 汽车销售管理系统 c语言版 课程设计,汽车销售管理系统C语言版.doc