内核版本:3.4.39

上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内核网络层收到UDP报文后如何将报文插入到对应套接字的接收队列里面。

我们直到网络层到传输层的最终的接口是ip_local_deliver_finish,下面是它的代码:

static int ip_local_deliver_finish(struct sk_buff *skb)
{struct net *net = dev_net(skb->dev);/* 拉出IP报文首部,因为马上就要脱离IP层,进入传输层了。 */__skb_pull(skb, ip_hdrlen(skb));/* 设置传输层首部地址 */skb_reset_transport_header(skb);rcu_read_lock();{/* 得到传输层协议 */int protocol = ip_hdr(skb)->protocol;int hash, raw;const struct net_protocol *ipprot;resubmit:/* 将数据包传递给对应的原始套接字 */raw = raw_local_deliver(skb, protocol);/* 根据传输协议确定对应的inet协议 */hash = protocol & (MAX_INET_PROTOS - 1);ipprot = rcu_dereference(inet_protos[hash]);if (ipprot != NULL) {/* 找到了匹配传输层的协议 */int ret;/* 检查名称空间是否匹配 */if (!net_eq(net, &init_net) && !ipprot->netns_ok) {if (net_ratelimit())printk("%s: proto %d isn't netns-ready\n",__func__, protocol);kfree_skb(skb);goto out;}/* 协议的安全策略检查 */if (!ipprot->no_policy) {if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {kfree_skb(skb);goto out;}nf_reset(skb);}/* 将数据包传递给传输层处理 */ret = ipprot->handler(skb);if (ret < 0) {protocol = -ret;goto resubmit;}IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);} else {/* 没有对应的传输层协议 */if (!raw) {/* 若没有匹配的原始套接字,则进行安全策略检查 */if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {/* 若没有对应的安全策略,则使用ICMP返回不可达错误 */IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);icmp_send(skb, ICMP_DEST_UNREACH,ICMP_PROT_UNREACH, 0);}} elseIP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);kfree_skb(skb);}}out:rcu_read_unlock();return 0;
}

内核通过调用ipprot->handler(skb)将数据包传递给了正确的传输层协议。对于IPv4协议来说,其传输层协议的处理函数的handler是在inet_init中添加的。下面是inet_init中的部分代码:

 /* 添加ICMP协议 */if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");/* 添加UDP协议 */if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");/* 添加TCP协议 */if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST/* 添加IGMP协议 */if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

通过调用inet_add_protocol函数,传输层将自己的处理函数添加到了inet_protos中,这样就可以在ip_local_deliver_finish中调用对应的传输层的处理函数了。

inet_init中的另一部分代码如下:

  for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)inet_register_protosw(q);

这部分代码用于注册AF_INET的各种协议,如UDP、TCP等。inet_add_protocol面向的是底层接口,而inet_register_protosw面向的是上层应用,所以将其分为了两个结构。

UDP协议的面向底层接口的处理结构为:

static const struct net_protocol udp_protocol = {.handler = udp_rcv,.err_handler = udp_err,.gso_send_check = udp4_ufo_send_check,.gso_segment = udp4_ufo_fragment,.no_policy = 1,.netns_ok = 1,
};

因此,如果是UDP数据包,会依次进入udp_rcv→__udp4_lib_rcv,下面来看看__udp4_lib_rcv的相关代码:

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,int proto)
{struct sock *sk;struct udphdr *uh;unsigned short ulen;struct rtable *rt = skb_rtable(skb);__be32 saddr, daddr;struct net *net = dev_net(skb->dev);/* 校验数据包至少要有UDP首部大小 */if (!pskb_may_pull(skb, sizeof(struct udphdr)))goto drop;        /* No space for header. *//* 得到UDP首部指针 */uh   = udp_hdr(skb);/* 得到UDP数据包长度、源地址、目的地址 */ulen = ntohs(uh->len);saddr = ip_hdr(skb)->saddr;daddr = ip_hdr(skb)->daddr;/* 如果UDP数据包长度超过数据包的实际长度,则出错 */if (ulen > skb->len)goto short_packet;/*判断协议是否为UDP协议。也许有的读者会觉得很奇怪,为什么在UDP的接收函数中还要判断协议是否为UDP?因为这个函数还用于处理UDPLITE协议。*/if (proto == IPPROTO_UDP) {/* 如果是UDP协议,则将数据包的长度更新为UDP指定的长度,并更新校验和 */if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))goto short_packet;/* 因为前面的操作可能会导致skb内存变化,所以需要重新获得UDP首部指针 */uh = udp_hdr(skb);}/* 初始化UDP校验和 */if (udp4_csum_init(skb, uh, proto))goto csum_error;/* 如果路由标志位广播或多播,则表明该UDP数据包为广播或多播 */if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))return __udp4_lib_mcast_deliver(net, skb, uh,saddr, daddr, udptable);/* 确定匹配的UDP套接字 */sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);if (sk != NULL) {/* 找到了匹配的套接字 *//* 将数据包加入到UDP的接收队列 */int ret = udp_queue_rcv_skb(sk, skb);sock_put(sk);/* a return value > 0 means to resubmit the input, but* it wants the return to be -protocol, or 0*/if (ret > 0)return -ret;return 0;}/* 进行xfrm策略检查 */if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))goto drop;/* 重置netfilter信息 */nf_reset(skb);/* 检查UDP检验和 */if (udp_lib_checksum_complete(skb))goto csum_error;/* 若不知道匹配的UDP套接字,则发送ICMP错误消息 */UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);/** Hmm.  We got an UDP packet to a port to which we* don't wanna listen.  Ignore it.*/kfree_skb(skb);return 0;/* 错误处理 */……
}

下面来看一下如何匹配UDP套接字,请看__udp4_lib_lookup_skb→__udp4_lib_lookup函数,代码如下:

static struct sock *__udp4_lib_lookup(struct net *net, __be32 saddr,__be16 sport, __be32 daddr, __be16 dport,int dif, struct udp_table *udptable)
{struct sock *sk, *result;struct hlist_nulls_node *node;unsigned short hnum = ntohs(dport);/* 使用目的端口确定hash桶索引 */unsigned int hash2, slot2, slot = udp_hashfn(net, hnum, udptable->mask);struct udp_hslot *hslot2, *hslot = &udptable->hash[slot];int score, badness;rcu_read_lock();/* 若该桶的套接字个数多于10个,则需要再次定位 */if (hslot->count > 10) {/* 使用目的地址和目的端口确定hash桶索引 */hash2 = udp4_portaddr_hash(net, daddr, hnum);slot2 = hash2 & udptable->mask;/*UDP套接字表维护了两个hash表:第一个hash表,使用端口来索引。第二个hash表,使用地址+端口来索引。在进行UDP套接字匹配的时候,优先使用第一个hash表,因为第一个hash表使用的是端口进行散列索引,那么只要端口相同,无论是监听的指定IP还是任意IP,都可以在一个桶中进行匹配。但是由于端口只有65535种可能,所以可能导致不够分散,一个桶的套接字个数会比较多。而第二个hash表是使用地址+端口来索引的,因此理论上套接字的分布会比第一个hash表更加分散。因此当第一个hash表对应桶的套接字多于10个时,内核会尝试去第二个hash表中进行匹配查找。*/hslot2 = &udptable->hash2[slot2];/* 尽管第二个hash表理论上会比第一个hash表分散,但是如果实际上第二个表的桶中套接字个数大于第一个表的桶中套接字个数,那么这时还是利用第一个hash表进行匹配 */if (hslot->count < hslot2->count)goto begin;/* 在第二个hash表的桶中匹配查找套接字 */result = udp4_lib_lookup2(net, saddr, sport,daddr, hnum, dif,hslot2, slot2);if (!result) {/* 若利用指定的IP和端口在该桶中没能找到匹配的套接字,则通常使用任意IP+端口来进行散列索引 */hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);slot2 = hash2 & udptable->mask;hslot2 = &udptable->hash2[slot2];/* 还是要与第一个hash桶中的个数进行比较 */if (hslot->count < hslot2->count)goto begin;/* 在第二个hash表中使用任意IP+端口进行匹配查找 */result = udp4_lib_lookup2(net, saddr, sport,htonl(INADDR_ANY), hnum, dif,hslot2, slot2);}rcu_read_unlock();return result;}
begin:result = NULL;badness = -1;/* 在第一个hash表的桶中进行查找 */sk_nulls_for_each_rcu(sk, node, &hslot->head) {/* 计算该套接字的匹配得分 */score = compute_score(sk, net, saddr, hnum, sport,daddr, dport, dif);/* 保证匹配得分最高的套接字为最终结果 */if (score > badness) {result = sk;badness = score;}}/*检查在查找的过程中,是否遇到了某个套接字被移到另外一个桶内的情况。这时,需要重新进行匹配。*/if (get_nulls_value(node) != slot)goto begin;/* 找到了匹配的套接字 */if (result) {/* 增加套接字引用计数 */if (unlikely(!atomic_inc_not_zero_hint(&result->sk_refcnt, 2)))result = NULL;/* 再次计算套接字得分,如小于最大分数,则重新匹配查找。之所以做二次检查,也是为了防止在匹配与增加引用的过程中,套接字发生变化。 */else if (unlikely(compute_score(result, net, saddr, hnum, sport,daddr, dport, dif) < badness)) {sock_put(result);goto begin;}}rcu_read_unlock();return result;
}

从上面的代码中可以看到,匹配UDP套接字的关键在于对应套接字的匹配得分。第一个hash表的得分计算函数为compute_score。

static inline int compute_score(struct sock *sk, struct net *net, __be32 saddr,unsigned short hnum,__be16 sport, __be32 daddr, __be16 dport, int dif)
{int score = -1;/* 比较名称空间,端口等 */if (net_eq(sock_net(sk), net) && udp_sk(sk)->udp_port_hash == hnum &&!ipv6_only_sock(sk)) {struct inet_sock *inet = inet_sk(sk);/* 若套接字指明为PF_INET,则加1分 */score = (sk->sk_family == PF_INET ? 1 : 0);/* 套接字绑定了接收地址 */if (inet->inet_rcv_saddr) {/* 如果数据包的目的地址与绑定接收地址不符,则分数为-1,相同则增加2分。 */if (inet->inet_rcv_saddr != daddr)return -1;score += 2;}/* 套接字设置了对端目的地址 */if (inet->inet_daddr) {/* 如果数据包的源地址与设置的目的地址不同,则分数为-1,相同则增加2分 */if (inet->inet_daddr != saddr)return -1;score += 2;}/* 套接字设置了对端目的端口 */if (inet->inet_dport) {/* 如果数据包的源端口与设置的目的端口不同,则分数为-1,相同则增加2分 */if (inet->inet_dport != sport)return -1;score += 2;}/* 套接字绑定了网卡 */if (sk->sk_bound_dev_if) {/* 如果接受数据包的网卡与绑定网卡不同,则分数为-1,相同则增加2分 */if (sk->sk_bound_dev_if != dif)return -1;score += 2;}}return score;
}

对于第二个hash,其匹配分数计算函数为compute_score2,算法与compute_score基本相同。总的来说UDP的套接字匹配有以下几个条件:

·接收端口:必须匹配。

·接收地址:如绑定了则必须匹配,分值为2分。

·对端目的地址:如设置了则必须匹配,分值为2分。

·对端目的端口:如设置了则必须匹配,分值为2分。

·网卡:如绑定了则必须匹配,分值为2分。

·套接字设置了PF_INET协议族,分值为1分。

根据上面的规则,匹配分值最高的套接字就为选中的UDP套接字,然后内核会将这个数据包加入到该UDP套接字的接收队列中。也就是说,即使数据包可以匹配多个UDP套接字(这是很有可能的),但是最终也只有一个最匹配的套接字会被选中,并且只有这个套接字可以收到数据包。

有一些开发人员想使用套接字的SO_REUSEADDR选项,让多个套接字绑定同一个地址或端口,然后让独立的线程或进程负责一个套接字的处理,希望利用这样的设计来提高服务的响应速度。这里面有个想当然的认为,当多个套接字负责同一个地址和端口的数据包接收时,它们可以分担负载。然而从上面的源码分析中,我们可以发现这样的设计方案是达不到预期效果的。因为内核在进行套接字的匹配时,对于绑定相同地址和端口的多个套接字,每次只会命中同一个套接字。结果在上面的设计中,只有一个套接字会收到数据包,也就说最后只有一个线程或进程在处理数据包。

不过Linux内核在3.9版本中引入了一个新的套接字选项SO_REUSEPORT用于解决上面的问题。当多个套接字绑定于同一个地址和端口时,并启用了SO_REUSEPORT时,内核会自动在这几个套接字之间做负载均衡,保证对应的数据包能尽量平均地分配到不同的套接字上。

参考文档:

1. 《Linux环境编程:从应用到内核》

2.  浅析Linux网络子系统(一)

tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二相关推荐

  1. tcp/ip 协议栈Linux内核源码分析13 udp套接字发送流程二

    内核版本:3.4.39 继续UDP套接字发送,上一篇讲到了sock_sendmsg,这里继续,下面是sock_sendmsg的相关代码 int sock_sendmsg(struct socket * ...

  2. tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

    内核版本:3.4.39 前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的. 与发送类似,内核也提供了多个接收数据的系统调用接口 ...

  3. tcp/ip 协议栈Linux内核源码分析12 udp套接字发送流程一

    内核版本:3.4.39 因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解.但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚 ...

  4. tcp/ip 协议栈Linux内核源码分析九 IPv6分片ip6_fragment 分析

    内核版本:3.4.39 IPv6的分片流程和IPv4基本一致,这一点内核源码作者也说了.流程比较简单,分片的时候判断是否满足快速分片,满足的话直接一个接一个加上分片扩展选项发送出去,不满足的话就只能走 ...

  5. tcp/ip 协议栈Linux内核源码分析十 邻居子系统分析一 概述通用邻居框架

    内核版本:3.4.39 为什么需要邻居子系统呢?因为在网络上发送报文的时候除了需要知道目的IP地址还需要知道邻居的L2 mac地址,为什么是邻居的L2地址而不是目的地的L2地址呢,这是因为目的地网络可 ...

  6. tcp/ip 协议栈Linux内核源码分析八 路由子系统分析三 路由表

    内核版本:3.4.39 Linux路由子系统代码量虽说不是很多,但是难度还是有的,最近在分析路由子系统这一块,对它的框架有了基本的了解,如果要想掌握的话估计还得再花点时间阅读代码,先把框架记录下来.路 ...

  7. tcp/ip 协议栈Linux内核源码分析11 邻居子系统分析二 arp协议的实现处理

    内核版本:3.4.39 内核邻居子系统定义了一个基本的框架,使得不同的邻居协议可以共用一套代码.比起其它的内核模块,邻居子系统框架代码还是比较简单易懂的.邻居子系统位于网络层和流量控制子系统中间,它提 ...

  8. tcp/ip 协议栈Linux内核源码分析七 路由子系统分析二 策略路由

    内核版本:3.4.39 策略路由就是根据配置策略查找路由表,早期的Linux版本是不支持策略路由的,默认的查找策略就是先查找local路由表,找不到再继续查找main表,当支持策略路由功能时,内核最多 ...

  9. tcp/ip 协议栈Linux内核源码分析六 路由子系统分析一路由缓存

    内核版本:3.4.39 收到报文或者发送报文的时候都需要查找路由表,频繁的路由表查找操作时需要耗费一部分CPU的,Linux提供了路由缓存来减少路由表的查询,路由缓存由hash表组织而成,路由缓存的初 ...

最新文章

  1. iOS 远程推送 总结
  2. java crontab_(定时任务)crontab和cron 的用法
  3. js改变style样式和css样式
  4. Nacos自定义共享 Data Id 配置
  5. MVC 之 Partial View 用法
  6. Ant Design Form.Item的label中文字换行的替代方式
  7. Zookeeper原理架构
  8. FluentAPI --- 用C#写的JS代码生成器
  9. nginx实时生成缩略图到硬盘上
  10. sqlserver 执行计划
  11. php 字符串压缩,PHP 压缩字符串的几种方法
  12. psp模拟java_PSP超强JAVA模拟器 PSPKVM v0.5 发布下载
  13. 【ffplay】视频的宽高比详解 -PAR、DAR 和 SAR
  14. 分享假如你买到缩水U盘了怎么办?认倒霉?肯定不能的!
  15. Java里线程的隔离方式_线程隔离浅析
  16. 关于getdate()的不同的日期格式
  17. 【Unity3D日常开发】Unity中的资源加载与文件路径
  18. 【编译原理】Python实现对一个英文文本的词频统计
  19. 四川一度智信:如何做好电商?
  20. TCP/IP协议(二、初识tcp)

热门文章

  1. new/delete与malloc/free的区别和联系
  2. pythonprint()_python基础1 print()函数
  3. GAN与力学系统的海森伯图像
  4. 做一个可以和时空分类的神经网络
  5. 游戏开发需要具备哪些技术_生鲜小程序需要具备哪些功能板块?生鲜小程序开发...
  6. STM32 进阶教程 20 - 串口+DMA实现OneWire总线
  7. ubuntu中安装kDevelop
  8. A组包含的前导码数( sizeOfRA-PreamblesGroupA)
  9. Android Nand Flash 分区
  10. 五天带你学完《计算机网络》·第三天·传输层