文章目录
- 1. 进程组
- 2. 会话
- 2.1 什么是会话
- 2.2 如何创建会话
- 2.3 守护进程
- 3. 作业控制

1. 进程组
我们运行下面的命令
sleep 10000 | sleep 20000 | sleep 30000
然后查看进程的信息:
可以看到,其实每一个进程除了有进程PID、PPID之外,还属于一个进程组(PGID)。 进程组是一个或者多个进程的集合,一个进程组可以包含多个进程,每一个进程组也有一个唯一的进程组 ID(PGID)。
进程组的ID一般是组长的ID,即组中最先创建的进程的PID。
-
进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
-
进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。 注意:只要某个进程组中有一个进程存在, 则该进程组就存在, 这与
其组长进程是否已经终止无关
。
2. 会话
2.1 什么是会话
系统登录时,会创建终端文件 + bash进程;会话 = bash进程 + 终端文件 (每一个新的登录,都会形成一个-bash进程)
终端文件:
/dev/pts/
向不同的终端文件中输出,就会在其会话页面中显示
- 会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。
- 每一个会话也有一个会话 ID(SID,一般是会话中的第一个进程,即bash )
下面是我在同一个会话中运行了两个程序查询出来的结果
2.2 如何创建会话
那如何创建一个会话呢?
可以调用 setsid
函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
#
include <unistd.h>
/*
*功能: 创建会话
*返回值: 创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时,新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端,则调用之后会切断联系
需要注意的是:
- 这个接口如果调用进程原来是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用 fork创建子进程,父进程终止,子进程继续执行。
- 因为子进程会继承父进程的进程组 ID,而进程 ID 则是新分配的,就不会出现错误的情况。
2.3 守护进程
守护进程(Daemon Process)是一种在后台运行的特殊进程,通常用于执行特定的任务或服务,而不需要用户直接交互。
守护进程的特点
- 后台运行:守护进程在后台运行,不会在前台显示任何界面或交互窗口。它独立于用户终端,即使用户注销,守护进程仍然可以继续运行。
- 独立于控制终端:守护进程通常与控制终端分离,这意味着它不会因终端的关闭而终止。例如,一些网络服务(如 Web服务器)会以守护进程的形式运行,确保服务的持续可用性。
那如何创建守护进程呢?
- 创建子进程并退出父进程:通过 fork() 创建子进程,父进程退出。这样可以确保子进程不会被终端挂起。
- 创建新会话:通过 setsid() 创建一个新的会话,使子进程成为会话的领导者,从而脱离控制终端。
- 改变工作目录:通常将工作目录更改为根目录(/),以避免守护进程因当前工作目录被卸载而导致问题。
- 关闭文件描述符:关闭所有打开的文件描述符,避免资源泄漏。
const char *root = "/";
const char *dev_null = "/dev/null";
void Daemon(bool ischdir,bool isclose)
{//1. 忽略可能引起程序异常退出的信号signal(SIGCHLD,SIG_IGN);signal(SIGPIPE,SIG_IGN);int rid = fork(); //2.创建子进程if(rid > 0){exit(1); //父进程直接退出}int id = setsid(); //3.子进程调用,子进程独立成立一个会话// 4. 每一个进程都有自己的 CWD, 是否将当前进程的 CWD 更改成为 /根目录//以避免守护进程因当前工作目录被卸载而导致问题if(ischdir)chdir(root);// 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了if(isclose){close(0);close(1);close(2);}else{//避免后面有向0,1,2中操作的,为了防止失败//一般将0、1、2重定向到“黑洞”(任何写入到 /dev/null 的数据都会被永久丢弃,不会保存在任何地方)int fd = open(dev_null,O_RDWR);if(fd > 0){dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd); //避免fd泄漏,重定向后关闭}}
}
如何将一个服务器守护进程化呢?特别简单,直接调用Daemon即可
// ./server port
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;
}
即使当前会话关闭了,守护进程依旧还在。它是一个独立的会话
3. 作业控制
- 什么是作业?
作业是针对用户来讲的,一个进程组要完成的任务,一般叫做作业。
一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务(通常是通过进程管道)。
Shell 分前后台来控制的不是进程而是作业或者进程组。 一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成, Shell 可以同时运行一个前台作业和任意多个后台作业,这称为作业控制。
例如:下面通过管道协作的sleep命令就是一个作业
此时无论执行什么(只要会话不退出),都不会影响后台作业。
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上
&
符号从而让Shell识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。
- 作业控制
将作业放回前台:fd + 作业号
如果再想将作业放回后台,要先暂停作业(Ctrl + z
),然后再bg + 作业号
,才可将作业切回到后台运行。
我们可以直接通过输入 jobs 命令查看本用户当前后台执⾏或挂起的作业
- 参数-l 则显示作业的详细信息
- 参数-p 则只显示作业的 PID
关于作业号后面的+、-号
对于一个用户来说,只能有一个默认作业(+号),同时也只能有一个即将成为默认作业的作业(-号),当默认作业退出后,该作业会成为默认作业。
- +:表示该作业号是默认作业
- -:表示该作业即将成为默认作业
- 无符号:表示其他作业
- 作业状态
常见的作业状态如下表所示:
- 作业控制相关的信号
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。
在 unix系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是:
- Ctrl + C: 中断字符, 会产生 SIGINT 信号
- Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
- Ctrl + Z: 挂起字符, 会产生 STGTSTP 信号