第四章 网络层接收数据包流程--基于Linux3.10

本文深入解析Linux内核网络栈的接收路径,从网卡接收数据包开始,详细介绍了数据包如何从物理层传递到网络层并最终到达传输层的过程。重点关注了NAPI机制、IP层处理、路由选择以及TCP接收等关键环节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

下载地址《http://download.youkuaiyun.com/detail/shichaog/8620701》

4.1 主机到网络层的过渡

从netif_receive_skb(struct sk_buff *skb)函数开始,网卡收到数据包后产生中断通知CPU有数据到达,在中断服务函数中触发接收软中断,等待内核在适当的时间调度NAPI方式的接收函数完成数据的接收,并非所有网卡或者MAC控制器都是支持NAPI方法(需要硬件能支持)的,NAPI服务函数最重要的工作就是调用netif_receive_skb将数据从主机到网络层送到网络层,其函数参数在第一章叙述过,收到的数据最终能不能送到应用层,是和拥塞控制、路由、协议层如何处理该数据包相关的,由于该函数是在软中断中调用的,所以该函数执行时硬件中断是开启的,这就意味着可能前一次MAC接收到的数据还没有传递到网络层时,MAC又接收到数据又产生新中断,而新的数据需要存放在一个通常被称为DMA缓冲区的地方,这也意味着要支持NAPI方式就需要多个缓冲区,这也是硬件为支持NAPI方式必须支持的一个特性。

int netif_receive_skb(struct sk_buff  *skb) 
{
//接收到的数据包的时间戳,netdev_tstamp_prequeue在dev.c文件里被初始化成了1,作为在SKB加入队列前是否打上时间戳的标志
net_timestamp_check(netdev_tstamp_prequeue, skb);
if (skb_defer_rx_timestamp(skb))
return NET_RX_SUCCESS;
//RPS是Receive Packet Steering的缩写,也许你还看到过GSO,TSO,RFS等等,这些特性后面专门有个讲,这里就略过了。
#ifdef CONFIG_RPS
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu, ret;
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow); //RPS见十五章
if (cpu >= 0) {
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
return ret;
}
rcu_read_unlock();
}
#endif
return __netif_receive_skb(skb);
}

经过几层调用后__netif_receive_skb会调用 __netif_receive_skb_core函数。

3419 static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc) 
3420 {
3421     struct packet_type *ptype, *pt_prev;// packet_type在第一章就见过了
3422     rx_handler_func_t *rx_handler;
3423     struct net_device *orig_dev;
3424     struct net_device *null_or_dev;
3425     bool deliver_exact = false;
3426     int ret = NET_RX_DROP;
3448 another_round:
3449     skb->skb_iif = skb->dev->ifindex;  //这是在sk_buff中提到的接口索引
3450 
3451     __this_cpu_inc(softnet_data.processed);  //这也是第二章中提到的per-CPU变量,增加正在处理标志计数器。
//这里是sniffer嗅探器相关的代码,ptype_all包括了所以协议类型,通过wireshark或者tcpdump工具将网卡至于混杂模式下//抓取数据,会执行这段代码
3470     list_for_each_entry_rcu(ptype, &ptype_all, list) {
3471         if (!ptype->dev || ptype->dev == skb->dev) {
3472             if (pt_prev)   //分片相关
3473                 ret = deliver_skb(skb, pt_prev, orig_dev);
3474             pt_prev = ptype;
3475         }
3476     }
//解引用设备接收处理函数,如何存在会向上层发送接收到数据包
3500     rx_handler = rcu_dereference(skb->dev->rx_handler); 
3501     if (rx_handler) {
3502         if (pt_prev) {
3503             ret = deliver_skb(skb, pt_prev, orig_dev);
3504             pt_prev = NULL;
3505         }
//根据处理结果,判断接下来对数据包如何进一步处理。
3506         switch (rx_handler(&skb)) {
//数据包已成功接收,不需要再处理
3507         case RX_HANDLER_CONSUMED:
3508             ret = NET_RX_SUCCESS;
3509             goto unlock;
//当rx_handler改变过skb->dev时,在接收回路中再一次处理。
3510         case RX_HANDLER_ANOTHER:
3511             goto another_round;
//不使用匹配的方式,精确传递。
3512         case RX_HANDLER_EXACT:
3513             deliver_exact = true;
//忽略rx_handler的影响。
3514         case RX_HANDLER_PASS:
3515             break;
3516         default:
3517             BUG();
3518         }
3519     }
//如果前面判段是精确发送方式,那么把nulll_or_dev设置成精确传送的设备。
3532     null_or_dev = deliver_exact ? skb->dev : NULL;
3533 
3534     type = skb->protocol;
//根据套接字类型处理,802.3的type值是0001。
3535     list_for_each_entry_rcu(ptype,
3536             &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
3537         if (ptype->type == type &&
3538             (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
3539              ptype->dev == orig_dev)) {
3540             if (pt_prev)
3541                 ret = deliver_skb(skb, pt_prev, orig_dev);  
3542             pt_prev = ptype;
3543         }
3544     }
3565 }

由上面可以看出,各种接收数据包情况下最后的函数均指向了deliver_skb函数,到这里,代码不再在/net/core/dev.c中了,转到了net/ipv4/ip_input.c了,这里真真切切的将代码转到了IP(网络)层了。

net/core/dev.c
1679 static inline int deliver_skb(struct sk_buff *skb,
1680                   struct packet_type *pt_prev,
1681                   struct net_device *orig_dev)
1682 {
//在接收端要将分片的数据包重组,这里判断是否因缺少分片包而导致一个skb成为一个孤儿skb。显然是不太可能是孤儿进
//程的,所以这里使用了unlikely知道gcc编译器优化程序,以减少指令流水被打断的概率。
1683     if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))   
1684         return -ENOMEM;
1685     atomic_inc(&skb->users);  //增加skb的使用者计数器
1686     return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);  
1687 }
<span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255);"><span style="color: rgb(51, 51, 51);"></span></span><pre name="code" class="cpp"><span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255);"></span>

1686行,对于ip数据包,这里的funcip_packet_type结构体中的指针。为了弄清楚这个函数到底是怎么指向ip_rcv的需要向下接着看。以太网初始化函数inet_init调用了dev_add_pack(&ip_packet_type);

static int __init inet_init(void)
{
/*注册了四个种协议类型*/
rc = proto_register(&tcp_prot, 1);
rc = proto_register(&udp_prot, 1);
rc = proto_register(&raw_prot, 1);
rc = proto_register(&ping_prot, 1);
//对应协议初始化,ping隶属icmp协议,在以前版本中没有独立出来,不过由于其重要性,所以默认各个系统都支持了。
arp_init();
ip_init();
tcp_v4_init();
tcp_init();
udp_init();
ping_init();
ipv4_proc_init();

dev_add_pack(&ip_packet_type);
}

dev_add_pack函数原型和参数如下:

void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
spin_lock(&ptype_lock);
list_add_rcu(&pt->list, head);
spin_unlock(&ptype_lock);
}
/参数/ af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};

ptype_head函数定义如下:

net/core/dev.c 
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly; 
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

4.2 进入网络层

到这里已经获得了ip_rcv的来历,并且知道ip_rcv已经被调用来处理接收到的frame了。

net/ipv4/ip_input.c

375 int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
376 {
    /* 如果packet的目的地址是其它主机,简单丢弃该数据包而不做处理      */
383     if (skb->pkt_type == PACKET_OTHERHOST)
384         goto drop;
        /*该宏跟新包相关统计信息,如丢弃、接收、过载等 */
387     IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);
//检查该skb是否共享的,如果共享则会复制该数据包,在遇到内存申请失败时,会更新discard计数器。
389     if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
390         IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
391         goto out;
392     }
393   /* 对接收的数据包IP头进行判断,如果IP头小于最短长度,即IP头有错,那么执行395行。
394     if (!pskb_may_pull(skb, sizeof(struct iphdr)))
395         goto inhdr_error;
397     iph = ip_hdr(skb);
409     /*头长和版本号判断。*/
410     if (iph->ihl < 5 || iph->version != 4)
411         goto inhdr_error;
412  /*处理头可选项字段*/
413     if (!pskb_may_pull(skb, iph->ihl*4))
414         goto inhdr_error;
416     iph = ip_hdr(skb); 
417 /*Ip校验*/
418     if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
419         goto csum_error;
/*防火墙钩子函数,防火墙规则验证通过后对调用最后一个参数对应的函数,即 ip_rcv_finish */
445     return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
446                ip_rcv_finish);
456 }

这个函数的处理逻辑思路非常清楚,首先是对IP数据包的正确性进行相关的验证,包括协议头长度、协议的版本号、IP校验和等。然后调用传送数据的核心函数进行相应的函数(防火墙钩子函数)进行数据处理。

445行,NF_HOOK不是宏,而是一个函数,通常称为钩子函数,NF是netfilter的缩写,netfilter在Linux内核中被称为包过滤防火墙,netfilter的内容见十一章。

在编译内核时没有配置netfilter时,NF_HOOK最后一个参数被调用,此例中即执行ip_forward_finish函数;否则进入HOOK点,执行通过nf_register_hook()登记的功能(这句话表达意义的可能比较含糊,实际是进入nf_hook_slow()函数,再由它执行登记的函数)。

钩子函数不是重点,重点是ip_rcv_finish函数,该函数完成两个功能,其一是根据数据包目的地址和路由配置将数据包送给本地(local)主机或者发送出去(forward),处理IP头字段中的可选字段。这里涉及到一个路由的概念,路由就是为packet找到合适的归宿,这个归宿可能是数据包被传送到本机协议栈上层,也可能通过网卡发送出去,关于Linux内核的trie路由内容见十二章。

311 static int ip_rcv_finish(struct sk_buff *skb)
312 {
313     const struct iphdr *iph = ip_hdr(skb); //获得ip头,20字节内容
314     struct rtable *rt;   //rtable是路由缓存
 /*判断skb的路由缓存是否已有路由项,对于发往本机的数据(回环)该套接字的会包含路由项,如果没有路由项会调用ip_route_input_noref为skb选择一个合适的路由项,并将该信息存储在skb指向的路由选项指针*/
332     if (!skb_dst(skb)) { 
333         int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
334                            iph->tos, skb->dev);
335         if (unlikely(err)) {//找不到路由项,则进行出错处理
336             if (err == -EXDEV)
337                 NET_INC_STATS_BH(dev_net(skb->dev),
338                          LINUX_MIB_IPRPFILTER);
339             goto drop;
340         }
341     }
//这里获得上面skb指向的路由项入口,_skb_refdst是套接字的一个成员,如果refcount没有使用,则最低一个bit是1,其它
//比特用于指示路由项的入口地址,根据skb类型,对多播和广播的统计信息如下,
357     rt = skb_rtable(skb); 
358     if (rt->rt_type == RTN_MULTICAST) {
359         IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INMCAST,
360                 skb->len);
361     } else if (rt->rt_type == RTN_BROADCAST)
362         IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INBCAST,
363                 skb->len);
365     return dst_input(skb);// 对数据包的实际处理函数。
367 drop:
368     kfree_skb(skb);
369     return NET_RX_DROP; //如果packet被丢弃了,没有进行forward或者local deliver操作则返回NET_RX_DROP。
370 }

到这里,从ip_rcv和ip_rcv_finish函数的注释中可以看出,ip层首先检查IP头的正确性,然后根据skb找到对应的路由信息,找到路由信息后就可以调用365行的函数dst_input完成实际的接收工作了。关于网络层的路由查找见第十二章,365行函数的原型如下:

static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}

input指针已经在333行建立路由项时进行赋值了,对于localdeliver,函数就是ip_local_deliver,forward就是ip_forward。

244 int ip_local_deliver(struct sk_buff *skb)
245 {
//对于分片的packet就逆向合并成一个packet,有些网卡硬件有offload特性,这个特性可以将分片和解分的过程在网卡处实现
//而不需要在ip层实现。此外,现在一般网络上的路由器都能支持1500的以太网数据包,有些还支持巨形包,分片的情况现在
//已经很少了。
250     if (ip_is_fragment(ip_hdr(skb))) {
251         if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
252             return 0;
253     }
254   //再一次看到NF_HOOK函数,这是钩子函数起作用的第二个地方,ip_local_deliver_finish函数,完成实际的处理工作。
255     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
256                ip_local_deliver_finish);
257 }

ip_local_deliver_finish()和ip_rcv()在同一个文件里。

189 static int ip_local_deliver_finish(struct sk_buff *skb)
190 {
191     struct net *net = dev_net(skb->dev); 
 //移动skb->data指针跳过ip头,获得tcp层起始地址。
193     __skb_pull(skb, skb_network_header_len(skb)); 
195     rcu_read_lock();
196     {
197         int protocol = ip_hdr(skb)->protocol; //获得ip层的协议,V4协议,传输层。
198         const struct net_protocol *ipprot; //对于v4版本该结构体的初始定义如下。
/**********************************
这篇文章是按照TCP-IP来跟踪网络协议栈的, tcp对应的 net_protocol对应结构体
static const struct net_protocol tcp_protocol = {
.early_demux  = tcp_v4_early_demux,
.handler  = tcp_v4_rcv, //对应接收函数的指针。
.err_handler  = tcp_v4_err,
.no_policy  = 1,
.netns_ok  = 1,
};
**********************************/
201     resubmit:
202         raw = raw_local_deliver(skb, protocol); // raw socket的deliver的方式。
204         ipprot = rcu_dereference(inet_protos[protocol]); //根据protocol获得net_protocol对应函数的指针。 
205         if (ipprot != NULL) { //如果找到,对于tcp协议其将是tcp_protocol,执行的是这段代码
206             int ret;
208             if (!ipprot->no_policy) {//为使用策略
209                 if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
210                     kfree_skb(skb);
211                     goto out;
212                 }
213                 nf_reset(skb);
214             }
215             ret = ipprot->handler(skb);
216             if (ret < 0) {
217                 protocol = -ret;
218                 goto resubmit;
219             }
220             IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
221         } else {                //对于raw socket的操作。
222             if (!raw) {
223                 if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
224                     IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS); 
//icmp协议的packet的deliver方式,该协议还是挺重要的,iP获取,路由表信息的建立等会用到该协议skb,这就验证了前面
//说的,这个sk_buff贯串整个协议栈,只有在必要时才会赋值该sk_buff的一个副本。至此ip到tcp的接收流程已经梳理完毕。
225                     icmp_send(skb, ICMP_DEST_UNREACH,
226                           ICMP_PROT_UNREACH, 0); 
227                 }
228                 kfree_skb(skb);
229             } else {
230                 IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
231                 consume_skb(skb); //非上面的情况,意味着packet应当被释放掉,该函数即完成该功能。
232             }
233         }
234     }
235  out:
236     rcu_read_unlock(); //rcu锁就是读写锁,支持并发读,而不支持并发写,为了防止写的starvation,有写请求时会阻塞读,可见写的优先级高于读,但是写不会抢占读。
239 }

215行, 结合198行上面的 .handler=tcp_v4_rcv,可知,调用了tcp_v4_rcv其参数是需要接收的skb套接字。



图4.1网络层接收函数调用流程







评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shichaog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值