telnet client窗口关闭后服务器端前台任务如何退出
一、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(¤t->sighand->siglock);
old_pgrp = current->signal->tty_old_pgrp;
current->signal->tty_old_pgrp = NULL;
spin_unlock_irq(¤t->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服务,特别是在嵌入式系统中,通常除了串口就是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(¤t->sighand->siglock);
old_pgrp = current->signal->tty_old_pgrp;
current->signal->tty_old_pgrp = NULL;
spin_unlock_irq(¤t->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客户端关闭之后依然存在,不受影响。