Linux数据包的接收过程
- https://segmentfault.com/a/1190000008836467
对于该文章的批注及摘要如下:
1 网卡接收数据包->DMA写入内存->驱动程序转换为skb
- 大致流程就是网卡获取数据包,然后通过DMA写入内存,然后驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式
2 对数据包进行处理
-
驱动程序将调用
napi_gro_receive
函数。内核的网络模块中,第11条提到“napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈”,这个地方提到的GRO计数和TSO计数相对。一台主机的网卡如果不支持TSO技术,那么TCP数据包会限制长度使得IP层不会发生数据分片,而如果支持TSO技术,则IP数据包可以分片。TSO技术可以使得网卡直接对大的运输层数据包进行IP分片,然后发送出去。而GRO技术是接收端的技术,是对于这种分片的数据包进行整合,重整为一个数据包,避免同一个数据包的多个分片都进入一遍协议栈,造成资源浪费。GRO这个地方涉及的关键函数是napi_gro_receive
:gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb) { skb_mark_napi_id(skb, napi); trace_napi_gro_receive_entry(skb); skb_gro_reset_offset(skb); return napi_skb_finish(dev_gro_receive(napi, skb), skb); }
-
接下来
napi_gro_receive
会直接调用__netif_receive_skb_core
。__netif_receive_skb_core
会看是不是有AF_PACKET
类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。然后,会进入内核协议栈。
3 进入协议栈
IP层
- ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
UDP层
- udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
- sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
- __skb_queue_tail: 将数据包放入socket接收队列的末尾
- sk_data_ready: 通知socket数据包已经准备好。调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。
|
|
↓
+---------+ +-----------------------+
| udp_rcv |----------->| __udp4_lib_lookup_skb |
+---------+ +-----------------------+
|
|
↓
+--------------------+ +-----------+
| sock_queue_rcv_skb |----->| sk_filter |
+--------------------+ +-----------+
|
|
↓
+------------------+
| __skb_queue_tail |
+------------------+
|
|
↓
+---------------+
| sk_data_ready |
+---------------+
Linux数据包的发送过程
- https://segmentfault.com/a/1190000008926093?utm_source=sf-similar-article
抓包原理
目前的理解(未手动验证)
BPF_PROG_TYPE_SOCKET_FILTER可能不止一个埋点函数,和创建的raw socket类型有关系。
-
如果抓的是进入协议栈之后的IP数据报,可能走的是
raw_rcv -> raw_rcv_skb -> sock_queue_rcv_skb
执行路径,这个时候sk_filter
就是BPF_PROG_TYPE_SOCKET_FILTER
埋点函数:int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) { int err; err = sk_filter(sk, skb); if (err) return err; return __sock_queue_rcv_skb(sk, skb); }
-
如果抓的是未在协议栈之内的数据包,就是下面讲的这种情况:
-
通过socket系统调用创建一个raw socket,这种socket和datagram socket以及stream socket不同,datagram socket一般用于获取UDP数据,stream socket用于获取TCP数据,此时经过协议栈处理,只包含数据负载。而raw socket是没有经过协议栈处理的,可以获得网卡上的完整数据包。
-
socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
通过这种方式创建socket时,会在内核创建一个packet_type(struct packet_type),并挂载到全局的ptype_all链表上(同时在packet_type设置回调函数packet_rcv)。这相当于是创建了一个虚拟协议,在内核处理数据包时会先匹配协议,并把数据包交给对应的处理逻辑。 -
网络收包/发包时,会在各自的处理函数(收包时:
__netif_receive_skb_core
,发包时:dev_queue_xmit_nit
)中遍历ptype_all链表,并同时执行其回调函数,tcpdump的注册的回调函数就是packet_rcv
(所有协议都是这样被匹配然后递交到对应协议栈逻辑处理的,比如有一些后续就是raw_rcv()
、ip_rcv()
、arp_rcv()
来负责处理)。比如下面的代码:list_for_each_entry_rcu(ptype, &ptype_all, list) { //遍历ptype_all链表注册的所有协议 if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); //拷贝一份数据包 pt_prev = ptype; }
-
packet_rcv
函数中调用了run_filter
函数,会执行用户挂载(SOCKET_FILTER)的eBPF程序,对于数据包做相应的处理。
-
-
参考https://baike.baidu.com/item/RAW%20SOCKET/995623?fr=aladdin对于raw socket类型的解释