1.前置知识
1.1 前台进程与终端控制
1.1.1 Linux 会话与 bash 进程的基础关联
当运行自己写的代码作为前台进程时,输入其他命令无法正常执行,主要是因为前台进程会占据当前终端的输入输出控制权,具体原因如下:
在Linux系统中,每次登录都会创建一个会话 ,每个会话会对应一个bash进程,负责处理用户的命令行输入与输出,直接关联键盘和显示器。默认情况下,一个会话中只有这一个bash进程提供命令行服务,用户输入的命令经它解释后,结果会打印到屏幕上。

1.1.2 前台进程占据终端控制权导致其他命令无法执行的原因
Linux 系统有明确的底层规则:同一会话在同一时间只能存在一个前台进程 ,且所有来自键盘的信号(例如用于终止进程的 Ctrl+C 信号)仅能发送给当前处于前台的这个进程。当用户通过 ./mypro
的方式运行自己编写的代码时,该代码对应的进程会成为当前会话的前台进程。一旦成为前台进程,它就会全面占据终端的输入控制权与输出控制权。此时,用户在终端中输入的 ls、pwd 等常规命令,由于终端输入控制权已被自定义代码进程占用,无法传递到负责处理命令的 bash 进程中。而 ls、pwd 这类命令的解释与执行必须依赖 bash 进程,因此在 bash 进程无法接收命令的情况下,这些输入的其他命令自然无法正常执行。

1.1.3 终止前台进程后终端恢复正常命令执行的原理
当我们通过Ctrl+C终止mypro进程时,由于系统规定一个会话中必须存在一个前台进程,此时系统会自动将bash进程切换到前台。这样一来,bash进程便能重新接管终端的输入输出控制权,继续为我们提供命令行解释服务,我们也就可以正常输入和执行新的命令了。
1.2 进程的输出特性
无论是前台进程还是后台进程,都具备向显示器打印输出的能力。不过,当后台进程运行时,其输出内容会直接显示在终端上,这可能干扰前台操作,比如打断正在输入的命令或混淆输出信息。
为避免这种影响,可采用输出重定向的方式,将后台进程的输出内容写入指定文件。例如通过命令"./mypro >> log.txt &"运行程序:
- ">> log.txt"表示将进程的输出追加到log.txt文件中;
- "&"则将进程置于后台运行。
输入该命令后,系统会显示该进程的后台任务号和进程PID。这样一来,后台进程的输出就不会干扰前台操作,我们既能正常使用终端,又能通过查看文件获取后台进程的输出信息。

1.3 前台与后台进程的核心区别
显示器并非区分前台与后台进程的关键指标,因为无论进程处于前台还是后台,都能向显示器输出内容。
真正的核心区别在于是否与键盘相关联:
- 前台进程拥有键盘的控制权,能够接收用户的键盘输入;
- 后台进程不直接关联键盘,无法接收键盘输入。
1.4 进程的管理操作
- 可以通过 jobs 命令查看当前会话中正在运行的任务及其状态,包括后台任务号。
- 若要将某个后台进程调回前台,只需使用 fg 命令并指定该进程的编号(例 fg 1 ),即可让对应进程重新获得键盘控制权,转为前台进程运行。此时输入ctrl + c就可以直接终止该进程了。
- 如果又想前台进程放回后台,输入ctrl+z,相当于将该前台进发送SIGSTOP信号。此时会把它暂停的进程放进后台,shell放回前台。
- bg 后台任务号,让暂停的进程继续运行。
2.Linux的进程关系
2.1 ps ajx 命令输出字段说明
- PPID:父进程的 PID(进程 ID),表示当前进程由哪个进程创建。
- PID:当前进程的唯一标识符(进程 ID)。
- PGID:进程组 ID,多个相关进程可以组成一个进程组,由该 ID 标识。
- SID:会话 ID,一个会话包含一个或多个进程组,通常与登录会话相关。
- TTY:进程关联的终端设备, ? 表示不依赖终端(如守护进程)。
- TPGID:终端进程组 ID,标识 "正在使用终端的进程组"。
- STAT:进程状态,如运行(R)、睡眠(S)、僵尸(Z)等,部分状态带附加信息(如 < 表示高优先级)。
- UID:进程所属用户的 ID。
- TIME:进程累计使用的 CPU 时间。
- COMMAND:启动进程的命令名称或路径。
2.2 进程组相关概念
2.2.1 进程组的定义
进程组是由一个或多个相互关联的进程组成的集合,由唯一的进程组 ID(PGID)标识。通常情况下,PGID 与组内第一个创建的进程(即组长进程)的 PID 相同。
2.2.2 "自成一组"的含义
"自成一组"指一个进程单独构成一个独立的进程组,此时该进程的 PID 与 PGID 一致。这种情况常见于单独运行的简单进程(如 ls 命令对应的进程),由于没有其他进程加入,该进程自身便成为一个进程组。
2.2.3 进程组的作用
主要是便于对一组相关进程进行统一管理,例如可以通过信号同时操作组内所有进程。而"自成一组"是进程组的基础形式,体现了进程组可最小化到单个进程的特性。、
2.3 进程组与任务、会话的关系
2.3.1 进程组与任务的关系
任务是偏向于任务层面的概念,任务会被指派给进程组。所谓的"前台进程"和"后台进程",更准确的说法是"前台任务"和"后台任务",其中后台任务可能包含多个进程(即对应一个包含多个进程的进程组)。
2.3.2 进程组与会话的关系
多个任务(进程组)在同一个会话内启动时,其 SID(会话 ID)是相同的。通常情况下,SID 会以 bash 进程的 ID 来命名,一个会话包含一个或多个进程组。
3.守护进程
关于注销
注销操作的本质是让用户退出当前登录状态,并将该用户本次登录后新创建的所有会话及其包含的进程、任务全部终止并清除。这意味着用户在本次会话中启动的所有任务(包括可能导致电脑卡顿的任务)都会被关闭。正因如此,当电脑因某些任务占用过多资源而卡顿,注销用户后,这些造成卡顿的任务被强制终止,系统资源得以释放,电脑便会恢复正常运行。
3.1 原理
当关闭一个会话时,其中的前台进程会随会话终止而退出,但后台进程不会直接退出,它们会被系统进程(PPID 显示为 1)"领养",此时会话虽被保留,但 TPGID 会显示为 -1(当前进程与终端的关联状态异常或未明确初始化),这表明后台进程仍会受到用户登录和退出的影响。
若要让后台进程完全不受用户登录与注销的影响,需要将其进行守护进程化处理,具体可通过让其自成一个新会话来实现,使其脱离原终端和会话的控制,成为独立运行的系统级进程,这样之前的会话不管如何登录、注销,都不会对这个新会话中的进程产生影响。

3.2 setsid接口
cpp
#include <unistd.h>
pid_t setsid(void);
**功能:**创建会话,设置调用进程为该会话的领头进程(组长),进程组 ID 与该进程的 PID 相同。(让该进程独立成会话)
返回值:
- 成功时,返回新会话的会话 ID(即调用进程的 PID)。
- 失败时,返回 -1,并设置 errno 以指示错误原因(例如,若调用进程已是进程组组长,则会调用失败)。
注意:调用的进程不能是这个组的组长。
要保证调用的进程不是所在进程组的组长,可按以下逻辑处理:在进程调用fork函数后,若返回值大于0,说明当前执行的是父进程,此时让父进程直接退出;若返回值等于0,说明当前是子进程,此时调用setsid函数。通过这样的操作,子进程会脱离原进程组并成为新进程组的组长,从而确保所调用的进程(即子进程)不是原进程组的组长。
由此可知,守护进程的本质也是孤儿进程,因为父进程直接退出,子进程会被系统领养。
3.3 代码实现
守护进程标准 I/O 重定向:优雅使用 /dev/null 避免终端资源占用
守护进程需在后台长期稳定运行,过程中应避免占用终端资源或产生不必要的输出,若直接关闭标准输入、标准输出和标准错误,方式不够优雅。
/dev/null是一个特殊的字符文件,被称为"垃圾桶",所有写入其中的内容都会被直接丢弃,不会产生任何输出。因此,针对守护进程的需求,常用的解决办法是将其标准输入、标准输出和标准错误重定向到/dev/null,这样既不会从终端读取输入,也不会向终端输出内容,还能保持操作的优雅性,确保守护进程在后台稳定运行。
Daemon.hpp
cpp
#pragma
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
const string nullfile="/dev/null";// 定义/dev/null路径,用于重定向标准输入输出
void Daemon(const string& cwd = ""){
// 忽略不必要的信号,防止守护进程被意外终止
signal(SIGCHLD,SIG_IGN);// SIGCHLD:子进程结束时会发送此信号,忽略它避免产生僵尸进程
signal(SIGSTOP,SIG_IGN);// SIGSTOP:暂停进程的信号,忽略它防止守护进程被暂停
signal(SIGPIPE,SIG_IGN);// SIGPIPE:向已关闭的管道写入数据时会发送此信号,忽略它避免进程退出
// 第一次fork:脱离父进程和终端控制,成为后台进程
if(fork()>0)exit(0);// 返回值>0表示当前是父进程,直接退出,仅保留子进程
setsid();
// 切换工作目录(可选)
// 如果指定了工作目录,则切换到该目录,避免守护进程依赖原工作目录
if(!cwd.empty()){
chdir(cwd.c_str());
}
// 重定向标准输入、输出、错误到/dev/null
int fd = open(nullfile.c_str(),O_RDWR);
if(fd>0){
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
}
服务端下调用Daemon,编译运行后,该进程会成为后台进程,即使关闭终端服务器也能24小时运行,并不影响客户端的访问。这也是我们半夜打开抖音仍能正常刷视频的原因。
输入ls /proc/[后台服务器进程pid]/fd -l可以看到该进程的输入输出已被重定向到/dev/null,不会与终端进行交互;而文件描述符3指向一个socket,说明该进程正在通过网络套接字进行通信,这也符合后台进程(如服务端程序)持续处理网络请求的特性。

由此可知,网络服务部署的本质是将网络服务器以守护进程的形式安装到系统中。这样一来,服务器就能在后台长期稳定运行,不受终端关闭等操作的影响,从而持续提供网络服务。
如果想关闭守护进程通过kill命令杀死即可。
3.4 应用场景
我们之所以能远程登录Linux系统,是因为系统默认会启动一个监听0.0.0.0:22端口的服务,这个服务其实就是SSH服务,而提供该服务的sshd进程正是一个守护进程。每当我们通过Xshell等客户端进行远程登录时,客户端会向sshd守护进程发起连接请求,sshd会对请求进行认证;认证通过后,它会为我们分配一个bash环境和相应的会话。之后,我们在本地输入的所有指令都会通过网络传输给远程的sshd进程,由其在Linux系统上执行,执行结果再通过网络返回给本地客户端。
3.5 系统中的daemon接口
cpp
#include <unistd.h>
int daemon(int nochdir, int noclose);
**功能:**将一个进程变为守护进程。
参数:
- nochdir :控制是否改变工作目录。若为0,进程会将工作目录切换到根目录(/);若为非0,则保持当前工作目录。
- noclose :控制是否关闭标准输入、输出、错误流。若为0,会关闭这三个流,并将其重定向到/dev/null;若为非0,则不关闭。
返回值:
- 成功时返回0。
- 失败时返回-1,并设置errno以指示错误原因(如fork失败、setsid失败等)。