1. 程式人生 > >任務退出文件自動關閉及tcp socket半關閉行為特征

任務退出文件自動關閉及tcp socket半關閉行為特征

應該 bug 剛才 bye hat 顯示 reset red 進行

一、任務退出時文件關閉
大多數時候,程序的執行就像人生一樣,並不是一帆風順,可能剛才還在運行的不亦樂乎,跑的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;
}

任務退出文件自動關閉及tcp socket半關閉行為特征