linux内核网络监听哈希表介绍

    技术2022-05-11  50

    以下基于linux内核 2.4.0 源码(转载请注明出处)

    松哥 jccz_zys@tom.com

     

        网络通信过程中,服务器必然提供监听socket响应客户端连接请求,也必然提供连接socket与客户端进行交互。一台主机上有不止一个的socket服务器,如ftptelnet服务器等,他们初始都处于监听状态,等待连接请求的到来。linux中为了管理这两类socket提供了两个哈希链表:tcp_listening_hashtcp_ehash,下面主要分析下监听哈希表,顺带说下连接hash表。

    一、链表定义

        include/net/tcp.h中定义了tcp_hashinfo结构,包含了tcp协议所涉及到的一些哈希表信息,这两个哈希表以tcp_hashinfo的成员形式出现,如下所示:

       

        extern struct tcp_hashinfo {

           /* 结构成员用于tcp状态迁移图中的相关状态:

            *          TCP_ESTABLISHED <= sk->state < TCP_CLOSE

            * 前半部份用于非超时状态,后半部份仅用于超时状态

            */

           struct tcp_ehash_bucket *__tcp_ehash;

     

           /* tcp的绑定哈希表,用于快速bind/connect*/

           struct tcp_bind_hashbucket *__tcp_bhash;

     

           int __tcp_bhash_size;

           int __tcp_ehash_size;

     

           /* 所有在监听状态的socket都存放在下面的哈希表中,其中键key为本地监听端口*/

           struct sock *__tcp_listening_hash[TCP_LHTABLE_SIZE];

     

           /*下面的成员缓冲区对齐*/

           rwlock_t __tcp_lhash_lock /*监听哈希表访问锁*/

                  __attribute__((__aligned__(SMP_CACHE_BYTES)));

           atomic_t __tcp_lhash_users;

           wait_queue_head_t __tcp_lhash_wait;

           /*上述三个成员主要用于用户通过/proc/ne获取监听套接字(间接访问监听哈希表时)信息时异步处理*/

           spinlock_t __tcp_portalloc_lock;

        } tcp_hashinfo;

        接下来定义了一些宏以简化引用:

        #define tcp_ehash   (tcp_hashinfo.__tcp_ehash) /*连接哈希链表*/

        #define tcp_bhash   (tcp_hashinfo.__tcp_bhash) /*地址bind哈希链表*/

        #define tcp_ehash_size   (tcp_hashinfo.__tcp_ehash_size) /*连接哈希链表长度*/

        #define tcp_bhash_size   (tcp_hashinfo.__tcp_bhash_size) /*bind哈希链表长度*/

        #define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash) /*监听哈希链表*/

        #define tcp_lhash_lock   (tcp_hashinfo.__tcp_lhash_lock)  /*监听哈希链表访问锁*/

        #define tcp_lhash_users  (tcp_hashinfo.__tcp_lhash_users) /*引用计数*/

        #define tcp_lhash_wait   (tcp_hashinfo.__tcp_lhash_wait) /*异步访问时的等待队列*/

        #define tcp_portalloc_lock (tcp_hashinfo.__tcp_portalloc_lock) /*SMP用途??*/

       

    二、链表的初始化

        tcp_listening_hashtcp_ehash的初始化在net/ipv4/tcp_ipv4.c,其中定义了全局变量tcp_hashinfo

        并赋初值,如下:

       

        /*

        * 所有的成员都要初始化,以防止gcc- 2.7.2 .3编译错误

        */

        struct tcp_hashinfo __cacheline_aligned tcp_hashinfo = {

           __tcp_ehash:          NULL,

           __tcp_bhash:          NULL,

           __tcp_bhash_size:     0, /*初始大小为0*/

           __tcp_ehash_size:     0,

           __tcp_listening_hash: { NULL, },

           __tcp_lhash_lock:     RW_LOCK_UNLOCKED, /*读写锁*/

           __tcp_lhash_users:    ATOMIC_INIT(0),

           __tcp_lhash_wait:

             __WAIT_QUEUE_HEAD_INITIALIZER(tcp_hashinfo.__tcp_lhash_wait), //初始化等待队列

           __tcp_portalloc_lock: SPIN_LOCK_UNLOCKED

        };  

    三、链表的元素增加

        服务器监听的函数调用过程如下:

        sys_listen-->inet_listen-->tcp_listen_start-->tcp_v4_hash-->__tcp_v4_hash 

        其中,在tcp_listen_start中,将监听socketaccept_queue队列以及内核sock结构的

        tcp_opt成员tp_pinfo.af_tcp所指向的listen_opt初始化。

        listen_optstruct tcp_listen_opt类型,定义在include/net/tcp.h中:

       

        struct tcp_listen_opt

        {

           u8                  max_qlen_log;       /* SYN包队列的最大长度 */

           int                 qlen;              /*当前实际长度*/

           int                 qlen_young;

           int                 clock_hand;    /**/

           /*syn_table用于tcp三次握手协议时保留SYN包请求,SYN Cookie配合可用于防止SYN flood攻击*/

           struct open_request *syn_table[TCP_SYNQ_HSIZE];

       };

       下面来看看__tcp_v4_hash函数,net/ipv4/tcp_ipv4.c

      

       static __inline__ void __tcp_v4_hash(struct sock *sk)

       {

           struct sock **skp;/*指向哈希表表项地址,其中每个哈希表项为链表.即指向链首指针的地址*/

           rwlock_t *lock;

     

           BUG_TRAP(sk->pprev==NULL);

           if(sk->state == TCP_LISTEN) {/*注意在tcp_listen_start函数中已经将sock状态置为TCP_LISTEN*/

                  skp = &tcp_listening_hash[tcp_sk_listen_hashfn(sk)];/*注意:tcp_sk_listen_hashfn封装了tcp_lhashfn*/

                  lock = &tcp_lhash_lock;

                  tcp_listen_wlock();

           } else {/*否则加入到连接哈希表,如果代码执行到这里,一般此时sockTCP_ESTABLISHED*/

                  skp = &tcp_ehash[(sk->hashent = tcp_sk_hashfn(sk))].chain;

                  lock = &tcp_ehash[sk->hashent].lock;

                  write_lock(lock);

           }

           /*sk->next:每个socketnext都指向后一个有相同hash(冲突)sock结构

           *每个socketpprev都指向前一个有相同hash值的sock结构*/

           if((sk->next = *skp) != NULL) /*注意此时skp是获得的链表头的地址,所以此处是"*skp"来引用*/

                  (*skp)->pprev = &sk->next; /*双向链表,pprev指向前一个sock结构(即刚加入的sock结构的next地址)*/

           *skp = sk;/*sk加入到链首*/

           sk->pprev = skp;

           sock_prot_inc_use(sk->prot);/*修改prot->stats[].inuse计数*/

           write_unlock(lock);

           if (sk->state == TCP_LISTEN)

                  wake_up(&tcp_lhash_wait); /*唤醒等待队列*/

        }

    四、哈希链表元素的删除   

       监听链表的元素的释放是在监听套接子关闭时处理。函数调用链如下:

       close-->(参考《情景阅读》)...-->inet_release-->tcp_close-->tcp_set_state-->tcp_unhash

       tcp_unhash代码在net/ipv4/tcp_ipv4.c,如下:

      

       void tcp_unhash(struct sock *sk)

       {

           rwlock_t *lock;

     

           if (sk->state == TCP_LISTEN) { /*监听套接字*/

                  local_bh_disable(); //

                  tcp_listen_wlock(); /*循环调度直至外部改变tcp_lhash_users0是才往下执行*/

                  lock = &tcp_lhash_lock;

           } else {  /*其他状况时脱离tcp连接哈希桶*/

                  struct tcp_ehash_bucket *head = &tcp_ehash[sk->hashent];

                  lock = &head->lock;

                  write_lock_bh(&head->lock);//加锁处理

           }

     

           if(sk->pprev) {

                  if(sk->next)

                         sk->next->pprev = sk->pprev; /*sk脱链*/

                  *sk->pprev = sk->next;/*sk->pprev指向前一节点的next地址,所以此处是将前一节点的next指针指向sk节点的后一节点*/

                  sk->pprev = NULL;

                  sock_prot_dec_use(sk->prot);

           }

           ...

        }

    五、哈希函数

        代码在include/net/Tcp.h,如下:

        static __inline__ int tcp_lhashfn(unsigned short num)

        {

           return num & (TCP_LHTABLE_SIZE - 1);

        }

       

        可见此哈希函数仅根据请求的端口来做哈希,监听哈希表的长度是TCP_LHTABLE_SIZE,32,定义如下:

        #define TCP_LHTABLE_SIZE     32    /* Yes, really, this is all you need. */

        注意:tcp_sk_listen_hashfn封装了tcp_lhashfn

       

    六、哈希表的作用

        内核中,每建立一个监听套接字,就将套接字挂入监听哈希表的某个表项链表中。则在内核收到连接请求的SYNACK等包传到TCP层时,要根据请求包的请求连接地址与端口号到哈希表中查找对应的服务器监听套接字是否存在,代码在net/ipv4/tcp_ipv4.c,如下:

        int tcp_v4_rcv(struct sk_buff *skb, unsigned short len)

        {

        ...

        /*__tcp_v4_lookup函数就是在监听哈希表和连接哈希桶中查找请求的服务器套接字*/

        sk = __tcp_v4_lookup(skb->nh.iph->saddr, th->source,

                          skb->nh.iph->daddr, ntohs(th->dest), tcp_v4_iif(skb));

        ...

        }

        __tcp_v4_lookup函数在同一文件下,代码如下:

        static inline struct sock *__tcp_v4_lookup(u32 saddr, u16 sport,

                                          u32 daddr, u16 hnum, int dif)

        {

           struct sock *sk;

           /*先在连接哈希桶中找*/

           sk = __tcp_v4_lookup_established(saddr, sport, daddr, hnum, dif);

     

           if (sk)  /*找到则返回*/

                  return sk;

           /*否则在监听哈希表中找*/        

           return tcp_v4_lookup_listener(daddr, hnum, dif);

        }

        我们来看tcp_v4_lookup_listener函数,其中入参有目标地址、目标端口

        __inline__ struct sock *tcp_v4_lookup_listener(u32 daddr, unsigned short hnum, int dif)

        {

           struct sock *sk;

     

           read_lock(&tcp_lhash_lock);

           sk = tcp_listening_hash[tcp_lhashfn(hnum)];/*根据目标端口找到哈希表项,为链表首节点指针*/

           if (sk) { /*如果链首不空*/

                  if (sk->num == hnum && /*如果端口相等*/

                      sk->next == NULL && /*如果仅此节点*/

                      (!sk->rcv_saddr || sk->rcv_saddr == daddr) && /*套接字绑定了IPv4地址,且此地址为请求的连接地址*/

                      !sk->bound_dev_if) /*如果存在绑定的设备接口*/

                         goto sherry_cache; //则直接返回

                  sk = __tcp_v4_lookup_listener(sk, daddr, hnum, dif); /*否则遍历链表,按照地址、端口、设备接口索引查找*/

           }

           if (sk) {/*找到则直接返回,否则sk=NULL*/

        sherry_cache:

                  sock_hold(sk);

           }

           read_unlock(&tcp_lhash_lock); //解锁

           return sk;

        }

       

        关于连接的哈希桶的功能也大致如此,主要用于保存建立连接的套接字,用于在接受到网络包后,找到对应

        的处理套接字。

       

    七、结束语

            内核中的很多地方都采用了这种简单的哈希表,其中冲突的解决方法就是表项采用链表方式。在tcp的实现中,还引入了其它哈希表,bind哈希表tcp_bhash等等。通过这些哈希表,内核可以简洁、方便、快速地查询这些哈希表中的目标套接字是否存在。

     

    最新回复(0)