原帖地址:
https://blog.csdn.net/yedushu/article/details/52588412?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%BD%91%E7%BB%9C%E6%8A%A5%E6%96%87%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-4-52588412
0、需要额外学习内容:
skb的结构
中断机制(硬件终端、软中断、上下部等)
netfilter
1、链路层接收分组过程
1.1 图解
1.2 第一步:产生中断,处理中断
当网卡在收到一个与自己的MAC地址匹配或者广播的以太网帧时,会产生一个硬件中断,随后网卡驱动处理这个中断。
- 从DMA/PIO或其他得到分组数据,写到内存中去。
DMA:(DirectMemoryAccess,直接内存存取),顾名思义DMA功能就是让设备可以绕过处理器,直接由内存来读取资料。打开硬盘的DMA模式将大幅度的提高硬盘系统的功能,使我们能更快更好的进行视频处理和文件传输
PIO:(Programming Input/Output Model)PIO模式是一种通过CPU执行I/O端口指令来进行数据的读写的数据交换模式。数据传输速率低下,CPU占有率也很高,大量传输数据时会因为占用过多的CPU资源而导致系统停顿,无法进行其它的操作。 - 随后分配一个新的套接字缓冲区skb,并调用一个与协议无关,网络设备均支持的通用网络接收处理函数 netif_rx(skb)。netif_rx()函数让内核进一步处理skb。
skb:struct sk_buffer 的简写。是 linux TCP/IP stack 中,用于管理Data Buffer的结构。Sk_buffer 在数据包的发送和接收中起着重要的作用。 - 随后skb进入到达队列等待CPU处理。对于多核CPU,每个CPU维护一个队列。如果此时FIFO(先进先出)队列已满,就丢弃此分组。skb排队后,调用_cpu_raise_softirq()函数标记NET_RX_SOFTIRQ网络接收软中断,等待CPU处理。
- 至此,netif_rx()调用完成,返回调用者状况信息(成功或失败)。此时,这个硬件中断完成任务,CPU出现软中断,数据分组等待被上层协议栈处理。
1.3 第二步:NET_RX_SOFTIRQ网络接收软中断
这一阶段根据协议(IP、ARP)的不同来处理数据分组。
- skb进入CPU到达队列后,产生网络接收软中断,CPU调用**do_softirq()**开始处理这个软件中断。
- 接着调用net_rx_action()处理前面标记的NET_RX_SOFTIRQ,把出队列的skb送到相应的列表进行处理(根据协议不同送到不同的列表)。在数据包的处理函数netif_receive_skb中,会先看ptype_all中是否有注册的协议,如果有,则调用相应的处理函数,然后再到ptype_base中,找到合适的协议,将skb发送到相关协议的处理函数。比如ip协议(ip_rcv)或者arp(arp_rcv)等等.
ptype_base为一个hash表,而ptype_all为一个双向链表.每一个里面注册的协议都用一个struct packet_type表示。如图所示。
2、网络层处理分组(以IPv4为例)
2.1 图解
以IPv4分组为例。
2.2 处理过程
2.2.1 ip_rcv()
skb被送到ip_rcv()函数进行处理。首先ip_rcv函数验证IP分组。比如目的地是否是本机地址,校验和是否正确等等。若正确,则交给netfilter的NF_IP_ROUTING;否则,丢弃
2.2.2 ip_rcv_finish()
随后将分组发送到ip_rcv_finish()函数处理。根据skb结构的目的或路由信息发送到不同的处理函数。
ip_rcv_finish()函数的具体处理过程如下:
从 skb->nh ( IP 头,由 netif_receive_skb 初始化)结构得到 IP 地址:
struct net_device *dev = skb->dev;
struct iphdr *iph = skb->nh.iph;
而 skb->dst 或许包含了数据分组到达目的地的路由信息,如果没有,则需要查找路由,如果最后结果显示目的地不可达,那么就丢弃该数据包:
if (skb->dst == NULL) {
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
goto drop; //丢弃
}
ip_rcv_finish() 函数最后执行 dst_input ,决定数据包的下一步的处理。
本机分组则由ip_local_deliver处理;
需要转发的数据则由ip_forward()函数处理;
组播数据包则由ip_mr_input()函数处理。
2.3 ip_forward()转发数据包
- 处理IP头选项。如果需要,会记录本地IP地址和时间戳;
- 确认分组可以被转发
- 将TTL减1,如果TTL为0,则丢弃分组,
TTL是 Time To Live的缩写,该字段指定IP包被路由器丢弃之前允许通过的最大网段数量。TTL是IPv4报头的一个8 bit字段。 - 根据MTU大小和路由信息,对数据分组进行分片。
MTU即最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。 - 将数据分组送往外出设备。
如果因为某种原因分组转发失败,则回应ICMP消息,来回复不能转发的原因。
如果对转发的分组进行各种检查无误后: - 执行ip_forward_finish()函数,准备发送。
- 然后执行dst_output(skb)将分组发到转发的目的主机或本地主机。dst_output(skb) 函数要执行虚函数 output (单播的话为 ip_output ,多播为 ip_mc_output )。
- 最后, 调用ip_finish_output() 进入邻居子系统。
邻居子系统:在数据链接层,必须要获取发送方和接收方的MAC地址,这样数据才能正确到达接收方。邻居子系统的作用就是把IP地址转换成对应的MAC地址。如果目的主机不是和发送发位于同一局域网时,解析的MAC地址就是下一跳网关地址
2.4 ip_local_deliver本地处理
ip_local_deliver中对ip分片进行重组,经过LOCAL_IN钩子点,然后调用ip_local_deliver_finish;
/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* 重组 IP fragments.
*/
struct net *net = dev_net(skb->dev);
/* 分片重组 */
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
/* 经过LOCAL_IN钩子点 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
最后调用ip_local_deliver_finish()函数:ip_local_deliver_finish函数处理原始套接字的数据接收,并调用上层协议的包接收函数,将数据包传递到传输层;
3. 传输层处理
TCP处理过程如图
接收到的分组由ip_local_deliver进入。
发送分组或者响应分组有ip_queue_xmit()函数出口出去:
发送时,ip_queue_xmit()函数检查socket结构体中是否有路由信息,如果没有则执行ip_route_flow()查找,并存储到skb数据结构中。如果找不到,则丢弃。
4. 流程总结
链路层
网卡收到以太帧,发出硬件中断 ——> 创建新的套接字缓冲区skb ——>调用netif_rx(skb)处理skb ——> 将skb进入CPU的FIFO队列 ——> 标记NET_RX_SOFTIRQ网络接收软中断 ——>CPU调用do_softirq()开始处理中断 ——> 调用net_rx_action处理NET_RX_SOFTIRQ ——> 根据skb不同协议,将skb送到不同列表进行处理 ——> 进入网络层ip.rcv()函数处理
网络层
ip_rcv() ——> ip_rcv()验证IP分组 ——> 若正确,则发送到ip_rcv_finish() ——> 根据skb结构中的目的或路由信息发送到不同处理函数
- —— > ip_forward()处理需要转发的数据 ——> 处理IP头选项,确认分组可以被转发 ——> TTL-1,对数据分组进行分片 ——> 如果可以发送,则执行ip_forward_finish()函数进行转发 ——> 执行ip_finish_output()函数进入邻居子系统
- —— > ip_local_deliver()处理本地分组 ——> 对IP分片进行重组,经过LOCAL_IN钩子点 ——> 调用ip_local_deliver_finish()函数处理原始套接字 ——> 将数据包送到传输层