目录
一、进程组
什么是进程组?进程组是一个或者多个进程的集合,一个进程组可以有多个进程。每一个进程组都有自己的进程组ID------PGID
如图:这里创建了3个进程
查看进程信息:
3个进程的PGID是一样的,说明它们是在同一个进程组里面
每一个进程组都有一个进程组长,有多个进程,那么进程组长是第一个进程,可看上图。第一个进程的PID与PGID相同;如果一个进程组里只有一个进程,那么这个进程自己就是组长。
一个进程组的组长可以创建进程组或者创建该组的进程:父进程创建子进程
进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。只要进程组还有一个进程存在,那么该进程组就是存在的,与该进程组的组长进程是否存在无关
让父进程先结束,子进程变成孤儿:
cpp
int main()
{
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << "I am child process" << endl;
sleep(1);
}
}
cout << "I am father process" << endl;
sleep(5);
return 0;
}
可以看出,只要进程组还有一个进程存在,进程组就不会消失
二、会话
什么是会话?会话是一个或者多个进程组的集合,一个会话可以包含多个进程组,会话也有自己的会话ID
当用户登录xshell时,系统会给用户提供一个终端文件和一个bash进程,这个bash进程自己为一个进程组;如果用户再登录新的窗口,那么也会有新的终端文件和bash进程。这两者的结合就是一个会话
一个会话可以有多个进程组:同时开启sleep进程组和mytest进程组
SID就是会话ID,会话ID通常是会话中的第一个进程组的第一个进程,一般是bash
这里是让sleep进程在后台运行,后面加个取地址;mytest在前台
规定:
- 一个会话可以运行同时存在多个进程组
- 但是只允许一个前台进程组,可以有多个后台进程组
三、终端控制
什么是终端控制?用户通过终端登录系统后得到一个Shell进程,这个终端就是Shell进程的控制终端。控制终端是保存在PCB中的信息,因为fork进程会复制PCB中的信息,所以由Shell进程启动的其他进程的控制终端也是这个终端。
四、作业控制
什么是作业?作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。
什么是作业控制?Shell 分前后台来控制的不是进程而是作业 或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运行一个前台作业和任意多个后台作业,这称为作业控制
如下图:一个进程组有三个进程
这是前台进程组,用ctrl C 杀掉了
还是这个进程组,3个进程,变成后台:
后面加上取地址符&就是后台进程组,中括号里面的1代表作业编号,后面跟的一串数字是进程ID:
可以看出,是进程组最后一个进程的pid
再创建一个进程组:变成后台
用jobs或者jobs -l 查看后台进程组的信息:
Running表示后台的进程组正在运行
后台 -> 前台:fg 作业编号
这里用ctrl C把一号作业杀了
再次查看:jobs
只有2号作业是后台,并且运行着
把2号作业也变成前台:
前台 -> 后台:先stop(ctrl Z),再 bg + 作业编号
总结:进程组以 sleep 1000 | sleep 2000 为例
创建前台进程组:sleep 1000 | sleep 2000
创建后台进程组:sleep 1000 | sleep 2000 &
注意 :后台进程组接收不到数据,所以用户使用ctrl C或ctrl Z无效,必须是前台进程组才能接收这些信号后台->前台:fg + 作业编号
前台->后台:stop(ctrl+Z) bg+作业编号
+: 表示该作业号是默认作业
-:表示该作业即将成为默认作业
无符号:表示其他作业
五、守护进程
什么是守护进程?一个会话有多个进程组,将一个进程组分离出来,这个进程组自己是独立的会话,如果这个进程组只有一个进程,那么该进程就是守护进程。
为什么有守护进程?如果之前该进程所在的会话销毁了,那么这个进程也随之销毁,变成守护进程,它就不受原来的会话的状态的影响。
怎么变成守护进程?=》一个进程如何将自己变成独立的会话?
调用setsid函数创建会话,调用进程不能是一个进程组的组长,所以为了避免这种情况,可以fork创建子进程,让子进程去调用setsid函数,父进程先退出,这样就不会出错。
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。此时,新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。如果在调用 setsid 之前该进程存在控制终端,则调用之后会切断联系
守护进程化:
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string root = "/";
const std::string path = "dev/null";
void Deamon()
{
// 1.先忽略掉引起程序异常退出的信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2.创建子进程, 让子进程调用setsid
pid_t id = fork();
if(id > 0) exit(0);
// 3.setsid
setsid();
// 4.更改当前进程的工作目录
chdir(root.c_str());
// 5.不需要和用户的输入输出错误关联
int fd = open(path.c_str(), O_RDWR);
if(fd > 0)
{
// 重定向
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
::close(fd);
}
}
这里与标准输入、输出、错误去关联,并不是直接close掉,因为有可能后续的代码还有输入输出的操作,就会导致出错。用重定向的方式,既能避免用户输入输出的干扰,又能防止其他问题出错。重定向到 /dev/null 这个文件中,这是字符设备文件
往这文件读取,什么也读不到,因为文件的内容已经被丢弃了;往这文件写入,写入的内容也会自动被丢弃。
下面做下测试:
cpp
int main()
{
// 守护进程化
Deamon();
while(true)
{
sleep(1);// 模拟一个任务
}
return 0;
}
运行后发现什么也没有,其实是那个进程已经变成了守护进程,它自己是独立的一个会话,不受当前会话的影响。可以查看该进程信息:
该进程就是调用setsid函数前父进程创建的子进程,调用setsid函数后,它是守护进程,也是孤儿进程,可以发现,它的PID、PGID、SID都是一样的,即这个进程是独立的一个会话、会话中唯一的一个进程组的进程。
查看守护进程的当前工作目录:
守护进程的标准输入、输出、错误重定向的位置:
杀掉这个守护进程: