日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

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

發布時間:2025/4/5 linux 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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網絡子系統(一)?

總結

以上是生活随笔為你收集整理的tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。