任务退出文件自动关闭及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.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;
}