TCP/IP学习(29)——kernel如何选择socket接收数据

本文深入解析Linux内核如何通过socket API告诉kernel使用哪个socket发送或接收数据,详细阐述了rawsocket和L4传输层socket的匹配过程,包括如何通过协议、端口、地址和接口等条件进行匹配。
摘要由CSDN通过智能技术生成


摘自:http://blog.chinaunix.net/uid-23629988-id-285722.html

本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net
    


socket编程中,在发送数据时,通过向api传递socket从而告诉kernel使用哪个socket发送数据。在接收数据时,也是向api传递socket来告诉kernel,应用程序想从哪个socket接收数据。乍一看,这一过程很清楚。但是仔细一想,当接收数据时,应用程序向kernel传递了socket,实际上只是告诉kernel,应用程序想从哪个socket的接收缓冲中接收数据。而kernel如何将数据放到对应的socket的接收缓冲中的呢?这是我们今天学习的目的。

在前面的博文中,学习了完整的接受数据的流程。在L3 IP层的处理函数ip_local_deliver_finish中,调用raw_local_deliver,将数据包的clone传给对应的raw socket。
  1. int raw_local_deliver(struct sk_buff *skb, int protocol)
  2. {
  3.     int hash;
  4.     struct sock *raw_sk;
     /* 使用protocol作为hash值,找到对应的raw socket链表  */
  1.     hash = protocol & (RAW_HTABLE_SIZE - 1);
  2.     raw_sk = sk_head(&raw_v4_hashinfo.ht[hash]);

  3.     /* If there maybe a raw socket we must check - if not we
  4.      * don't care less
  5.      */
  6.     /* 如果有对应该IP协议的raw socket,则通过raw_v4_input继续匹配 */
  7.     if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))
  8.         raw_sk = NULL;

  9.     return raw_sk != NULL;

  10. }
从上面可以看出,kernel中的raw socket是以协议为key,每个协议的raw socket为一个链表。通过协议来确定去哪个raw socket 链表进行匹配。这里可以看出,IP层的协议是第一个raw socket的匹配条件。
  1. static int raw_v4_input(struct sk_buff *skb, struct iphdr *iph, int hash)
  2. {
  3.     struct sock *sk;
  4.     struct hlist_head *head;
  5.     int delivered = 0;
  6.     struct net *net;

  7.     read_lock(&raw_v4_hashinfo.lock);
  8.     head = &raw_v4_hashinfo.ht[hash];
  9.     if (hlist_empty(head))
  10.         goto out;
     /* 查找匹配的raw socket */
  1.     net = dev_net(skb->dev);
  2.     sk = __raw_v4_lookup(net, __sk_head(head), iph->protocol,
  3.              iph->saddr, iph->daddr,
  4.              skb->dev->ifindex);

  5.     while (sk) {
  6.         delivered = 1;
  7.         if (iph->protocol != IPPROTO_ICMP || !icmp_filter(sk, skb)) {
  8.             /* 
  9.             如果数据包不是ICMP 或者不是需要过滤的ICMP数据包,
  10.             那么就clone一个数据包
  11.             */
  12.             struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);

  13.             /* Not releasing hash */
  14.             /* clone 成功,将clone的数据包传给找到的raw socket */
  15.             if (clone)
  16.                 raw_rcv(sk, clone);
  17.         }
  18.         /* 继续匹配下一个raw socket */
  19.         sk = __raw_v4_lookup(net, sk_next(sk), iph->protocol,
  20.                  iph->saddr, iph->daddr,
  21.                  skb->dev->ifindex);
  22.     }
  23. out:
  24.     read_unlock(&raw_v4_hashinfo.lock);
  25.     return delivered;
  26. }
这个函数主要是循环匹配raw socket,可以看出,其遍历了对应协议的raw socket。这就是为什么所有匹配的raw socket都可以收到一份数据包的clone。

下面是真正的raw socket的匹配函数。

  1. static struct sock *__raw_v4_lookup(struct net *net, struct sock *sk,
  2.         unsigned short num, __be32 raddr, __be32 laddr, int dif)
  3. {
  4.     struct hlist_node *node;

  5.     sk_for_each_from(sk, node) {
  6.         struct inet_sock *inet = inet_sk(sk);

  7.         if (net_eq(sock_net(sk), net) && inet->inet_num == num    &&
  8.          !(inet->inet_daddr && inet->inet_daddr != raddr)     &&
  9.          !(inet->inet_rcv_saddr && inet->inet_rcv_saddr != laddr) &&
  10.          !(sk->sk_bound_dev_if && sk->sk_bound_dev_if != dif))
  11.             goto found; /* gotcha */
  12.     }
  13.     sk = NULL;
  14. found:
  15.     return sk;
  16. }
这个函数是真正的匹配函数,去比较了socket的net——这个是一个network namespace,在一般应用中,肯定是相等的,协议,目的地址,源地址,绑定的interface。在比较的时候,都是在raw socket设置了对应的匹配条件才进行比较的。也就是说,如果创建的raw socket没有调用过bind,connect等,只要对应的协议匹配,那么这个raw socket就是匹配的。

这就是raw socket的匹配过程,下面看L4 传输层的匹配。以UDP为例。
在__udp4_lib_rcv中,其调用__udp4_lib_rcv->__udp4_lib_lookup->udp4_lib_lookup2查找最佳的一个匹配的UDP socket。主要这里与raw socket的区别。到了L4层,只确定一个最佳的socket,也就是说即使有多个匹配的socket,也只有一个socket能收到数据包。
  1. static struct sock *udp4_lib_lookup2(struct net *net,
  2.         __be32 saddr, __be16 sport,
  3.         __be32 daddr, unsigned int hnum, int dif,
  4.         struct udp_hslot *hslot2, unsigned int slot2)
  5. {
  6.     ...... ......

  7.     udp_portaddr_for_each_entry_rcu(sk, node, &hslot2->head) {
  8.         score = compute_score2(sk, net, saddr, sport,
  9.                  daddr, hnum, dif);
  10.         if (score > badness) {
  11.             result = sk;
  12.             badness = score;
  13.             if (score == SCORE2_MAX)
  14.                 goto exact_match;
  15.         }
  16.     }
     
     ...... ......
  1. }
该函数调用compute_score2计算socket的匹配程度,也就是得分score,分数最高的即为最佳匹配的socket。

下面看comute_score2的代码
  1. static inline int compute_score2(struct sock *sk, struct net *net,
  2.                  __be32 saddr, __be16 sport,
  3.                  __be32 daddr, unsigned int hnum, int dif)
  4. {
  5.     int score = -1;

  6.     if (net_eq(sock_net(sk), net) && !ipv6_only_sock(sk)) {
  7.         struct inet_sock *inet = inet_sk(sk);
  8.         /* socket的本地地址不匹配数据包的目的地址 */
  9.         if (inet->inet_rcv_saddr != daddr)
  10.             return -1;
  11.         /* 
  12.         socket的端口不匹配数据包的目的端口
  13.         */
  14.         if (inet->inet_num != hnum)
  15.             return -1;
         /* 匹配family */
  1.         score = (sk->sk_family == PF_INET ? 1 : 0);
  2.         /* 比较socket的目的地址和数据包的源地址 */
  3.         if (inet->inet_daddr) {
  4.             if (inet->inet_daddr != saddr)
  5.                 return -1;
  6.             score += 2;
  7.         }
  8.         /* 比较socket的目的端口和数据包的源端口 */
  9.         if (inet->inet_dport) {
  10.             if (inet->inet_dport != sport)
  11.                 return -1;
  12.             score += 2;
  13.         }
  14.         /* 比较socket bind的interface */
  15.         if (sk->sk_bound_dev_if) {
  16.             if (sk->sk_bound_dev_if != dif)
  17.                 return -1;
  18.             score += 2;
  19.         }
  20.     }
  21.     return score;
  22. }
看这个函数,可能大家会有一点疑问。这个函数的前两个条件是比较socket的源地址和源端口,对于udp socket来说,如果不调用bind,可以使用通用地址和端口,那么这两个比较失败了。怎么办?岂不是使用通用地址和端口的socket,无法收到数据包了?

这需要我们回头看__udp4_lib_lookup这个函数
  1. static struct sock *__udp4_lib_lookup(struct net *net, __be32 saddr,
  2.         __be16 sport, __be32 daddr, __be16 dport,
  3.         int dif, struct udp_table *udptable)
  4. {
  5.     struct sock *sk, *result;
  6.     struct hlist_nulls_node *node;
  7.     unsigned short hnum = ntohs(dport);
  8.     unsigned int hash2, slot2, slot = udp_hashfn(net, hnum, udptable->mask);
  9.     struct udp_hslot *hslot2, *hslot = &udptable->hash[slot];
  10.     int score, badness;

  11.     rcu_read_lock();
  12.     if (hslot->count > 10) {
  13.         /* 当socket个数过多时,使用精确匹配 */
  14.         ...... ......
  15.         /* 如上面所示,要精确匹配地址和端口 */
  16.         result = udp4_lib_lookup2(net, saddr, sport,
  17.                      daddr, hnum, dif,
  18.                      hslot2, slot2);
  19.         if (!result) {
  20.             /* 
  21.             精确匹配失败,则使用通用地址进行匹配。
  22.             利用通用地址的所关联的hash替代原来精确匹配的hash。
  23.             直接使用通用地址代替数据包的的目的地址
  24.             */
  25.  
  26.             hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
  27.             slot2 = hash2 & udptable->mask;
  28.             hslot2 = &udptable->hash2[slot2];
  29.             if (hslot->count < hslot2->count)
  30.                 goto begin; //跳到begin

  31.             result = udp4_lib_lookup2(net, saddr, sport,
  32.                          htonl(INADDR_ANY), hnum, dif,
  33.                          hslot2, slot2);
  34.         }
  35.         rcu_read_unlock();
  36.         return result;
  37.     }
  38. begin:
  39.     result = NULL;
  40.     badness = -1;
  41.     /* 使用compute_score查找匹配的socket */
  42.     sk_nulls_for_each_rcu(sk, node, &hslot->head) {
  43.         score = compute_score(sk, net, saddr, hnum, sport,
  44.                  daddr, dport, dif);
  45.         if (score > badness) {
  46.             result = sk;
  47.             badness = score;
  48.         }
  49.     }
     ...... ...... 
  1. }

compute_score与compute_score2的代码基本相同,不过只有bind了目的地址时,才会进行比较。这样通用的地址和端口也可以匹配了。具体的代码就不在此列出了。

到此,基本上对于kernel如何查找对应数据包的socket已经基本清晰了。对于raw socket,只要匹配了raw socket设置的一些过滤条件,如地址,端口等,那么所有的raw socket均可以收到一个数据包的clone。对于L4 传输层的socket,kernel会遍历socket,找出一个最匹配的socket,匹配条件为协议,端口,地址,interface等。通用匹配的优先级要低于精确匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值