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

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

daemon任务如何主动释放终端控制权--以telnetd为例  

2012-04-09 23:09:14|  分类: Linux系统编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、后台任务
关于后台任务,我就不在这里拷贝一条一条的定义了。所谓的后台任务引起我的注意,是突然想起了telnetd的一个很拉轰的特征,那就是我们在终端里执行telnetd程序,它不像其它的任务(包括几乎我们可以见到的所有程序,例如cat、login等)只要获得了执行,它就毫不客气的抓住整个终端的输入,直到自己退出,也就是“春蚕到死丝方尽、蜡炬成灰泪始干”。再直观的说,就是当我们在终端上执行一个命令的时候,可以看到终端提示符会停止显示。比方说,在终端中执行
sleep 1000
可以看到,此时的整个终端不再接收命令输入,而这个进程就是所谓的前台任务。
但是telnetd就比较特殊,当我们在终端中执行telnetd的时候,控制权会马上返回,通俗的说,可以马上执行新的命令,而telnetd进程则依然存在,所以我们就想看一下这个telnetd是如何实现这个特征的。
二、tty设备的前端进程组概念
一个shell一般会引领一个会话(session),这个会话中可以有任意多个任务组,其中有一个是唯一的前台进程组,这一点要注意,这是一个强限制,一个会话只能有一个前台进程组。对于这个前台进程组,内核中的一个tty结构是能够并且必须感知到的,因为tty收到控制字符,例如CTRL+C对应的SIGINT需要由tty驱动发送给某一个前端进程组,所以tty必须知道自己的前端进程组,这个结构在内核中的表示为:
struct tty_struct {
……
    struct pid *pgrp;这个成员表示了一个tty设备的当前前端任务组
    struct pid *session;
三、如何设置tty设备的前端任务组
设置前端任务组是shell一个重要而基本的功能,它在派生用户输入的命令之后,就会通过命令告诉驱动是否更改前端进程组。因为作为驱动来说,它相对比较底层,它只提供机制而没有策略,何时设置哪个进程为前端进程,这些由上层(shell)决定。而用户和操作系统的接口通过ioctl来实现
linux-2.6.21\drivers\char\tty_io.c
int tty_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long arg)
    case TIOCSPGRP:
            return tiocspgrp(tty, real_tty, p);
其中的static int tiocspgrp(struct tty_struct *tty, struct tty_struct *real_tty, pid_t __user *p)函数中最为核心的操作为
    real_tty->pgrp = get_pid(pgrp);
当shell执行一个子命令之后,它会通过这个ioctl命令将当前会话的前端任务组设置为新派生的进程,当然,如果是希望新进程在后台运行,也就是添加了'&'符号,那么这一步会省略,并且shell会继续把持对终端输入的控制权。
四、前端任务组的意义
前端任务组对于shell来说,就是派生完进程之后,把终端前端进程设置为新派生的进程,然后shell会阻塞在waitpid上,等待子进程返回,也就是说在子进程返回之前,shell将会一直阻塞。
1、信号发送
前端任务的意义就在于当用户输入一些控制信息,这些信息可以被转换为信号,这个信号将会发送给谁。例如,我在一个终端上输入CTRL+C,这个组合键会被tty设备转换为一个SIGINT,这个信号是准备好了,那么发给谁呢?发谁谁倒霉啊。这个时候就体现了前端进程组的意义了。
linux-2.6.21\drivers\char\n_tty.c
static inline void isig(int sig, struct tty_struct *tty, int flush)
{
    if (tty->pgrp)
        kill_pgrp(tty->pgrp, sig, 1);这里可以看到,驱动还是比较笨的,它是忠实的把信号发送给了tty中设置的前端任务组
    if (flush || !L_NOFLSH(tty)) {
        n_tty_flush_buffer(tty);
        if (tty->driver->flush_buffer)
            tty->driver->flush_buffer(tty);
    }
}
2、数据读入
这一点更厉害了,对于一个终端,只有前端任务才能从tty中读入数据,如果一个任务不是前端任务,那么它如果执行了终端读入操作,那么它将有幸收到一个SIGTTIN信号,这个信号默认的默认处理是将整个进程组挂起,关于这个行为的描述参考
linux-2.6.21\kernel\signal.c中的注释
 *    |  SIGTTIN           |    stop(*)      |
而这个信号的发送时机为:tty_read--->>>read_chan--->>>job_control
static int job_control(struct tty_struct *tty, struct file *file)
{
    /* Job control check -- must be done at start and after
       every sleep (POSIX.1 7.1.1.4). */
    /* NOTE: not yet done after every sleep pending a thorough
       check of the logic of this change. -- jlc */
    /* don't stop on /dev/console */
    if (file->f_op->write != redirected_tty_write &&
        current->signal->tty == tty) {
        if (!tty->pgrp)
            printk("read_chan: no tty->pgrp!\n");
        else if (task_pgrp(current) != tty->pgrp) {
            if (is_ignored(SIGTTIN) ||
                is_current_pgrp_orphaned())
                return -EIO;
            kill_pgrp(task_pgrp(current), SIGTTIN, 1);
            return -ERESTARTSYS;
        }
    }
    return 0;
}
 五、telnetd如何主动释放控制终端
这一点其实没有那么神秘,我看了之后,比较失望,感觉这个实现简直是猥琐。前面说到,当shell派生一个前端进程的时候,它会把新进程设置为tty前端任务,并通过waitpid等待,只是刚才没有说等待之后,现在补充子进程退出之后shell执行的操作:当子进程退出之后,shell从waitpid返回,然后再次通过前面说的ioctl把前端任务设置为shell自己。根据这个思路,后端任务实现是通过“作弊”的方法来欺骗shell的。既然shell是在等待新派生的任务,那么我就通过fork再派生一个新的子进程,然后父进程(这个是shell唯一识别的并且waitpid等待的进程)通过exit退出,从而让shell返回,而新fork的、相同的子进程则执行真正的telnetd功能,是不是很像孙子兵法中讲的金蝉脱壳。
这里摘录一下busybox中telnetd的相关代码:
telnetd_main--->>>bb_daemonize_or_rexec
if (!(flags & DAEMON_ONLY_SANITIZE)) {
        if (fork_or_rexec(argv))
            exit(EXIT_SUCCESS); /* parent */父进程从这里退出任务,从而让shell的waitpid返回,并回收tty的前端进程控制权,此时telnetd就主动释放了自己的前端控制权
        /* if daemonizing, make sure we detach from stdio & ctty */
        setsid();
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
    }
六、如果前端进程不接受输入,它退出后积压输入如何处理
这里说法比较抽象,大致的意思是:例如我在前端执行
sleep 1000
虽然此时前端任务sleep 1000虽然不接收输入,但是键盘始终是可以输入的,驱动也会接收。不管你接不接收,数据就在那里,不多不少。那么当这个sleep 100任务退出之后,这些输入的数据会到哪里去,或者说从哪里消失呢?
这里分了三种情况,为了说明,我截了个图:
后台任务如何主动释放终端控制权--以telnetd为例 - Tsecer - Tsecer的回音岛
 这里总共执行了三次 sleep 1000,然后在终端中随机输入乱码,只是结束方式不同,然后看一下它们的反应:
1、直接ctrl+C结束
此时可以看到,我对sleep 1000输入的内容从系统中无声无息的消失,这一点可能是大家最为常见的形式,可能也见怪不怪了。
2、在其它窗口通过kill杀死
此时可以看到,我对sleep输入的字符在sleep退出之后被shell再次读到,导致shell提示很多命令找不到的错误,我很遗憾。
3、修改tty设置使能noflsh
通过
stty noflsh
使能终端的noflsh功能,其它操作和第一操作相同,此时再次执行ctrl+C,sleep退出之后,shell同样可以读到乱码输入。
4、三种现象的解释
关于这一点的解释,要看之前我就已经贴过的那个isig函数,为了不让大家翻屏,我把那个代码再完整的贴一下
static inline void isig(int sig, struct tty_struct *tty, int flush)
{
    if (tty->pgrp)
        kill_pgrp(tty->pgrp, sig, 1);
    if (flush || !L_NOFLSH(tty)) {
        n_tty_flush_buffer(tty);
        if (tty->driver->flush_buffer)
            tty->driver->flush_buffer(tty);
    }
}
如果tty没有设置上面的属性,那么会执行n_tty_flush_buffer--->>.reset_buffer_flags
static void reset_buffer_flags(struct tty_struct *tty)
{
    unsigned long flags;

    spin_lock_irqsave(&tty->read_lock, flags);
    tty->read_head = tty->read_tail = tty->read_cnt = 0;
    spin_unlock_irqrestore(&tty->read_lock, flags);
    tty->canon_head = tty->canon_data = tty->erasing = 0;
    memset(&tty->read_flags, 0, sizeof tty->read_flags);
    n_tty_set_room(tty);
    check_unthrottle(tty);
}
所有读入数据被清空。
七、扩充话题及todo
1、setsid()函数意义
在telnetd中,还执行了一个setsid操作,这个操作意义何在?
内核中sys_setsid中两个最为抢眼的操作为:
    group_leader->signal->leader = 1;
……
    group_leader->signal->tty = NULL;
也就是设置调用者的leader标志位,清空进程的控制终端,等待用户为会话设置新的控制终端。而在设置控制终端的操作中,其中的signal->leader==1是一个前置条件,也就是说,非seesionleader无权设置控制终端,对应代码为:
static int tiocsctty(struct tty_struct *tty, int arg)
{
……
    /*
     * The process must be a session leader and
     * not have a controlling tty already.
     */
    if (!current->signal->leader || current->signal->tty) {
        ret = -EPERM;
        goto unlock;
    }
……
}
而这个设置控制终端一般来说就是各种getty工具的功能了,从名字中的tty就可以知道它主要是操作tty的。但是我在看busybox的getty代码的时候只看到了setsid的函数调用,但是没有看到对于ctty的设置,那是不是我就猜错了呢?只能说猜对了一半,这个操作是由内核非常体贴的自动完成的,对应代码为:linux-2.6.21\drivers\char\tty_io.c
static int tty_open(struct inode * inode, struct file * filp)
if (!noctty &&
        current->signal->leader &&
        !current->signal->tty &&
        tty->session == NULL
)
        old_pgrp = __proc_set_tty(current, tty);
对于一个新的session,当打开一个tty设备的时候,前面if中所有的条件都是满足的,所以内核毫不客气的把这个新打开的tty设备作为了这个会话的控制tty。
2、close文件描述符的意义
从telnetd的操作流程来看,它还关闭了自己所有的文件描述符,这个操作的意义何在?实不相瞒,我也不清楚,如果不关闭会有什么问题,现在也看不出来,所以本着不懂不装不回避的精神,这个问题的答案留白。
  评论这张
 
阅读(1325)| 评论(0)
推荐 转载

历史上的今天

评论

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

页脚

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