Linux笔记---进程间关系与守护进程
1. 进程组
在操作系统中,进程组(Process Group) 是由一个或多个相关进程组成的集合,用于简化对多个关联进程的统一管理。它是 Unix/Linux 系统中进程组织的重要概念,主要为了方便信号传递、作业控制等操作。
每个进程组由唯一的 进程组 ID(PGID) 标识。通常,进程组的创建者(称为 “组长进程”)的进程 ID(PID)会作为该进程组的 PGID(即 PGID = 组长进程的PID)。
什么是有关联的进程呢?有父子关系的进程、通过管道相连的进程……这些在操作系统层面上有关联,需要相互协作的进程就叫有关联的进程。
例如,我们在命令行中输入如下的命令:
sleep 1000 | sleep 2000 | sleep 3000
这三个sleep进程之间就是有关联的进程,我们使用ps命令查看这三个进程的信息:
我们可以发现,三者的PGID是相同的,这就代表他们是一个进程组的。其中,sleep 1000的PID和PGID是相同的,这就代表它是这个进程组的组长。
当一个进程没有与其他进程产生关联时,它自己就构成一个进程组,同时也是组长。
进程组的主要作用:
- 批量信号处理:可以向整个进程组发送信号(而非单个进程),简化对关联进程的统一控制。例如,在终端中按下 Ctrl+C 时,内核会向前台进程组的所有进程发送 SIGINT 信号(在上面的例子当中,如果我们按下Ctrl+C,三个sleep进程会一起被终止)。
- 作业控制:shell 利用进程组实现 “前台 / 后台作业” 管理。例如,通过 & 让命令在后台运行时,shell 会为其创建一个独立的进程组。一个进程组成为前台进程组时,本质上是组长进程成为前台进程,并与终端交互。
2. 会话
在 Unix/Linux 操作系统中,会话(Session) 是比进程组更高一级的进程组织单位,由一个或多个进程组组成,主要用于管理进程与控制终端的关联,是作业控制和终端交互的核心机制。
每个会话由唯一的 会话 ID(SID) 标识,通常以创建会话的进程(称为 “会话首进程”)的 PID 作为 SID(即 SID = 会话首进程的PID)。
用户登录(或者说开启一个终端),操作系统就会为用户创建一个会话,这个会话就关联到该终端。shell程序(bash)就作为会话的首进程:
而在某个终端当中启动的进程(组)就属于这个终端关联的会话:
当用户退出时(关闭终端),对应的会话就会被关闭,其中的进程组也就随之被全部终止。
一个会话只关联一个终端,所以会话当中的进程组就可以分为两类:
- 一个前台进程组
- 若干后台进程组
会话的主要作用就是进行终端资源管理:会话将进程组与控制终端绑定,确保终端资源(输入、输出、信号)被有序分配给前台进程组,避免多个进程同时争抢终端。
会话的生命周期与属于其的进程组相关,当会话中的进程组全部终止之后,会话就会结束。
若会话关联了控制终端,终端关闭时内核会向会话首进程发送 SIGHUP 信号(默认导致进程终止),并向前台进程组发送 SIGHUP 和 SIGCONT 信号。若这些进程因此终止,且会话中无其他进程组,则会话结束。
3. 守护进程
在 Unix/Linux 操作系统中,守护进程(Daemon) 是一类在后台长期运行的特殊进程,通常用于提供系统服务或周期性执行任务,不与用户直接交互,完全脱离终端控制。
特点:
- 后台运行:守护进程不占用终端,用户无法通过终端直接输入指令与其交互,其运行状态也不会干扰终端的正常使用。
- 脱离终端:守护进程与启动它的终端完全脱离,即使终端关闭(如用户退出登录),守护进程也能继续运行(不会收到终端关闭产生的 SIGHUP 信号)。
- 生命周期长:守护进程通常随系统启动而启动,随系统关闭而终止,在整个系统运行期间持续提供服务(如 Web 服务器 nginx、数据库服务 mysqld 等)。
- 无控制终端:守护进程没有控制终端(tty),其标准输入、输出、错误输出通常会被重定向到 /dev/null 或日志文件。
守护进程是系统服务的核心载体,常见用途包括:
- 网络服务:如 nginx(Web 服务器)、sshd(远程登录服务)、dnsmasq(DNS 服务)等,持续监听网络请求并响应。
- 系统管理:如 crond(定时任务服务)、systemd(系统进程管理器)等,负责周期性任务或系统资源管理。
- 后台处理:如日志收集进程、数据同步进程等,在后台默默处理系统或应用数据。
创建守护进程的核心就是让某个进程成为一个新会话的首进程。如此一来,只要守护进程不退出,会话就不会结束,守护进程就能长期存在。
3.1 setsid系统调用
在 Unix/Linux 系统中,setsid() 是一个核心的系统调用,主要用于创建一个新的会话,并让调用进程成为该会话的会话首进程。它是实现创建守护进程(进程脱离终端控制)的关键函数。
#include <unistd.h>
pid_t setsid(void);
返回值:
- 成功时,返回新会话的 ID(即调用进程的 PID)。
- 失败时,返回 -1,并设置 errno 指示错误原因(最常见的错误是 EPERM:调用进程已是某个进程组的组长)。
setsid() 有一个重要限制:调用进程不能是任何进程组的组长。 如果调用进程恰好是进程组组长(例如,直接在主进程中调用 setsid()),函数会调用失败(返回 -1,errno=EPERM)。
解决这个限制条件的常见做法就是在调用setsid前先调用fork函数,并让父进程立即退出。
3.2 创建守护进程的标准流程
- 创建子进程,父进程退出:为调用setsid()做准备。
- 创建新会话(
setsid()
):脱离原终端的控制(原终端不再与该会话关联)。 - 再次 fork 并退出父进程(可选但推荐):孙子进程不再是会话首进程,确保它无法再打开新的控制终端(只有会话首进程能关联终端)。
- 改变工作目录(可选):将工作目录切换到根目录(/)或特定目录(如 /var/run),避免守护进程占用某个可卸载的文件系统(如挂载的 U 盘)。
- 输入输出重定向(可选):将继承的所有文件描述符(stdin、stdout、stderr 等)重定向到 /dev/null(“黑洞” 设备,丢弃所有输入输出)。
#pragma once
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>// 将进程切换为守护进程
int Daemon(int nochdir, int noclose)
{// 忽略管道关闭和子进程退出signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 切换为孙子进程, 因为进程组组长无法调用setsidif (fork() > 0) exit(0);// 设置为独立的会话组if (setsid() == -1){return -1;}// 切换为孙子进程(不是新会话的首进程), 确保无法打开新的控制终端if (fork() > 0) exit(0);// 将工作目录切换为根目录if (!nochdir){chdir("/");}// 将标准输入、输出、错误重定向到/dev/nullif (!noclose){int fd = ::open("/dev/null", O_RDWR);if (fd == -1){return -1;}else{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);}}return 0;
}
当然,操作系统也为我们提供了对应的接口:
#include <unistd.h>
int daemon(int nochdir, int noclose);
上面的接口就是仿照这个接口设计的,参数与返回值的含义都一样,就不过多介绍了。