深入理解TCP协议及其源代码

话不多说,上图,看一下TCP协议在TCP/IP协议族中的位置。

 

 

 

 

 

 

 

 

 

 

 

关于tcp协议,其中tcp建立连接(三次握手)的过程,不少人对其已经有了较为深入的理解,那么tcp断开连接的过程呢?本文将深入分析close背后的连接终止过程。

1、 TCP终止连接的过程(四次挥手)

  TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

  (1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。

  (2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。

  (3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。

  (4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。

   TCP协议的连接是全双工连接,一个TCP连接存在双向的读写通道。简单说来是 “先关读,后关写”,一共需要四个阶段。以客户机发起关闭连接为例:

 1)服务器读通道关闭

 2)客户机写通道关闭

 3)客户机读通道关闭

 4)服务器写通道关闭

  关闭行为是在发起方数据发送完毕之后,给对方发出一个FIN(finish)数据段。直到接收到对方发送的FIN,且对方收到了接收确认ACK之后,双方的数据通信完全结束,过程中每次接收都需要返回确认数据段ACK。

详细过程:

第一阶段 客户机发送完数据之后,向服务器发送一个FIN数据段,序列号为i;

 1)服务器收到FIN(i)后,返回确认段ACK,序列号为i+1,关闭服务器读通道;

 2)客户机收到ACK(i+1)后,关闭客户机写通道;(此时,客户机仍能通过读通道读取服务器的数据,服务器仍能通过写通道写数据)第二阶段 服务器发送完数据之后,向客户机发送一个FIN数据段,序列号为j;

 3)客户机收到FIN(j)后,返回确认段ACK,序列号为j+1,关闭客户机读通道;

 4)服务器收到ACK(j+1)后,关闭服务器写通道。

  这是标准的TCP关闭两个阶段,服务器和客户机都可以发起关闭,完全对称。FIN标识是通过发送最后一块数据时设置的,标准的例子中,服务器还在发送数据,所以要等到发送完的时候,设置FIN(此时可称为TCP连接处于半关闭状态,因为数据仍可从被动关闭一方向主动关闭方传送)。如果在服务器收到FIN(i)时,已经没有数据需要发送,可以在返回ACK(i+1)的时候就设置FIN(j)标识,这样就相当于可以合并第二步和第三步。

2、 结合源码进一步跟踪分析

   在应用层要关闭一个连接非常简单,只需要指定要关闭的连接对应的套接字即可。内核中处理TCP连接关闭的系统调用是sys_close(),该函数做的事情不多。设置断点,可以看到系统调用函数sys_close()函数在fs/open.c文件中。

 

在Linux内核中,系统调用close的定义如下所示。

SYSCALL_DEFINE1(close, unsigned int, fd)
{
    int retval = __close_fd(current->files, fd);
    /* can't restart close syscall because file table entry was cleared */
    if (unlikely(retval == -ERESTARTSYS ||
             retval == -ERESTARTNOINTR ||
             retval == -ERESTARTNOHAND ||
             retval == -ERESTART_RESTARTBLOCK))
        retval = -EINTR;
    return retval;
}

  close()函数对应的系统调用是sys_close(),在fs/open.c中定义。在sys_close()中,会首先根据文件描述符在进程的打开文件表中查找对应的file结构实例,然后调用filp_close()来关闭文件。关闭操作是在fput()(由filp_close()调用)中进行的,引用数减1后为零,才会调用__fput()来释放文件占用的内存。filp_close函数用于完成close系统调用的主要操作

 源代码如下所示。

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;
    }

    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(filp);
    return retval;
}

fput函数在文件指针引用计数file->f_count为零时释放在open等系统调用中所使用的资源。

void fput(struct file *file)
{
    if (atomic_long_dec_and_test(&file->f_count)) //引用计数为零则执行__fput函数销毁文件指针
        __fput(file);
}

/* the real guts of fput() - releasing the last reference to file
 */
static void __fput(struct file *file)
{
    struct dentry *dentry = file->f_path.dentry;
    struct vfsmount *mnt = file->f_path.mnt;
    struct inode *inode = file->f_inode;

    if (unlikely(!(file->f_mode & FMODE_OPENED)))
        goto out;
    might_sleep();
    fsnotify_close(file);
    /*
     * The function eventpoll_release() should be the first called
     * in the file cleanup chain.
     */
    eventpoll_release(file);
    locks_remove_file(file);

    ima_file_free(file);
    if (unlikely(file->f_flags & FASYNC)) {
        if (file->f_op->fasync)
            file->f_op->fasync(-1, file, 0);
    }
    if (file->f_op->release)
        file->f_op->release(inode, file);
    if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL &&
             !(file->f_mode & FMODE_PATH))) {
        cdev_put(inode->i_cdev);
    }
    fops_put(file->f_op);
    put_pid(file->f_owner.pid);
    if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
        i_readcount_dec(inode);
    if (file->f_mode & FMODE_WRITER) {
        put_write_access(inode);
        __mnt_drop_write(mnt);
    }
    dput(dentry);
    mntput(mnt);
out:
    file_free(file);
}

3、结合代码对__fput()进一步分析

  file->f_op指向的是文件操作实例,套接字的文件操作由socket_file_ops提供。socket_file_ops属于socket层,socket层是vfs和底层协议栈连接的桥梁,真正的操作还是由协议栈来提供。在这里,file->f_op->release指向sock_close()函数。在socket层下面,接着是协议族,在这个层,不同的传输层协议都会提供自己的操作接口。在协议族层,TCP和UDP协议提供的接口都是inet_release(),这个函数最终会调用到不同的传输层协议提供的close接口。TCP协议提供的是tcp_close()函数,UDP协议提供的是udp_lib_close()。
  tcp_close()中会首先将套接字的sk_shutdown标志设置为SHUTDOWN_MASK,表示双向关闭。然后检查接收缓冲区是否有数据未读(不包括FIN包),如果有数据未读,协议栈会发送RST包,而不是FIN包。如果套接字设置了SO_LINGER选项,并且lingertime设置为0,这种情况下也会发送RST包来终止连接。其他情况下,会检查套接字的状态,只有在套接字的状态是TCP_ESTABLISHED、TCP_SYN_RECV和TCP_CLOSE_WAIT的状态下,才会发送FIN包。在决定了是否发包以及发送什么类型的包之后,协议栈会进行套接字占用的资源的清理,包括sock结构、缓冲区和错误队列占用的内存等,并进行状态的变更。如果是发送FIN包进行正常关闭,后续会进行四次关闭操作,这个过程是在协议栈中完成的,和用户进程没有关系,用户进程也不能再操作这个套接字。
   udp_lib_close()中只是简单地调用了sk_common_release()函数,sk_common_release()中会调用udp_destroy_sock()来释放发送队列中占用的内存。如果UDP套接字已绑定本地端口,会添加到udp_table哈希表中,所以套接字如果已经被添加到哈希表中,udp_lib_unhash()中会将套接字从哈希表中移除。接下来会调用sock_orphan()解除进程和套接字的关系,然后释放sock结构占用的资源。
   socket结构实例占用的内存,是在dput()调用到的sock_destroy_inode()函数来释放的,sock_destroy_inode()中只是简单地调用kmem_cache_free()释放占用的内存。


参考:https://blog.csdn.net/justlinux2010/article/details/20913755

posted @ 2019-12-26 14:13  爱晴天  阅读(534)  评论(0)    收藏  举报