进程关系和守护进程
进程组
每一个进程除了有一个进程ID(PID)之外还属于一个进程组。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程,每一个进程组也有一个唯一的进程组ID(PGID), 并且这个PGID 类似于进程ID, 同样是一个正整数, 可以存放在 pid_t
数据类型中
每一个进程组都有一个组长进程。组长进程的ID 等于其进程ID,可以通过ps 命令看到组长进程的
Shell
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 输出结果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat
从结果上看ps
进程的PID
和PGID
相同,那也就是说明ps
进程是该进程组的组长进程, 该进程组包括ps
和cat
两个进程
进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程
进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关,只有当进程组中的最后一个进程退出时,进程组才会退出
会话
当 Linux 收到用户登录的请求时,会对该用户进行鉴权,成功登录后,就会给这个用户创建一个终端文件,这个终端文件是伪终端文件,是内核动态创建的,位于/dev/pts 目录下,不是物理设备文件,该终端用于接收用户输入和输出结果,同时还会给这个终端关联一个 bash 进程,用户向 bash 进程发送命令时,终端文件会接收到,然后发送给 bash 进程,bash 进程将命令解析后再发送回终端文件,创建终端文件和启动 bash 进程的过程就叫做构建了一个会话
命令的传输过程:
会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID),会话ID 在有些地方也被称为会话首进程的进程组ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的,只能有一个前台进程(组),但是可以允许多个后台进程(组),前台进程可以从标准输入中获取数据,而后台进程则不可以
通常是通过管道将几个进程编进一个进程组的:
sleep 10 | sleep 20 | sleep 30
当关闭终端时,这个终端的会话也就会被关闭,会话中的进程组可能会退出,也可能不会,但是一定都会受到影响
- 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端
- 建立与控制终端连接的会话首进程被称为控制进程
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组
- 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
- 无论何时进入终端的中断键(
ctrl+c
)或退出键(ctrl+\
),就会将中断信号发送给前台进程组的所有进程。 - 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)
创建会话
可以调用setseid
函数来创建一个会话, 前提是调用进程不能是一个进程组的组长
pid_t setsid(void);
返回值:创建成功返回SID, 失败返回-1
调用进程会变成新会话的会话首进程。此时, 新会话中只有唯一的一个进程,调用进程会变成进程组组长。新进程组ID 就是当前调用进程ID,该进程没有控制终端。如果在调用setsid 之前该进程存在控制终端, 则调用之后会切断联系
这个接口如果调用进程原来是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用fork 创建子进程,父进程终止,子进程继续执行, 因为子进程会继承父进程的进程组ID,而进程ID 则是新分配的,就不会出现错误的情况
不关闭文件描述符的情况下使进程忽略对应文件描述符的信息:在 Linux 中有一个 dev/null,这个文件会将所有传入的信息忽略,因此可以将其他文件描述符重定向到这个文件描述符
int fd=open(dev_null,O_RDWR);
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
作业控制
作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道
Shell 分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制
在命令的后面加上一个&
表示将进程后台运行,此时会显示命令的相关信息,通过 jobs
命令可以查看到这个作业,通过 fg
命令可以将后台的进程从后台放到前台运行,但是前台的进程需要先使用ctrl+z
让进程暂停,然后再使用 bg
命令将其放入到后台
fg 命令的相关参数
注意: 当通过fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有“+”的作业号的作业
直接通过输入jobs 命令查看本用户当前后台执⾏或挂起的作业
参数-l 则显示作业的详细信息
参数-p 则只显示作业的PID
sleep 100 | sleep 200 | sleep 300&
[1] 44210
#这里的1就是作业号,通过这个作业号对进程进行控制,后面的为进程组的id,也就是整个进程组第一个进程的id
fg 1
#将1号进程放到前台运行,此时可以接收到键盘的输入
#想要放回到后台,需要先使用ctrl+z将进程暂停,然后使用bg 1将进程放回后台
作业状态
作业控制相关的信号
Ctrl + C: 中断字符, 会产生SIGINT 信号
Ctrl + \: 退出字符, 会产生SIGQUIT 信号
Ctrl + Z:挂起字符, 会产生STGTSTP 信号
守护进程
将进程守护进程化:
std::string path="/";
std::string dir="dev/null";
void Daemon(bool isclose,bool ischdir)
{signal(SIGCHLD,SIG_IGN);signal(SIGPIPE,SIG_IGN);if(fork()>0)exit(1);setsid();if(ischdir){chdir(dir.c_str());}if(isclose){::close(0);::close(1);::close(2);}else{int fd=open(dir.c_str(),O_RDWR);dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);}
}
将服务守护进程化
在启动服务前启动守护进程
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;
}