tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二
內(nèi)核版本:3.4.39
上篇我們分析了UDP套接字如何接收數(shù)據(jù)的流程,最終它是在內(nèi)核套接字的接收隊(duì)列里取出報(bào)文,剩下的問(wèn)題就是誰(shuí)會(huì)去寫入這個(gè)隊(duì)列,當(dāng)然,這部分工作由內(nèi)核來(lái)完成,本篇剩下的文章主要分析內(nèi)核網(wǎng)絡(luò)層收到UDP報(bào)文后如何將報(bào)文插入到對(duì)應(yīng)套接字的接收隊(duì)列里面。
我們直到網(wǎng)絡(luò)層到傳輸層的最終的接口是ip_local_deliver_finish,下面是它的代碼:
static int ip_local_deliver_finish(struct sk_buff *skb) {struct net *net = dev_net(skb->dev);/* 拉出IP報(bào)文首部,因?yàn)轳R上就要脫離IP層,進(jìn)入傳輸層了。 */__skb_pull(skb, ip_hdrlen(skb));/* 設(shè)置傳輸層首部地址 */skb_reset_transport_header(skb);rcu_read_lock();{/* 得到傳輸層協(xié)議 */int protocol = ip_hdr(skb)->protocol;int hash, raw;const struct net_protocol *ipprot;resubmit:/* 將數(shù)據(jù)包傳遞給對(duì)應(yīng)的原始套接字 */raw = raw_local_deliver(skb, protocol);/* 根據(jù)傳輸協(xié)議確定對(duì)應(yīng)的inet協(xié)議 */hash = protocol & (MAX_INET_PROTOS - 1);ipprot = rcu_dereference(inet_protos[hash]);if (ipprot != NULL) {/* 找到了匹配傳輸層的協(xié)議 */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;}/* 協(xié)議的安全策略檢查 */if (!ipprot->no_policy) {if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {kfree_skb(skb);goto out;}nf_reset(skb);}/* 將數(shù)據(jù)包傳遞給傳輸層處理 */ret = ipprot->handler(skb);if (ret < 0) {protocol = -ret;goto resubmit;}IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);} else {/* 沒(méi)有對(duì)應(yīng)的傳輸層協(xié)議 */if (!raw) {/* 若沒(méi)有匹配的原始套接字,則進(jìn)行安全策略檢查 */if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {/* 若沒(méi)有對(duì)應(yīng)的安全策略,則使用ICMP返回不可達(dá)錯(cuò)誤 */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; }內(nèi)核通過(guò)調(diào)用ipprot->handler(skb)將數(shù)據(jù)包傳遞給了正確的傳輸層協(xié)議。對(duì)于IPv4協(xié)議來(lái)說(shuō),其傳輸層協(xié)議的處理函數(shù)的handler是在inet_init中添加的。下面是inet_init中的部分代碼:
/* 添加ICMP協(xié)議 */if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");/* 添加UDP協(xié)議 */if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");/* 添加TCP協(xié)議 */if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n"); #ifdef CONFIG_IP_MULTICAST/* 添加IGMP協(xié)議 */if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n"); #endif通過(guò)調(diào)用inet_add_protocol函數(shù),傳輸層將自己的處理函數(shù)添加到了inet_protos中,這樣就可以在ip_local_deliver_finish中調(diào)用對(duì)應(yīng)的傳輸層的處理函數(shù)了。
inet_init中的另一部分代碼如下:
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)inet_register_protosw(q);這部分代碼用于注冊(cè)AF_INET的各種協(xié)議,如UDP、TCP等。inet_add_protocol面向的是底層接口,而inet_register_protosw面向的是上層應(yīng)用,所以將其分為了兩個(gè)結(jié)構(gòu)。?
UDP協(xié)議的面向底層接口的處理結(jié)構(gòu)為:
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數(shù)據(jù)包,會(huì)依次進(jìn)入udp_rcv→__udp4_lib_rcv,下面來(lái)看看__udp4_lib_rcv的相關(guān)代碼:
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);/* 校驗(yàn)數(shù)據(jù)包至少要有UDP首部大小 */if (!pskb_may_pull(skb, sizeof(struct udphdr)))goto drop; /* No space for header. *//* 得到UDP首部指針 */uh = udp_hdr(skb);/* 得到UDP數(shù)據(jù)包長(zhǎng)度、源地址、目的地址 */ulen = ntohs(uh->len);saddr = ip_hdr(skb)->saddr;daddr = ip_hdr(skb)->daddr;/* 如果UDP數(shù)據(jù)包長(zhǎng)度超過(guò)數(shù)據(jù)包的實(shí)際長(zhǎng)度,則出錯(cuò) */if (ulen > skb->len)goto short_packet;/*判斷協(xié)議是否為UDP協(xié)議。也許有的讀者會(huì)覺(jué)得很奇怪,為什么在UDP的接收函數(shù)中還要判斷協(xié)議是否為UDP?因?yàn)檫@個(gè)函數(shù)還用于處理UDPLITE協(xié)議。*/if (proto == IPPROTO_UDP) {/* 如果是UDP協(xié)議,則將數(shù)據(jù)包的長(zhǎng)度更新為UDP指定的長(zhǎng)度,并更新校驗(yàn)和 */if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))goto short_packet;/* 因?yàn)榍懊娴牟僮骺赡軙?huì)導(dǎo)致skb內(nèi)存變化,所以需要重新獲得UDP首部指針 */uh = udp_hdr(skb);}/* 初始化UDP校驗(yàn)和 */if (udp4_csum_init(skb, uh, proto))goto csum_error;/* 如果路由標(biāo)志位廣播或多播,則表明該UDP數(shù)據(jù)包為廣播或多播 */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) {/* 找到了匹配的套接字 *//* 將數(shù)據(jù)包加入到UDP的接收隊(duì)列 */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;}/* 進(jìn)行xfrm策略檢查 */if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))goto drop;/* 重置netfilter信息 */nf_reset(skb);/* 檢查UDP檢驗(yàn)和 */if (udp_lib_checksum_complete(skb))goto csum_error;/* 若不知道匹配的UDP套接字,則發(fā)送ICMP錯(cuò)誤消息 */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;/* 錯(cuò)誤處理 */…… }下面來(lái)看一下如何匹配UDP套接字,請(qǐng)看__udp4_lib_lookup_skb→__udp4_lib_lookup函數(shù),代碼如下:?
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();/* 若該桶的套接字個(gè)數(shù)多于10個(gè),則需要再次定位 */if (hslot->count > 10) {/* 使用目的地址和目的端口確定hash桶索引 */hash2 = udp4_portaddr_hash(net, daddr, hnum);slot2 = hash2 & udptable->mask;/*UDP套接字表維護(hù)了兩個(gè)hash表:第一個(gè)hash表,使用端口來(lái)索引。第二個(gè)hash表,使用地址+端口來(lái)索引。在進(jìn)行UDP套接字匹配的時(shí)候,優(yōu)先使用第一個(gè)hash表,因?yàn)榈谝粋€(gè)hash表使用的是端口進(jìn)行散列索引,那么只要端口相同,無(wú)論是監(jiān)聽的指定IP還是任意IP,都可以在一個(gè)桶中進(jìn)行匹配。但是由于端口只有65535種可能,所以可能導(dǎo)致不夠分散,一個(gè)桶的套接字個(gè)數(shù)會(huì)比較多。而第二個(gè)hash表是使用地址+端口來(lái)索引的,因此理論上套接字的分布會(huì)比第一個(gè)hash表更加分散。因此當(dāng)?shù)谝粋€(gè)hash表對(duì)應(yīng)桶的套接字多于10個(gè)時(shí),內(nèi)核會(huì)嘗試去第二個(gè)hash表中進(jìn)行匹配查找。*/hslot2 = &udptable->hash2[slot2];/* 盡管第二個(gè)hash表理論上會(huì)比第一個(gè)hash表分散,但是如果實(shí)際上第二個(gè)表的桶中套接字個(gè)數(shù)大于第一個(gè)表的桶中套接字個(gè)數(shù),那么這時(shí)還是利用第一個(gè)hash表進(jìn)行匹配 */if (hslot->count < hslot2->count)goto begin;/* 在第二個(gè)hash表的桶中匹配查找套接字 */result = udp4_lib_lookup2(net, saddr, sport,daddr, hnum, dif,hslot2, slot2);if (!result) {/* 若利用指定的IP和端口在該桶中沒(méi)能找到匹配的套接字,則通常使用任意IP+端口來(lái)進(jìn)行散列索引 */hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);slot2 = hash2 & udptable->mask;hslot2 = &udptable->hash2[slot2];/* 還是要與第一個(gè)hash桶中的個(gè)數(shù)進(jìn)行比較 */if (hslot->count < hslot2->count)goto begin;/* 在第二個(gè)hash表中使用任意IP+端口進(jìn)行匹配查找 */result = udp4_lib_lookup2(net, saddr, sport,htonl(INADDR_ANY), hnum, dif,hslot2, slot2);}rcu_read_unlock();return result;} begin:result = NULL;badness = -1;/* 在第一個(gè)hash表的桶中進(jìn)行查找 */sk_nulls_for_each_rcu(sk, node, &hslot->head) {/* 計(jì)算該套接字的匹配得分 */score = compute_score(sk, net, saddr, hnum, sport,daddr, dport, dif);/* 保證匹配得分最高的套接字為最終結(jié)果 */if (score > badness) {result = sk;badness = score;}}/*檢查在查找的過(guò)程中,是否遇到了某個(gè)套接字被移到另外一個(gè)桶內(nèi)的情況。這時(shí),需要重新進(jìn)行匹配。*/if (get_nulls_value(node) != slot)goto begin;/* 找到了匹配的套接字 */if (result) {/* 增加套接字引用計(jì)數(shù) */if (unlikely(!atomic_inc_not_zero_hint(&result->sk_refcnt, 2)))result = NULL;/* 再次計(jì)算套接字得分,如小于最大分?jǐn)?shù),則重新匹配查找。之所以做二次檢查,也是為了防止在匹配與增加引用的過(guò)程中,套接字發(fā)生變化。 */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套接字的關(guān)鍵在于對(duì)應(yīng)套接字的匹配得分。第一個(gè)hash表的得分計(jì)算函數(shù)為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) {/* 如果數(shù)據(jù)包的目的地址與綁定接收地址不符,則分?jǐn)?shù)為-1,相同則增加2分。 */if (inet->inet_rcv_saddr != daddr)return -1;score += 2;}/* 套接字設(shè)置了對(duì)端目的地址 */if (inet->inet_daddr) {/* 如果數(shù)據(jù)包的源地址與設(shè)置的目的地址不同,則分?jǐn)?shù)為-1,相同則增加2分 */if (inet->inet_daddr != saddr)return -1;score += 2;}/* 套接字設(shè)置了對(duì)端目的端口 */if (inet->inet_dport) {/* 如果數(shù)據(jù)包的源端口與設(shè)置的目的端口不同,則分?jǐn)?shù)為-1,相同則增加2分 */if (inet->inet_dport != sport)return -1;score += 2;}/* 套接字綁定了網(wǎng)卡 */if (sk->sk_bound_dev_if) {/* 如果接受數(shù)據(jù)包的網(wǎng)卡與綁定網(wǎng)卡不同,則分?jǐn)?shù)為-1,相同則增加2分 */if (sk->sk_bound_dev_if != dif)return -1;score += 2;}}return score; }?
對(duì)于第二個(gè)hash,其匹配分?jǐn)?shù)計(jì)算函數(shù)為compute_score2,算法與compute_score基本相同。總的來(lái)說(shuō)UDP的套接字匹配有以下幾個(gè)條件:
·接收端口:必須匹配。
·接收地址:如綁定了則必須匹配,分值為2分。
·對(duì)端目的地址:如設(shè)置了則必須匹配,分值為2分。
·對(duì)端目的端口:如設(shè)置了則必須匹配,分值為2分。
·網(wǎng)卡:如綁定了則必須匹配,分值為2分。
·套接字設(shè)置了PF_INET協(xié)議族,分值為1分。
根據(jù)上面的規(guī)則,匹配分值最高的套接字就為選中的UDP套接字,然后內(nèi)核會(huì)將這個(gè)數(shù)據(jù)包加入到該UDP套接字的接收隊(duì)列中。也就是說(shuō),即使數(shù)據(jù)包可以匹配多個(gè)UDP套接字(這是很有可能的),但是最終也只有一個(gè)最匹配的套接字會(huì)被選中,并且只有這個(gè)套接字可以收到數(shù)據(jù)包。
有一些開發(fā)人員想使用套接字的SO_REUSEADDR選項(xiàng),讓多個(gè)套接字綁定同一個(gè)地址或端口,然后讓獨(dú)立的線程或進(jìn)程負(fù)責(zé)一個(gè)套接字的處理,希望利用這樣的設(shè)計(jì)來(lái)提高服務(wù)的響應(yīng)速度。這里面有個(gè)想當(dāng)然的認(rèn)為,當(dāng)多個(gè)套接字負(fù)責(zé)同一個(gè)地址和端口的數(shù)據(jù)包接收時(shí),它們可以分擔(dān)負(fù)載。然而從上面的源碼分析中,我們可以發(fā)現(xiàn)這樣的設(shè)計(jì)方案是達(dá)不到預(yù)期效果的。因?yàn)閮?nèi)核在進(jìn)行套接字的匹配時(shí),對(duì)于綁定相同地址和端口的多個(gè)套接字,每次只會(huì)命中同一個(gè)套接字。結(jié)果在上面的設(shè)計(jì)中,只有一個(gè)套接字會(huì)收到數(shù)據(jù)包,也就說(shuō)最后只有一個(gè)線程或進(jìn)程在處理數(shù)據(jù)包。
不過(guò)Linux內(nèi)核在3.9版本中引入了一個(gè)新的套接字選項(xiàng)SO_REUSEPORT用于解決上面的問(wèn)題。當(dāng)多個(gè)套接字綁定于同一個(gè)地址和端口時(shí),并啟用了SO_REUSEPORT時(shí),內(nèi)核會(huì)自動(dòng)在這幾個(gè)套接字之間做負(fù)載均衡,保證對(duì)應(yīng)的數(shù)據(jù)包能盡量平均地分配到不同的套接字上。
參考文檔:
1.?《Linux環(huán)境編程:從應(yīng)用到內(nèi)核》
2.??淺析Linux網(wǎng)絡(luò)子系統(tǒng)(一)?
總結(jié)
以上是生活随笔為你收集整理的tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: tcp/ip 协议栈Linux内核源码分
- 下一篇: 大众交通是什么性质的单位