【Linux TCP close】深入理解TCP協議及其源代碼


深入理解TCP協議及其源代碼

TCP close分析

close背后的連接終止過程

TCP協議作為一個可靠的、基於連接的流協議,要通過IP層的不可靠傳輸來,給上層協議提供"可靠"的數據流。

  • 可靠:TCP要保證用戶數據完整以及數據的順序。
  • 基於連接:啟動前要建立連接,結束后要斷開連接。
  • 流協議:TCP的數據是以字節為單位的,而沒有進行分包。

其中TCP協議的使用有建立連接和斷開連接是TCP與UDP的區別之一,本文主要對TCP的close進行源碼分析和運行跟蹤。

首先TCP斷開連接的過程是4次揮手:

  • 主機1發送完自己要發送的所有數據,決定斷開連接
  • 主機1使用close發送fin|ack(附帶對主機2前面數據的ack,斷開連接的過程開始,此時主機1的發送窗口關閉,接受窗口還在工作;
  • 主機2接受到主機1的fin后,發送ack告知主機1對方的fin已收到;
  • 主機2繼續發送數據,直到主機2發送完所有數據
  • 主機2使用close發送fin,表示自己的數據也發送完畢
  • 主機1接受fin,發送ack,告知主機2對方的fin已收到

也就是主機1和主機2連接斷開的過程中,某一主機已經表示斷開連接了時另一主機還可能繼續發數據,當然也存在兩者同時發送完畢,那就成了第3次揮手主機2的fin和第2次對主機1ack一起發送。

因為TCP是可靠的協議,所以需要Ack來保證發送的fin已到達對方,且存在兩者不同時發送完數據的情況,所以通常情況下需要4次揮手。

所以TCP的close分為先后兩個情況,同時關閉的情況不做描述。

TCP close源碼

C語言中使用close來關閉對應的TCPSocket套接字,比如

int socket_fp = socket(AF_INET, SOCK_STREAM, 0);
close(socket_fp);

close函數通過系統調用sys_close來執行,代碼位於fs/open.c

/* fs/open.c:1191 */
SYSCALL_DEFINE1(close, unsigned int, fd)
{
    int retval = __close_fd(current->files, fd);
    //...
    return retval;
}

系統調用又sys_close又通過fs/file.c中的__close_fd函數來釋放文件指針,最終調用flip_close方法

/* fs/file.c */
int __close_fd(struct files_struct *files, unsigned fd)
{
    struct file *file;
    struct fdtable *fdt;
    // 獲得訪問鎖
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    if (fd >= fdt->max_fds)
        goto out_unlock;
    file = fdt->fd[fd];
    if (!file)
        goto out_unlock;
    rcu_assign_pointer(fdt->fd[fd], NULL);
    // 釋放文件描述符
    __put_unused_fd(files, fd);
    // 釋放訪問鎖
    spin_unlock(&files->file_lock);
    // 調用flip_close方法
    return filp_close(file, files);

out_unlock:
    spin_unlock(&files->file_lock);
    return -EBADF;
}

flip_close位於fs/open.c中。

/* fs/open.c */
int filp_close(struct file *filp, fl_owner_t id)
{
    int retval = 0;
    // 檢測文件描述符引用數目
    if (!file_count(filp)) {
        printk(KERN_ERR "VFS: Close: file count is 0\n");
        return 0;
    }
    // 調用flush方法
    if (filp->f_op->flush)
        retval = filp->f_op->flush(filp, id);

    if (likely(!(filp->f_mode & FMODE_PATH))) {
        dnotify_flush(filp, id);
        locks_remove_posix(filp, id);
    }
    // 調用fput方法
    fput(filp);
    return retval;
}

位於fs/file_table.c中的fput調用fput_many,接着啟動task____fput調用__fput,最終跟蹤到指針函數f_op->release

/* fs/file_table.c */
static void __fput(struct file *file)
{
    // ...
    // 調用指針函數file->f_op->release
    if (file->f_op->release)
        file->f_op->release(inode, file);
    // ...
}

void fput_many(struct file *file, unsigned int refs)
{
    if (atomic_long_sub_and_test(refs, &file->f_count)) {
        struct task_struct *task = current;

        if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
            // 這里啟動了____fput
            init_task_work(&file->f_u.fu_rcuhead, ____fput);
            if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
                return;
        }

        if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))
            schedule_delayed_work(&delayed_fput_work, 1);
    }
}
void fput(struct file *file)
{
    // 調用fput_many
    fput_many(file, 1);
}

fp_ops->release

fp_ops->release這個指針函數在套接字初始化的時候被賦值,可以定位到函數sock_close

/* net/socket.c */
static const struct file_operations socket_file_ops = {
    .owner =        THIS_MODULE,
    .llseek =       no_llseek,
    .read_iter =    sock_read_iter,
    .write_iter =   sock_write_iter,
    .poll =         sock_poll,
    .unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_sock_ioctl,
#endif
    .mmap =         sock_mmap,
    .release =      sock_close,
    .fasync =       sock_fasync,
    .sendpage =     sock_sendpage,
    .splice_write = generic_splice_sendpage,
    .splice_read =  sock_splice_read,
};

通過socket_close調用__sock_release中的sock->ops->release函數

/* net/socket.c */
static void __sock_release(struct socket *sock, struct inode *inode)
{
    if (sock->ops) {
        struct module *owner = sock->ops->owner;

        if (inode)
            inode_lock(inode);
        sock->ops->release(sock);
        sock->sk = NULL;
        if (inode)
            inode_unlock(inode);
        sock->ops = NULL;
        module_put(owner);
    }

    if (sock->wq.fasync_list)
        pr_err("%s: fasync list not empty!\n", __func__);

    if (!sock->file) {
        iput(SOCK_INODE(sock));
        return;
    }
    sock->file = NULL;
}
static int sock_close(struct inode *inode, struct file *filp)
{
    __sock_release(SOCKET_I(inode), inode);
    return 0;
}

這里的sock->ops->release指針函數就根據傳輸層的協議不同,指向不同的函數,由於我們這里是TCP,所以最后調用inet_stream_ops->release


TCP關閉調用過程

close(socket_fd)
    |
    f_op->release
        |---sock_close
            |---sock->ops->release
                |--- inet_stream_ops->release(tcp_close)

tcp_close

/* net/ipv4/tcp.c */
void tcp_close(struct sock *sk, long timeout)
{
	struct sk_buff *skb;
	int data_was_unread = 0;
	int state;

	lock_sock(sk);
	sk->sk_shutdown = SHUTDOWN_MASK;

	if (sk->sk_state == TCP_LISTEN) {
        // 套接字處於Listen狀態,將狀態調整未close
		tcp_set_state(sk, TCP_CLOSE);

		inet_csk_listen_stop(sk);

		goto adjudge_to_death;
	}
    // 清空buffer
	while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
		u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq;

		if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
			len--;
		data_was_unread += len;
		__kfree_skb(skb);
	}

	sk_mem_reclaim(sk);

	if (sk->sk_state == TCP_CLOSE)
		goto adjudge_to_death;

	// ...
	} else if (tcp_close_state(sk)) {  // 將狀態設為fin_wait
		tcp_send_fin(sk); // 調用tcp_send_fin(sk)
	}

	sk_stream_wait_close(sk, timeout);

adjudge_to_death:
	// ...
}
EXPORT_SYMBOL(tcp_close);

現在進入了tcp關閉連接的關鍵部分,先關閉者將套接字狀態由listen設為close,然后清空發送區緩存,接着通過tcp_send_fin來發送fin請求,自身進入fin_wait1狀態。

第一次揮手

/* net/ipv4/tcp_output.c */
void tcp_send_fin(struct sock *sk)
{
	......
	// 設置flags為ack|fin
	TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN);
	......
	// 發送fin包
	__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_OFF);
}

第二次揮手

接着等待另一方回應,處理TCP不同狀態碼的函數為net\ipv4\tcp_input.c中的tcp_rcv_state_process,現在主要是等待對方對finack,讓套接字進入fin_wait2狀態。

case TCP_FIN_WAIT1: {
    // ...
    // 判斷ack是否正確
    if (tp->snd_una != tp->write_seq)
        break;
    // 進入fin_wait2狀態
    tcp_set_state(sk, TCP_FIN_WAIT2);
    sk->sk_shutdown |= SEND_SHUTDOWN;

    // ...
    // 設置超時定時器,超時自動關閉套接字
    tmo = tcp_fin_time(sk);
    if (tmo > TCP_TIMEWAIT_LEN) {
        inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
    } else if (th->fin || sock_owned_by_user(sk)) {
        inet_csk_reset_keepalive_timer(sk, tmo);
    } else {
        tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
        goto discard;
    }
    break;
}
switch (sk->sk_state) {
// ...
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2:
    if (sk->sk_shutdown & RCV_SHUTDOWN) {
        if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
        after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
            tcp_reset(sk);
            return 1;
        }
    }
// ...

這時,另一主機收到fin,同樣是tcp_rcv_state_process處理,根據狀態(現在是連接建立ESTABLISHED)調用tcp_data_queue進入close_wait狀態

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb){
// ...
switch (sk->sk_state){
    // ...
    case TCP_ESTABLISHED:
        tcp_data_queue(sk, skb);
        queued = 1;
        break;
    // ...
    }
}

函數tcp_data_queue中調用了tcp_fin函數,該函數將套接字狀態切換為close_wait,然后等待新數據發送ack

void tcp_fin(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    inet_csk_schedule_ack(sk);

    sk->sk_shutdown |= RCV_SHUTDOWN;
    sock_set_flag(sk, SOCK_DONE);

    switch (sk->sk_state) {
    case TCP_SYN_RECV:
    case TCP_ESTABLISHED:
        /* Move to CLOSE_WAIT */
        tcp_set_state(sk, TCP_CLOSE_WAIT);
        inet_csk_enter_pingpong_mode(sk);
        break;
    }
    // ...
}

第三次揮手

主機2也調用了close,狀態將由close_wait變為last_ack

void tcp_close(struct sock *sk, long timeout)
{
	......
	else if (tcp_close_state(sk)){
		// tcp_close_state會將sk從close_wait狀態變為last_ack
		// 發送fin包
		tcp_send_fin(sk);
	}
}

第四次揮手

主機1,接收到fin后,回復ack,並進入time_wait,回收資源

static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
{
	switch (sk->sk_state) {
		......	
		case TCP_FIN_WAIT2:
			// 收到FIN之后,發送ACK進入TIME_WAIT
			tcp_send_ack(sk);
			tcp_time_wait(sk, TCP_TIME_WAIT, 0);
	}
}

主機2收到fin的ack后進入closed狀態,回收資源

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len)	
{
	// ...
	if (th->ack) {
		// ...
		case TCP_LAST_ACK:
			if (tp->snd_una == tp->write_seq) {
					tcp_update_metrics(sk);
					// 設置socket為closed,並回收socket的資源
					tcp_done(sk);
					goto discard;
			}
		// ...
	}
}	

Qmenu中啟動MenuOS,進入調試

上上次實驗編譯了一個帶調試功能,且帶有TCP服務器和客戶端的MenuOS系統

同樣打開一個終端,進入LinuxKernel目錄,啟動之前編譯好的帶調試的MenuOS

~$ cd LinuxKernel
~/LinuxKernel$ qemu-system-i386 -kernel linux-5.4.2/arch/x86/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S

進入調試

這時候虛擬機進入停止在一個黑屏界面,等待gdb的接入和下一步指令。

新開一個終端窗口,進入gdb調試。

接着分別

  • 導入符號表
  • 連接調試服務器
  • 設置斷點
jett@ubuntu:~/LinuxKernel$ gdb
(gdb) file ~/LinuxKernel/linux-5.4.2/vmlinux
Reading symbols from ~/LinuxKernel/linux-5.4.2/vmlinux...done.
(gdb) target remote:1234
Remote debugging using :1234
0x0000fff0 in ?? ()
(gdb) break start_kernel
Breakpoint 1 at 0xc1db5885: file init/main.c, line 576.

然后輸入c讓系統繼續執行,執行到斷點start_kernel ()則說明成功。

(gdb) c
Continuing.

Breakpoint 1, start_kernel () at init/main.c:576
576	{

添加新斷點sys_close, tcp_close, tcp_rcv_state_process, tcp_fin, tcp_send_fin

(gdb) b sys_close
Breakpoint 3 at 0xc119fe60: file fs/open.c, line 1191.
(gdb) info b    # 查看設置的斷點

c讓系統繼續執行

啟動replyhi服務

服務器經過4次sys_socketcall進入監聽狀態,分別是SYS_SOCKET, SYS_BIND, SYS_LISTEN, SYS_ACCEPT

啟動hello客戶端

continue到sys_close斷點,說明客戶端發送完消息,准備關閉套接字

此時可以print *sk來查看套接字的狀態

tcp_closetcp_send_fin后函數,等待主機2ack,這是第一次揮手

主機2調用tcp_fin發送ack,接着也進行了tcp_close,第二次和第三次揮手同時進行(兩者同時關閉)

接着主機1進入tcp_rcv_state_process處理函數,根據狀態,向主機2通過tcp_send_fin回復ack並關閉套接字;主機2收到ack后關閉套接字

Breakpoint 4, tcp_close (sk=0xc726c6c0, timeout=0) at net/ipv4/tcp.c:2340
2340	{
(gdb) c
Continuing.

Breakpoint 6, tcp_send_fin (sk=0xc726c6c0) at net/ipv4/tcp_output.c:3122
3122	{
(gdb) c
Continuing.

Breakpoint 8, tcp_fin (sk=0xc726cd80) at net/ipv4/tcp_input.c:4146
4146	{
(gdb) c
Continuing.

Breakpoint 4, tcp_close (sk=0xc726cd80, timeout=0) at net/ipv4/tcp.c:2340
2340	{
(gdb) c
Continuing.

Breakpoint 7, tcp_rcv_state_process (sk=0xc726c6c0, skb=0xc78f7000) at net/ipv4/tcp_input.c:6126
6126	{
(gdb) c
Continuing.

Breakpoint 6, tcp_send_fin (sk=0xc726cd80) at net/ipv4/tcp_output.c:3122
3122	{
(gdb) c
Continuing.

Breakpoint 7, tcp_rcv_state_process (sk=0xc726cd80, skb=0xc78f7000) at net/ipv4/tcp_input.c:6126
6126	{
(gdb) c
Continuing.

作者:SA19225176,萬有引力丶

參考資料來源:USTC Socket與系統調用深度分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM