注:本文分析基於3.10.0-693.el7內核版本,即CentOS 7.4
背景
之前梳理TCP建鏈的過程時,發現一個問題,百思不得其解。各種文章和書籍裏說的都是服務端在接收到客戶端的SYN報文後就進入SYN_RECV狀態,然而當我看內核源碼時,發現卻不是這樣的。連接在第一次握手後還是保持LISTEN狀態,只是請求被放入了半連接狀態。等到第三次握手後,服務端才根據半連接隊列裏的請求重新構造一個socket,此時新構造的,用於後續通信的socket的狀態才被置爲SYN_RECV,同時放入全連接隊列和established哈希表時,狀態再切爲established。疑問就此產生,爲什麼netstat命令查詢第一次握手後的連接時顯示的是SYN_RECV,這和內核狀態不一致啊。。。。雖然內核狀態和我們平時理解的也不一樣。所以就要看看netstat是怎麼查詢狀態的。
前提準備
1、由於netstat命令由net-tools組件提供,因此需要下載net-tools的源碼包。關於源碼包的下載,可以參考——《使用yum下載rpm源碼包》。
2、源碼包下載後使用sourceinsight創建工程,便於閱讀。
查找netstat打印狀態實現源碼
以往都是正向梳理代碼流程,這次我就說說怎麼在net-tools源碼包中找到netstat命令實現的部分。
1、netstat中會輸出連接狀態,因此隨機選取一個狀態作爲關鍵詞進行搜索,這裏使用ESTABLISHED。搜索後如下,
只有五處,簡直太美麗了。
2、在netstat.c文件中可以看到對狀態的幾個枚舉值,
static const char *tcp_state[] =
{
"",
N_("ESTABLISHED"),
N_("SYN_SENT"),
N_("SYN_RECV"),
N_("FIN_WAIT1"),
N_("FIN_WAIT2"),
N_("TIME_WAIT"),
N_("CLOSE"),
N_("CLOSE_WAIT"),
N_("LAST_ACK"),
N_("LISTEN"),
N_("CLOSING")
};
這個枚舉值和內核中是一樣的,包括順序,爲了後面打印狀態。
3、既然tcp_state是狀態數組,打印的時候肯定也是會用到的,因此再用tcp_state作爲關鍵詞搜索,
只有三個結果,又是美麗的結果。一眼看過去就知道是在第三個結果裏打印的連接狀態。
4、那我們就來看看tcp_do_one這個函數了。
static void tcp_do_one(int lnr, const char *line, const char *prot)
{
...
//從緩衝區讀取連接信息
num = sscanf(line,
"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n",
&d, local_addr, &local_port, rem_addr, &rem_port, &state,
&txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode);
...
//打印連接信息
printf("%-4s %6ld %6ld %-*s %-*s %-11s",
prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr, (int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state]));
finish_this_one(uid,inode,timers);
}
這下我們的關注點就在line這個入參了。
5、通過sourceinsight的調用關係圖,
可以知道只有tcp_info裏有調用。
6、那就看看tcp_info怎麼搞的唄。
#define _PATH_PROCNET_TCP "/proc/net/tcp"
#define _PATH_PROCNET_TCP6 "/proc/net/tcp6"
static int tcp_info(void)
{
INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
tcp_do_one , "tcp", "tcp6");
}
#define INFO_GUTS6(file,file6,name,proc,prot4,prot6) \
char buffer[8192]; \
int rc = 0; \
int lnr = 0; \
if (!flag_arg || flag_inet) { \
INFO_GUTS1(file,name,proc,prot4) \
} \
if (!flag_arg || flag_inet6) { \
INFO_GUTS2(file6,proc,prot6) \
} \
INFO_GUTS3
#define INFO_GUTS1(file,name,proc,prot) \
procinfo = proc_fopen((file)); \
if (procinfo == NULL) { \
if (errno != ENOENT) { \
perror((file)); \
return -1; \
} \
if (!flag_noprot && (flag_arg || flag_ver)) \
ESYSNOT("netstat", (name)); \
if (!flag_noprot && flag_arg) \
rc = 1; \
} else { \
do { \
if (fgets(buffer, sizeof(buffer), procinfo)) \
(proc)(lnr++, buffer,prot); \
} while (!feof(procinfo)); \
fclose(procinfo); \
}
...
所以最終發現,其實netstat也只是從**/proc/net/tcp**這個文件中讀取的。
好吧,壓力又來到了內核這邊。
內核如何生成/proc/net/tcp的內容
1、根據/proc/net/tcp的內容中有local_address字樣,因此以此爲關鍵字在內核裏搜索,
2、很好,這下我們看看tcp4_seq_show這個函數。
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
struct tcp_iter_state *st;
struct sock *sk = v;
int len;
if (v == SEQ_START_TOKEN) {
seq_printf(seq, "%-*s\n", TMPSZ - 1,
" sl local_address rem_address st tx_queue "
"rx_queue tr tm->when retrnsmt uid timeout "
"inode");
goto out;
}
st = seq->private;
switch (st->state) {
case TCP_SEQ_STATE_LISTENING:
case TCP_SEQ_STATE_ESTABLISHED:
if (sk->sk_state == TCP_TIME_WAIT)
get_timewait4_sock(v, seq, st->num, &len);
else
get_tcp4_sock(v, seq, st->num, &len);
break;
case TCP_SEQ_STATE_OPENREQ:
get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
break;
}
seq_printf(seq, "%*s\n", TMPSZ - 1 - len, "");
out:
return 0;
}
static void get_openreq4(const struct sock *sk, const struct request_sock *req,
struct seq_file *f, int i, kuid_t uid, int *len)
{
const struct inet_request_sock *ireq = inet_rsk(req);
long delta = req->expires - jiffies;
seq_printf(f, "%4d: %08X:%04X %08X:%04X"
" %02X %08X:%08X %02X:%08lX %08X %5u %8d %u %d %pK%n",
i,
ireq->ir_loc_addr,
ntohs(inet_sk(sk)->inet_sport),
ireq->ir_rmt_addr,
ntohs(ireq->ir_rmt_port),
TCP_SYN_RECV,
0, 0, /* could print option size, but that is af dependent. */
1, /* timers active (only the expire timer) */
jiffies_delta_to_clock_t(delta),
req->num_timeout,
from_kuid_munged(seq_user_ns(f), uid),
0, /* non standard timer */
0, /* open_requests have no inode */
atomic_read(&sk->sk_refcnt),
req,
len);
}
由此可知,當st->state的狀態爲TCP_SEQ_STATE_OPENREQ時,通過get_openreq4直接打印SYN_RECV狀態的連接。因此,我們需要知道是在哪裏設置的這個值。
3、以TCP_SEQ_STATE_OPENREQ爲關鍵詞搜索,
天公作美,只有一處是賦值的。
4、千呼萬喚始出來,就在listening_get_next函數中。
static void *listening_get_next(struct seq_file *seq, void *cur)
{
struct inet_connection_sock *icsk;
struct hlist_nulls_node *node;
struct sock *sk = cur;
struct inet_listen_hashbucket *ilb;
struct tcp_iter_state *st = seq->private;
struct net *net = seq_file_net(seq);
if (!sk) {
//直接獲取listen哈希表裏的連接
ilb = &tcp_hashinfo.listening_hash[st->bucket];
spin_lock_bh(&ilb->lock);
sk = sk_nulls_head(&ilb->head);
st->offset = 0;
goto get_sk;
}
ilb = &tcp_hashinfo.listening_hash[st->bucket];
++st->num;
++st->offset;
if (st->state == TCP_SEQ_STATE_OPENREQ) {
struct request_sock *req = cur;
icsk = inet_csk(st->syn_wait_sk);
req = req->dl_next;
while (1) {
while (req) {
//半連接隊列裏不爲空,且是我們需要查找的協議族(TCPV4|TCPV6等)
if (req->rsk_ops->family == st->family) {
cur = req;//找到了,返回
goto out;
}
req = req->dl_next;
}
if (++st->sbucket >= icsk->icsk_accept_queue.listen_opt->nr_table_entries)
break;
get_req:
//獲取半連接隊列裏的請求
req = icsk->icsk_accept_queue.listen_opt->syn_table[st->sbucket];
}
sk = sk_nulls_next(st->syn_wait_sk);
st->state = TCP_SEQ_STATE_LISTENING;
read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
} else {
icsk = inet_csk(sk);
read_lock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
//如果半連接隊列不爲空
if (reqsk_queue_len(&icsk->icsk_accept_queue))
goto start_req;
read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
sk = sk_nulls_next(sk);
}
get_sk:
sk_nulls_for_each_from(sk, node) {
if (!net_eq(sock_net(sk), net))
continue;
if (sk->sk_family == st->family) {
cur = sk;
goto out;
}
icsk = inet_csk(sk);
read_lock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
if (reqsk_queue_len(&icsk->icsk_accept_queue)) {
start_req:
st->uid = sock_i_uid(sk);
st->syn_wait_sk = sk;
//設置TCP_SEQ_STATE_OPENREQ狀態,標識爲SYN_RECV狀態
st->state = TCP_SEQ_STATE_OPENREQ;
st->sbucket = 0;
//獲取請求
goto get_req;
}
read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
}
spin_unlock_bh(&ilb->lock);
st->offset = 0;
if (++st->bucket < INET_LHTABLE_SIZE) {
ilb = &tcp_hashinfo.listening_hash[st->bucket];
spin_lock_bh(&ilb->lock);
sk = sk_nulls_head(&ilb->head);
goto get_sk;
}
cur = NULL;
out:
return cur;
}
所以說對於SYN_RECV狀態,並不是按照內核連接狀態打印的,而是和我們理解的一樣,在第一次握手後連接狀態就是SYN_RECV。
總結
我們知道TCP連接會存在於以下三個表或隊列中,
1、listen哈希表
2、半連接隊列
3、established哈希表
因此連接狀態的顯示邏輯爲,在listen哈希表中的連接就是LISTEN狀態,使用連接本身的狀態。在半連接隊列裏的連接也是LISTEN狀態,但是不使用連接本身狀態,而定義爲SYN_RECV狀態。而對於established哈希表裏的連接,其狀態和我們認爲的是一致的,因此直接使用內核中連接的狀態,這就是爲什麼開始時net-tools裏定義的tcp_state和內核中的一致的原因了。
終於把這個疑問解決了,開心。