<b>sys_listen</b>
對面向連接配接的協定,在調用 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! */
};
還有一個相應的用來進行狀态位運算的枚舉結構:
TCPF_ESTABLISHED = (1 TCPF_SYN_SENT = (1 TCPF_SYN_RECV = (1 TCPF_FIN_WAIT1 = (1 TCPF_FIN_WAIT2 = (1 TCPF_TIME_WAIT = (1 TCPF_CLOSE = (1 TCPF_CLOSE_WAIT = (1 TCPF_LAST_ACK = (1 TCPF_LISTEN = (1 TCPF_CLOSING = (1 };
值得一提的是,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;
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 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);
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 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;
tp->listen_opt = NULL;
kfree(lopt);
return -EADDRINUSE;
在切換了有限狀态機狀态後,調用了
sk->sk_prot->hash(sk);
也就是 tcp_v4_hash()函數。這裡涉到到另一個 hash 表:TCP監聽 hash 表。
<b>TCP監聽 hash表</b>
所謂 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 的值。