文章目录
![img](https://img-blog.csdnimg.cn/0048350e14a24a86842848cc843ace55.gif#pic_center)
Linux进程间关系和守护进程
1、进程组
1.1、概念
进程组是一个或多个进程的集合,一个进程组可以包含多个进程。每一个进程组都有唯一一个进程组ID(PGID),这个PGID类似进程ID,同样是一个正整数,可以存放在pid_t的数据类型中。
查看PGID命令:
ps axj|head -1 & ps axj|grep tcp_server
或者:
ps -eo pid,pgid,command|head -1 & ps -eo pid,pgid,command|grep tcp_server
其中:
-e
选项表示 every 的意思, 表示输出每一个进程信息,-o
选项以逗号操作符(,)作为定界符,可以指定要输出的列。xp2@VM-8-12-ubuntu:~/Linux_Advance/day_01/demo_01/tcp_server_cal$ ps -eo pid,pgid,command|head -1 & ps -eo pid,pgid,command|grep tcp_server [1] 1332904 PID PGID COMMAND 1329787 1329787 ./tcp_server 8888 1332906 1332905 grep --color=auto tcp_server [1]+ Done ps -eo pid,pgid,command | head -1 xp2@VM-8-12-ubuntu:~/Linux_Advance/day_01/demo_01/tcp_server_cal$
1.2、组长进程
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命令看到组长进程的现象。
xp2@VM-8-12-ubuntu:~/Linux_Advance/day_01/demo_01/tcp_server_cal$ ps -o pid,pgid,ppid,comm | cat PID PGID PPID COMMAND 1329337 1329337 1329334 bash 1334687 1334687 1329337 ps 1334688 1334687 1329337 cat
可以看到,这里ps进程的PID和bash进程数据不同的进程组,并且都是组长,因为他们的PID和PGID相同。而cat进程是属于ps进程组的,因为它的PID和PGID是不同的,而其PGID和ps的PGID是相同的。
进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。
注意:只要某个进程组中有一个进程存在, 则该进程组就存在(组长可能交给最先创建的进程),这与其组长进程是否已经终止无关。
2、会话
2.1、概念
会话可以看成一个或多个进程组的集合,一个会话可以包含多个进程。各一个会话又会有一个会话ID(SID)。
也就是说:会话包含进程组,进程组包含进程。
通常都是使用管道将几个进程编程一个进程组。如上图的进程4、进程5和进程6可能是下面的命令形成的:
xp2@VM-8-12-ubuntu:~$ sleep 100|sleep 200|sleep 300 & [1] 1341237 xp2@VM-8-12-ubuntu:~$ ps axj|head -1 & ps axj|grep sleep| grep -v grep [2] 1341552 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1340967 1341235 1341235 1340967 pts/6 1341553 S 1002 0:00 sleep 100 1340967 1341236 1341235 1340967 pts/6 1341553 S 1002 0:00 sleep 200 1340967 1341237 1341235 1340967 pts/6 1341553 S 1002 0:00 sleep 300 3439736 1341545 1341545 3439689 ? -1 S 0 0:00 sleep 1 [2]+ Done ps axj | head -1 xp2@VM-8-12-ubuntu:~$
注意:
&
表示将进程组放在后台执行
-a
选项表示不仅列当前⽤户的进程,也列出所有其他⽤户的进程
-x
选项表示不仅列有控制终端的进程,也列出所有⽆控制终端的进程
-j
选项表示列出与作业控制相关的信息, 作业控制后续会讲grep 的
-v
选项表示反向过滤, 即不过滤带有 grep 字段相关的进程从上面打印店信息可以看到,sleep的三个进程对应的PGID相同,属于同一个进程组,且会话ID即SID也相同,属于同一个会话。
2.2、创建会话
使用setsid函数可以创建一个会话,但是前提是调用进程不能是一个进程组的组长。
#include <sys/types.h> #include <unistd.h> pid_t setsid(void);
调用该接口后,当前调用进程将编程会话首进程。此时新会话只有唯一的一个进程。
调用进程变成进程组组长。新进程组ID就是当前调用进程ID
该进程没有控制终端。如果在调用之前该进程存在控制终端(bash是会话首进程),则调用后会切断联系。
注意:如果调用该接口的进程本来是进程组组长,则会报错。解决方案是先调用fork创建子进程,然后父进程退出,让孙子进程继续执行。因为孙子进程肯定不是进程组长!
即:
if(fork() > 0) exit(0); setsid();
2.3、会话ID
会话首进程是只有一个进程 ID 的单个进程(所有ID,比如进程PID、PGID、SID都是一样的), 那么我们可以将会话首进程的进程 ID 当作会话 ID。
注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
3、控制终端
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由Shell 进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组,因为输入输出只有一个控制终端来完成。
如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。
这些特性的关系如下图:
查看当前控制终端打开的个数:
ls /dev/pts
xp2@VM-8-12-ubuntu:~$ ls /dev/pts/ 0 2 3 4 5 6 ptmx
4、作业控制
4.1、作业(Job)和作业控制(Job Control)
作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。
通俗来说,作业就是进程组(一个或者多个进程)。
Shell 分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制。
例如下列命令就是一个作业,它包括两个命令,在执⾏时 Shell 将在前台启动由两个进程组成的作业:
xp2@VM-8-12-ubuntu:~$ cat /etc/fonts/fonts.conf | head -n 5 <?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <!-- /etc/fonts/fonts.conf file to configure system font access --> <fontconfig> <its:rules xmlns:its="http://www.w3.org/2005/11/its" version="1.0"> xp2@VM-8-12-ubuntu:~$
打印
/etc/fonts/fonts.conf
文件的内容,并且只显示前5行。
4.2、作业号
放在后台执⾏的程序或命令称为后台命令,可以在命令的后面加上
&
符号从而让Shell 识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。例如下面的命令在后台启动了一个作业, 该作业由两个进程组成, 两个进程都在后台运⾏:
xp2@VM-8-12-ubuntu:~$ sleep 100|sleep 200 & [1] 1361762 xp2@VM-8-12-ubuntu:~$ cat /etc/fonts/fonts.conf | grep ext & [2] 1362540 xp2@VM-8-12-ubuntu:~$ jobs -l [1]- 1361761 Running sleep 100 1361762 Running | sleep 200 & [2]+ 1362539 Done cat /etc/fonts/fonts.conf 1362540 Exit 1 | grep --color=auto ext xp2@VM-8-12-ubuntu:~$
[1] 1361762
表示的是作业号和进程组的最后一个进程的ID,可以看到作业号是1,当前进程组的最后一个进程ID是1361762。[1]- 1361761 Running sleep 100
表示的分别是第一个作业,-
表示该进程即将成为默认作业的作业;然后是进程ID,进程运行状态,进程名。注意:
查看作业状态命令:
jobs -l
+
:表示该作业号是默认作业
-
:表示该作业即将成为默认作业无符号: 表示其他作业
4.3、作业状态
常见的作业状态:
4.4、作业的挂起和切回
4.4.1、挂起
我们在执行某个作业时,可以使用Ctrl+Z键将该作业挂起,然后Shell会显示相关的作业号、状态以及所执行的命令及信息。
执行下列死循环程序:
#include <stdio.h> int main(){ while(1){ printf("hello\n"); } return 0; }
./test运行该程序,再使用Ctrl+Z键将该作业挂起:
xp2@VM-8-12-ubuntu:~$ gcc -o test test.c xp2@VM-8-12-ubuntu:~$ ./test hello hello ^Z [1]+ Stopped ./test
可以看到,该作业状态变为了Stopped状态(停止状态)。
4.4.2、切回
如果想将挂起的作业切回,可以通过 fg 命令,fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为 1 的作业切到前台来执⾏,若当前系统只有一个作业在后台进⾏,则可以直接使用 fg 命令不带参数直接切回。 具体的参数参考如下:
案例:
把刚刚挂起的test作业切回到前台:
xp2@VM-8-12-ubuntu:~$ fg %1 ./test hello hello
可以发现,又开始无限循环打印hello,该作业被切换回前台了。
注意: 当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有“+”的作业号的作业。
4.5、查看作业状态
可以使用jobs命令查看本用户当前后台或挂起的作业
-l
则显示作业的详细信息-p
则只显示作业的PID案例:
在后台运行一个作业,在前台运行一个作业并挂起,使用jobs命令查看状态:
xp2@VM-8-12-ubuntu:~$ sleep 100 & [1] 1451122 xp2@VM-8-12-ubuntu:~$ ./test hello hello ^Z [2]+ Stopped ./test xp2@VM-8-12-ubuntu:~$ jobs -l [1]- 1451122 Running sleep 100 & [2]+ 1451179 Stopped ./test xp2@VM-8-12-ubuntu:~$ jobs -p 1451122 1451179
4.6、作业控制相关的信号
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在 unix系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
▪ Ctrl + C: 中断字符, 会产生 SIGINT 信号
▪ Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
▪ Ctrl + Z:挂起字符, 会产生 STGTSTP 信号
终端的 I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。我们可以通过下体来看到作业控制的功能:
5、守护进程
守护进程就是使用daemon调用(setsid)接口后在后台运行的进程。
daemon接口:
#include <unistd.h> int daemon(int nochdir, int noclose);
实现daemon接口:
Daemon.hpp
文件:#pragma once #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string> const std::string work_dir = "/"; const std::string path = "/dev/null"; bool Daemon(bool ischdir, bool isclose) { // 1.忽略不要的信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 2.创建子进程,父进程退出 // 因为进程组长不能创建新会话,那就子进程创建新会话 if (fork() > 0) exit(0); // 3.创建新会话 setsid(); // 4.是否需要更改工作目录 if (ischdir) { chdir(work_dir.c_str()); } // 5.是否需要重定向012 if (isclose) { ::close(0); ::close(1); ::close(2); } else { int fd = open(path.c_str(), O_RDONLY); dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); } return true; }
调用接口后,守护进程将在后台执行,与控制终端断开联系。中断进程需要使用kill命令。
6、将服务守护进程化
就将网络版计算器进行守护进程化:网络版计算器代码在第六节
这里我们仅需在
Main.cc
文件修改两处代码:
OKOK,Linux进程间关系和守护进程就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。