300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

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

时间:2023-01-15 08:30:04

相关推荐

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

内核版本: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网络子系统(一)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。