《UNUX环境高级编程》(9)进程关系

1、前言

2、终端登录

  • 在早期的UNIX系统,用户用哑终端(用硬连接到主机)进行登录,因为连接到主机上的终端设备数是固定的,所以同时登录数也就有了已知的上限。

  • 随着位映射图像终端的出现,开发出了窗口系统,它向用户提供了与主机系统进行交互的新方式。创建终端窗口的应用也被开发出来,它仿真了基于字符的终端,使得用户可用用熟悉的方式(即shell命令)与主机进行交互。

  • 我们现在描述的过程用于经由终端登录至UNIX系统。该过程几乎与所使用的终端类型无关,所使用的终端可以是基于字符的终端、仿真基于字符的终端,或者运行窗口系统的图形终端

  • 这里说明两种平台的终端登录:

    • BSD终端登录
      BSD的终端登录过程比较经典,linux也是其后继者。

      • 系统管理者创建通常名为/etc/ttys的文件,其中每个终端设备都有一行,每行说明设备名和传到getty程序的参数。例如其中一个参数说明了波特率等等。

      • 当系统自举时,内核创建进程ID为1的进程,也就是init进程。

      • init进程使系统进入多用户模式。

      • init读取/etc/ttys,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则exec getty(get teletypewriter) 程序。init以空环境exec getty程序。

      • getty对终端设备调用open函数,以读、写方式打开终端,此时会得到该终端的文件描述符。一旦该设备被打开,文件描述符012就被通过dup2函数关联到一起,从而共享终端设备的文件表项。然后getty输出“login :”之类的信息,并等待用户键入用户名。

      • 当用户键入了用户名后,getty的工作就完成了。然后它以类似于以下方式调用login程序:

        execle("/bin/login","login","-p",username,(char *)0,envp);
        
      • 其中envp环境变量是根据gettytab文件中的环境字符串生成的,“-p”参数通知login保留传递给它的环境,也可以将其它环境字符串添加到该环境中,但是不要替换它。

      在这里插入图片描述

      • login能处理多项工作。因为它得到了用户名,所以能调用getpwnam取得相应用户的口令文件登陆项。然后login调用getpass以显示“Password:”,接着读入用户键入的口令,它调用crypt将口令加密,并与该用户在阴影口令文件中登录项的pw_passwd字段相比较。

      • 如果用户几次键入的口令都无效,则login以参数1调用exit表示登陆失败。父进程(init)了解到子进程的终止情况后,将再次调用fork,然后又执行了getty,对此终端执行上述过程。
        在这里插入图片描述

      • 如果用户正确登录,login就将完成如下工作:

        • 将当前工作目录更改为该用户的起始目录(chdir)。
        • 调用chown更改该终端的所有权,使登录用户成为它的所有者。
        • 将对该终端设备的访问权限改变成“用户读和写”。
        • 调用setgidinitgroups设置进程的组ID。
        • login得到的所有信息初始化环境:起始目录、shell、用户名、以及一个系统默认路径(PATH).
        • login进程更改为登录用户的ID(setuid)并调用登录用户的shell,类似于:execl("/bin/sh","-sh",(char *)0);
      • 至此,用户登录的登录shell得以开始运行。其父进程是init进程,所以此shell终止时,init会得到通知(接收到SIGCHLD信号),它会对该终端重复全部上述过程。登录shell的文件描述符012设置为终端设备。

      • 现在,登录shell读取其启动文件(.profile),这些启动文件通常更改某些环节变量设置他们自己的PATH,当执行完启动文件后,用户最后得到shell提示符,并能键入命令

    • Linux终端登录

      • linux的终端登录过程非常类似于BSD,它们的主要区别在于说明终端配置的方式。
      • Ubuntu使用的init程序叫作“Upstart”,并使用存放在/etc/init目录的*.conf命名的配置文件。例如:运行/dev/tty1上的getty需要的说明可能放在/etc/init/tty1.conf文件中。

3、网络登录

  • 通过串行终端登录至系统和经由网络登录至系统两者之间的主要区别是:网络登录时,在终端和计算机之间的连接不再是点到点的。在网络登录情况下,login仅仅是一种可用的服务,这与其他网络服务(如FTPSMTP)性质相同。

  • 在上述的终端登录中,init知道哪些终端设备可用用来登录,并为每个设备生成一个getty进程。但是,对网络登录情况有所不同,因为事先并不知道有多少个这样的登录。因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登录。

  • 为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。

  • BSD网络登录

    • 在BSD中,有一个inetd进程(有时称为英特网超级服务器),它等待大多数网络连接。

    • 作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inetd。一旦此shell脚本终止,inetd的父进程就变成init

    • inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后子进程exec适当的程序。
      在这里插入图片描述

    • telnet为例:

      • 主机A启动telnet客户端进程,通过 telnet hostname登录远端名为hostname的主机B。
      • 主机B的inetd进程收到来自主机A的请求。
      • 主机B的inetd进程fork一个子进程并exec主机B上的TELNET进程(被称为talnetd)。
      • 然后talnetd进程打开一个伪终端,并用fork分成两个进程。父进程通过网络连接的通行,子进程执行login程序。
    • 需要理解的是:当通过终端或网络登录时,我们得到一个登录shell,其标准输入、标准输出、标准错误连接到一个终端设备或一个伪终端上。后面将会了解到这一登录shell是一个会话的开始,而此终端或伪终端则是会话的控制终端。

      在这里插入图片描述

  • linux网络登录

    • 除了有些版本使用扩展的因特网服务进程xinetd代替inetd进程外,Linux网络登录的其他方面与BSD网络登录相同。xinetd进程对它所启动的各种服务的控制比inetd提供的控制更加精细。

补充A:线程组

学习Linux内核的时候会涉及到线程组、进程组和会话的概念,本书没有介绍到线程组,所以在此进行补充。

  • 线程组:就是一组线程或者线程组,可以包含线程,也可以包含线程组
    在这里插入图片描述
    • 线程对象关联
      • 1级关联:就是线程组中全部是线程,没有线程组,这样往下就没有可延伸的了。
      • 多级关联:就是线程组中,还有线程组,可以向下延伸
    • 思考:这种线程组中嵌套线程组的方式,可不可以理解为父进程fork出的子进程?
  • POSIX 标准规定,进程的所有线程共享同一进程 ID(即每个线程调用 getpid()都应返回相
    同值),Linux 从 2.4 版本开始引入了线程组(threads group),以满足这一需求。
    在这里插入图片描述
    • 线程组就是共享同一线程组标识(TGID)(thread group identifier)的一组内核调度实体(KSE,kernel scheduling entity
    • getpid()所返回的就是调用者的 TGID。换言之,TGID 和进程 ID 是一回事。(这句话可能有问题,在《深入Linux内核架构》中介绍到:没有使用线程的进程,TGIDPID才相同…有点绕获取可以忽略)。
    • 一个线程组内的每个线程都拥有一个唯一的线程标识符(thread identifier,TID),用以标识自身。
    • 线程 ID 在整个系统中是唯一的,且除了线程担当进程中线程组首线程的情况之外,内核能够保证系统中不会出现线程 ID 与进程 ID 相同的情况。
    • 线程组中首个线程的线程 ID 与其线程组 ID 相同,也将该线程称之为线程组首线程(thread group leader)。
    • 线程组中的所有线程拥有同一父进程 ID,即与线程组首线程 ID 相同。仅当线程组中的所有线程都终止后,其父进程才会收到 SIGCHLD 信号(或其他终止信号)。

4、进程组

  • 每个进程除了有一个进程ID外,还属于一个进程组。

  • 进程组是一个或多个进程的集合。通常,他们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每一个进程组有一个唯一的进程组ID。例如:为了执行命令行ls | sort | more,Shell为三个相应的进程lssortmore创建了一个新的组。Shell以这种方式作用于这三个进程,就好像它们是一个单独的实体(更准确地说是作业

  • 进程组ID类似于进程ID,它是一个正整数,并可存放在pid_t数据类型中。

    #include <unistd.h>
    pid_t getpgrp(void);       //返回调用进程的进程组ID.
    
    pid_t getpgid(pid_t pid);  //返回进程号为pid的进程组ID,若pid=0,则等价于getpgrp
    
  • 每个进程组有一个组长进程。组长进程的进程ID等于该进程组的进程组ID。

  • 只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到最后一个进程离开为止的时间区间称为进程组的生命周期。某个进程组中的最后一个进程可以终止,也可以转移到另一个新的进程组。

  • 进程调用setpgid可以创建一个进程组也可以键入到一个现有的进程组。

    #include <unistd.h>
    int setpgid(pid_t pid,pid_t pgid);  
    
    • setpgid函数将pid进程的进程组ID设置为pgid,如果这两个参数相等,则由pid 指定的进程变成进程组组长;
    • 如果pid=0,则使用调用者的进程ID;
    • 如果pgid=0,则由pid指定的进程ID用作进程组ID;
    • 一个进程只能为自己或它的子进程设置进程组ID。在它的子进程调用了exec后,他就不能更改该子进程的ID了。
  • 在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程进程组ID,并且也使子进程设置其自己的进程组ID,这两个调用中有一个是冗余的,但让父子进程都这样做可以保证,在父进程和子进程认为子进程已经进入了该进程组之前,这确实已经发生了。如果不这样做,在fork之后,由于父进程和子进程运行的先后顺序不确定,会因为子进程的组员身份取决于哪个进程首先执行而产生竞争条件。

5、 会话

  • 会话(session)是一个或多个进程组的集合。其结构可以如下,在一个会话中有3个进程组:
    这里写图片描述

  • 通常是由shell的管道将几个进程编成一组的。上面的安排可能由下列形式的shell命令形成:

    proc1 | proc2 & # 这是后台进程组
    proc3 | proc4 | proc5 # 这是前台进程组
    
  • 进程调用setsid函数建立一个新会话。

    #include <unistd.h>
    pid_t setsid(void);  
    
  • 如果调用此函数的进程不是一个进程组组长,则此函数创建一个新会话。具体会发生以下3件事:

    • 该进程会变成新会话的首进程session leader,会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
    • 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
    • 该进程没有控制终端,如果之前有一个控制终端,那么这种联系也被切断
  • 如果该调用进程已经是一个进程组的组长,则此函数返回出错。 为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。

  • getsid函数返回会话首进程的进程组ID:

    #include <unistd.h>
    pid_t getsid(pid_t pid);  
    
    • 如果pid0getsid返回调用进程的会话首进程的进程组ID

6、控制终端

  • 控制终端对应的文件是/dev/tty

  • 这是一个逻辑概念,即用户正在控制的终端,可以为串行终端虚拟终端伪终端

    • 一个会话可以有一个控制终端(controlling terminal)。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
    • 建立与控制终端连接的会话首进程被称为控制进程(controlling process) 。
      一个会话中的几个进程组可以被分成一个前台进程组(foreground process group)以及一个或多个后台进程组(background process group)。
    • 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
    • 无论何时键入终端的退出键(常常是ctrl+\),都会将退出信号发送至前台进程组的所以进程。
    • 如果终端接口检测到调制解调器或网络已经断开,则将挂断信号发送至控制进程(会话首进程)。
  • 以用户登录系统为例,可能存在如下图所示的情况:
    在这里插入图片描述

  • 通常我们不必担心控制终端,登录时,将自动建立控制终端(如通过终端登录Unix时,getty通过open函数以读写方式打开该终端设备,把文件描述符0、1、2都指向该控制终端)。

  • 如何为会话分配一个控制终端:

    • 当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用open时没有O_NOCTTY,将会将此终端作为控制终端分配给该会话
    • 会话首进程以TIOCSCTTY作为request参数调用ioctl时,会为该会话分配控制终端。为了使此函数成功执行,此会话不能已经有一个控制终端(因此此操作通常跟在setsid调用之后,setsid保证此进程是一个没有控制终端的会话首进程)
  • 程序能与控制终端对话的方法是open文件/dev/tty,如果程序没有控制终端,则打开此设备将失败。

7、 函数tcgetpgrp、tcsetpgrp和tcgetsid

  • 需要一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处

    #include <unistd.h>
    pid_t tcgetpgrp(int fd);    //返回终端为fd的前台进程组ID
    int tcsetpgrp(int fd ,pid_t pgrpid);   //将前台进程组ID设置为pgrpid,终端为fd。
    
    • tcgetpgrp函数返回前台进程组ID,fd引用该会话的控制终端
    • tcsetpgrp函数将前台进程组ID设为pgrppgrp应为同一会话中的一个进程组ID,fd引用该会话的控制终端
  • 大多数应用程序不直接调用这两个函数,它们通常由作业控制shell调用

  • 通过tcgetsid函数获取控制终端的会话首进程的进程组ID

    #include <termios.h>
    pid_t tcgetsid(int fd);
    

8、作业控制

  • 它允许在一个终端上启动多个作业(进程组),控制哪一个作业可以访问该终端,以及哪些作业在后台运行。

  • 我们可以键入一个影响前台作业的特殊字符:挂起键(一般采用Ctrl+Z)与终端进行交互作用。键入此字符使终端驱动程序将信号 SIGTSTP 送至前台进程组中的所有进程,后台进程组作业则不受影响。实际上有三个特殊字符可使终端驱动程序产生信号,并将它们送至前台进程组,它们是:

    • 中断字符(一般采用DELETECtrl+C)产生 SIGINT
    • 退出字符(一般采用Ctrl+\)产生 SIGQUIT
    • 挂起字符(一般采用Ctrl+Z)产生 SIGTSTP
  • 注意只有前台作业接受终端输入,因为每一个对话只有一个前台作业。当后台试图去读取时,终端驱动会使其STOPPED信号(SIGTTIN)停止这个后台作业,而shell则向用户提供相关信息。然后用户就可以用shell命令(fg)将此作业转为前台作业
    在这里插入图片描述

  • shell 在后台起动 cat 进程,但是当 cat 试图读其标准输入(控制终端)时,终端驱动程序知道它是个后台作业,于是将 SIGTTIN 信号送至该后台作业。shell 检测到其子进程的状态改变(回忆对 wait 和 waitpid 的讨论),并通知我们该作业已被停止。然后,**用 shell 的 fg 命令将此停止的作业送入前台运行。这样做使 shell 将此作业转为前台进程组( tcsetpgrp),并将继续信号( SIGCONT )送给该进程组。**因为该作业现在前台进程组中,所以它可以读控制终端。

  • 对于后台作业写入控制终端,我们可以允许或禁止后台作业输出到控制终端。
    在这里插入图片描述

  • 在用户禁止后台作业向控制终端写时,该作业的cat命令试图写其标准输出,此时终端驱动程序识别出该写操作来源于后台进程,于是向该作业SIGTTOU信号,cat进程阻塞。与之前例子一样,用户使用shell的fg命令将该作业转为前台时,该作业继续完成。

9、 shell执行程序(以bash为例)

  • 对于以下指令:

    $ ps -o pid,ppid,pgid,sid,comm
      PID  PPID  PGID   SID COMMAND
      565   453   565   565 bash
      588   565   588   565 ps
    
    • 可见ps的父进程是shell。ps和shell位于同一会话中(会话首进程即为shell进程),且ps位于前台进程组中。
  • 对于管道命令,执行以下命令:

    $ ps -o pid,ppid,pgid,sid,comm | cat | cat
      PID  PPID  PGID   SID COMMAND
      565   453   565   565 bash
     1007   565  1007   565 ps
     1008   565  1007   565 cat
     1009   565  1007   565 cat
    
    • 可见管道两边的进程都是shell的子进程,即shell对于管道中的每一条命令fork一个子进程。且上面的ps和两个cat都位于前台进程组。
  • 但如果是下面的情况:ps和两个cat都位于同一后台进程组。

    $ ps -o pid,ppid,pgid,sid,comm | cat | cat &
    [1] 1204
    $ PID  PPID  PGID   SID COMMAND
      565   453   565   565 bash
     1202   565  1202   565 ps
     1203   565  1202   565 cat
     1204   565  1202   565 cat
    
  • 如果一个后台进程试图读其控制终端:

    $ cat > ttt &
    [1] 1497
    $
    [1]+  Stopped                 cat > ttt
    $ fg %1
    cat > ttt
    wudi     
    123456
    ^D
    
  • 如果后台作业试图读控制终端,则会产生信号SIGTTIN。导致后台进程停止。之后通过fg命令将指定1号作业设为前台作业,此时该作业可以从控制终端中获取输入。

  • 注意:不同shell对创建各个进程的方式不同。

  • 本节中例子是以bash作为示例,而如果是经典的sh,那么对于管道命令则是管道的最后一个进程是shell的子进程,而管道的其他进程则是最后一条命令的子进程。除此之外还有其他区别,具体看书

在这里插入图片描述

10、孤儿进程组

  • 孤儿进程

    • 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  • 僵尸进程:

    • 如果子进程先退出了,父进程还未结束并且没有调用 wait或者 waitpid获取 子进程的状态信息,则子进程残留的状态信息会变成僵尸进程。
    • 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
    • 孤儿进程不会占用系统资源,最终是由init进程托管,由init进程来释放。
  • 孤儿进程组: 整个进程组也可以成为孤儿

    • 该组中每个成员的父进程要么是该组的一个成员,要么在其它会话中。
    • 或者说,一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一个会话的另一个组中。
  • 孤儿进程组提出原因(作用)

    • 当一个终端控制进程(即会话首进程)终止后,那么这个终端可以用来建立一个新的会话。这可能会产生一个问题,当该终端控制进程终止后,原来旧的会话(一个或者多个进程组的集合)中的任一进程可再次访问这个的终端。
  • 为了防止这类问题的产生,**在会话首进程(也是控制进程)终止后仍继续运行的进程组被标记为孤儿进程组。**当一个进程组成为孤儿进程组时,posix.1要求向孤儿进程组中处于停止状态的进程发送SIGHUP(挂起)信号,系统对于这种信号的默认处理是终止进程,然而如果无视这个信号或者另行处理的话那么这个挂起进程仍可以继续执行。但它仍然无法再访问终端。

    • 父进程终止后,进程组成为了孤儿进程组。那么新的孤儿进程组中处于停止(stopped)状态的每一个进程都会收到挂断(SIGHUP)信号,接着又收到继续(SIGCONT)信号。

    • 也就是说,进程组成为孤儿进程组后,孤儿进程组中的状态为stopped的进程会被激活。前提是需要对SIGHUP信号自处理,对挂断信号系统默认的动作是终止进程。

    • 孤儿进程组成为后台进程组,且没有控制终端

    • 孤儿进程组去读控制终端时,read返回出错并将errno设置为EIO。

11、FreeBSD中会话、进程组、进程、控制终端之间的关系实现

在这里插入图片描述

  • session结构体:每个会话都分配一个session结构(如调用setsid时)

    • s_count:进程组数
    • s_leader:会话首进程
    • s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)
    • s_ttyp:指向控制终端tty结构
    • s_sid:会话ID(会话首进程ID)
  • tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构

    • t_session:指向将此终端用作控制终端的会话
    • t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
    • t_termios:包含与该终端的有关信息(如波特率等)
    • t_winsize:包含终端窗口当前大小等信息
  • pgrp结构:包含进程组信息

    • pg_id:进程组ID
    • pg_session:此进程组所属会话
    • pg_members:此进程组的进程成员
  • proc结构:包含一个进程的所有信息

    • p_pid:进程pid
    • p_pptr:指向父进程
    • p_pgrp:指向所属进程组
    • p_pglist:指向所属进程组中上一个和下一个进程

为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体。

  • s_leader:会话首进程

  • s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)

  • s_ttyp:指向控制终端tty结构

  • s_sid:会话ID(会话首进程ID)

  • tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构

    • t_session:指向将此终端用作控制终端的会话
    • t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
    • t_termios:包含与该终端的有关信息(如波特率等)
    • t_winsize:包含终端窗口当前大小等信息
  • pgrp结构:包含进程组信息

    • pg_id:进程组ID
    • pg_session:此进程组所属会话
    • pg_members:此进程组的进程成员
  • proc结构:包含一个进程的所有信息

    • p_pid:进程pid
    • p_pptr:指向父进程
    • p_pgrp:指向所属进程组
    • p_pglist:指向所属进程组中上一个和下一个进程

为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值