APUE读书笔记-第九章 进程关系

时间:2022-11-12 04:26:09

终于把第八章看完了,最后四节直接没写。再来是第九章貌似又不是特别简单的一章。

9.2 终端登陆

9.2 对主流的几种Linux/Unix操作系统的登陆流程进行了简单的介绍。由于本人对于这部分内容了解的知识不是很多,所以就对书中的内容做一个简单的总结。

书中给出了有关于BSD终端登陆的详细过程。BSD终端登陆的流程主要包括以下几个步骤。

  1. init调用fork,子进程exec getty程序。(Ubuntu中有关于getty的说明位于/etc/init/tty1.conf等文件中)
  2. getty对终端设备调用open函数,以读、写方式将终端打开。等待用户输入用户名。
  3. 用户输入用户名后,调用login程序。
  4. 由login完成用户的登陆工作。
  5. 登陆shell读取启动文件:bash是.bash_profile、.bash_login或.profile

终端登陆流程如下图:

9.3 网络登陆

网络登陆的流程与终端登陆的流程稍有不同,我仅就我看懂的内容给大家分享一些,其中肯定有不准确的地方,欢迎大家指出来。

书中给出的有关于BSD网络登陆的流程如下:

  1. init启动inetd,这个进程是守护进程,inetd等待TCP/IP连接请求到达主机。当连接请求到达主机时,inetd执行fork,然后生成的子进程exec某个程序。
  2. 当客户进程打开一个到hostname主机的TCP连接时,inetd通过exec启动telnetd服务进程。

网络登陆流程如下图:

终端登陆与网络登陆都提到了伪终端的概念,给大家也补充一些相关的内容:http://blog.sina.com.cn/s/blog_67c294ca01014uy8.html

9.4 进程组

进程组是一个或多个进程的集合。同一进程组中的进程接受来自同一终端的各种信号。通过以下函数可以获得调用进程的进程组ID。

#include <unistd.h>
extern __pid_t getpgrp (void) __THROW;

每个进程有一个组长进程。组长进程的组ID与进程ID相同。

进程调用setpgid可以加入一个现有的进程组或创建一个新的进程组。函数原型如下:

#include <unistd.h>
extern int setpgid (__pid_t __pid, __pid_t __pgid) __THROW;

setpgid将pid进程的进程组ID设置为pgid。若两个参数相等,则由pid指定的进程变为进程组组长。若pid为0,则使用调用者的进程ID。若pgid为0,则由pid指定的进程ID用作进程组ID。一个进程只能为它自己或它的子进程设置进程组ID。

9.5 会话

有了进程组的概念,再来看看会话的概念。会话就是一个或多个进程组的集合。通常由shell的管道将几个进程编成一组,构成一个进程组。

进程调用以下函数建立会话:

<pre name="code" class="cpp">#include <unistd.h>
extern __pid_t setsid (void) __THROW;

如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新的会话,具体会发生以下三件事: 

  1. 该进程变成新会话的会话首进程(session leader,会话首进程是创建该会话的进程)。此时,该进程新会话中的唯一进程。
  2. 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
  3. 该进程没有控制终端。若在调用setsid之前该进程有一个控制终端,那么这种联系也被切断。

如果该调用进程已经是一个进程组的组长,则此函数返回出错。

getsid函数返回会话首进程的进程组ID。注意这个会话ID一定不等于调用进程的ID与进程组ID。

先看一个示例程序,示例程序来自于一下blog:http://blog.csdn.net/liuxingen/article/details/45586793

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>

int main(int argc, char *argv[])
{
pid_t pid;

fprintf(stdout, "PID:%d, PGID:%d, SID:%d\n", getpid(), getpgrp(), getsid(0));

if(setsid() < 0)
{
fprintf(stderr, "setsid error:%s\n", strerror(errno));
}

return 0;
}
运行结果如下:

PID:3376, PGID:3376, SID:3346
setsid error:Operation not permitted

此处我们故意使用组长进程创建会话,发现确实出错。

再来看看我们之前下的结论,getsid已经具有返回值,说明程序在shell中运行已经存在于某个会话中,则该会话一定不是当前进程所创建。因为若该会话是由当前进程创建,这又与当前进程是组长进程相矛盾,所以当前进程所在的会话ID一定不等于调用进程的ID与进程组ID。

通过上面的输出我们可以推测在当前shell中存在一个会话,该会话包含有两个进程组,进程组ID分别是3346、3376,其中3346是会话的会话首进程的进程组ID。由于setsid在创建会话的同时,还会创建一个新的进程,因此这一ID还是进程组的组长进程。

该会话的关系请见下图:

9.6、9.7、9.8小节我大概看了一下,感觉作用不是很大,在此就不深入研究了。

9.9 shell执行程序

本小节通过两个实例对之前学习的内容进行了验证,分别使用了不支持作业控制的Bsh与支持作业控制的Bash。由于Ubuntu默认的shell就是Bash所以直接通过Bash对书中的知识进行验证,命令如下:

ps -o pid,ppid,pgid,sid,tpgid,comm
PID PPID PGID SID TPGID COMMAND
1983 1977 1983 1983 1996 bash
1996 1983 1996 1983 1996 ps

通过上述命令的运行结果可以发现,ps命令是bash命令的子进程,此时ps进程具有控制终端,所以它是前台进程组。bash是后台进程组。

在后台执行此程序:

 ps -o pid,ppid,pgid,sid,tpgid,comm &
[1] 2002
 PID  PPID  PGID   SID TPGID COMMAND
 1983  1977  1983  1983  1983 bash
 2002  1983  2002  1983  1983 ps

[1]+  Done                    ps -o pid,ppid,pgid,sid,tpgid,comm

此时bash是前台进程组,ps命令是后台进程组。

再来是在一个管道中执行上述命令:

ps -o pid,ppid,pgid,sid,tpgid,comm|cat
PID PPID PGID SID TPGID COMMAND
1983 1977 1983 1983 2058 bash
2058 1983 2058 1983 2058 ps
2059 1983 2058 1983 2058 cat

通过输出可以发现ps、cat命令处于一个进程组(2058),ps进程是组长进程,同时ps与cat所在的进程组是前台进程组。bash是ps与cat的父进程。

9.10 孤儿进程组

之前讨论过孤儿进程的有关概念,在这一章里又提出了一个孤儿进程组的概念。这一概念是指该组中每个进程的父进程要么是该组的一个成员,要么不是该组所属会话的成员。通过定义可以发现,“孤儿进程组”有与“孤儿进程”相类似的地方,“孤儿进程”是与其父进程失去了联系,而“孤儿进程组”是与其会话中的其他进程组失去了联系。

POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。

书中通过一个实例对孤儿进程组的,源码直接使用这篇blog:http://blog.chinaunix.net/uid-15084954-id-190350.html

先把程序的思路简单交代一下。从孤儿进程组的定义出发:“该组中每个进程的父进程要么是该组的一个成员”,通过进程fork产生的子进程自然符合这一条件,“要么不是该组所属会话的成员”,程序最开始执行的进程是由shell通过fork产生而后调用exec启动的,因此这个进程的父进程是同一个会话中不同组的成员。

通过以上分析我们的思路也就出来了(其实也是看过了答案反推的):可以由fork产生子进程,待父进程终止后,子进程自然由init进程继承(Ubuntu下是Upstart进程),Upstart进程不属于当前会话,则此时孤儿进程组就行程了。根据咱们之前的交代:进程在退出时,首先会检查是否会变为孤儿进程组,若变为孤儿进程组,则向其中处于暂停状态的进程发送SIGHUP与SIGCONT信号,由于接收到SIGHUP信号后进程会停止,所以必须提供一个SIGHUP的信号处理函数。但由于当前进程处于暂停状态,因此无法响应SIGHUP信号,所以对于上述两个信号的处理顺序是:先处理SIGCONT信号,进程恢复到运行态,而后才能处理SIGHUP信号。运行结果也验证了这一点。

以上就是程序的基本思路,源码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

#define errexit(msg) do{ perror(msg); exit(EXIT_FAILURE); } while(0)

static void sig_hup(int signo)
{
        printf("SIGHUP received, pid = %d\n", getpid());
}

static void sig_cont(int signo)
{
        printf("SIGCONT received, pid = %d\n", getpid());
}

static void pr_ids(char *name)
{
        printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n",
                        name, getpid(), getppid(), getpgrp(), tcgetpgrp(STDIN_FILENO));
}

int main(int argc, char *argv[])
{
        char c;
        pid_t pid;

        setbuf(stdout, NULL);

        pr_ids("parent");
        if ((pid = fork()) < 0) {
                errexit("fork error");
        } else if (pid > 0) { /* parent */
                sleep(5);
                printf("parent exit\n");
                exit(0);
        } else { /* child */
                pr_ids("child...1");

                signal(SIGCONT, sig_cont);
                signal(SIGHUP, sig_hup);

                kill(getpid(), SIGTSTP); //向自己发送SIGTSTP信号,让自己处于暂停状态

                pr_ids("child...2");

                if (read(STDIN_FILENO, &c, 1) != 1) {
                        printf("read error from controlling TTY, errno = %d\n", errno);
                }
                printf("child exit\n");
        }

        exit(0);
}

运行结果如下:

./test_1
parent: pid = 2633, ppid = 2582, pgrp = 2633, tpgrp = 2633
child...1: pid = 2634, ppid = 2633, pgrp = 2633, tpgrp = 2633
parent exit
SIGCONT received, pid = 2634
SIGHUP received, pid = 2634
child...2: pid = 2634, ppid = 839, pgrp = 2633, tpgrp = 2582
read error from controlling TTY, errno = 5
child exit

程序的最后还从标准输入中读入,由于此时进程组位于后台(父进程在前后执行,子进程在后台执行)。如前所述,当后台进程组试图读控制终端时,对该后台进程组产生SIGTTIN。但在这里,这是一个孤儿进程组,如果内核用此信号停止它,则此进程组中的进程就再也不会继续。POSIX.1规定,read返回出错,其errno设置为EIO(Ubuntu系统上为5)。

由于此处程序产生的就是孤儿进程组,因此后台进程组试图读控制终端时并不能产生SIGTTIN信号,而是令read返回出错,并将其errno设置为EIO。

因此我们在此处尝试一个后台进程组读控制终端的程序,程序源码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

static void sig_ttin(int signo)
{
printf("SIGTTIN received, pid = %d\n", getpid());
}

static void pr_ids(char *name)
{
printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n",
name, getpid(), getppid(), getpgrp(), tcgetpgrp(STDIN_FILENO));
}

int main(int argc, char *argv[])
{
char c;

signal(SIGTTIN, sig_ttin);

if (read(STDIN_FILENO, &c, 1) != 1) {
printf("read error from controlling TTY, errno = %d\n", errno);
}

exit(0);
}

运行命令如下:

./test_2 &

通过“&”,令程序在后台上运行,运行结果如下:

SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
SIGTTIN received, pid = 2353
... //无限循环

上述程序出现无限循环的原因是read函数自动重启(有关内容会在下一章中讲解)。

最后给大家补充一点知识,这是一篇从源码角度分析孤儿进程组的文章:http://blog.chinaunix.net/uid-27767798-id-3711413.html