seesion,进程组及终端 TTY 串口 溢出


2013-05-01 19:03:00|  分类: Linux内核|举报|字号 订阅

一、终端的作用
去年的这个时候再早一些,我觉得串口终端在嵌入式中是至关重要的,所以花费了一些时间对linux的终端进行了分析,现在大部分内容已经忘光了,看了之前写的东西,感觉印象很少,里面的内容跟新的差不多。就好象翻陈年的衣服,从里面找到了一张一分钱的钞票一样,有的只是一点诧异和不解。脑袋中的知识和CPU内或者内存数据库中的cache一样,被时间不断的冲刷,最终可能什么都没有留下,只要外界的一些物质,勉强能够唤醒一些记忆中的残留。
之后,可能会认为终端在之后的工作中会渐渐的淡出,慢慢的只是作为一个陈年的老朋友,熟悉但又联系很少。后来发现只是串口终端不再使用,而基于ssh和telnet的伪终端依然是不可缺少的工具,再后来遇到了一个后台启动进程的问题,再次触发了对于终端的兴趣,所以这里还是补充一下。
问题的起因是这样的,我们需要使用一个服务,这个服务是一个作为服务程序启动的后台任务。按照我常规的理解,一个提供服务的进程应该是作为守护(daemon)任务存在的。但是这个任务就偏不,它非常不专业的在启动之后依然向启动的串口中打印自己的调试信息,当时让我大吃一斤。奇怪的是这种做法并没有出问题,那么很多人也可能会觉得,这样为什么就会出问题呢?
这又促使我想了想所谓的后台任务、守护任务、进程组、会话组这些到底有什么区别,为什么守护进程启动的时候要执行一系列的setsid、chdir之类的操作,如果不执行这些操作会有什么后果?
二、后台任务
所谓的后台任务,它有两种实现形式,一种是让shell帮忙,在shell启动的时候通过在命令的后面加上逻辑与符号表示创建的子进程希望成为一个后台任务,这样方法是把进程是否作为后台任务运行的决策权放在了bash这一层,而本质上是放给了程序的最终使用者。这种对于一些可以和用户交互的程序来说是有一定道理的,比如shell本身,它可以执行脚本,也可以接受用户输入,和用户实时交互。一些专有的服务程序本身就不支持和用户交互,所以它放在前台没有意义,也就是只能作为后台程序运行。
程序如果只能作为后台运行,那么程序就可以自己把自己设置为后台任务,这方面的例子就是telnetd,它在启动的时候不论有没有在进程后面加上后台标志,它都不会抓住输入不方,而是在启动后自己直至返回。而实现的方法也非常简单,它就是通过fork+exit的方式来个金蝉脱壳。父进程(shell)对于前台启动的进程会通过waitpid来等待自己直接等待的子进程的退出,当子进程退出之后,shell就可以回收终端控制权(之前终端控制权在创建子进程时下发给子进程),从而可以接受用户的下一次收入。
1、终端控制权的转移
所谓的终端控制权,其实就是说当一个终端上有数据到来的时候,它到底发送给哪一个进程。可以想象,对于socket来说,报文本身包含了操作系统全局的 host+protocol+port,所以底层的驱动很容易找到接受结构。而对于一个终端来说,它的输入发送给谁就不太容易确定。用户输入一个字符,它可能大家每个人都来响应一下一样(这样说并不准确,例如信号是例外的),所以驱动程序就要知道这个这个输入到底是发送到哪里?当它到来的时候哪个进程获得输入?这个问题如果采用自上而下的角度来看比较简单,因为答案就是当前的前台进程,但是对于驱动层来说,它不可能依赖于上层的进程,更别说shell,前端这样的高级货。
确定的方法比大家想象的可能都要简单,那就是上层直接指定你要把这个信号发送给谁,并且内核会做校验,如果说当前终端的接受进程(进程组)是A,那么所有除了A之外的进程如果尝试读取这个终端,会有幸获得一个SIGIN信号,这信号大家可能很少听说过,但是结束掉一个进程是足够乐得。
设置前台进程组的方法是通过ioctr(fd,TIOCSPGRP,pid)来实现的,不过这个名字过于大众化,所以用户态的封装名字叫做tcsetpgrp。
它的意义有两方面,
对于用户层来说,只有一个终端的前台进程组才有权限从这个设备中读取信息,其它的进程读取时会收到SIGIN(如果屏蔽了该信号,read返回EIO)。
对于驱动层来说,当一个CTRL+C的组合生成了SIGINT信号,此时这个信号将会在进程组内进行广播,大家都会同时收到一份这样的信号。
2、内核中代码
2、1 上层read时检测
linux-2.6.21\drivers\char\n_tty.c
static ssize_t read_chan(struct tty_struct *tty, struct file *file,unsigned char __user *buf, size_t nr)--->>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;
}
2、2 驱动层控制信号生成
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);
    }
}
2、3 终端前台进程设置
linux-2.6.21\drivers\char\tty_io.c
static int tiocspgrp(struct tty_struct *tty, struct tty_struct *real_tty, pid_t __user *p)
real_tty->pgrp = get_pid(pgrp);
但是这里并不是全部,不要以为调用tcsetpgrp就可以随意设置自己为终端的前台任务,因为之前还有一个检测代码
int tty_check_change(struct tty_struct * tty)
{
    if (current->signal->tty != tty)
        return 0;
    if (!tty->pgrp) {
        printk(KERN_WARNING "tty_check_change: tty->pgrp == NULL!\n");
        return 0;
    }
    if ( task_pgrp(current) == tty->pgrp) 那就是如果要设置一个终端的前台进程,设置者必须是当前终端的拥有者,为什么会强调一下这个细节呢?因为我自己写测试程序验证的时候第一次就遇到了这个情况,并且收到了接下来的SIGOUT信号,导致进程组处于T状态
        return 0;
    if (is_ignored(SIGTTOU))
        return 0;
    if (is_current_pgrp_orphaned())
        return -EIO;
    (void) kill_pgrp(task_pgrp(current), SIGTTOU, 1);
    return -ERESTARTSYS;
}
2、4我的问题测试代码
[root@Harry session]# cat giveup.c 
#include <unistd.h>
#include <errno.h>
int main()
{
    pid_t forker = fork();
    if (0 == forker)    
    {
        int isetret =    tcsetpgrp(0,getpid());
        printf("set pgrp %d errno %d pid %d\n", isetret, errno,getpid());
        sleep(1000);
    }
    else if (forker > 0)
    {
        printf("parent exit\n");
        return 0;
    }
    printf ("forker error\n");
}
[root@Harry session]# 
2、5 bash如何实现从子进程回收
如果是这样,bash应该也会遇到这个问题,因为当子进程退出之后,bash和子进程一定不在同一个进程组中,所以bash也应该受到这个信号,那bash为什么就可以这么淡定呢?
bash的代码写的比较随意,名字也是如此,相关代码如下
bash-4.1\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); 也就是bash在回收终端控制权的时候首当其冲的屏蔽了这个TTOU信号,属于霸王硬上弓,强制修改了这个终端的前台进程
      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;
}
3、进程组的意义
3、1   默认对输出没有限制
前面其实已经说到了进程组的两个主要意义,这里需要补充一点,默认情况下,非前台的任务虽然不能读取终端输入,但是可以向终端进行输出操作。所以一个任务作为后台任务运行时,它就依然可以向终端输出自己信息。控制的标志位TOSTOP标志,默认情况下终端是没有使能改标志的,例如我的终端设置
root@Harry session]# stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase  -tostop -echoprt
echoctl echoke
内核中
static ssize_t write_chan(struct tty_struct * tty, struct file * file, const unsigned char * buf, size_t nr)
    /* 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;
    }
3、2   setpgid的作用
之前一致说前台进程,事实上前台进程是一个进程组,也就是可以有多(任意多)个进程来共享这个“前台”的地位。最为常用的用法就是大家时时刻刻都在使用,但是时时刻刻都没有在意的管道,一个管道中所有的进程都会在同一个进程组中。这是bash代劳完成的,由于通常管道对外的输入只有第一个进程的输入和最后一个输出(中间的输入和输出互相连接),所以没有多个进程同时读取的情况。
作为一个演示的例子,我们可以试一下在子进程中主动退出进程组,然后看一下它是否会收到信号的影响。
[root@Harry session]# cat quiter.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    setpgid(getpid(), getpid());
    sleep(1000);
}
[root@Harry session]# ./quiter 1 | ./quiter 2 | ./quiter 3
^C
^C
^C
^C
[root@Harry ~]# ps aux | grep quit
root      4959  0.0  0.0   1740   280 pts/2    S    19:21   0:00 ./quiter 2
root      4960  0.0  0.0   1740   276 pts/2    S    19:21   0:00 ./quiter 3
root      4984  0.0  0.0   4220   692 pts/5    S+   19:22   0:00 grep quit
后启动的自立门户的进程没有收到SIGINT信号。
3、3 如果从bash中抢夺了终端控制权
和bash一样,在创建的子进程中从bash中抢夺终端控制权。由于在子进程退出之后,shell会再次把自己设置为前台进程,所以先创建子进程,之后父进程退出,子进程休眠之后(等待shell回收控制权),再次从把自己设置为前台进程,此时看一下shell的反应。
[root@Harry session]# cat graber.c
#include <stdio.h>
#include <signal.h>
#include <errno.h>

int main()
{
    pid_t pid = fork();
    if (0 == pid)
    {
    while (1)
{
    sleep(2);
          sigset_t set, oset;
  sigemptyset (&set);
      sigaddset (&set, SIGTTOU);
      sigaddset (&set, SIGTTIN);
      sigaddset (&set, SIGTSTP);
      sigaddset (&set, SIGCHLD);
      sigemptyset (&oset);
      sigprocmask (SIG_BLOCK, &set, &oset);
    int iSet = tcsetpgrp(1, getpid());
    pid_t owner = tcgetpgrp(1);
    printf("iSet %d ,errno %d owner %d \n", iSet, errno, owner);
    
//    sleep(1000);
    char buff[100];
//    while (1) // 如果把循环放在这里,通常bash不会有问题,测试程序不断返回-EIO,所以将while放在更外层,每次读取前都设置
    {
        int iRead = read(1, buff, sizeof buff);
        sleep(2);
        printf("iRead %d errno %d\n", iRead, errno);
    }
 sigprocmask (SIG_SETMASK, &oset, (sigset_t *)NULL);
}
    sleep(10000);
    }
}
[root@Harry session]# 
测试的现象就是会发现bash会突然间就直接退出了,但是借助一些小技巧,是很容易知道退出原因的。这里只是分析一下问题的原因:
在shell回收前端控制权之后,它通过read(readline库)来读取用户输入,此时用户终端前台任务依然是shell,所以通过验证,shell被挂在tty的输入唤醒队列中,当终端有输入到达时,shell被唤醒,读到终端控制权,然后再次修正,再次挂接,如此循环不息。
如果测试程序也不断的再把这个抢占回来,那么总有一个时间,测试程序获得了前台进程的地位,并且消耗掉终端输入,此时shell再次读取时不再是前台任务,所以收到SIGTOU信号。libreadline中对于这种情况的处理就是直接返回错误,完整调用链如下
(gdb) bt
#0  0x002d6ed0 in read () from /lib/libc.so.6
#1  0x080eda23 in rl_getc (stream=0x37f420) at input.c:469
#2  0x080ed9c4 in rl_read_key () at input.c:446
#3  0x080d85e1 in readline_internal_char () at readline.c:517
#4  0x080d86e6 in readline_internal_charloop () at readline.c:579
#5  0x080d8707 in readline_internal () at readline.c:593
#6  0x080d824d in readline (prompt=0x814a188 "[root@Harry busybox-1.14.2]# ")
    at readline.c:342
#7  0x08063a29 in yy_readline_get () at /Users/chet/src/bash/src/parse.y:1433
#8  0x08063970 in yy_getc () at /Users/chet/src/bash/src/parse.y:1366
#9  0x08064604 in shell_getc (remove_quoted_newline=1)
    at /Users/chet/src/bash/src/parse.y:2215
#10 0x080653d8 in read_token (command=0)
    at /Users/chet/src/bash/src/parse.y:2871
#11 0x08064ccf in yylex () at /Users/chet/src/bash/src/parse.y:2493
#12 0x08060e6a in yyparse () at y.tab.c:2029
#13 0x08060b65 in parse_command () at eval.c:228
#14 0x08060c40 in read_command () at eval.c:272
#15 0x08060960 in reader_loop () at eval.c:137
#16 0x0805ea89 in main (argc=1, argv=0xbffff3a4, env=0xbffff3ac) at shell.c:749
(gdb) 
在readline_internal_char ()中,此时返回的字符c为EIO,所以bash认为读取到了eof,直接退出
     /* look at input.c:rl_getc() for the circumstances under which this will
     be returned; punt immediately on read error without converting it to
     a newline. */
      if (c == READERR)
    {
#if defined (READLINE_CALLBACKS)
      RL_SETSTATE(RL_STATE_DONE);
      return (rl_done = 1);
#else
      eof_found = 1;
      break;
#endif
    }
三、会话的意义
1、会话的创建
新会话的创建一般是由login、telnetd、sshd之类的进程来创建,创见的方法和setgpid类似,此时执行的就是setsid。会话同样是一个内核感知的逻辑结构。不同的session一般有自己的控制终端,也就是tty,这个tty在前面看到,可以不断的设置自己的前端进程组。
2、会话中tty的由来
会话中tty可以通过ioctl设置,当然也可以让程序自动识别。具体来说,如果一个session的leader打开一个终端设备,并且会话组没有控制终端,打开时没有指定非控制终端选项,那么这个终端就会成会会话组的控制终端。
static int tty_open(struct inode * inode, struct file * filp)
retry_open:
    noctty = filp->f_flags & O_NOCTTY;
    if (! noctty &&  没有指定不能作为控制终
         current->signal->leader &&  当前进程为session leader
        ! current->signal->tty &&  当前进程没有控制终端
         tty->session == NULL) 终端没有指定会话
        old_pgrp = __proc_set_tty(current, tty);
3、会话结束
当一个会话的leader进程退出时,整个进程组将会群龙无首,所以它们很可能会被灭门,这就是所谓的orphan 会话组的概念。在session leader退出的时候,有下面逻辑
linux-2.6.21\kernel\exit.c
    if (group_dead && tsk->signal->leader)
        disassociate_ctty(1);
void disassociate_ctty(int on_exit)
if (tty) {
        tty_pgrp = get_pid(tty->pgrp);
        mutex_unlock(&tty_mutex);
……
if (tty_pgrp) {
        kill_pgrp(tty_pgrp, SIGHUP, on_exit);
        if (!on_exit)
            kill_pgrp(tty_pgrp, SIGCONT, on_exit);
        put_pid(tty_pgrp);
    }

这里也就看出为什么一个守护进程启动的时候要执行setsid,其实这里的set本质上都是一个create操作,也就是另立门户,脱离之前的组织关系。对于守护任务来说,它执行了setsid之后,当一个session leader退出或者tty出现挂起故障的时候,它只会向自己同一个会话组中的进程发送SIGHUP信号,从而不再受外界的影响。
四、守护任务的创建
守护任务的创建还包括了close自己的文件描述符,更改自己的工作路径等操作,这些可能都是为了释放它对资源的引用计数,打开的文件自然不用说明,肯定会占用文件的引用计数。另一方面对于设置当前工作目录,同样会占用文件引用计数,这个相对不是很明显而已。
asmlinkage long sys_chdir(const char __user * filename)---->>>>>
void set_fs_pwd(struct fs_struct *fs, struct vfsmount *mnt,
        struct dentry *dentry)
{
    struct dentry *old_pwd;
    struct vfsmount *old_pwdmnt;

    write_lock(&fs->lock);
    old_pwd = fs->pwd;
    old_pwdmnt = fs->pwdmnt;
     fs->pwdmnt = mntget(mnt);
     fs->pwd = dget(dentry);
    write_unlock(&fs->lock);

    if (old_pwd) {
        dput(old_pwd);
        mntput(old_pwdmnt);
    }
}
如果你占用了文件的引用计数,那么就会导致这个文件没有办法被实质性删除(只是通过ls没有看到,但事实上还在硬盘上存在),而且可能对挂载的文件系统的卸载也有影响(待确定)。
五、串口设备的接收及溢出
1、中断的使能及生效
通常我们常见的串口为uart串口,也就是8250芯片,位于内核的6250.c文件中。它在中断处理中需要识别一个tty结构,这个指针是在uart_open中初始化,而中断本身也是在uart_open中注册,所以当中断到来时,tty指针一定非空。在tty_open中,会判断同一个tty设备只会被打开一次。
2、接收的溢出
溢出有两种,一种是硬件溢出,一种是软件溢出。硬件溢出比如串口芯片缓冲区满,而驱动程序还没来得及读取,此时就会出现串口的数据丢失现象,这个现象通过芯片的状态寄存器知道,上层通过标志位来获得
#define TTY_OVERRUN    4
软件的溢出不太常见,主要是由于读取程序对于输入处理过慢,或者一个前台进程不再从终端读入,而是在计算圆周率,那么此时驱动即使来得及读取,此时也不会有足够的内存来存放所有的数据。当然网卡也有这种问题,只是TCP有重传机制。
tty_insert_flip_char--->>>tty_insert_flip_string_flags--->tty_buffer_request_room--->>tty_buffer_find--->>>tty_buffer_alloc
{
    struct tty_buffer *p;

    if ( tty->buf.memory_used + size > 65536超过上线不再分配,认为数据丢失
        return NULL;
    p = kmalloc(sizeof(struct tty_buffer) + 2 * size, GFP_ATOMIC);
    if(p == NULL)
        return NULL;
    p->used = 0;
    p->size = size;
    p->next = NULL;
    p->commit = 0;
    p->read = 0;
    p->char_buf_ptr = (char *)(p->data);
    p->flag_buf_ptr = (unsigned char *)p->char_buf_ptr + size;
    tty->buf.memory_used += size;
    return p;
}

你可能感兴趣的:(seesion,进程组及终端 TTY 串口 溢出)