zoukankan      html  css  js  c++  java
  • 任务退出文件自动关闭及tcp socket半关闭行为特征

    一、任务退出时文件关闭
    大多数时候,程序的执行就像人生一样,并不是一帆风顺,可能刚才还在运行的不亦乐乎,跑的CPU直冒青烟,但是一会有人发个信号过来就把进程杀死了。就像《让子弹飞》里师爷说的:“刚才还在吃着火锅,唱着小曲,突然就被麻匪劫了”。这样程序有很多事情是来得及完成的,例如我们最为关心的就是程序可能打开了很多的文件,这些文件的close函数是否会被执行,何时会被执行。这个问题可能对于普通的文件意义并不大,但是在可以想到的下面两个问题中还是有意义的:
    1、对于TCP来说,它的关闭中会涉及到通知对方的动作,告诉对方自己这里的套接口要关闭了,要相忘于江湖,不要再相望于江湖了。
    2、对于其他的poll(select)操作,假设说有另一个线程在select这个文件,然后文件被关闭,那么此时等待者也应该被唤醒而不是一直无意义的等待下去。
    二、任务退出时关闭
    1、关闭的时机
    do_exit--->>__exit_files--->>put_files_struct--->>close_files
        fdt = files_fdtable(files);
        for (;;) {
            unsigned long set;
            i = j * __NFDBITS;
            if (i >= fdt->max_fds)
                break;
            set = fdt->open_fds->fds_bits[j++];
            while (set) {
                if (set & 1) {
                    struct file * file = xchg(&fdt->fd[i], NULL);
                    if (file) {
                        filp_close(file, files);这个接口也是sys_close中调用的接口,所以内核会保证任务(线程)退出时对进程未关闭的文件执行close操作
                        cond_resched();
                    }
                }
                i++;
                set >>= 1;
            }
        }
    2、关闭的条件
    这里忽略了一个细节,那就是在put_files_struct中,进行这些关闭是有条件的,那条件就是
    void fastcall put_files_struct(struct files_struct *files)
    {
        struct fdtable *fdt;

        if (atomic_dec_and_test(&files->count)) {
            close_files(files);
    这个地方其实也没有什么,主要是考虑到多线程的问题,在进程每创建一个线程的时候,它就会在
    copy_process--->>>copy_files
    中有如下判断
        if (clone_flags & CLONE_FILES) {
            atomic_inc(&oldf->count);对于线程创建,这里只是增减这个计数值,而不是真正的分配一个结构
            goto out;
        }
    对于新的结构,在copy_files--->>>dup_fd--->>>alloc_files
    atomic_set(&newf->count, 1);
    也就是新创建的files_struct中的引用计数就是1(而不是0)。
    3、为什么使用files_struct.count而不是task_struct.signal->count来判断共享个数
    那么这里不使用signal中task_struct.signal->count这个成员来计算有多少个线程呢?毕竟,proc/pid/status中的Threads就是通过这里的成员显示的(相关代码位于linux-2.6.21fsprocarray.c:task_sig函数)。这是因为并不是所有的线程都必须公用一个文件表(例如可以通过sys_clone来指定各种共享粒度),只是pthread线程库是这么实现的,内核也不是专门为pthread库定制的,而且即使是使用pthread库创建的线程,也可以通过新添加的内核API  sys_unshare来取消共享,从而自己独占一份。
    三、文件关闭时唤醒select等待者
    这个感觉是一个道德性问题,比方说,文件都关闭了,还让别人痴情的等,这样至少一个线程可能算是报废了(如果select/poll没有设置超时时间的话,虽然select/poll同时脚踩几条船也不太合适),所以关闭的时候应该通知自己等待队列上的任务,这一点大家可能没什么异议,因为是合情合理的。但是现在的问题是,我们想一下当等待者被唤醒的时候,它将会有什么行为,是否会从这个等待返回?返回值是什么?从哪条路径返回?这里以比较简单和典型的pipe为例(socket还是有点复杂)。
    1、close时唤醒
    sys_close-->>pipe_read_release--->>>pipe_release
    static int
    pipe_release(struct inode *inode, int decr, int decw)
    {
    ………………
        if (!pipe->readers && !pipe->writers) {
            free_pipe_info(inode);
        } else {
            wake_up_interruptible(&pipe->wait);
            kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
            kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
        }
        mutex_unlock(&inode->i_mutex);

        return 0;
    }
    其中的pipe_poll中等待的位置也就是其中提到的pipe->wait等待队列头部。
    pipe_poll(struct file *filp, poll_table *wait)
    ……
        poll_wait(filp, &pipe->wait, wait);
    2、poll/select会如何反应这次唤醒
    因为从pipe_poll函数来看,如果文件被关闭的话,它并不会有特殊行为,不会返回错误、可读、可写等状态,也就是说select并不会从这个pipe_poll返回正确或者错误,返回值为零。那么这次唤醒select将如何知道一个文件已经关闭了。
    ①、poll如何知道这个关闭
    do_sys_poll--->>>do_poll--->>>do_pollfd
        if (fd >= 0) {
            int fput_needed;
            struct file * file;

            file = fget_light(fd, &fput_needed);
            mask = POLLNVAL;由于文件已经关闭,所以这个值将会作为错误值返回,所以当文件关闭之后,这个poll系统调用将会返回这个错误码
            if (file != NULL) { 对于关闭的文件,不满足这个条件,将会从这里返回。
    这个也将会作为系统返回值,所以poll可以被正常唤醒。
    ②、select
    我搜索了一些,没有发现哪里会唤醒这个select,后来看了一下2.6.37内核,同样找不到可能的唤醒位置。自己写个程序测试了一下,的确不会被唤醒:
    [tsecer@Harry selectclose]$ cat selectclose.c 
    #include <sys/select.h>
    #include <stdio.h>
    #include <pthread.h>
    #include <errno.h>

    void * selector(void * fd)
    {
     fd_set fds;
    FD_ZERO(&fds);
    FD_SET((int)fd,&fds);
    while(1)
    {
    int ret;
    printf("will select %d ",(int)fd);
    ret = select((int)fd+1,&fds,NULL,NULL,NULL);
    printf("return is %d errno is %d ",ret,errno);
    }
    }
    int main()
    {
    pthread_t selectthread;
    pthread_create(&selectthread,NULL,&selector,0);
    sleep(10);
    printf("closing stdin ");
    close(0);
    sleep(1000);
    }
    [tsecer@Harry selectclose]$ cat Makefile 
    default:
        gcc *.c -o selector.exe -static -lpthread
    [tsecer@Harry selectclose]$ make
    gcc *.c -o selector.exe -static -lpthread
    /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../libpthread.a(libpthread.o): In function `sem_open':
    (.text+0x6d1a): warning: the use of `mktemp' is dangerous, better use `mkstemp'
    [tsecer@Harry selectclose]$ sleep 1234 | ./selector.exe 利用shell自带管道功能,让selector标准输入为一个管道
    will select 0  这里子线程开始等待文件,
    closing stdin  在子线程select的文件关闭之后,select线程依然没有被唤醒。
    [root@Harry ~]# ps aux
    …………
    tsecer   16119  0.0  0.0   3940   476 pts/6    S+   22:17   0:00 sleep 1234
    tsecer   16120  0.0  0.0  11100   248 pts/6    Sl+  22:17   0:00 ./selector.exe
    root     16122  0.0  0.0   4688   988 pts/7    R+   22:17   0:00 ps aux
    root     30052  0.0  0.2   7532  2964 pts/1    S    Mar16   0:00 su -
    root     30058  0.0  0.1   5120  1684 pts/1    S+   Mar16   0:00 -bash
    tsecer   31748  0.0  0.1   5252  1792 pts/3    Ss+  Mar16   0:00 bash
    You have new mail in /var/spool/mail/root
    [root@Harry ~]# ls /proc/16120/fd -l  可以看到,文件的标准输入已经关闭
    total 0
    lrwx------. 1 tsecer tsecer 64 2012-03-17 22:18 1 -> /dev/pts/6
    lrwx------. 1 tsecer tsecer 64 2012-03-17 22:17 2 -> /dev/pts/6
    不过这个测试并不公平,因为select并不是永远没有机会被唤醒,只要父进程(sleep 1234)关闭自己的标准输出(也就是管道的另一侧),selector的select系统调用就会返回,查看pipe_poll的代码,返回值的mask应该为POLLHUP。但是这里至少说明了poll和select的一点不同。
    四、TCP 套接口对于单方关闭之后的行为特征
    有些时候,TCP通讯的某一方关闭了套接口,而对方并没有执行这个close操作,此时未关闭一方进入CLOSE_WAIT状态。一般来说,通讯的双方应该有一个协议,约定好什么情况下结束回话,例如FTP的bye命令,telnet的quit命令等,但是正如刚才所说,在某些时候,程序只能由内核代劳关闭,所以根本不能按照约定履行这个应用层协议,所以此时另一方就会进入尴尬的CLOSE_WAIT状态。
    1、另一方如何进入CLOSE_WAIT
    正如刚才所说,幸好内核会代劳执行进程退出时未关闭文件的close接口(内容中为file_operations中的release,而不是对应的用户态的close),这样,一个进程的套接口就有机会执行自己的关闭操作,对于TCP的套接口,在关闭的时候会发送一个FIN,也就是自己要关闭的一个报文,这个报文将会促使通讯的另一方进入CLOSE_WAIT状态。
    关闭方:
    tcp_close---->>>tcp_send_fin--->>>__tcp_push_pending_frames
    接收方
    tcp_fin
        switch (sk->sk_state) {
            case TCP_SYN_RECV:
            case TCP_ESTABLISHED:
                /* Move to CLOSE_WAIT */
                tcp_set_state(sk, TCP_CLOSE_WAIT);
                inet_csk(sk)->icsk_ack.pingpong = 1;
                break;
    2、CLOSE_WAIT读入时行为
    当一个套接口进入该状态之后,上层对这个信息是不知道的,假设说上层来通过套接口来读取数据,相关操作将会在tcp_recvmsg函数中完成,调用链为:
    (gdb) bt
    #0  tcp_recvmsg (iocb=0xcf6a3e7c, sk=0xcfe6a4a0, msg=0xcf6a3e3c, len=10, 
        nonblock=0, flags=0, addr_len=0xcf6a3d8c) at net/ipv4/tcp.c:1473
    #1  0xc06dbf68 in sock_common_recvmsg (iocb=0xcf6a3e7c, sock=0xcff48800, 
        msg=0xcf6a3e3c, size=10, flags=0) at net/core/sock.c:1615
    #2  0xc06d50e9 in __sock_recvmsg (flags=0, size=10, msg=0xcf6a3e3c, 
        sock=0xcff48800, iocb=0xcf6a3e7c) at net/socket.c:604
    #3  do_sock_read (flags=0, size=10, msg=0xcf6a3e3c, sock=0xcff48800, 
        iocb=0xcf6a3e7c) at net/socket.c:693
    #4  0xc06d5171 in sock_aio_read (iocb=0xcf6a3e7c, iov=0xcf6a3f00, nr_segs=1, 
        pos=0) at net/socket.c:711
    #5  0xc01bf0a3 in do_sync_read (filp=0xc12c9960, buf=0xbfa9cf2c "?36", 
        len=10, ppos=0xcf6a3f84) at fs/read_write.c:241
    #6  0xc01bf242 in vfs_read (file=0xc12c9960, buf=0xbfa9cf2c "?36", 
        count=10, pos=0xcf6a3f84) at fs/read_write.c:274
    #7  0xc01bf716 in sys_read (fd=4, buf=0xbfa9cf2c "?36", count=10)
        at fs/read_write.c:365
    #8  0xc0107a84 in ?? ()
    #9  0x00000004 in ?? ()
    #10 0xbfa9cf2c in ?? ()
    #11 0x0000000a in ?? ()
    #12 0x00000000 in ?? ()
    (gdb)
    其相关代码为
        /* Next get a buffer. */

            skb = skb_peek(&sk->sk_receive_queue);
            do {
                if (!skb当FIN报文被消耗掉之后的read将会从这个分支跳出循环
                    break;

                /* Now that we have two receive queues this
                 * shouldn't happen.
                 */
                if (before(*seq, TCP_SKB_CB(skb)->seq)) {
                    printk(KERN_INFO "recvmsg bug: copied %X "
                           "seq %X ", *seq, TCP_SKB_CB(skb)->seq);
                    break;
                }
                offset = *seq - TCP_SKB_CB(skb)->seq;
                if (skb->h.th->syn)
                    offset--;
                if (offset < skb->len)
                    goto found_ok_skb;
                if (skb->h.th->fin当对方关闭之后,第一次读入时会收到感受到这个fin标志,从而满足该条件跳出
                    goto found_fin_ok;
    …………
    }//do 循环结束
        if (sock_flag(sk, SOCK_DONE))这里将会导致没有读到任何数据返回,所以CLOSE_WAIT状态读取数据为零
                    break;
    这个SOCK_DONE的设置同样位于对方发送FIN时的操作,对应代码为:
    static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
    {
        struct tcp_sock *tp = tcp_sk(sk);

        inet_csk_schedule_ack(sk);

        sk->sk_shutdown |= RCV_SHUTDOWN;
        sock_set_flag(sk, SOCK_DONE);
    3、CLOSE_WAIT时写入操作
    当CLOSE_WAIT第一次写入的时候,它会发送成功,这个报文将会经过网络之后到达对方,但是由于对方套接口已经关闭,所以对方毫不客气的给这里的发送方回敬了一个RESET报文,导致本地的socket进入“管道断裂”状态。进入该状态之后,事情就大条了,问题也严重了,如果上层再次执行发送操作,发送线程将会收到一个SIGPIPE信号,而这个信号的默认行为就是关闭线程组。
    ①、本地发送之后reset报文处理路径
    (gdb) bt
    #0  tcp_reset (sk=0xc12d1640) at net/ipv4/tcp_input.c:2839
    #1  0xc0745803 in tcp_rcv_state_process (sk=0xc12d1640, skb=0xcfe45200, 
        th=0xcfd96034, len=20) at net/ipv4/tcp_input.c:4478
    #2  0xc0757022 in tcp_v4_do_rcv (sk=0xc12d1640, skb=0xcfe45200)
        at net/ipv4/tcp_ipv4.c:1584
    #3  0xc06db24e in __release_sock (sk=0xc12d1640) at net/core/sock.c:1247
    #4  0xc06dbd25 in release_sock (sk=0xc12d1640) at net/core/sock.c:1547
    #5  0xc0730ece in tcp_sendmsg (iocb=0xcfe5de7c, sk=0xc12d1640, msg=0xcfe5de3c, 
        size=10) at net/ipv4/tcp.c:858
    #6  0xc0772a9c in inet_sendmsg (iocb=0xcfe5de7c, sock=0xcff45500, 
        msg=0xcfe5de3c, size=10) at net/ipv4/af_inet.c:667
    #7  0xc06d52f6 in __sock_sendmsg (size=10, msg=0xcfe5de3c, sock=0xcff45500, 
        iocb=0xcfe5de7c) at net/socket.c:553
    #8  do_sock_write (size=10, msg=0xcfe5de3c, sock=0xcff45500, iocb=0xcfe5de7c)
        at net/socket.c:735
    #9  0xc06d537e in sock_aio_write (iocb=0xcfe5de7c, iov=0xcfe5df00, nr_segs=1, 
        pos=0) at net/socket.c:753
    #10 0xc01bf44c in do_sync_write (filp=0xc12a1e40, buf=0xbfef7b8c "?36", 
        len=10, ppos=0xcfe5df84) at fs/read_write.c:299
    #11 0xc01bf5eb in vfs_write (file=0xc12a1e40, buf=0xbfef7b8c "?36", 
        count=10, pos=0xcfe5df84) at fs/read_write.c:332
    #12 0xc01bf7d1 in sys_write (fd=4, buf=0xbfef7b8c "?36", count=10)
        at fs/read_write.c:383
    在tcp_reset函数中,其操作为
    static void tcp_reset(struct sock *sk)
    {
        /* We want the right error as BSD sees it (and indeed as we do). */
        switch (sk->sk_state) {
            case TCP_SYN_SENT:
                sk->sk_err = ECONNREFUSED;
                break;
            case TCP_CLOSE_WAIT:
                sk->sk_err = EPIPE;
                break;
    ②、信号发送路径
    tcp_sendmsg--->>sk_stream_error
    int sk_stream_error(struct sock *sk, int flags, int err)
    {
        if (err == -EPIPE)
            err = sock_error(sk) ? : -EPIPE;
        if (err == -EPIPE && !(flags & MSG_NOSIGNAL))
            send_sig(SIGPIPE, current, 0);
        return err;
    }

  • 相关阅读:
    表值参数学习
    js闭包
    vue相关
    js的面向对象
    JavaScript中template模板引擎
    使用原生的ajax的步骤(五个步骤)
    (a ==1 && a== 2 && a==3) 有可能是 true 吗?
    这道JS笔试题你做对了吗?
    JS事件分类
    JS事件绑定模型
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486241.html
Copyright © 2011-2022 走看看