一文理解session, 进程组, 进程和终端之间的关系

现在我们从Session和进程组的角度重新来看登录和执行命令的过程。

  1. getty或telnetd进程在打开终端设备之前调用setsid函数创建一个新的Session,该进程称为Session Leader,该进程的id也可以看作Session的id,然后该进程打开终端设备作为这个Session中所有进程的控制终端。在创建新 Session的同时也创建了一个新的进程组,该进程是这个进程组的Process Group Leader,该进程的id也是进程组的id。

  2. 在登录过程中,getty或telnetd进程变成login,然后变成Shell,但仍然是同一个进程,仍然是Session Leader。

  3. 由Shell进程fork出的子进程本来具有和Shell相同的Session、进程组和控制终端,但是Shell调用setpgid函数将作业中的某个子进程指定为一个新进程组的Leader,然后调用setpgid将该作业中的其它子进程也转移到这个进程组中。如果这个进程组需要在前台运行,就调用tcsetpgrp函数将它设置为前台进程组,由于一个Session只能有一个前台进程组,所以Shell所在的进程组就自动变成后台进程组。

例如:
$ls 这是一个进程组,进程组中只有一个进程
$find ./ -name “*.sh” | xargs grep --color ‘kill’ 这也是一个进程组,有多个进程
一个进程组也叫做一个job, shell job控制命令 bg,fg用来切换前后台进程组

setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new session, the process group leader of the new process group, and has no controlling terminal. The process group ID and session ID of the calling process are set to the PID of the calling process. The calling process will be the only process in this new process group and in this new session.

When a session-leader without a controlling-terminal opens a terminal-device-file and the flag O_NOCTTY is clear on open, that terminal becomes the controlling-terminal assigned to the session-leader if the terminal is not already assigned to some session (see open(2))

前台进程组处于focus的状态,能够读取终端的输入,对于驱动层来说,当一个CTRL+C的组合生成了SIGINT信号,此时这个信号将会广播给前台进程组,大家都会同时收到一份这样的信号。

后台进程组读取终端输入会收到SIGTTIN的信号,这个信号的默认动作是stop,进程组会暂停执行, man 7 signal可以看到:
在这里插入图片描述
如果把进程组变为前台,并且发送SIGCONT信息,可以让该进程组接受终端输入继续执行。
后台进行是可以继续输出的,控制终端的标志位TOSTOP,默认情况下是没有使能的,如果使能了则后台终端也不能输出了,不过bash默认都是没有使能的。

//drivers/tty/n_tty.c
static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
               const unsigned char *buf, size_t nr)
{
    const unsigned char *b = buf; 
    DEFINE_WAIT_FUNC(wait, woken_wake_function);
    int c;
    ssize_t retval = 0; 

    /* Job control check -- must be done at start (POSIX.1 7.1.1.4). */
    if (L_TOSTOP(tty) && file->f_op->write != redirected_tty_write) {                                                                         
        retval = tty_check_change(tty);
        if (retval)
            return retval;
    } 
    ...
}

如果前台进程组运行结束,则Shell就调用tcsetpgrp函数将自己提到前台继续接受用户的下一步命令。

The function tcsetpgrp() makes the process group with process group ID pgrp the foreground process group on the terminal associated to fd, which must be the controlling terminal of the calling process, and still be associated with its session. Moreover, pgrp must be a (nonempty) process group belonging to the same session as the calling process.

If tcsetpgrp() is called by a member of a background process group in its session, and the calling process is not blocking or ignoring SIGTTOU, a SIGTTOU signal is sent to all members of this background process group. These functions are implemented via the TIOCGPGRP and TIOCSPGRP ioctls.

它底层是通过ioctr(fd,TIOCSPGRP,pid)来实现的

static int tiocspgrp(struct tty_struct *tty, struct tty_struct *real_tty, pid_t __user *p)
{
    struct pid *pgrp;
    pid_t pgrp_nr;
    int retval = tty_check_change(real_tty);  //首先需要进行一些检查

    if (retval == -EIO)
        return -ENOTTY;
    if (retval)
        return retval;
    if (!current->signal->tty ||
        (current->signal->tty != real_tty) ||
        (real_tty->session != task_session(current))) //要设置的终端和当前进程是同一个终端,并且同一个sesion
        return -ENOTTY;
    if (get_user(pgrp_nr, p))
        return -EFAULT;
    if (pgrp_nr < 0) 
        return -EINVAL;
    rcu_read_lock();
    pgrp = find_vpid(pgrp_nr);
    retval = -ESRCH;
    if (!pgrp)
        goto out_unlock;
    retval = -EPERM;
    if (session_of_pgrp(pgrp) != task_session(current)) //被设置focus的进程组和当前进程组在同一个session内
        goto out_unlock;
    retval = 0;
    spin_lock_irq(&tty->ctrl_lock);
    put_pid(real_tty->pgrp);
    real_tty->pgrp = get_pid(pgrp);  //tty中记录当前处于focus的进程组
    spin_unlock_irq(&tty->ctrl_lock);
out_unlock:
    rcu_read_unlock();
    return retval;
}
int tty_check_change(struct tty_struct *tty)
{
    return __tty_check_change(tty, SIGTTOU);
}
int __tty_check_change(struct tty_struct *tty, int sig)
{                                                                                                                                             
    unsigned long flags;
    struct pid *pgrp, *tty_pgrp;
    int ret = 0;

    if (current->signal->tty != tty)//同一终端
        return 0;

    rcu_read_lock();
    pgrp = task_pgrp(current);

    spin_lock_irqsave(&tty->ctrl_lock, flags);
    tty_pgrp = tty->pgrp;
    spin_unlock_irqrestore(&tty->ctrl_lock, flags);

    if (tty_pgrp && pgrp != tty->pgrp) { //当前进程处于focus状态
        if (is_ignored(sig)) {
            if (sig == SIGTTIN)
                ret = -EIO;
        } else if (is_current_pgrp_orphaned())
            ret = -EIO;
        else {
            kill_pgrp(pgrp, sig, 1);
            set_thread_flag(TIF_SIGPENDING);
            ret = -ERESTARTSYS;
        }
    }
    rcu_read_unlock();

    if (!tty_pgrp)
        tty_warn(tty, "sig=%d, tty->pgrp == NULL!\n", sig);

    return ret;
}

通过上述的代码,可以发现,要满足一些条件才能调用tcsetpgrp()成功:
1.当前调用的进程处于focus状态;
2.同一个终端
3.同一个session

那么问题来了,shell将终端控制权交给前台进程组之后,怎么收回终端呢?
看bash的代码(这里是bash5.0)

//./jobs.c
/* Give the terminal to PGRP.  */
int
give_terminal_to (pgrp, force)
     pid_t pgrp;
     int force;
{
  sigset_t set, oset;
  int r, e;

  r = 0; 
  if (job_control || force)
    {    
      sigemptyset (&set);
      sigaddset (&set, SIGTTOU);
      sigaddset (&set, SIGTTIN);
      sigaddset (&set, SIGTSTP);
      sigaddset (&set, SIGCHLD);
      sigemptyset (&oset);
      sigprocmask (SIG_BLOCK, &set, &oset);

      if (tcsetpgrp (shell_tty, pgrp) < 0) 
    {    
      /* Maybe we should print an error message? */
#if 0
      sys_error ("tcsetpgrp(%d) failed: pid %ld to pgrp %ld",
        shell_tty, (long)getpid(), (long)pgrp);
#endif
      r = -1;
      e = errno;
    }
      else
    terminal_pgrp = pgrp;
      sigprocmask (SIG_SETMASK, &oset, (sigset_t *)NULL);
    }

  if (r == -1)
    errno = e;

  return r;
}

看懂了吗,bash直接将SIGTTOU信号给屏蔽掉了,再仔细看上面的内核代码,is_ignored(sig),在信号是SIGTTOU时不会报错,这样一切都可以说通了。

如果一个session的leader打开一个终端设备,并且会话组没有控制终端,打开时没有指定非控制终端选项,那么这个终端就会成会会话组的控制终端,看代码说话,
所有tty设备打开的入口都是tty_open(),至于为什么,有兴趣的可以自己去看,这里只是简单的提一下:

struct device *tty_register_device(struct tty_driver *driver, unsigned index,
                   struct device *device)
{
    return tty_register_device_attr(driver, index, device, NULL, NULL);                                                                       
}
static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
        unsigned int index, unsigned int count)
{
    int err;

    /* init here, since reused cdevs cause crashes */
    driver->cdevs[index] = cdev_alloc();
    if (!driver->cdevs[index])
        return -ENOMEM;
    driver->cdevs[index]->ops = &tty_fops;
    driver->cdevs[index]->owner = driver->owner;
    err = cdev_add(driver->cdevs[index], dev, count);
    if (err)
        kobject_put(&driver->cdevs[index]->kobj);
    return err;
}

看到tty_fops明白了吧。
回到正题,看看tty_open()函数:

static int tty_open(struct inode *inode, struct file *filp)
{
	...
	tty = tty_open_by_driver(device, inode, filp);
	...
    if (!noctty &&      //打开时没有设置O_NOCTTY标志位
        current->signal->leader &&   //当前进程是session leader
        !current->signal->tty &&    //当前进程没有控制终端
        tty->session == NULL) {     //终端没有被任何session关联
        /*
         * Don't let a process that only has write access to the tty
         * obtain the privileges associated with having a tty as
         * controlling terminal (being able to reopen it with full
         * access through /dev/tty, being able to perform pushback).
         * Many distributions set the group of all ttys to "tty" and
         * grant write-only access to all terminals for setgid tty
         * binaries, which should not imply full privileges on all ttys.
         *
         * This could theoretically break old code that performs open()
         * on a write-only file descriptor. In that case, it might be
         * necessary to also permit this if
         * inode_permission(inode, MAY_READ) == 0.
         */
        if (filp->f_mode & FMODE_READ)
            __proc_set_tty(tty);
    }
}
static void __proc_set_tty(struct tty_struct *tty)
{
    unsigned long flags;

    spin_lock_irqsave(&tty->ctrl_lock, flags);
    /*
     * The session and fg pgrp references will be non-NULL if
     * tiocsctty() is stealing the controlling tty
     */
    put_pid(tty->session);
    put_pid(tty->pgrp);
    tty->pgrp = get_pid(task_pgrp(current));  //设置终端的focus进程组
    spin_unlock_irqrestore(&tty->ctrl_lock, flags);
    tty->session = get_pid(task_session(current));  //记录终端关联的sessiion
    if (current->signal->tty) {
        tty_debug(tty, "current tty %s not NULL!!\n",
              current->signal->tty->name);
        tty_kref_put(current->signal->tty);
    }
    put_pid(current->signal->tty_old_pgrp);
    current->signal->tty = tty_kref_get(tty); //设置当前进程有控制终端
    current->signal->tty_old_pgrp = NULL;
}   
static struct tty_struct *tty_open_by_driver(dev_t device, struct inode *inode,
                         struct file *filp)
{
	...
	driver = tty_lookup_driver(device, filp, &index);
	...
}
static struct tty_driver *tty_lookup_driver(dev_t device, struct file *filp,
        int *index)
{
	...
	switch (device) {
	...
    default:
        driver = get_tty_driver(device, index); //根据打开设备文件的设备号找到tty_driver
        if (!driver)
            return ERR_PTR(-ENODEV);
        break;
    }
    return driver;
}

会话结束
当一个会话的leader进程退出时,整个进程组将会群龙无首,所以它们很可能会被灭门,这就是所谓的orphan 会话组的概念。

//kernel/exit.c 
void __noreturn do_exit(long code)
{
	...
	 if (group_dead)
        disassociate_ctty(1);
    ...
}
void disassociate_ctty(int on_exit)
{
    struct tty_struct *tty;

    if (!current->signal->leader)
        return;

    tty = get_current_tty();
    if (tty) {
        if (on_exit && tty->driver->type != TTY_DRIVER_TYPE_PTY) {
            tty_vhangup_session(tty);                                                                                                         
        } else {
            struct pid *tty_pgrp = tty_get_pgrp(tty);
            if (tty_pgrp) {
                kill_pgrp(tty_pgrp, SIGHUP, on_exit);
                if (!on_exit)
                    kill_pgrp(tty_pgrp, SIGCONT, on_exit);
                put_pid(tty_pgrp);
            }    
        }    
        tty_kref_put(tty);

    }
    ...
}

当一个session leader退出或者tty出现挂起故障的时候,它只会向自己同一个会话组中的进程发送SIGHUP信号, SIGHUP的默认动作是Term。
nohup
nohup是咋回事呢?nohup干了这么几件事:

将stdin重定向到/dev/null,于是程序读标准输入将会返回EOF

将stdout和stderr重定向到nohup.out或者用户通过参数指定的文件,程序所有输出到stdout和stderr的内容将会写入该文件(有时在文件中看不到输出,有可能是程序没有调用flush)

屏蔽掉SIGHUP信号

调用exec启动指定的命令(nohup进程将会被新进程取代,但进程ID不变)

从上面nohup干的事可以看出,通过nohup启动的程序有这些特点:

nohup程序不负责将进程放到后台,这也是为什么我们经常在nohup命令后面要加上符号“&”的原因

由于stdin、stdout和stderr都被重定向了,nohup启动的程序不会读写tty

由于stdin重定向到了/dev/null,程序读stdin的时候会收到EOF返回值

nohup启动的进程本质上还是属于当前session的一个进程组,所以在当前shell里面可以通过jobs看到nohup启动的程序

当session leader退出后,该进程会收到SIGHUP信号,但由于nohup帮我们忽略了该信号,所以该进程不会退出

由于session leader已经退出,而nohup启动的进程属于该session,于是出现了一种情况,那就是通过nohup启动的这个进程组所在的session没有leader,这是一种特殊的情况,内核会帮我们处理这种特殊情况,这里就不再深入介绍

通过nohup,我们最后达到了就算session leader(一般是shell)退出后,进程还可以照常运行的目的。

daemonize的步骤:

1.setsid()
2.xchdir("/");
3.close(0);
close(1);
close(2);
fd = open(bb_dev_null, O_RDWR);
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);

init进程是一个daemonize进程

本地系统登录过程:

//loginutils/getty.c
1.setsid()
2.open_tty()
static void open_tty(void)
{
	...
	close(0);
	xopen(G.tty_name, O_RDWR | O_NONBLOCK); /* uses fd 0 */
	...
}
	xdup2(STDIN_FILENO, 1);
	xdup2(STDIN_FILENO, 2);
    /* Steal ctty if we don't have it yet */
    tsid = tcgetsid(STDIN_FILENO);
    if (tsid < 0 || pid != tsid) {
        if (ioctl(STDIN_FILENO, TIOCSCTTY, /*force:*/ (long)1) < 0)
            bb_perror_msg_and_die("TIOCSCTTY");
    }

#ifdef __linux__
    /* Make ourself a foreground process group within our session */
    if (tcsetpgrp(STDIN_FILENO, pid) < 0)
        bb_perror_msg_and_die("tcsetpgrp");
#endif

BB_EXECLP(G.login, G.login, "--", logname, (char *)0);   //用login程序替换

4. 执行login程序
        pw = getpwnam(username);
        if (!pw) {                                                                                                                            
            strcpy(username, "UNKNOWN");
            goto fake_it;
        }   

        if (pw->pw_passwd[0] == '!' || pw->pw_passwd[0] == '*') //该用户不允许登录
            goto auth_failed;

        if (opt & LOGIN_OPT_f)
            break; /* -f USER: success without asking passwd */

        if (pw->pw_uid == 0 && !is_tty_secure(short_tty))
            goto auth_failed;

        /* Don't check the password if password entry is empty (!) */
        if (!pw->pw_passwd[0])
            break;
 fake_it:
        /* Password reading and authorization takes place here.
         * Note that reads (in no-echo mode) trash tty attributes.
         * If we get interrupted by SIGALRM, we need to restore attrs.
         */
        if (ask_and_check_password(pw) > 0)
            break;
 ...
change_identity(pw);    //改变身份
run_shell(pw->pw_shell, 1, NULL);   //用登录shell替换login

5.执行登录shell

远程系统登录过程:
这里以telnet为例,当telnet尝试连接telnetd时

static struct tsession *
make_new_session(
        IF_FEATURE_TELNETD_STANDALONE(int sock)
        IF_NOT_FEATURE_TELNETD_STANDALONE(void)
) {
	...
	fd = xgetpty(tty_name);  //得到伪终端的名字
	pid = vfork(); /* NOMMU-friendly */
    if (pid < 0) {
        free(ts);
        close(fd);
        /* sock will be closed by caller */
        bb_perror_msg("vfork");
        return NULL;
    }
    if (pid > 0) {
        /* Parent */
        ts->shell_pid = pid;
        return ts;
    }

    /* Child */
    /* Careful - we are after vfork! */
	pid = getpid();
	/* Make new session and process group */
    setsid();
    /* Open the child's side of the tty */
    /* NB: setsid() disconnects from any previous ctty's. Therefore
     * we must open child's side of the tty AFTER setsid! */
    close(0);
    xopen(tty_name, O_RDWR); /* becomes our ctty */
    xdup2(0, 1);
    xdup2(0, 2);                                                                                                                              
    tcsetpgrp(0, pid); /* switch this tty's process group to us */
	...
	BB_EXECVP(G.loginpath, (char **)login_argv);
}

接下来就是执行login程序了,过程和本地登录一样

login<----伪终端----->telnetd<--------网络------------------->telnet

参考资料:
https://blog.csdn.net/mirkerson/article/details/38107927
https://segmentfault.com/a/1190000009152815

发布了85 篇原创文章 · 获赞 26 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/whuzm08/article/details/104002415