1. 程式人生 > >telnet client窗口關閉後服務器端前臺任務如何退出

telnet client窗口關閉後服務器端前臺任務如何退出

為我 got ive get color inux gin 並不是 window

一、telnet客戶端窗口粗暴關閉
一般很多共享式系統都會啟動telnet服務,特別是在嵌入式系統中,通常除了串口就是telnet來和單板交互了。典型的場景是一個用戶可能通過後臺的windows或者linux系統的telnet客戶端來telnet連接到服務器上,然後執行操作。在理想情況下,這是一個友好而和諧的溝通方式,但是在工程中往往會出現一些異常路徑,而對於這些異常路徑的行為我們不能用依據"implementation defined"或者說"不確定"來描述,而需要對這種即時是不確定的行為也要描述一下不確定發生在哪裏。
通常的情況是同一個用戶可能打開多個telnet客戶端來連接到同一個服務器上,然後在每個窗口做不同的操作,當操作結束之後,用戶會逐個關閉這些窗口,即使是單獨telnet會話,很多人也不會輸入exit來友好的結束這次會話,而是直接關閉掉telnet客戶端窗口,此時我們就需要知道,此時回話中正在運行的程序會如何退出,因為應用中可能會要求在任務退出的時候來執行一些特殊的清理工作。
二、busybox telnetd運行框架和原理
由於busybox的源代碼比較精簡,但是功能齊全,所以通過這個來分析是一個比較好的入口。對於telnetd來說,它就是在自己的23端口偵聽(這個值可以通過-p選項配置,默認23),這個是telnetd所說的偵聽實現
xlisten(master_fd, 1);
每當有請求到達這個端口的時候,telnetd都會派生一個新的會話(make_new_session),這個會話需要兩個文件,一個是accept返回的socket,這個套接口專門負責為這次會話通訊,然後打開偽終端的主控文件“/dev/ptmx”,生成一對偽終端,終端的一側為telnetd打開,另一側為新派生的會話驗證程序(默認為/bin/login,可以通過-l選項指定)的標準輸入,而login負責用戶身份驗證及命令解析器(sh)的派生和執行(我們通過ps是看不到login的,那是因為login在密碼驗證通過之後直接執行exec,使用sh替換了自己)。而telnetd的主體就是在自己協調的這些主控偵聽端口、會話端口、偽終端之間進行select,喚醒之後在 <套接口,偽終端> 之間中轉。
三、telnet客戶端tcp 套接口發送fin報文
在之前的一篇博客中說過,當進程退出時,內核會代勞執行進程打開的所有文件的close方法。對於TCP套接口,它的close接口中包含了FIN報文的發送(LInux內核中通過tcp_send_fin發送)。當通訊的另一方(這裏就是telentd所select的一個通訊套接口)接受到這個FIN報文之後,內核的處理函數會經過tcp_fin函數,這個函數中有一個非常貼心的喚醒操作:
\linux-2.6.21\net\ipv4\tcp_input.c
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE
);
……
if (!sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk);

/* Do not send POLL_HUP for half duplex close. */
if (sk->sk_shutdown == SHUTDOWN_MASK ||
sk->sk_state == TCP_CLOSE)
sk_wake_async(sk, 1, POLL_HUP);
else
sk_wake_async(sk, 1, POLL_IN);
}
當telnetd被從select中喚醒之後,它歡歡喜喜的去這個套接口中去讀取數據,但是在read-->>>……--->>tcp_recvmsg函數中會判斷套接口關閉
if (sock_flag(sk, SOCK_DONE))
break;
此時telnetd從套接口中讀取時返回值為零。然後看一下busybox中對於這個read返回值為零的行為如何反應:
count = safe_read(ts->sockfd_read, TS_BUF1 + ts->rdidx1, count);
if (count <= 0) {
if (count < 0 && errno == EAGAIN)
goto skip3;
goto kill_session;
}
……
kill_session:
free_session(ts);
而free_session的功能非常簡單,對於我們關心的操作只有下面兩個,此處並沒有殺死子進程的動作
close(ts->ptyfd);
close(ts->sockfd_read);
四、偽終端對於關閉的響應
tty_release--->>>release_dev--->>>tty->driver->close(tty, filp)--->>>pty_close--->>>tty_vhangup--->>do_tty_hangup
if (tty->session) {
do_each_pid_task(tty->session, PIDTYPE_SID, p) {
spin_lock_irq(&p->sighand->siglock);
if (p->signal->tty == tty)
p->signal->tty = NULL;
if (!p->signal->leader) {
spin_unlock_irq(&p->sighand->siglock);
continue;
}
__group_send_sig_info(SIGHUP, SEND_SIG_PRIV, p);執行到這裏,這個tty對應的會話首領將有幸收到一個SIGHUP信號
__group_send_sig_info(SIGCONT, SEND_SIG_PRIV, p);
put_pid(p->signal->tty_old_pgrp); /* A noop */
if (tty->pgrp)
p->signal->tty_old_pgrp = get_pid(tty->pgrp);註意這個tty_old_pgrp將會在接下來的操作中使用
spin_unlock_irq(&p->sighand->siglock);
} while_each_pid_task(tty->session, PIDTYPE_SID, p);
}
但是這裏只是給會話首領發送SIGHUP,這裏的首領一般是telentd-->>login--->>sh,也就是命令解釋器,而正在sh前臺中運行的任務並沒有這個機會,所以這裏並不是sh前臺任務退出的原因。但是這個信號將會導致sh退出還是沒有問題的,因為sh一般是不會註冊這個信號的處理函數的,而且即使註冊了,它的信號處理函數也應該會退出。
五、sh如何關閉前臺任務
當sh由於退出時,內核流程為
do_exit--->>disassociate_ctty
struct pid *old_pgrp;
spin_lock_irq(&current->sighand->siglock);
old_pgrp = current->signal->tty_old_pgrp;
current->signal->tty_old_pgrp = NULL;
spin_unlock_irq(&current->sighand->siglock);
if (old_pgrp) {
kill_pgrp(old_pgrp, SIGHUP, on_exit);
kill_pgrp(old_pgrp, SIGCONT, on_exit);
put_pid(old_pgrp);
}
mutex_unlock(&tty_mutex);
unlock_kernel();
return;
}
當sh退出時,內核會通過這個機制來給它前臺運行的任務發送一個SIGHUP,從而將前臺任務結束。
六、引申問題
1、如果前臺任務系統對於這種粗暴關閉telnent客戶端的行為也進行特殊清理的話,就需要註冊對SIGHUP的處理函數,在其中做處理。反過來說,如果希望在telnet客戶端關閉之後還繼續運行,那就需要註冊這個信號處理函數,但是不退出,或者直接忽略這個信號。
2、telent會話中後臺任務在telent客戶端關閉之後依然存在,不受影響。

telnet client窗口關閉後服務器端前臺任務如何退出