当前位置: 首页 > news >正文

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

原文地址:TCP/IP学习(29)——kernel如何选择socket接收数据 作者:GFree_Wind

本文的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等。通用匹配的优先级要低于精确匹配。

相关文章:

  • Core Data 的简单使用
  • 配置防盗链,访问控制
  • 过完年想要元气满满?赶紧看看这些VR AR大事件回个血
  • RocketMq部署(四)
  • oracle时间操作结合to_char和to_date使用
  • VS2017 Debug断点后显示UTF8字符串
  • 拥抱.NET Core系列:MemoryCache 缓存选项
  • 树莓派打造无线扫描仪.
  • 用“世界上最好的编程语言”制作的敲诈者木马揭秘
  • Docker容器部署时区问题的坑
  • 【转】Tesla Model S的设计失误
  • JavaScript 奇技淫巧
  • 测试经验1_2016-2017
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • EF Core:一统SQL和NoSQL数据库
  • Android Studio:GIT提交项目到远程仓库
  • Asm.js的简单介绍
  • create-react-app做的留言板
  • Electron入门介绍
  • JavaScript设计模式系列一:工厂模式
  • JS实现简单的MVC模式开发小游戏
  • Laravel核心解读--Facades
  • leetcode388. Longest Absolute File Path
  • Linux各目录及每个目录的详细介绍
  • mongo索引构建
  • MYSQL 的 IF 函数
  • October CMS - 快速入门 9 Images And Galleries
  • React-生命周期杂记
  • Vue实战(四)登录/注册页的实现
  • 检测对象或数组
  • 前言-如何学习区块链
  • 如何实现 font-size 的响应式
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 为视图添加丝滑的水波纹
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • Semaphore
  • 长三角G60科创走廊智能驾驶产业联盟揭牌成立,近80家企业助力智能驾驶行业发展 ...
  • 第二十章:异步和文件I/O.(二十三)
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • #define与typedef区别
  • #pragma预处理命令
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • $GOPATH/go.mod exists but should not goland
  • (a /b)*c的值
  • (C语言)球球大作战
  • (libusb) usb口自动刷新
  • (草履虫都可以看懂的)PyQt子窗口向主窗口传递参数,主窗口接收子窗口信号、参数。
  • (二)linux使用docker容器运行mysql
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)php新闻发布平台 毕业设计 141646
  • (附源码)springboot工单管理系统 毕业设计 964158
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包