进程关系与守护进程
- 1. 进程组
- 2. 会话
- 3. 控制终端
- 4. 作业控制
- 5. 守护进程
1. 进程组
- 什么是进程组
之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID)之外 还属于一个进程组。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中。
C++
$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
- 组长进程
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID, 如果只有一个进程那么他自己就是组长。我们可以通过 ps 命令看到组长进程的现象:
我们执行这条指令:sleep 1000 | sleep 2000 | sleep 300
会得到三个进程
所以第一创建出来的就是进程组长
- 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
int main()
{pid_t pid = fork();if (pid == 0){while (1){std::cout << "I am child process pid:" << getpid() << std::endl;}}std::cout << "I am father process pid:" << getpid() << std::endl;sleep(5);exit(0);return 0;
}
2. 会话
- 什么是会话
当我在连接一个终端的时候,就会新增加一个会话。
会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话 ID(SID)。
通常我们都是使用管道将几个进程编成一个进程组。 如上图的进程组 2 和进程组 3 可能是由下列命令形成的:
Shell
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
我们举一个例子观察一下这个现象:
Shell
#用管道和 sleep 组成一个进程组放在后台运行
[node@localhost code]$ sleep 1000 | sleep 2000 | sleep 3000 &
并将刚才我们写的代买执行起来
[node@localhost code]$ ./a.out
#查看 ps 命令打出来的列描述信息
[node@localhost code]$ ps axj | head -1 && ps axj | grep -E ‘sleep|a.out’
#&表示将进程组放在后台执行
#-E 选项:支持一点选项的通配
-
创建会话
-
会话id
上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
3. 控制终端
先说一下什么是控制终端?
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
○ 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
○ 建立与控制终端连接的会话首进程被称为控制进程。
○ 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
○ 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
○ 无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
○ 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。
这些特性的关系如下图所示:
4. 作业控制
- 什么是作业(job)和作业控制
1.作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。
2.Shell 分前后台来控制的不是进程而是作业 或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制。
- 作业号
放在后台执⾏的程序或命令称为后台命令,可以在命令的后面加上&符号从而让Shell 识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。
可以通过命令:jobs -l查看详细信息
对于一个用户来说,只能有一个默认作业(+),同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。
+: 表示该作业号是默认作业
-:表示该作业即将成为默认作业
无符号: 表示其他作业
- 作业状态
- 作业的挂起和切回
- fg [作业号]切换到前台工作
ctrl + z 终端运行
- 可以通过命令:bg [作业号] 唤醒作业
- 作业控制相关的信号
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在 unix系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
- Ctrl + C: 中断字符, 会产生 SIGINT 信号
- Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
- Ctrl + Z:挂起字符, 会产生 STGTSTP 信号
终端的 I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。我们可以通过下体来看到作业控制的功能:
5. 守护进程
- 什么是守护进程
这里我们要明白的是,一个进程或者是进程组,无论是前台或者后台都是属于同样个会话的。也就是说一旦我们的会话出现问题了,会话里的进程/进程组都会收到影响。但是向我们日常生活当中的服务器我们是不希望出现这种状况的,因为我们在一个会话中肯定不止会出现一个进程,而且我们也不能保证我们创建的这个会话会一直存在,一直不会出现问题,而服务器确实启动在我们创建的会话里的。所以为了不让服务器收到影响,我们需要将服务器单独的放到一个会话中,形成一个独立的会话,所以这个时候,服务器就由原来的与我们一开始创建的会话成包含关系,变成了并列关系,所以就算我一开始创建的会话出问题了甚至是删除了也不会影响到服务器。所以像这样一个会话中只包含一个进程/进程组我们称之为守护进程
所以一旦服务器称为了守护进程,服务器就不会受到用户的登陆和注册的影响。
- 如何将服务守护进程化
调用函数
C
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时, 新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端, 则调用之后会切断联系
- 需要注意的是: 这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是先调用 fork 创建子进程, 父进程终止, 子进程继续执行,而就算父进程结束了,子进程的组长依旧是父进程, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误情况,这个时候子进程就变成了孤儿进程,所以守护进程是孤儿进程的一种特殊进程。
在介绍一个函数daemon
这里daemon函数是有setsid封装而来的:
daemon()函数适用于希望从控制终端脱离并作为系统守护进程在后台运行的程序。
- 如果nochdir为零,daemon()将进程的当前工作目录更改为根目录(“/”); 否则,当前工作目录保持不变。
- 如果noclose为零,daemon()将标准输入,标准输出和标准错误重定向到/dev/null; 否则,不会对这些文件描述符进行更改。(参数为0时有效)
封装实现
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const static std::string defaultworkdir = "/";
const static std::string defaultdev = "/dev/null";void Daemon(int ischdir, int isclose)
{// 1. 忽略不必要的型号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2. fork 让子进程进行创建if (fork() > 0)exit(0);// 3. setsid()setsid();// 4. 确定是否更改工作目录if (!ischdir)chdir(defaultworkdir.c_str());// 5. 对标准文件012进行重定性if (!isclose){::close(0);::close(1);::close(2);}else{int fd = open(defaultdev.c_str(), O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);::close(fd);}}
}
int main()
{Daemon(1,1);while (1){sleep(1);}return 0;
}