进程间关系与守护进程

2442 字
12 分钟
进程间关系与守护进程

1. 进程组 (Process Group)#

1.1 什么是进程组?#

除了拥有唯一的进程 ID (PID) 之外,每一个进程还属于一个进程组。

  • 概念:进程组是一个或多个进程的集合。

  • PGID:每个进程组都有一个唯一的进程组 ID (PGID),类似于 PID,是一个正整数,存放在 pid_t 数据类型中。

可以通过 ps 命令查看进程所属的进程组:

Terminal window
# -e: 显示所有进程 (every)
# -o: 自定义输出列,以逗号作为定界符
ps -eo pid,pgid,ppid,comm | grep test

1.2 组长进程#

每一个进程组都有一个组长进程。

  • 特征:组长进程的 PID 等于其 PGID(即 PID == PGID)。

  • 作用:组长进程可以创建一个进程组,或者创建该组中的进程。

  • 示例:

Terminal window
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps # ps 是组长进程,PID=2880, PGID=2880
2881 2880 2806 cat # cat 属于 ps 创建的进程组,PGID=2880

说明:从结果上看,ps 进程的 PID 和 PGID 相同,说明 ps 是该进程组的组长进程,该进程组包含 ps 和 cat 两个进程。_

1.3 进程组的生命周期#

  • 从进程组创建开始,到其中最后一个进程离开为止。

  • 注意:只要某个进程组中有一个进程存在,该进程组就存在,这与组长进程是否已经终止无关。


2. 会话 (Session)#

2.1 什么是会话?#

会话与进程组息息相关,可以看成是一个或多个进程组的集合。

  • SID:每一个会话也有一个唯一的会话 ID (SID)。

  • 形成方式:通常我们使用管道将几个进程编成一个进程组,放到后台执行。

# 使用管道将多个命令编成进程组,并放在后台(&)执行
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
  • 查看会话信息:
Terminal window
[node@localhost code]$ ps axj | head -n1
[node@localhost code]$ ps axj | grep sleep | grep -v grep

参数解析:

  • -a:列出当前用户以及所有其他用户的进程。

  • -x:列出所有有控制终端和无控制终端的进程。

  • -j:列出与作业控制相关的信息(如 PGID, SID 等)。

  • grep -v grep:反向过滤,排除 grep 自身的进程。 (结果中属于同一个管道命令的多个进程,其 PGID 和 SID 是相同的。)

2.2 如何创建会话#

可以通过调用 setsid() 函数来创建一个新的会话。

#include <unistd.h>
/*
* 功能:创建会话
* 返回值:创建成功返回 SID,失败返回 -1
*/
pid_t setsid(void);

调用前提:调用进程不能是一个进程组的组长,否则会报错。

调用后的变化:

  1. 调用进程变成新会话的会话首进程。此时新会话中只有该进程一个进程。

  2. 调用进程变成新进程组的组长。新进程组 ID 就是当前调用进程的 PID。

  3. 脱离控制终端。如果调用前该进程有控制终端,调用后会切断联系。

标准实践(避免报错):通常先调用 fork() 创建子进程,然后父进程直接退出 (exit),子进程继续执行 setsid()。因为子进程继承了父进程的组 ID,而获得了新的 PID,从而保证了子进程绝不会是组长进程。

2.3 会话首进程与 SID#

  • 会话首进程:具有唯一 PID 的单个进程,它是会话的创建者。

  • SID(会话 ID):会话首进程的 PID 即被当做是会话 ID。(由于会话首进程总是一个进程组的组长,因此 SID 很多时候也等同于该会话首进程的 PGID)。


3. 控制终端 (Controlling Terminal)#

3.1 概念#

在 UNIX 系统中,用户通过终端登录系统后会得到一个 Shell 进程,这个终端就成为了该 Shell 进程的控制终端。

  • 控制终端的信息保存在进程的 PCB 中。

  • fork() 出来的子进程会继承父进程的 PCB 信息,因此由 Shell 启动的其他进程默认也共用这个控制终端。

  • 默认情况下,进程的标准输入、标准输出、标准错误都指向控制终端(即从键盘读,向显示器写)。

3.2 终端与会话、进程组的关系特性#

  • 一个会话可以有一个控制终端。通常会话首进程打开一个终端后,该终端就成为该会话的控制终端。

  • 建立了与控制终端连接的会话首进程被称为控制进程。

  • 一个会话中的进程组可被分为:一个前台进程组 和 一个或多个后台进程组。

  • 只要会话有控制终端,它就一定会有一个前台进程组。

  • 信号产生与传递:

    • 键盘中断:键入中断键 (Ctrl+C) 或退出键 (Ctrl+\) 时,终端驱动程序会将对应的信号发送给前台进程组的所有进程。

    • 网络断开:如果终端接口检测到连接(如调制解调器或网络)已断开,会将挂断信号(SIGHUP)发送给控制进程(会话首进程)。


4. 作业与作业控制 (Job Control)#

4.1 什么是作业与作业控制?#

  • 作业 (Job):针对用户而言,用户为完成某项任务而启动的进程(或进程管道组合)。一个作业可以包含一个或多个互相协作的进程。

  • 作业控制:Shell 分前后台控制的对象是作业(或进程组),而不是单个进程。Shell 可以同时运行一个前台作业和多个后台作业,这种机制称为作业控制。

4.2 作业号与后台执行#

在命令后加 & 符号,可以让 Shell 识别这是一个后台命令(后台作业)。

后台命令无需等待执行完成,终端可立即接收新命令。后台作业提交后,会返回一个作业号和 PID。

Terminal window
[node@localhost code]$ cat /etc/filesystems | grep ext &
[1] 2202 # [作业号] PID
  • 默认作业 (+) 与即将成为默认作业 (-):

    • +:表示该作业号是当前的默认作业。

    • -:表示当默认作业退出后,该作业即将成为下一个默认作业。

    • 无符号:其他普通后台作业。

4.3 作业状态#

常见作业状态对照表:

作业状态含义说明
正在运行 [Running]后台作业 (&),表示正在正常执行中
完成 [Done]作业已完成,返回的状态码为 0
完成并退出 [Done(code)]作业已完成并退出,返回的状态码非 0
已停止 [Stopped]运行中的作业被挂起(如被 Ctrl+Z 暂停)
已终止 [Terminated]作业被异常终止

4.4 作业的挂起与切回#

  1. 作业挂起 (Ctrl + Z):

    • 在执行前台作业时,键入 Ctrl + Z 会将 SIGTSTP 信号发送给前台进程组的所有进程,使作业挂起变为停止状态 (Stopped)。
  2. 作业切回 (fg):

    • 将挂起或后台的作业切回前台执行。

    • 语法格式:fg [参数]

    • 参数说明:

      • 不带参数:默认将带有 + 号的默认作业切回前台。

      • %n:n 为正整数,表示切回指定的作业号。

      • %string:切回命令以该字符串开头的作业。

      • %?string:切回命令包含该字符串的作业。

      • %+ 或 %%:切回最近提交的作业。

      • %-:切回倒数第二个提交的作业。

4.5 查看作业#

使用 jobs 命令查看当前用户在后台执行或被挂起的作业。

  • jobs:查看简略信息。

  • jobs -l:显示作业的详细信息(包含 PID 等)。

  • jobs -p:只显示作业的 PID。

4.6 终端信号总结#

这 3 个特殊按键可以使终端驱动程序产生信号,并将信号发送至前台进程组(后台进程不受影响):

  • Ctrl + C:中断字符,产生 SIGINT 信号。

  • Ctrl + \:退出字符,产生 SIGQUIT 信号。

  • Ctrl + Z:挂起字符,产生 SIGTSTP 信号。


5. 守护进程 (Daemon Process)#

守护进程是脱离于终端并在后台运行的特殊进程。为了将一个普通程序转化为守护进程,需要执行以下核心步骤:

5.1 守护进程的创建代码封装 (Daemon.hpp)#

#pragma once
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
const char *root = "/";
const char *dev_null = "/dev/null";
void Daemon(bool ischdir, bool isclose)
{
// 1. 忽略可能引起程序异常退出的信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN); // 重点:防止向断开的连接写数据导致进程因 SIGPIPE 崩溃
// 2. 保证自己不是组长进程,以便后续顺利调用 setsid()
if (fork() > 0) {
exit(0); // 父进程退出,子进程继续
}
// 3. 设置自己成为一个新的会话,彻底脱离原有的控制终端
setsid();
// 4. 更改当前工作目录 (CWD)
if (ischdir) {
chdir(root);
}
// 5. 处理标准输入、输出、错误 (0, 1, 2)
if (isclose) {
// 直接关闭
close(0);
close(1);
close(2);
} else {
// 推荐做法:重定向到 /dev/null(数据黑洞)
int fd = open(dev_null, O_RDWR);
if (fd > 0) {
dup2(fd, 0); // 标准输入重定向到 /dev/null
dup2(fd, 1); // 标准输出重定向到 /dev/null
dup2(fd, 2); // 标准错误重定向到 /dev/null
close(fd);
}
}
}

5.2 如何将服务守护进程化#

在网络服务(如 TcpServer)启动之前,调用我们封装好的 Daemon 函数即可让服务以守护进程的形态在后台悄悄运行。

// main.cpp 示例
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]);
// 调用守护进程化接口
// false, false 意味着:不更改工作目录到根目录,不直接 close fd 而是重定向到 /dev/null
Daemon(false, false);
// 启动业务逻辑
std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
svr->Loop();
return 0;
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
HandlerOne
おかえり
公告
本站已整理 138 篇技术笔记,并按 8 条主线排成学习路线(141 站);站内搜索与桌宠对话均已上线。
分类
标签
最新动态
站点统计
文章
138
分类
12
标签
42
总字数
597,044
站点信息
构建平台
腾讯云 · Nginx
博客版本
v6.16.8
文章目录