Linux 进程间关系与守护进程
一.进程间关系
1.进程组
经过这么久的学习,我们对进程的概念已经很熟悉。其实对于每个进程,除了有一个唯一标识它的PID,它们往往还属于某个进程组,并且每个进程组也有一个独立标识的PGID。进程组是一个进程或多个进程的集合。我们知道,进程是用户的代理,那么进程组就类似于为了完成某项任务而成立的“专项小组”。我们可以看看进程组是什么:
wujiahao@VM-12-14-ubuntu:~$ ps -o pid,pgid,ppid,comm | catPID PGID PPID COMMAND578322 578322 578321 bash578480 578480 578322 ps578481 578480 578322 cat
我们可以看到,ps命令和cat命令的父进程都是bash,而ps和cat同属于一个进程组,因为他们的PGID相同。
再仔细观察,我们可以看到:对于ps,它的PID和PGID是相同的——我们称这样的进程叫做组长进程。
• 进程组组⻓的作⽤: 进程组组⻓可以创建⼀个进程组或者创建该组中的进程
• 进程组的⽣命周期: 从进程组创建开始到其中最后⼀个进程离开为⽌。注意: 主要某个进程组中有⼀个进程存在, 则该进程组就存在, 这与其组⻓进程是否已经终⽌⽆关。
2.会话
2.1什么是会话
我们刚谈论了进程组的概念,一个进程组可以有一个或多个进程。那么会话是什么?它其实早就在我们之前的应用中出现过了。
会话可以看成是⼀个或多个进程组的集合, ⼀个会话可以包含多个进程组。每⼀个会话也有⼀个会话ID(SID)。

我们登录云服务器Linux使用的方法就是用终端xshell进行会话式的登录。登录成功,系统必须为用户创建一个会话,会话内部默认一定有一个进程组叫bash!
我们通常用管道将几个进程编程一个进程组。例如上面的进程组3,可能由下面的命令形成:
proc 4 | proc 5 | proc 6 &(注意,这里&标识将进程调度到后台)
我们可以举一个具体的例子观察这个现象。
wujiahao@VM-12-14-ubuntu:~/TcpEchoServer$ sleep 100 | sleep 200 | sleep 300 &
[1] 581659
wujiahao@VM-12-14-ubuntu:~$ ps ajx|head -n1

可以看到,这三个sleep进程同属于一个进程组,因为他们的PGID相同
2.2进程组与任务
进程组和任务:任务就是某种工作,任务需要通过进程组来完成。
为什么我们直接启动一个任务(进程组),就无法输入命令并执行了?
为什么我们把任务放在后台执行,又能够执行对应的命令?
登录认证模块(账号密码)成功之后,会创建出bash进程和一个终端文件/dev/pts/XX,也就是说会打开标准输出标准错误标准输入。fork并且exec创建出bash。根据子进程对父进程资源的继承,所以所有其他进程都会默认打开标准输入标准输出标准错误。
键盘输入的数据据,必须明确指定一个进程处理(前台进程),所以bash默认在前台。
会话内部,进程组必须区分为前台进程组和后台进程组。为什么?
再一次会话中,有且只能有一个前台进程组,而后台进程组可以有多个。
对进程的相关操作:
jobs:查看当前系统的后台任务
fd 任务号:把指定的任务提到前台,front ground
ctrl+C:只能终止前台任务,后台任务不受键盘输入的影响
ctrl+Z:暂停进程或进程组,会被自动切换到后台,同时把bash进程调度到前台。
bg 任务:让后台任务运行起来。
2.3控制终端
在UNIX系统中,⽤⼾通过终端登录系统后得到⼀个Shell进程,这个终端成为Shell进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输⼊、标准输出和标准错误都指向控制终端,进程从标准输⼊读也就是读⽤⼾的键盘输⼊,进程往标准输出或标准错误输出写也就是输出到显⽰器上。
另外会话、进程组以及控制终端还有⼀些其他的关系,我们在下边详细介绍:
◦ ⼀个会话可以有⼀个控制终端,通常会话⾸进程打开⼀个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
◦ 建⽴与控制终端连接的会话⾸进程被称为控制进程。
◦ ⼀个会话中的⼏个进程组可被分成⼀个前台进程组以及⼀个或者多个后台进程组。
◦ 如果⼀个会话有⼀个控制终端,则它有⼀个前台进程组,会话中的其他进程组则为后台进程组。
◦ ⽆论何时进⼊终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
◦ 如果终端接⼝检测到调制解调器(或⽹络)已经断开,则将挂断信号发送给控制进程(会话⾸进程)。

2.4创建会话
可以调用setsid函数来创建一个会话,前提是不能由一个进程组的组长调用它。
NAMEsetsid - run a program in a new sessionSYNOPSISsetsid [options] program [arguments]DESCRIPTIONsetsid runs a program in a new session. The command calls fork(2) if already a process group leader. Otherwise, it executes a program in the current process. This default behavior is possible tooverride by the --fork option.
该接⼝调⽤之后会发⽣:
◦ 调⽤进程会变成新会话的会话⾸进程(组长)。 此时, 新会话中只有唯⼀的⼀个进程 ◦ 调⽤进程会变成进程组组⻓。 新进程组ID就是当前调⽤进程ID。◦ 该进程没有控制终端。 如果在调⽤setsid之前该进程存在控制终端, 则调⽤之后会切断联系
根据setsid的特殊性,我们往往使用fork创建子进程来执行setsid。⼦进程继续执⾏, 因为⼦进程会继承⽗进程的进程组ID, ⽽进程ID则是新分配的, 就不会出现错误(父进程为组长)的情况。
3.作业
3.1作业控制
作业和进程组是一体两面的关系。对于用户,作业指的是用户为了完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程。进程之间互相协作完成任务,通常是一个进程管道。
Shell分前后台来控制的不是进程⽽是作业 或者进程组。⼀个前台作业可以由多个进程组成,⼀个后台作业也可以由多个进程组成,Shell可以同时运⾏⼀个前台作业和任意多个后台作业,这称为作业控制。
例如,下列命令就是一个作业:它包含两个命令,执行时bash会再前台启动由两个进程组成的作业。
cat /etc/filesystems | head -n 5
3.2作业号与作业状态
放在后台执⾏的程序或命令称为后台命令,可以在命令的后⾯加上 & 符号从⽽让Shell识别这是⼀个后台命令,后台命令不⽤等待该命令执⾏完成,就可⽴即接收新的命令,另外后台进程执⾏完后会返回⼀个作业号以及⼀个进程号(PID)。
例如下⾯的命令在后台启动了⼀个作业, 该作业由两个进程组成, 两个进程都在后台运⾏:
cat /etc/filesystems | grep ext &
[1] 2202
ext4
ext3
ext2
# 按下回⻋[1]+ 完成 cat /etc/filesystems | grep --color=auto ext
执行结果:
◦ 第⼀⾏表⽰作业号和进程ID, 可以看到作业号是1, 进程ID是2202
◦ 第3-4⾏表⽰该程序运⾏的结果, 过滤 /etc/filesystems 有关 ext 的内容 ◦ 第6号分别表⽰作业号、默认作业、作业状态以及所执⾏的命令 关于默认作业:对于⼀个⽤⼾来说,只能有⼀个默认作业 (+) ,同时也只能有⼀个即将成为默 认作业的作业 (-) ,当默认作业退出后,该作业会成为默认作业。 ▪ + : 表⽰该作业号是默认作业 ▪ - :表⽰该作业即将成为默认作业▪ ⽆符号: 表⽰其他作业
作业状态:

作业控制的功能:

二.守护进程
通过上面的讲解我们知道,登录就是建立会话的过程,而关闭终端可能会影响你的服务器的运行。
而日常使用的网络服务器,不能收到任何用户登录和注销的影响。
为服务器创立一个新会话,并将当前任务切换到新会话中。这样的进程,我们叫做守护进程!
守护进程vs前台进程vs后台进程
前台与后台同属于一个会话,拥有标准输入使用权的叫前台进程,守护进程是后台进程的一种,但她有自己的独立会话。
那么,我们如何将之前的服务,变成一个独立的绘画,并成为会话中独立的进程呢?
使用pid_t setsid创建新会话即可
守护进程实际上也是孤儿进程的一种,调用该接口的进程会变成新会话的会话首进程。
1.创建守护进程的步骤
1. 创建子进程,父进程退出
pid_t pid = fork();
if (pid < 0) {exit(EXIT_FAILURE);
} else if (pid > 0) {// 父进程退出,子进程成为孤儿进程,被init接管exit(EXIT_SUCCESS);
}作用:
让出shell控制权
子进程不再是进程组组长,为后续setsid创造条件
2. 创建新会话
// 子进程创建新的会话
pid_t sid = setsid();
if (sid < 0) {exit(EXIT_FAILURE);
}作用:
脱离原控制终端
成为新会话的领头进程
成为新进程组的组长
3. 再次fork(可选但推荐)
// 再次fork,确保不是会话领头进程,防止意外获取控制终端
pid_t pid2 = fork();
if (pid2 < 0) {exit(EXIT_FAILURE);
} else if (pid2 > 0) {exit(EXIT_SUCCESS);
}作用:
确保进程永远不会重新获得控制终端
增加一层隔离
4. 改变工作目录
// 改变工作目录到根目录,避免占用可卸载的文件系统
if (chdir("/") < 0) {exit(EXIT_FAILURE);
}参数含义:
Daemon(0, 0)中的第一个参数控制是否改变工作目录0表示不改变,1表示改变到根目录
5. 重设文件权限掩码
// 重设文件权限掩码,增加守护进程创建文件的灵活性
umask(0);6. 关闭文件描述符
// 关闭所有打开的文件描述符
for (int i = sysconf(_SC_OPEN_MAX); i >= 0; i--) {close(i);
}// 或者只关闭标准输入、输出、错误
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);参数含义:
Daemon(0, 0)中的第二个参数控制是否关闭标准文件描述符0表示不关闭,1表示关闭
2.完整的守护进程Daemon
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const std::string dev = "/dev/null";// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{// 1. 忽略IO,子进程退出等相关的信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN); // SIG_DFL// 2. 父进程直接结束if (fork() > 0)exit(0);// 3. 只能是子进程,孤儿了,父进程就是1setsid(); // 成为一个独立的会话if(nochdir == 0) // 更改进程的工作路径???为什么??chdir("/");// 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的.// 守护进程,不从键盘输入,也不需要向显示器打印// 方法1:关闭0,1,2 -- 不推荐// 方法2:打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/nullif (noclose == 0){int fd = ::open(dev.c_str(), O_RDWR);if (fd < 0){LOG(LogLevel::FATAL) << "open " << dev << " errno";exit(OPEN_ERR);}else{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
3.将网络服务器守护进程化
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage : " << argv[0] << " port" << std::endl;return 0;}uint16_t localport = std::stoi(argv[1]);Daemon(false, false);std::unique_ptr<TcpServer> svr(new TcpServer(localport,HandlerRequest));svr->Loop();return 0;}