一、sys_listen
 

对面向连接的协议,在调用 bind(2)后,进一步调用 listen(2),让套接字进入监听状态: 

int listen(int sockfd, int backlog);


backlog 表示新建连接请求时,最大的未处理的积压请求数。 


 


这里说到让套接字进入某种状态,也就是说,涉及到套接字的状态变迁,前面 create 和bind 时,也遇到过相应的代码。 


sock和 sk 都有相应的状态字段,先来看 sock 的:  

 typedef enum {  

         SS_FREE = 0,                        /*  套接字未分配                */  

         SS_UNCONNECTED,                        /*  套接字未连接        */  

         SS_CONNECTING,                        /*  套接字正在处理连接        */  

         SS_CONNECTED,                        /*  套接字已连接                */  

         SS_DISCONNECTING                /*  套接字正在处理关闭连接 */ } socket_state;


在创建套接字时,被初始化为 SS_UNCONNECTED。 


 


对于面向连接模式的SOCK_TREAM来讲,这样描述状态显然是不够的。 这样, 在sk中, 使用sk_state维护了一个有限状态机来描述套接字的状态: 


 

enum {  

   TCP_ESTABLISHED = 1,  

   TCP_SYN_SENT,  

   TCP_SYN_RECV,  

   TCP_FIN_WAIT1,  

   TCP_FIN_WAIT2,  

   TCP_TIME_WAIT,  

   TCP_CLOSE,  

   TCP_CLOSE_WAIT,  

   TCP_LAST_ACK,  

   TCP_LISTEN,  

   TCP_CLOSING,         /* now a valid state */  

   

   TCP_MAX_STATES /* Leave at the end! */  

 };


还有一个相应的用来进行状态位运算的枚举结构: 


enum {  

   TCPF_ESTABLISHED = (1 << 1),  

   TCPF_SYN_SENT  = (1 << 2),  

   TCPF_SYN_RECV  = (1 << 3),  

   TCPF_FIN_WAIT1 = (1 << 4),  

   TCPF_FIN_WAIT2 = (1 << 5),  

   TCPF_TIME_WAIT = (1 << 6),  

   TCPF_CLOSE     = (1 << 7),  

   TCPF_CLOSE_WAIT = (1 << 8),  

   TCPF_LAST_ACK  = (1 << 9),  

   TCPF_LISTEN    = (1 << 10),  

   TCPF_CLOSING   = (1 << 11)   

 };


值得一提的是,sk 的状态不等于 TCP的状态,虽然 sk 是面向协议栈,但它的状态并不能同 TCP状态一一直接划等号。虽然这些状态值都用 TCP-XXX 来表式,但是只是因为 TCP协议状态非常复杂。sk 结构只是利用它的一个子集来抽像描述而已。 


 


同样地,操作码 SYS_LISTEN的任务会落到 sys_listen()函数身上: 


/* Maximum queue length specifiable by listen.  */    
#define SOMAXCONN        128 
 int sysctl_somaxconn = SOMAXCONN;  

   

 asmlinkage long sys_listen(int fd, int backlog)  

 {  

         struct socket *sock;  

         int err;  

           

         if ((sock = sockfd_lookup(fd, &err)) != NULL) {  

                 if ((unsigned) backlog > sysctl_somaxconn)  

                         backlog = sysctl_somaxconn;  

   

                 err = security_socket_listen(sock, backlog);  

                 if (err) {  

                         sockfd_put(sock);  

                         return err;  

                 }  

   

                 err=sock->ops->listen(sock, backlog);  

                 sockfd_put(sock);  

         }  

         return err;  

 }



 


同样地,函数会最终转向协议簇的 listen 函数,也就是 inet_listen(): 


 

/*  

 *        Move a socket into listening state.  

 */  

 int inet_listen(struct socket *sock, int backlog)  

 {  

         struct sock *sk = sock->sk;  

         unsigned char old_state;  

         int err;  

   

         lock_sock(sk);  

   

         err = -EINVAL;  

         /* 在 listen 之前,sock 必须为未连接状态,且只有 SOCK_STREAM 类型,才有 listen(2)*/  

         if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)  

                 goto out;



        /*  临时保存状态机状态 */ 


        old_state = sk->sk_state;         /*  只有状态机处于 TCP_CLOSE  或者是 TCP_LISTEN  这两种状态时,才可能对其调用listen(2)  ,这个判断证明了 listen(2)是可以重复调用地(当然是在转向 TCP_LISTEN 后没有再进行状态变迁*/ 


 

if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))  

                 goto out;  

   

         /*  如果接口已经处理 listen 状态,只修改其 max_backlog,否则先调用 tcp_listen_start,继续设置协议的 listen 状态  */  

         if (old_state != TCP_LISTEN) {  

                 err = tcp_listen_start(sk);  

                 if (err)  

                         goto out;  

         }  

         sk->sk_max_ack_backlog = backlog;  

         err = 0;  

   

 out:  

         release_sock(sk);  

         return err;  

 }


inet_listen 函数在确认 sock->state 和 sk->sk_state 状态后,会进一步调用 tcp_listen_start 函数,最且最后设置 sk_max_ack_backlog  。 


 


tcp 的 tcp_listen_start 函数,完成两个重要的功能,一个是初始化 sk 的一些相关成员变量,另一方面是切换有限状态机的状态。 sk_max_ack_backlog表示监听时最大的 backlog 数量,它由用户空间传递的参数决定。而 sk_ack_backlog表示当前的的 backlog数量。 


 


当 tcp 服务器收到一个 syn 报文时,它表示了一个连接请求的到达。内核使用了一个 hash 表来维护这个连接请求表: 


 

struct tcp_listen_opt  

 {  

         u8                        max_qlen_log;        /* log_2 of maximal queued SYNs */  

         int                        qlen;  

         int                        qlen_young;  

         int                        clock_hand;  

         u32                        hash_rnd;  

         struct open_request        *syn_table[TCP_SYNQ_HSIZE];  

 };


syn_table,  是open_request结构,就是连接请求表,连中的最大项,也就是最大允许的 syn 报文的数量,由 max_qlen_log 来决定。当套接字进入 listen 状态,也就是说可以接收 syn 报文了,那么在此之前,需要先初始化这个表:  



int tcp_listen_start(struct sock *sk)  

 {  

         struct inet_sock *inet = inet_sk(sk);                //获取 inet结构指针  

         struct tcp_sock *tp = tcp_sk(sk);                //获取协议指针  

         struct tcp_listen_opt *lopt;  

           

         //初始化 sk 相关成员变量  

         sk->sk_max_ack_backlog = 0;  

         sk->sk_ack_backlog = 0;  

           

         tp->accept_queue = tp->accept_queue_tail = NULL;  

         rwlock_init(&tp->syn_wait_lock);  

         tcp_delack_init(tp);  

           

         //初始化连接请求 hash 表  

         lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL);  

         if (!lopt)  

                 return -ENOMEM;  

   

         memset(lopt, 0, sizeof(struct tcp_listen_opt));  

         //初始化 hash 表容量,最小为 6,其实际值由 sysctl_max_syn_backlog 决定  

         for (lopt->max_qlen_log = 6; ; lopt->max_qlen_log++)  

                 if ((1 << lopt->max_qlen_log) >= sysctl_max_syn_backlog)  

                         break;  

         get_random_bytes(&lopt->hash_rnd, 4);  

   

         write_lock_bh(&tp->syn_wait_lock);  

         tp->listen_opt = lopt;  

         write_unlock_bh(&tp->syn_wait_lock);  

   

         /* There is race window here: we announce ourselves listening,  

          * but this transition is still not validated by get_port().  

          * It is OK, because this socket enters to hash table only  

          * after validation is complete.  

          */


         /*  修改状态机状态,表示进入 listen 状态,根据作者注释,当宣告自己进入 listening 状态后,但是这个状态转换并没有得到 get_port 的确  认。所以需要调用 get_port()函数。但是对于一点,暂时还没有完全搞明白,只有留待后面再来分析它 */ 


sk->sk_state = TCP_LISTEN;  

         if (!sk->sk_prot->get_port(sk, inet->num)) {  

                 inet->sport = htons(inet->num);  

                  sk_dst_reset(sk);  

                 sk->sk_prot->hash(sk);  

   

                 return 0;  

         }  

   

         sk->sk_state = TCP_CLOSE;  

         write_lock_bh(&tp->syn_wait_lock);  

         tp->listen_opt = NULL;  

         write_unlock_bh(&tp->syn_wait_lock);  

         kfree(lopt);  

         return -EADDRINUSE;  

 } 

   

 在切换了有限状态机状态后,调用了  

 sk->sk_prot->hash(sk);


也就是 tcp_v4_hash()函数。这里涉到到另一个 hash 表:TCP监听 hash 表。 


 


二、TCP监听 hash表


所谓 TCP 监听表,指的就内核维护“当前有哪些套接字在监听”的一个表,当一个数据包进入 TCP栈的时候,内核查询这个表中对应的 sk,以找到相应的数据  结构。 (因为 sk 是面向网络栈调用的,找到了 sk,就找到了 tcp_sock,就找到了 inet_sock,就找到了 sock,就找到了 fd……就到了组  织了)。 


 


TCP所有的 hash 表都用了tcp_hashinfo来封装,前面分析 bind已见过它: 

extern struct tcp_hashinfo {  

                  ……  

         /* All sockets in TCP_LISTEN state will be in here.  This is the only  

          * table where wildcard'd TCP sockets can exist.  Hash function here  

          * is just local port number.  

          */  

         struct hlist_head __tcp_listening_hash[TCP_LHTABLE_SIZE];  

   

                  ……  

         spinlock_t __tcp_portalloc_lock;  

 } tcp_hashinfo;  

   

 #define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash)

 


函数 tcp_v4_hash 将一个处理监听状态下的 sk 加入至这个 hash 表: 

static void tcp_v4_hash(struct sock *sk)  

 {  

         if (sk->sk_state != TCP_CLOSE) {  

                 local_bh_disable();                 __tcp_v4_hash(sk, 1);  

                 local_bh_enable();  

         }  

 }




因为__tcp_v4_hash 不只用于监听 hash 表,它也用于其它 hash 表,其第二个参数 listen_possible 为真的时候,表示处理的是监听 hash表: 


static __inline__ void __tcp_v4_hash(struct sock *sk, const int listen_possible)  

 {  

         struct hlist_head *list;  

         rwlock_t *lock;  

   

         BUG_TRAP(sk_unhashed(sk));  

         if (listen_possible && sk->sk_state == TCP_LISTEN) {  

                 list = &tcp_listening_hash[tcp_sk_listen_hashfn(sk)];  

                 lock = &tcp_lhash_lock;  

                 tcp_listen_wlock();  

         } else {  

                                 ……  

         }  

         __sk_add_node(sk, list);  

         sock_prot_inc_use(sk->sk_prot);  

         write_unlock(lock);  

         if (listen_possible && sk->sk_state == TCP_LISTEN)  

                 wake_up(&tcp_lhash_wait);  

}

 else 中的部份用于另一个 hash 表,暂时不管它。代表很简单,如果确认是处理的是监听 hash 表。则先根据 sk计算一个 hash 值,在hash 桶中找到入口。再调用__sk_add_node 加入至该 hash 链。  

   

 tcp_sk_listen_hashfn()函数事实上是 tcp_lhashfn 的包裹,前面已经分析过了。  

   

 __sk_add_node()函数也就是一个简单的内核 hash处理函数 hlist_add_head()的包裹:  

 static __inline__ void __sk_add_node(struct sock *sk, struct hlist_head *list)  

 {  

         hlist_add_head(&sk->sk_node, list);  

 }


小结 


 


一个套接字的 listen,主要需要做的工作有以下几件: 


1、初始化 sk 相关的成员变量,最重要的是 listen_opt,也就是连接请求 hash 表。 


2、将 sk 的有限状态机转换为 TCP_LISTEN,即监听状态; 


3、将 sk 加入监听 hash表; 


4、设置允许的最大请求积压数,也就是 sk 的成员 sk_max_ack_backlog 的值。