1. 进程组、作业、会话


    1.1 进程组(Process Group)

    a. 每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。

    b. 每一个进程组有一个进程组id和一个组长进程,组长进程的id即为进程组id。

    c. 组长进程可以创建一个进程组。

    d. 只要进程组还有一个进程存在,则进程组存在,与组长进程是否终止无关。


    1.2 作业(Job)

    a. Shell分前后台来控制的不是进程而是作业或者进程组。

    b. Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。

    c. 一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成。

    d. 一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组


    1.3 会话(Session)

    a. 一个或多个进程组的集合。

    b. 一个会话可以有一个控制终端。

    c. 建立与控制终端连接的会话首进程被称为控制进程。

    d. 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。


    1.4 作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。


  2. 终端

    内核中处理终端设备的模块包括硬件驱动程序和线路规程(Line Discipline)。


    2.1 硬件驱动程序

     硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器。


    2.2线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl- Z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。


    2.3终端设备有输入和输出队列缓冲区,如下图所示。

    wKioL1cwj0eCSF1LAABcoyW0UY4936.png

    解析:以输入队列为例,当从键盘输入的字符经过线路规程过滤后,用户已先入先出的顺序读取数据。当输入队列满时再向输入队列输入数据会导致数据丢失,系统也会发出响铃警报。这时,终端可以通过配置回显模式,在这种模式下,终端可以把每个字符给输出队列也可以给用户程序。即,字符不仅可以被程序读取也可以回显在屏幕上。


  3. 终端登录过程

    1、系统启动时,init进程根据配置文件/etc/inittab确定需要打开哪些终端。例如配置文件中有这样一行:1:2345:respawn:/sbin/getty 9600 tty1

    和/etc/passwd类似,

    a. 每个字段用:号隔开。开头的1是这一行配置的id,通常要和tty的后缀一致,配置tty2的那一行id就应该是2。

    b. 第二个字段2345表示运行级别2~5都执行这个配置。

    c. 最后一个字段/sbin/getty 9600 tty1是init进程要fork/exec的命令,打开终端/dev/tty1,波特率是9600(波特率只对串口和Modem终端有意义),然后提示用户输入帐号。中间的respawn字段表示init进程会监视getty进程的运行状态,一旦该进程终止,init会再次fork/exec这个命令,所以我们从终端退出登录后会再次提示输入帐号。

    d. 有些新的Linux发行版已经不用/etc/inittab这个配置文件了,例如Ubuntu用/etc/event.d目录下

    的配置文件来配置init。


    2、getty根据命令行参数打开终端设备作为它的控制终端,把文件描述符0、1、2都指向控制终端,然后提示用户输入帐号。用户输入帐号之后,getty的任务就完成了,它再执行login程序:

    execle("/bin/login", "login", "-p", username, NULL, envp);


    3、login程序提示用户输入密码(输入密码期间关闭终端的回显),然后验证帐号密码的正确性。 如果密码不正确,login进程终止,init会重新fork/exec一个getty进程。如果密码正确,login程序设置一些环境变量,设置当前工作目录为该用户的主目录,然后执行Shell:

    execl("/bin/bash", "-bash", NULL);

    注意argv[0]参数的程序名前面加了一个-,这样bash就知道自己是作为登录Shell启动的,执行登录Shell的启动脚本。从getty开始exec到login,再exec到bash,其实都是同一个进程,因此控制终端没变,文件描述符0、1、2也仍然指向控制终端。由于fork会复制PCB信息,所以由Shell启动的其它进程也都是如此。


  4. 作业控制(Job Control):一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业。

    4.1. Shell分前后台来控制的不是进程而是作业或者进程组。


    4.2. 当用户在控制终端输入特殊的控制键(例如Ctrl-C)时,内核会发送相应的信号(例如SIGINT)给前台进程组的所有进程。



    4.3. 相关信号

    a. &将信号放在后台运行。

    b. jobs命令可以查看当前有哪些作业。

    c. fg命令可以将某个作业提至前台运行,如果该作业的进程组正在后台运行则提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行。参数%1表示将第1个作业提至前台运行。

    d. Ctrl-Z,前台切换到后台,以后台作业的形式存在。

    e. bg命令可以让作业在后台继续运行。

    f. kill命令

    1> 等进程准备继续运行之前处理,默认动作是终止进程。

    2> 给一个停止的进程发SIGKILL信号按系统的默认动作,立刻处理。

    3> SIGSTOP信号与此类似。给一个进程发SIGSTOP信号会使进程停止,这个默认的处理动 作不能改变。这样保证了不管什么样的进程都能用SIGKILL终止或者用SIGSTOP停止,当系统出现异常时管理员总是有办法杀掉有问题的进程或者暂时停掉怀疑有问题的进程。

    wKiom1cy8P3ibLQ7AACNSopJiZw583.png



    4.4 后台进程不允许向控制终端读,但通常是允许写的。如果觉得后台进程向控制终端输出信息干扰了用户使用终端,可以设置禁止后台进程写。

    具体做法:首先用stty命令设置终端选项,禁止后台进程写,然后启动一个后台进程准备往终端写,这时进程收到一个SIGTTOU信号,使进程停止。


  5. 守护进程(Daemon)

    5.1 定义

    a. 系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程叫叫守护进程。

    b. 守护进程也称精灵进程,是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。

    c. Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器http等。守护进程完成许多系统任务。比如,作业规划进程crond等。


    5.2用ps axj命令查看系统中的进程。

    a. 参数a表示不仅列当前用户的进程,也列出所有其他用户的进程;

    b. 参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程;

    c. 参数j表示列出与作业控制相关的信息。

     

    5.3 小常识

    a. 凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。

    b. 在COMMAND一列用[]括起来的名字表示内核线程。通常采用以k开头的名字,表示Kernel



    5.4 相关函数

    wKioL1cy_fCCTIFeAAARN-KC6m4604.png

    a. 成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。

    b. 调用setsid函数创建一个新的Session,并成为Session Leader。调用这个函数之前,当前进程不允许是进程组的Leader,否则该函数返回-1。

    c. 实现:先fork再调用setsid就行了。在子进程中调用setsid。



    5.5 实现步骤

     1. 调用umask将文件模式创建屏蔽字设置为0.

     2. 调用fork,父进程退出(exit)。

     原因:1)如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止使得shell认为该命令已经执行完毕。

         2)保证子进程不是一个进程组的组长进程。

    3. 调用setsid创建一个新会话。

    setsid会导致:1)调用进程成为新会话的首进程。

             2)调用进程成为一个进程组的组长进程 。

             3)调用进程没有控制终端。(再次fork一次,保证daemon进程,之后不会打开tty设备)

    4. 将当前工作目录更改为根目录。

    5. 关闭不在需要的文件描述符。

    6. 其他:忽略SIGCHLD信号。


    5.6代码实现

  1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 
  6 void create_daemon()
  7 {
  8     pid_t id;
  9     struct sigaction sa;
 10 
 11     umask(0);
 12 
 13     id=fork();
 14     if(id<0)
 15     {
 16         printf("fork\n");
 17     }
 18     else if(id>0)
 19     {
 20         //father
 21         exit(0);
 22     }
 23 
 24     setsid();
 25 
 26     sa.sa_handler=SIG_IGN;
 27     sigemptyset(&sa.sa_mask);
 28     sa.sa_flags=0;
 29 
 30     if(sigaction(SIGCHLD,&sa,NULL)<0)
 31         return;
 32 
 33     id=fork();
 34     if(id<0)
 35     {
 36         printf("fork\n");
 37     }
 38     else if(id>0)
 39     {
 40         //father
 41         exit(0);
 42     }
 43 
 44     chdir("/");
 45 
 46     close(0);
 47     close(1);
 48     close(2);
 49 }
 50 int main()
 51 {
 52     create_daemon();
 53     while(1)
 54     {
 55         sleep(1);
 56     }
 57     return 0;
 58 }

输出结果:

wKioL1c0kYuRMiCGAAAgxfY9tTU103.png