摘自: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。
- int raw_local_deliver(struct sk_buff *skb, int protocol)
- {
- int hash;
- struct sock *raw_sk;
/* 使用protocol作为hash值,找到对应的raw socket链表 */
- hash = protocol & (RAW_HTABLE_SIZE - 1);
- raw_sk = sk_head(&raw_v4_hashinfo.ht[hash]);
-
- /* If there maybe a raw socket we must check - if not we
- * don't care less
- */
- /* 如果有对应该IP协议的raw socket,则通过raw_v4_input继续匹配 */
- if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))
- raw_sk = NULL;
-
- return raw_sk != NULL;
-
- }
从上面可以看出,kernel中的raw socket是以协议为key,每个协议的raw socket为一个链表。通过协议来确定去哪个raw socket 链表进行匹配。这里可以看出,IP层的协议是第一个raw socket的匹配条件。
- static int raw_v4_input(struct sk_buff *skb, struct iphdr *iph, int hash)
- {
- struct sock *sk;
- struct hlist_head *head;
- int delivered = 0;
- struct net *net;
-
- read_lock(&raw_v4_hashinfo.lock);
- head = &raw_v4_hashinfo.ht[hash];
- if (hlist_empty(head))
- goto out;
/* 查找匹配的raw socket */
- net = dev_net(skb->dev);
- sk = __raw_v4_lookup(net, __sk_head(head), iph->protocol,
- iph->saddr, iph->daddr,
- skb->dev->ifindex);
-
- while (sk) {
- delivered = 1;
- if (iph->protocol != IPPROTO_ICMP || !icmp_filter(sk, skb)) {
- /*
- 如果数据包不是ICMP 或者不是需要过滤的ICMP数据包,
- 那么就clone一个数据包
- */
- struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
-
- /* Not releasing hash */
- /* clone 成功,将clone的数据包传给找到的raw socket */
- if (clone)
- raw_rcv(sk, clone);
- }
- /* 继续匹配下一个raw socket */
- sk = __raw_v4_lookup(net, sk_next(sk), iph->protocol,
- iph->saddr, iph->daddr,
- skb->dev->ifindex);
- }
- out:
- read_unlock(&raw_v4_hashinfo.lock);
- return delivered;
- }
这个函数主要是循环匹配raw socket,可以看出,其遍历了对应协议的raw socket。这就是为什么所有匹配的raw socket都可以收到一份数据包的clone。
下面是真正的raw socket的匹配函数。
- static struct sock *__raw_v4_lookup(struct net *net, struct sock *sk,
- unsigned short num, __be32 raddr, __be32 laddr, int dif)
- {
- struct hlist_node *node;
-
- sk_for_each_from(sk, node) {
- struct inet_sock *inet = inet_sk(sk);
-
- if (net_eq(sock_net(sk), net) && inet->inet_num == num &&
- !(inet->inet_daddr && inet->inet_daddr != raddr) &&
- !(inet->inet_rcv_saddr && inet->inet_rcv_saddr != laddr) &&
- !(sk->sk_bound_dev_if && sk->sk_bound_dev_if != dif))
- goto found; /* gotcha */
- }
- sk = NULL;
- found:
- return sk;
- }
这个函数是真正的匹配函数,去比较了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能收到数据包。
- static struct sock *udp4_lib_lookup2(struct net *net,
- __be32 saddr, __be16 sport,
- __be32 daddr, unsigned int hnum, int dif,
- struct udp_hslot *hslot2, unsigned int slot2)
- {
- ...... ......
-
- udp_portaddr_for_each_entry_rcu(sk, node, &hslot2->head) {
- score = compute_score2(sk, net, saddr, sport,
- daddr, hnum, dif);
- if (score > badness) {
- result = sk;
- badness = score;
- if (score == SCORE2_MAX)
- goto exact_match;
- }
- }
...... ......
- }
该函数调用compute_score2计算socket的匹配程度,也就是得分score,分数最高的即为最佳匹配的socket。
下面看comute_score2的代码
- static inline int compute_score2(struct sock *sk, struct net *net,
- __be32 saddr, __be16 sport,
- __be32 daddr, unsigned int hnum, int dif)
- {
- int score = -1;
-
- if (net_eq(sock_net(sk), net) && !ipv6_only_sock(sk)) {
- struct inet_sock *inet = inet_sk(sk);
- /* socket的本地地址不匹配数据包的目的地址 */
- if (inet->inet_rcv_saddr != daddr)
- return -1;
- /*
- socket的端口不匹配数据包的目的端口
- */
- if (inet->inet_num != hnum)
- return -1;
/* 匹配family */
- score = (sk->sk_family == PF_INET ? 1 : 0);
- /* 比较socket的目的地址和数据包的源地址 */
- if (inet->inet_daddr) {
- if (inet->inet_daddr != saddr)
- return -1;
- score += 2;
- }
- /* 比较socket的目的端口和数据包的源端口 */
- if (inet->inet_dport) {
- if (inet->inet_dport != sport)
- return -1;
- score += 2;
- }
- /* 比较socket bind的interface */
- if (sk->sk_bound_dev_if) {
- if (sk->sk_bound_dev_if != dif)
- return -1;
- score += 2;
- }
- }
- return score;
- }
看这个函数,可能大家会有一点疑问。这个函数的前两个条件是比较socket的源地址和源端口,对于udp socket来说,如果不调用bind,可以使用通用地址和端口,那么这两个比较失败了。怎么办?岂不是使用通用地址和端口的socket,无法收到数据包了?
这需要我们回头看__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);
- 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();
- if (hslot->count > 10) {
- /* 当socket个数过多时,使用精确匹配 */
- ...... ......
- /* 如上面所示,要精确匹配地址和端口 */
- result = udp4_lib_lookup2(net, saddr, sport,
- daddr, hnum, dif,
- hslot2, slot2);
- if (!result) {
- /*
- 精确匹配失败,则使用通用地址进行匹配。
- 利用通用地址的所关联的hash替代原来精确匹配的hash。
- 直接使用通用地址代替数据包的的目的地址
- */
- hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
- slot2 = hash2 & udptable->mask;
- hslot2 = &udptable->hash2[slot2];
- if (hslot->count < hslot2->count)
- goto begin; //跳到begin
-
- result = udp4_lib_lookup2(net, saddr, sport,
- htonl(INADDR_ANY), hnum, dif,
- hslot2, slot2);
- }
- rcu_read_unlock();
- return result;
- }
- begin:
- result = NULL;
- badness = -1;
- /* 使用compute_score查找匹配的socket */
- 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;
- }
- }
...... ......
- }
-
compute_score与compute_score2的代码基本相同,不过只有bind了目的地址时,才会进行比较。这样通用的地址和端口也可以匹配了。具体的代码就不在此列出了。
到此,基本上对于kernel如何查找对应数据包的socket已经基本清晰了。对于raw socket,只要匹配了raw socket设置的一些过滤条件,如地址,端口等,那么所有的raw socket均可以收到一个数据包的clone。对于L4 传输层的socket,kernel会遍历socket,找出一个最匹配的socket,匹配条件为协议,端口,地址,interface等。通用匹配的优先级要低于精确匹配。