SecPower

来源:百度文库 编辑:神马文学网 时间:2024/05/05 15:02:12


标题: Linux TCP/IP协议栈之Scoket实现分析(三 socket的Listen) [打印本页]

作者: kendo    时间: 2008-1-2 14:27     标题: Linux TCP/IP协议栈之Scoket实现分析(三 socket的Listen)

一、sys_listen

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

[Copy to clipboard] [ - ]CODE:int listen(int sockfd, int backlog);
backlog表示新建连接请求时,最大的未处理的积压请求数。

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

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

[Copy to clipboard] [ - ]CODE:typedef enum {
        SS_FREE = 0,                        /* 套接字未分配                */
        SS_UNCONNECTED,                        /* 套接字未连接        */
        SS_CONNECTING,                        /* 套接字正在处理连接        */
        SS_CONNECTED,                        /* 套接字已连接                */
        SS_DISCONNECTING                /* 套接字正在处理关闭连接 */
} socket_state;
在创建套接字时,被初始化为SS_UNCONNECTED。

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

[Copy to clipboard] [ - ]CODE: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! */
};
还有一个相应的用来进行状态位运算的枚举结构:

[Copy to clipboard] [ - ]CODE: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()函数身上:

[Copy to clipboard] [ - ]CODE:/* 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():

[Copy to clipboard] [ - ]CODE:/*
*        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报文了,那么在此之前,需要先初始化这个表:

[Copy to clipboard] [ - ]CODE: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;
}
在切换了有限状态机状态后,调用了

[Copy to clipboard] [ - ]CODE: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已见过它:

[Copy to clipboard] [ - ]CODE: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表:

[Copy to clipboard] [ - ]CODE: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表:

[Copy to clipboard] [ - ]CODE: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()的包裹:

[Copy to clipboard] [ - ]CODE: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的值。




欢迎光临 SecPower (http://www.skynet.org.cn/) Powered by Discuz! 5.5.0