注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

任务退出文件自动关闭及tcp socket半关闭行为特征  

2012-03-17 22:25:37|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、任务退出时文件关闭
大多数时候,程序的执行就像人生一样,并不是一帆风顺,可能刚才还在运行的不亦乐乎,跑的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.21\fs\proc\array.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\n",(int)fd);
ret = select((int)fd+1,&fds,NULL,NULL,NULL);
printf("return is %d\t errno is %d\n",ret,errno);
}
}
int main()
{
pthread_t selectthread;
pthread_create(&selectthread,NULL,&selector,0);
sleep(10);
printf("closing stdin\n");
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 "?\036",
    len=10, ppos=0xcf6a3f84) at fs/read_write.c:241
#6  0xc01bf242 in vfs_read (file=0xc12c9960, buf=0xbfa9cf2c "?\036",
    count=10, pos=0xcf6a3f84) at fs/read_write.c:274
#7  0xc01bf716 in sys_read (fd=4, buf=0xbfa9cf2c "?\036", 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\n", *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 "?\036",
    len=10, ppos=0xcfe5df84) at fs/read_write.c:299
#11 0xc01bf5eb in vfs_write (file=0xc12a1e40, buf=0xbfef7b8c "?\036",
    count=10, pos=0xcfe5df84) at fs/read_write.c:332
#12 0xc01bf7d1 in sys_write (fd=4, buf=0xbfef7b8c "?\036", 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;
}
  评论这张
 
阅读(1559)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017