zoukankan      html  css  js  c++  java
  • daemon任务如何主动释放终端控制权--以telnetd为例

    一、后台任务
    关于后台任务,我就不在这里拷贝一条一条的定义了。所谓的后台任务引起我的注意,是突然想起了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.21driverschar ty_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.21driverschar _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.21kernelsignal.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! ");
            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.21driverschar ty_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的操作流程来看,它还关闭了自己所有的文件描述符,这个操作的意义何在?实不相瞒,我也不清楚,如果不关闭会有什么问题,现在也看不出来,所以本着不懂不装不回避的精神,这个问题的答案留白。
     
     
     
     
     
  • 相关阅读:
    EntityFramework优缺点
    领导者与管理者的区别
    七个对我最好的职业建议(精简版)
    The best career advice I’ve received
    Difference between Stored Procedure and Function in SQL Server
    2015年上半年一次通过 信息系统项目管理师
    Difference between WCF and Web API and WCF REST and Web Service
    What’s the difference between data mining and data warehousing?
    What is the difference between a Clustered and Non Clustered Index?
    用new创建函数的过程发生了什么
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486311.html
Copyright © 2011-2022 走看看