对udp dns的一次思考

  目前昨天查一个线上问题:“”dns服务器在我们的设备, 有大量的终端到设备上请求解析域名,但是一直是单线程,dns报文处理不过来”, 然而设备是多核,dns服务器一直不能利用多核资源,所以能不能使用多线程进行处理呢?

  udp不像tcp那样,udp没有连接的概念,也就是没有通过建立多个连接来提高对dns服务器并发访问,然而在多核环境下那就只能通过多线程来访问一个共享的udp socket,但是还是一个socket , 会涉及到多线程抢占资源问题。

  来看一下内核协议栈udp收到包代码:根据以前分析tcpip协议栈文章可以知道,报文在内核协议栈流程大约如下:

  •   netif_receive_skb
  •   pt_prev->func(skb, skb->dev, pt_prev, orig_dev);   调用ip_rcv  arp_rcv处理
  •         ip_rcv  ------- ip_rcv_finish
  •         根据路由选项是否local_input还是ip_forward准发
  •        ip_local_deliver(期间会涉及到ip 分片重组)
  •        ip_local_deliver_finish   (涉及到raw socket 收包问题)------>ret = ipprot->handler(skb);//这里面会进入udp tcp传输层
  •      udp_rcv  //收发包处理位置----->__udp4_lib_rcv
  •    sk = __udp4_lib_lookup; ////根据报文的端口号和目的端口号查询udptable,寻找应该接收该数据包的传输控制块
  •    udp_queue_rcv_skb//涉及到内核态和 用户态抢占socket的问题, 所以有收包队列 以及 sk_add_backlog队列
  •   sk->sk_data_ready(sk, skb_len) wake up 唤醒等待队列,
  •    如果找不到对应监听socket 则icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); 回复不可达 这个和tcp回复的rst不一样!!!!

也就是 报文送到那个socket是由__udp4_lib_lookup这个函数做出选择, 选择的依据是 ip 端口号 接口来进行处理选择对应的socket,对于

dhcp dns服务器来说一般不会绑定接口,所以一般就是 设置ip  udp.port, 所以内核选择socket的时候一般也是通过对比ip port来查找。通过找出匹配层度最高的socket作为收包sk。

如果要是允许有多个socket呢??

那么不就是可以通过轮询选择或者hash选择出对应的socket 吗!!!!!!!! 所以在linux 3.9内核版本后面增加reuseport,允许多个socket绑定同一个ip port, 通过hash散列在桶里面,

后面就允许多线程/多进程服务器的每个线程都bind 同一个端口,并且最终每个线程拥有一个独立的socket,而不是所有线程都访问一个socket;没有reuseport这个patch的话,这么做的后果就是服务器会报出一个类似“地址/端口被占用的”错误信息。

socket  bind ip port时 会调用get_port  计算是否存在ip port存在冲突, linux 3.9patch中对hash 以及计算方式加入reuseport,可以允许多个socket bind同样的ip port。

同一个客户端的数据总是分配给同一个 udp_sock。so!! 在写 UDP server 的时候,为了提高处理能力,可以起多个线程,每个线程读写自己的 UDP socket

顺便看一看udp 怎样查找port:

int udp_v4_get_port(struct sock *sk, unsigned short snum)
{
    unsigned int hash2_nulladdr =
        udp4_portaddr_hash(sock_net(sk), htonl(INADDR_ANY), snum);
    unsigned int hash2_partial =
        udp4_portaddr_hash(sock_net(sk), inet_sk(sk)->inet_rcv_saddr, 0);

    /* precompute partial secondary hash */
    udp_sk(sk)->udp_portaddr_hash = hash2_partial;
    return udp_lib_get_port(sk, snum, ipv4_rcv_saddr_equal, hash2_nulladdr);
}

UDP 对于porttable维护一个是一port 进行hash  一个是以sip +port(port=0) 进行hash。

struct udp_sock {
    /* inet_sock has to be the first member */
    struct inet_sock inet;
#define udp_port_hash        inet.sk.__sk_common.skc_u16hashes[0]
#define udp_portaddr_hash    inet.sk.__sk_common.skc_u16hashes[1]

}

UDP 协议的主要数据结构是两张 hash 表,指向 UDP 协议控制块 struct udp_sock。其中 hash1 以 port 为 key,

hash2 以 IP+port (port=0)为 key 但是后续会使用udp_portaddr_hash  ^port 进行hash查找,实际上也就是ip+port表 ;

所以一开始看的udp_portaddr_hash 是以ip+port=0 进行hash计算有点懵逼!!!

  1 int udp_lib_get_port(struct sock *sk, unsigned short snum,
  2              int (*saddr_comp)(const struct sock *sk1,
  3                        const struct sock *sk2,
  4                        bool match_wildcard),
  5              unsigned int hash2_nulladdr)
  6 {
  7     struct udp_hslot *hslot, *hslot2;
  8     struct udp_table *udptable = sk->sk_prot->h.udp_table;
  9     int    error = 1;
 10     struct net *net = sock_net(sk);
 11 
 12     if (!snum) {
     ............................
...........................
51 } else {
//以portnum 为key查找散列表
52 hslot = udp_hashslot(udptable, net, snum); 53 spin_lock_bh(&hslot->lock); 54 if (hslot->count > 10) {//当端口hash表冲突链长度大于10时,启用二元组hash查询 55 int exist; 56 unsigned int slot2 = udp_sk(sk)->udp_portaddr_hash ^ snum;//相当于ip+port 选择key值 57 58 slot2 &= udptable->mask; 59 hash2_nulladdr &= udptable->mask; 60 61 hslot2 = udp_hashslot2(udptable, slot2); 62 if (hslot->count < hslot2->count)//如果hslot2项的冲突数目比hslot还多,那么查找hash2表inuse2是不划算的,返回直接查找hash1表inuse 63 goto scan_primary_hash; 64 65 exist = udp_lib_lport_inuse2(net, snum, hslot2, 66 sk, saddr_comp);//使用udp_lib_lport_inuse2()查找是否有匹配项;如果没有找到,则使用新的键值hash2_nulladdr,即[INADDR_ANY, snum]从hash2中取出表项 67 if (!exist && (hash2_nulladdr != slot2)) { 68 hslot2 = udp_hashslot2(udptable, hash2_nulladdr); 69 exist = udp_lib_lport_inuse2(net, snum, hslot2, 70 sk, saddr_comp);//再使用udp_lib_lport_inuse2()查找是否有匹配 71 } 72 if (exist) 73 goto fail_unlock; 74 else 75 goto found; 76 } 77 scan_primary_hash://scan_primary_hash代码段是在hash表的hslot项中查找,只有当在hash2中查找更费时时才会执行 78 if (udp_lib_lport_inuse(net, snum, hslot, NULL, sk, 79 saddr_comp, 0)) 80 goto fail_unlock; 81 } 82 found://执行sk的插入操作 83 inet_sk(sk)->inet_num = snum; 84 udp_sk(sk)->udp_port_hash = snum; 85 udp_sk(sk)->udp_portaddr_hash ^= snum; 86 if (sk_unhashed(sk)) { 87 if (sk->sk_reuseport && 88 udp_reuseport_add_sock(sk, hslot, saddr_comp)) { 89 inet_sk(sk)->inet_num = 0; 90 udp_sk(sk)->udp_port_hash = 0; 91 udp_sk(sk)->udp_portaddr_hash ^= snum; 92 goto fail_unlock; 93 } 94 95 sk_add_node_rcu(sk, &hslot->head); 96 hslot->count++; 97 sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); 98 99 hslot2 = udp_hashslot2(udptable, udp_sk(sk)->udp_portaddr_hash); 100 spin_lock(&hslot2->lock); 101 if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport && 102 sk->sk_family == AF_INET6) 103 hlist_add_tail_rcu(&udp_sk(sk)->udp_portaddr_node, 104 &hslot2->head); 105 else 106 hlist_add_head_rcu(&udp_sk(sk)->udp_portaddr_node, 107 &hslot2->head); 108 hslot2->count++; 109 spin_unlock(&hslot2->lock); 110 } 111 sock_set_flag(sk, SOCK_RCU_FREE); 112 error = 0; 113 fail_unlock: 114 spin_unlock_bh(&hslot->lock); 115 fail: 116 return error; 117 }

 如果snum==0,即没有绑定本地端口,此时执行if部分代码段,这种情况一般发生在客户端使用socket,此时内核会为它选择一个未使用的端口:

udptable中的hash公司为 jhash_1word((__force u32)saddr, net_hash_mix(net)) ^ port------>(num + net_hash_mix(net)) & mask简写一下, net_hash_mix(net)返回为0

所以大约就是sip^port; 具体就不细看hash函数了。也许不是这样的。。。。

if (!snum) {
        /*
 如果snum==0,即没有绑定本地端口,此时执行if部分代码段,这种情况一般发生在客户端使用socket,
 此时内核会为它选择一个未使用的端口:

udptable中的hash公司为 jhash_1word((__force u32)saddr, net_hash_mix(net)) ^ port---
--->(num + net_hash_mix(net)) & mask简写一下, net_hash_mix(net)返回为0

所以大约就是sip^port; 具体就不细看hash函数了。也许不是这样的。。。。

声明bitmap数组,大小为udp_table每个键值最多存储的表项,即最大端口号/哈希表大小。
端口号的值规定范围是1-65536,而哈希表一般大小是256,因此实际分配bitmap[8]。
low和high代表可用本地端口的下限和上限;remaining代表位于low和high间的端口号数目。
用随机值rand生成first,注意它是unsigned short类型,16位,表示起始查找位置;last表示终止查找位置,
first和last相差表大小保证了所有键值都会被查询一次(表的大小为偶数,所以用奇数防止落在同一个桶里面)。
随机值rand最后处理成哈希表大小的奇数倍,
之所以要是奇数倍,是为了保证哈希到同一个键值的所有端口号都能被遍历,
可以试着1开始,每次+2和每次+3,直到回到1,所遍历的数有哪些不同,
        */
        int low, high, remaining;
        unsigned int rand;
        unsigned short first, last;
        //hash表达小一般是256
        DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);//PORTS_PER_CHAIN (MAX_UDP_PORTS / UDP_HTABLE_SIZE_MIN)----65536/256

        inet_get_local_port_range(net, &low, &high);
        remaining = (high - low) + 1;

        rand = prandom_u32();
        first = reciprocal_scale(rand, remaining) + low;
        /*
         * force rand to be an odd multiple of UDP_HTABLE_SIZE
         */
        rand = (rand | 1) * (udptable->mask + 1);
        last = first + udptable->mask + 1;
        do {/* 使用first值作为端口号,从udptable的hash表中找到hslot项,重置bitmap数组全0,
        调用函数udp_lib_lport_inuse()遍历hslot项的所有表项,将所有已经使用的sport对应于bitmap的位置置1。*/
            hslot = udp_hashslot(udptable, net, first);
            bitmap_zero(bitmap, PORTS_PER_CHAIN);
            spin_lock_bh(&hslot->lock);
            udp_lib_lport_inuse(net, snum, hslot, bitmap, sk,
                        saddr_comp, udptable->log);

            snum = first;
            /*
             * Iterate on all possible values of snum for this hash.
             * Using steps of an odd multiple of UDP_HTABLE_SIZE
             * give us randomization and full range coverage.
             此时bitmap中包含了所有哈希到hslot的端口的使用情况,下面要做的就是从first位置开始,
             每次递增rand(保证哈希值不变),查找符合条件的端口:端口在low~high的可用范围内;
             端口还没有被占用。
             do{}while循环的判断条件snum!=first和snum+=rand一起保证了所有哈希到hslot的端口号都会被遍历到。
             如果找到了可用端口号,即跳出,执行插入sk的操作,否则++first,
             查找下一个键值,直到fisrt==last,表明所有键值都已轮循一遍,仍没有结果,则退出
             */
            do {
                if (low <= snum && snum <= high &&
                    !test_bit(snum >> udptable->log, bitmap) &&
                    !inet_is_local_reserved_port(net, snum))
                    goto found;
                snum += rand;
            } while (snum != first);
            spin_unlock_bh(&hslot->lock);
        } while (++first != last);
        goto fail;

这部分是查找网上分析结果!!还没有仔细研究他的这个hash算法

问题2:由于udp是包模式!! 每次只能copy一个包!!!能不能copy多个!!!目前是可以的!!

可以使用recvmmsg来继续降低系统调用的开销。recvmmsg是一个批量接口,它可以从socket里一次读出多个udp数据包,不像recvfrom那样一次只能读一个。如果客户端多、请求量大的话,recvmmsg的批量读就很有优势了。

就像hash表一样存在冲突!!! 也就是读取的多个包可能不是一个客户端发过来的数据!!!! 那怎样区分多个客户端发来的数据呢??在udp数据部分来区分??还是??

看内核代码应该是通过rcvmmsg的控制信息区分。。。。。读取数据包文的时候也会带上控制信息这样就可以知道对端了

原文地址:https://www.cnblogs.com/codestack/p/12774946.html