
研究工具
用于监视进程:
bash
while :; do ps -ajx|head -n 1&& ps -ajx|grep -Ei "mytest|sleep"; echo "---
------------
----------------------" ;sleepp 2; done;

作用监视当前非终端进程,简单介绍一下: ps 是查看进程状态指令,选项 a:表示全部信息,j表示作业信息(这个待会聊守护进程的重点),x:表示忽略不显示关联终端的进程,像你的bash进程系统进程等等
grep -Ei : 筛选指令,选项 正则表达式 (增加匹配规则比如开启 "a|b") 表示对|对字符处理理解为或的意思,i表示忽略大小写查找
键盘输入快捷键 ctrl+c ctrl+z
操作系统的进程调度算法推荐的: 操作系统调度算法3------CFS,完全公平调度器 - 知乎
一. 终端进程组
1. 1 终端&终端文件!
Linux 中终端设备是字符设备文件 (路径如
/dev/tty1、/dev/pts/0),属于内核管理的设备文件(而非普通 "显示文件"),核心功能是承接人机交互的输入 / 输出,意味着终端设备会存放键盘的输入,以及将来发给显示器的信息 。比如你当前终端设备可能会受到你输入的ctrl+c终端本质上也是一个文件,一个操作系统下可以有多个终端文件存放位置在: /dev/pts/
如下图查看我目前linux打开几个终端
显然打开了两个终端终端号分别为0和1,其次就是ptmx是伪终端号用来管理当前操作系统打开的终端文件。
如何查看当前我的终端号呢,具体来说就是当前终端:tty 指令
1.2 浅谈进程组
进程组(process group),首先要理解一个事情,在操作系统当中,对于处理同一个任务的所有进程放到一块来管理叫做进程组,进程组也有自己的标识符pgid,最常见的例子就是
当你的父进程创建了多个子进程,那么这个进程组的组长是父进程,大家都在一个子进程下面: 如下图可以看出来,一个父进程一个子进程, 题目的PGID是一样的,操作系统把他们统一当作在处理同一个作业(jobs) 来管理起来 。我提前些了一个mytest的进程代码如下还有监控结果如下
调用你的监控代码:


bash
int main(){
pid_t pid1 = fork();
pid_t pid2 = fork();
pid_t pid3 = fork();
pid_t pid4 = fork();
(void)pid;
while(1){
// 让父子进程都不退出
}
return 0;
}
1.3 会话&&进程组

首先你已经理解了进程组是多个进程执行同一个任务,以及终端是一个具象的终端文件来理解,那再来理解会话,会话是指当前终端的逻辑交换方式,
那么会话最直观的形式是什么 如下图shell

我打开了两个会话界面,这个界面是shell软件给我展现的页面,我可以在其中一个会话里面输入键盘信息,然后在会话里面会回显我的信息,这是什么?
我的键盘输入的信息是给了谁? 是当前会话的终端设备文件!!! 只不过bash进程也就是命令行进程去终端文件(字符设备文件)然后执行相关的终端命令。
1.4 会话到底是什么? 它不是进程!
一句话总结: 在内核中,会话是一个数据结构,每一个会话下放着: 前台进程组与后台进程组的信息,会话还得去自己的终端设备来存放来自外设的信息和回显给外设信息!!!
内核数据结构的会话就是一个用来管理的逻辑结构:
内核数据结构的session:
cpp
struct session{
sid_t sid; // 当前会话的sid
pid_t pid;// 会话首进程的pid
ppid_t PGID_arr[PGID_NUM];// 进程组数组
ppid_t PGID;// 进程组
int tty; // 当前会话的终端设备
....../
};

如图我们操作系统为了更好的管理所有的进程,以及提高用户的交互体验,就用会话来管理它子下的全部进程组,比如我在当前会话输入信息的文件,过程:
操作系统会把键盘的输入的信息放入到当前会话下的终端设备文件,只会该会话会把数据发送给前台进程组,这也就是为什么:
当我们输入ctrl +z(生成终止信号) 会把整个前台进程终止
二. 前台进程与后台进程
2.1 图解前台进程

同样是这张图,首先在当前会话中它会有自己的前台进制组,只有前台进制组会读取到我们的键盘信息。 如下图: 来理解谁是第一个前台进制组,首先bash进程也就是读取我们一开始输入bash命令的就是bash进程,如果我们执行一个死循环的进程的时候,该子进程会是bash进程的子进程,这就是我们常说的,在bash启动的程序都是bash的子进程,当我们启动一个程序以后mytest进程就成为前台进程组了,它就会读取我们的键盘信息

mytest成为前台进程以及被键盘输入 ctrl+c 杀死

2.2 图解后台进程
其实对于会话来说,不是前台进程就是它在进程信息表的进程组咯,因为对于会话来说管理进程组就足够管理当前会话下的所有进程了,后台进程意味着它不好占据前台,让我的bash不能继续执行任务,那长什么样呢 如下图
bash
// 输入
./mytest & 以&结尾表示进程以后台进程组开始启动
// 监视脚本
ps -ajx|head -n 1&&ps -ajx|grep mytes

问题是什么 ? 问题是现在这个整个进制组都在后台挂着,我的bash依然是前台进程 那我就还可以继续输入前台的命令,原来这个mytest进程是无限循环总是占着前台进程,现在我可以进行输入命令如下图: 他们依旧在后台运行我依旧可以让bash去读取键盘信息

2.3 前台进程与后台进程的操作
我收集了一些管理进程组的一些命令 如下:
bash
fg +任务号 (job号) // 把进程组变为前台进程 并允许
bg +任务号 // 让进程组作为后台进程运行起来
./进程 & // 让进程组成为后台进程组运行起来
jobs查看所有的后台进程组的信息
bash
[lrp@VM-16-15-centos damon_process]$ jobs
[1]- Running ./mytest &
[2]+ Stopped ./mytest
[lrp@VM-16-15-centos damon_process]$ bg 2
[2]+ ./mytest &
[lrp@VM-16-15-centos damon_process]$ jobs
[1]- Running ./mytest &
[2]+ Running ./mytest &
要注意jobs查看的是整个后台进程组哦,我这上面输入ctrl +z 让进程进入到休眠状态,它会被放到后台进程等待被唤醒,我们在用bg 任务号把整个进制组
要理解 一个进制组可以只有一个进程也可以有多个进程,管理进制组就可以管理整个进程组内的进程了,一个进程组的队长通常是父进程它不能突然挂掉或者立刻进程否则它的组员都变成孤儿了!
2.3 ctrl+c 与单独发送信号kill -2的区别

依旧是这张图,我们要深刻理解,对于一个会话来说,进程组才是它的子单位,我们一个会话有自己的终端设备文件,只有前台进制组才会读取我们的终端设备文件的信息,也就是说只有前台进程会读取键盘信息,其次,一个进程组可能只有一个进程比如bash进程当前台进程的时候,一个进程组也可能有多个进程,但是ctrl+c发送的信号是把整个前台进程组杀掉
也就是说它下面的进程都被干掉,区别于kill -2 pid 它只会杀一个具体的pid的进程
更准确的来说ctrl+c 应该是 kill -2 -pgid 是的杀死的是pgid 进程组
实验:

bash
[lrp@VM-16-15-centos damon_process]$ jobs
[1]- Stopped ./mytest
[2] Running ./mytest &
[3]+ Stopped sleep 2
[lrp@VM-16-15-centos damon_process]$ fg2
-bash: fg2: command not found
[lrp@VM-16-15-centos damon_process]$ fg 2
./mytest
^C
[lrp@VM-16-15-centos damon_process]$ while :; do ps -ajx|head -n 1&& ps -ajx|grep -Ei "mytest|sleep"; echo "-------------------------------------" ;sleep 2; done;
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1896 1896 1896 ? -1 Ssl 0 10:16 /bin/sh -c sleep 100
4753 5677 5677 4753 pts/3 4753 T 1001 0:00 sleep 2
20428 14478 14478 20428 pts/2 18519 T 1001 1:36 ./mytest
14478 14479 14478 20428 pts/2 18519 T 1001 1:36 ./mytest
14478 14480 14478 20428 pts/2 18519 T 1001 1:36 ./mytest
14479 14481 14478 20428 pts/2 18519 T 1001 1:36 ./mytest
14478 14482 14478 20428 pts/2 18519 T 1001 1:36 ./mytest
20428 18410 18410 20428 pts/2 18519 T 1001 0:00 sleep 2
20428 18520 18519 20428 pts/2 18519 R+ 1001 0:00 grep --color=auto -Ei mytestsleep
-------------------------------------
^C
[lrp@VM-16-15-centos damon_process]$
我们把在后台运行的进程组 mytest 他有很多子进程 ,我们直接把它用fg 换到前台来然后输入ctrl +c 把整个进制组干掉 ,我们还可以模拟 把上面的正在停止的T状态的全部进程干掉就是把整个进程组干掉
杀死进程组 : kill -9 -pgid
区别于你些的kill -9 pid 因为要区分 加上负号 表示你控制的是进程组
ctrl +c 本质上就是给前台进程组发了一个2号信号
所有本质上就是 kill -2 -pgid
如下实例:
bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4479 14485 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14481 14486 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14481 14487 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14480 14488 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14480 14489 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14482 14490 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14483 14491 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14488 14492 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
14486 14493 14478 20428 pts/2 19259 R 1001 1:36 ./mytest
20428 19260 19259 20428 pts/2 19259 R+ 1001 0:00 grep -
[lrp@VM-16-15-centos damon_process]$ kill -2 -14478
[lrp@VM-16-15-centos damon_process]$ while :; do ps -ajx|head -n 1&& ps -ajx|grep -Ei "mytest|sleep"; echo "-------------------------------------" ;sleep 2; done;
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1896 1896 1896 ? -1 Ssl 0 10:16 /bin/sh -c sleep 100
4753 5677 5677 4753 pts/3 4753 T 1001 0:00 sleep 2
20428 19517 19516 20428 pts/2 19516 S+ 1001 0:00 grep --color=auto -Ei mytestsleep
-------------------------------------
三 . 深入理解会话&&浅谈守护进程
3.1 内核的设计

如果前面的实验都跟着做了,对相关的信息都理解了,那么恭喜你这张图就是让你逻辑闭环的一张图啦。
一起来回答这个问题: 操作系统为什么要用这么多会话呢?会话又怎么管理起来?
首先每一个会话都是人机交互的起点,每一个会话都会有自己的终端设备,我们每次都会像一个指定的会话里面开始交互,一般跟我交互的进程也就是前台进程: 它会读取我们的键盘输入的信息,这个进程一般都是bash进程。
每一个会话都要通过进程组的形式来管理它内部创建的所有进程,但是一定一定一定注意,至始至终这些进程都是bash进程的子进程,以及,会话只是一个数据结构,它帮我们管理起来这些进程而已,这些进程至始至终都在这同一个操作系统当中,所以我们用ps -ajx 查看所有进程我们是查看了操作系统中所有进程的信息,所以我们也能在不同的会话查看不同的被其他会话管理的进程组
所以我们能理解会话其实有很多很多,每一个会话都会创建自己的bash进程如图:

如图我打开了两个会话,第一个会话的终端tty 是2 第二个会话是3
第一个会话的sid是 20428 第二个会话的sid是 4753看吧还有刚刚执行的管道语句它的会话也是在20428,
我们可以在左边这个会话查看的ps但是同样可以查看在右边会话的进程组 如图:
左侧会话终端文件是2 但是我们找到了终端设备为3 的会话的进程 看这个进程组的sid是 4753

对所有进程的管理转为对会话的管理: 会话首进程
、
原本一个系统有无数多个进程,一个用户也可能开辟很多后台进程,有了会话把进程按照进程组管理起来 ,会话这个数据结构存放着它下面的每一个进程组的信息 ,操作系统把所有会话管理起来就把所有进程管理起来了。
如何管理: 先描述在组织:
我们操作系统会用链表: 会话首进程让他把众多会话: 一个个的数据结构组织起来 还是那句话:"先描述在组织"
基于这我们的操作系统就实现了,每一个会话的进程我的都帮你管理起来来了!!!
我只需要管理好每一个会话就足以管理好每一个进程
会话首进程是什么:
就是 每一个会话被创建: 事实上对用户来说就是和你交互的一个页面: 第一个进程: 也叫做会话首进程通常都是bash 所以其实不同的会话直接的 bash进程都是不一样的,但是大家都在同一个操作系统下面 ,其实可以看见对方的进程 这一点如下可以证明 :

如上 command表示你执行的bash命令 通常是执行可执行程序的名字,看 我们 当前的会话的终端设备 3 但是它的会话sid: 16336,以及 我们还看到前台会话的: 如sid: 30320的bash进程 : 它的baash进程的pid跟我们的会话bash 进程的pid就不一样 ,进制组也不一样
2. 浅谈守护化进程

首先你想一个事情我们的操作系统在远端的云服务器挂着永远都不会关机,那么如果我写了一个服务器,而我本地只是打开了一个会话而已,我是通过这个会话和我的linux进行交互的,所以哪怕我把这个会话关闭了,我的linux系统依然在远端开着机呢!
守护进程就仅仅是后台进程?
所以如果我写的一个http服务器进程在远端linux挂着呢,只要那台linux不关机,那我这个进程就一直存在了!
那关键是什么 如上图,我们的进程是什么? 我们用户自己写的进程通常都是在某一个当前打开的会话如下图,这里显示了各自会话的bash进程,我们所有的进程都是当前bash进程的进程哦!!!这一点我前文说过哈。
其次就是 当我们关闭当前会话,你猜呢 当前会话的所有进程组会怎么样! 被杀死!是的!

那么也就是想实现一个进程挂到后台,永远不能被会话杀死!!!!
他就是守护进程!!!!
四. 守护进程
4.2 小实验
本实验验证我们的会话关闭会带走它下面的一系列进进程组:
实验内容:
我们复制当前会话然后在另一个会话监测前者会话开辟的进程组中的多个进程的状况
验证当会话关闭,该会话下的进程组被杀死了
监视工具
cppwhile :; do ps -ajx|head -n 1&& ps -ajx|grep -Ei "mytest|sleep"; echo "--- ------------ ----------------------" ;sleep 2; done;
- 代码 : 依旧是提前创建一批子进程 与当前进程构成一组进程组

- 开辟新会话


- 启动监控开始观察:
双方是不同的终端设备 会话也不同
左侧运行代码 右侧会话来监控:

动态监控结构如下:当我们关闭左侧窗口 查看发现该进程组下的进程全部被杀死了;

4.2 守护进程原理
独立会话
重定向输入和输出 到/dev/null 这相当于一个垃圾回收文件定期会被清理
注意一般都是进程的子进程去当守护进程,因为进程组的组长(通常是父进程不允许独立会话) 如果它独立会话了,进程组下面的子进程都会变成孤儿,不同的会话之间的进程组是具有一定独立性的
如下图来理解独立会话的过程:
我们让子进程调用 setsid: 让他独立出去成为新的会话的首进程,会话首进程(类似于会话的bash进程),注意这个时候该进程跟你原来的会话也就是你现在的页面,就无关了,意味着它的输入和输出也不会跟你公用一个终端设备,所以我们重定义它的输入和输出到 /dev/null
实现到这你的进程就是一个不会首会话影响的守护进程了
- 切换到根目录,通常我们的守护进程都会把工作目录切换到根目录下面,因为如果当前目录实际上是一个临时目录,它可能会被删除,所以要切换到根目录(这个操作也叫做去挂载),因为根目录最稳定,我们的服务器通常就会这样做 。

4.2 自主进程守护化实现
核心函数 :
cpp
fork():创建子进程,父进程退出,子进程脱离终端控制;
setsid():创建新会话,让进程成为会话首进程,彻底脱离终端;
chdir("/"):切换工作目录到根目录,避免占用挂载点;
umask(0):重置文件权限掩码,保证守护进程创建文件权限可控;
close():关闭 STDIN/STDOUT/STDERR(0/1/2)等冗余文件描述符。
我们把守护进程化封装为一个函数吧 :
cpp
int mydaemon(){
// 信号忽略
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
pid_t pid = fork();
sleep(5);
if(pid==0){
//1. 创建子进程
pid_t r = setsid();
if(r<0){
return -1;
}
printf("我是子进程 我已经独立会话了\n");
// 2. 重定向stdin stdout
umask(0);
int fd = open("/dev/null",O_RDWR,0664);
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
// 工作目录
chdir("/");
}
// 父进程退出
exit(0);
return 0;
}
//
// 子进程守护化
int main(){
// 让服务一直不退出
//调用守护进程化
mydaemon();
// 下面执行服务
while(1){
; // 服务 不退出
}
return 0;
}

如图该子进程6940 在过了几秒之后sid跟原理不一样了 以及它的sid也变了,目前来看是分配了一个6940的会话sid给他,终端设备是 ? 本质上就是 null设备去了 。
当系统调用 也有就是下面这个接口 
cpp
int main(){
// 让服务一直不退出
//调用守护进程化
// mydaemon();
// 第一个0 表示重定向 第二0表示确认更改工作目录到根目录
daemon(0,0);
// 下面执行服务
while(1){
; // 服务 不退出
}
return 0;
}
把刚刚的代码改一下就是这样啦 ,直接调用运行结果跟我们的实现差不多如下调用的系统调用分配的sid也是跟他的pid一样 tty(终端设备 )也是为?




