Linux 守护进程:会话、终端与后台运行的底层逻辑

目录

​编辑

一、进程组

二、作业

[2.1 什么是作业](#2.1 什么是作业)

[2.2 前台作业](#2.2 前台作业)

[2.3 后台作业](#2.3 后台作业)

[2.4 作业管理](#2.4 作业管理)

[2.4.1 前后台作业的创建](#2.4.1 前后台作业的创建)

[2.4.2 查看作业](#2.4.2 查看作业)

[2.4.3 前后台作业的切换](#2.4.3 前后台作业的切换)

[1. 前台作业 → 后台(暂停 + 转入后台)](#1. 前台作业 → 后台(暂停 + 转入后台))

[2. 后台作业 → 前台(占用终端)](#2. 后台作业 → 前台(占用终端))

[三、 终端文件](#三、 终端文件)

四、会话

[3.1 核心属性](#3.1 核心属性)

[3.2 进程组、终端文件与会话](#3.2 进程组、终端文件与会话)

五、守护进程(精灵进程)

[5.1 为什么需要守护进程](#5.1 为什么需要守护进程)

[5.2 手动创建守护进程](#5.2 手动创建守护进程)

[5.3 守护进程的特征](#5.3 守护进程的特征)

一、进程组

进程组是一个或多个进程的集合,它们相互关联,通常是为了完成同一个作业或任务。进程组是Linux系统中进程管理的重要机制之一。

每个进程组都有一个唯一的进程组ID,进程组的组长(第一个进程)的PID就是该进程组的PGID

子进程通常继承父进程的进程组;

进程组的的生命周期是从第一个进程创建开始到最后一个进程离开而结束。只要进程组中有任何一个进程存在该进程组就存在,这与组长进程是否终止无关。

cpp 复制代码
#include<unistd.h>
#include<sys/types.h>
#include<iostream>
int main()
{	
	pid_t id=fork();
	if(id>0)
	{
		//父进程
		while(1)
		{
			sleep(4);
			std::cout<<"我是父进程,我的PID是"<<getpid()<<std::endl;
		}
	}
	//子进程
	while(1)
	{
		sleep(3);
		std::cout<<"我是子进程,我的PID是"<<getpid()<<std::endl;
	}
	return 0;
}

二、作业

2.1 什么是作业

通俗一点来讲作业与进程组就是一个硬币的正反面,没有本质上的区别。作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含 一个进程,也可以包含多个进程,进程之间互相协作完成任务。

例如,在上面的例子中我们的程序创建了一对父子进程组成的进程组来完成用户下达的任务,这个进程组也可以理解为一个作业。

2.2 前台作业

前台作业 是指:

  • 在Shell中直接启动占用终端阻塞用户输入的命令或命令序列

  • 用户必须等待其完成后才能继续输入其他命令

  • 接收终端信号(Ctrl+C、Ctrl+Z等)

在Linux系统中,当我们执行一个命令或者命令序列的时候默认就是一个前台作业,例如我们在指令行中启动程序,此时程序默认就是一个前台作业:

在一个终端中只能存在一个前台作业,它会占用终端并阻塞用户输入,核心原因是 **前台作业会独占终端的输入输出资源,且 Shell 会进入等待状态放弃终端控制权,**此时用户输入的指令无法被shell接受和解析。

前台作业会占用并阻塞用户终端的原因本质上就是为了满足自己的交互式需求并遵循Linux 终端会话的资源管控逻辑。

很多命令本身就是交互式的,需要持续接收用户的键盘输入才能完成工作,这类程序必须独占终端的输入通道

  • 比如文本编辑器 vim/nano:运行时需要接收光标移动、文字输入、快捷键操作等,必须让终端的所有输入都直接传递给它,而不是被 Shell 拦截。

如果这类交互式程序不占用终端、不阻塞 Shell,就会出现输入混乱 ------ 你输入的内容既会传给程序,又会传给 Shell,导致程序无法正常运行。

从进程调度的角度看,前台作业的阻塞机制是 Shell 会话管理的核心策略

  • Shell 作为会话首进程,需要对旗下的进程进行有序管控。通过 wait() 系统调用挂起自身,等待前台作业结束,本质是一种同步等待机制,确保进程的执行顺序符合用户预期。
  • 前台 / 后台的区分,本质是对终端 I/O 资源的分时复用:前台作业 "占用" 资源时,后台作业只能等待或静默运行;前台作业释放资源后,Shell 才会重新接管,让用户调度下一个任务。

2.3 后台作业

后台作业是 Linux Shell 中与前台作业相对的概念,指在终端会话中启动、但不占用终端输入通道的进程组,运行期间用户可以在同一个终端里继续输入新命令。

他有以下的核心特征:

1. 不阻塞终端输入

后台作业启动后,终端的标准输入(stdin)控制权会立刻交还给 Shell,用户无需等待作业结束,就能继续执行其他命令。比如我们执行sleep 1000 &(&是后台作业的标志)指令后,我们依然还可以输入ls,ls -al,pwd等指令并执行:

2. 输出仍关联终端

后台作业的标准输出(stdout)和标准错误(stderr)默认还是绑定到当前终端,运行过程中产生的输出会直接打印到终端屏幕上,可能会和你后续输入的命令、输出结果混在一起。

比如我们执行如下代码并以后台作业的方式运行:

cpp 复制代码
#include<unistd.h>
#include<sys/types.h>
#include<iostream>
int main()
{	
	pid_t id=fork();
	if(id>0)
	{
		//父进程
		while(1)
		{
			sleep(4);
			std::cout<<"我是父进程,我的PID是"<<getpid()<<std::endl;
		}
	}
	//子进程
	while(1)
	{
		sleep(3);
		std::cout<<"我是子进程,我的PID是"<<getpid()<<std::endl;
	}
	return 0;

需要注意的是,因为后台作业没有关联终端的标准输入(stdin),所以后台作业无法通过ctrl+c等指令来终止,必须通过显式地指明后台作业进程ID的方式通过kill指令来终止。

2.4 作业管理

2.4.1 前后台作业的创建

前后台作业主要通过命令的启动方式来区分:

直接输入命令并回车,默认就是前台作业(会阻塞终端输入)。

在命令末尾添加 & 符号,即可将命令作为后台作业启动(不阻塞终端)。

2.4.2 查看作业

在 Linux Shell 中,查看作业的核心指令是 jobs,它可以列出当前终端会话中所有后台作业的状态(作业号、PID、运行状态等)。

直接执行 jobs,会显示当前会话的所有作业信息:

bash 复制代码
jobs
# 输出示例:
[1]-  Running                 sleep 1000 &
[2]+  Stopped                 vim test.txt

输出字段含义:

  • [1]/[2]:作业号;
  • +/-+ 表示 "默认作业"(;- 表示 "次默认作业";

作业的状态字段相关的含义我们可以对照下表:

状态值 含义说明 典型触发场景
Running 作业正在后台正常运行 执行 sleep 1000 & 后,作业处于后台运行状态
Stopped 作业被暂停(未终止,资源仍保留) 按下 Ctrl+Z 挂起前台作业;后台作业尝试读 stdinSIGTTIN 信号暂停
Done 作业已正常完成(退出码为 0) 后台作业执行完毕(比如 sleep 1 & 运行 1 秒后)
Done(code) 作业完成但退出码非 0(执行出错) 后台运行的命令执行失败(比如 ls /不存在的目录 &
Terminated 作业被强制终止(资源已释放) kill %作业号kill -9 PID 终止作业

除了直接调用jobs查看作业的相关信息,jobs还为我们提供了多种选项:

  • -l 参数,会额外显示作业的主进程 PID(方便用 kill 终止)
  • -n 参数,只显示状态有变化的作业(比如刚完成 / 刚暂停的)
  • 若要显示所有作业(包括已完成),可以用 jobs -x

2.4.3 前后台作业的切换

1. 前台作业 → 后台(暂停 + 转入后台)

适用场景:前台作业运行中,想释放终端但保留作业运行

操作步骤

  1. 按下 Ctrl+Z 挂起前台作业(作业进入 Stopped 状态);
  2. 执行 bg %作业号 恢复作业为后台运行(Running 状态)。
2. 后台作业 → 前台(占用终端)

适用场景:需要和后台作业交互(比如输入内容、调试)

操作指令fg %作业号(省略作业号则操作默认作业)

三、 终端文件

我们启动本地的终端应用并向服务器发送ssh连接请求,服务器接受到收到请求后,会调用内核接口 pty_alloc() ,动态创建一对伪终端设备,然后在系统的/dev/pts 目录下生成对应的终端文件,比如 /dev/pts/2(数字是自增的,按需分配)之后服务器启动 的Shell 进程(如 bash),会将 Shell 的 stdin/stdout/stderr 绑定到 /dev/pts/2;。

为什么要存在终端文件??

在远程云服务器不存在键盘、显式器这样的硬件设备所以系统的标准输入、标准输出、标准错误并不会关联到某个硬件设备而是统一关联事先创建的终端文件,这个终端文件会将远程数据通过网络发送给用户完成用户与服务器进程的 I/O 交互。

  • 你在本地输入一个字符,会立刻通过 SSH 传到服务器,写入终端文件 → 绑定该终端的 Shell 进程读取到这个字符,实现命令的实时输入;
  • 服务器进程的输出(如 ls 的结果)写入终端文件 → 内核立刻把数据通过 SSH 推回本地 → 本地终端实时显示结果。

简单说,云服务器上的终端文件,就是物理键盘 / 显示器的网络化身,实现了 "无硬件但可交互" 的远程操作。

终端文件与终端是一对一的关系,一个会话只能拥有一个终端文件。当连接断开或者窗口关闭时终端文件也就会随之关闭。

四、会话

在 Linux/Unix 系统中,会话是内核层面用于管理终端关联进程的高层级进程组容器,它的核心作用是绑定一个控制终端,并统一管控会话内所有进程的生命周期与终端交互权限。

3.1 核心属性

1. 由会话首进程创建

每个会话都由一个会话首进程创建,这个进程的 PID 就是整个会话的 ID(SID)。终端启动时,默认会创建一个 Shell 进程(如 bash),这个 Shell 就是会话首进程 ------ 它会调用 setsid() 系统调用创建新会话,并将当前终端设为会话的控制终端。

这个bash进程除了创建新会话还会打开标准输入stdin、标准输出intout、标准错误stderror并关联到终端文件。

当shell收到用户的指令的时候会创建子进程执行指令并继承shell的文件描述符表,所以我们在终端无论是执行系统命令还是运行自己的程序其三个标准流都会默认指向终端文件。

2. 绑定唯一控制终端

一个会话只能绑定一个控制终端,一个控制终端也只能被一个会话绑定,二者是一对一的关系。控制终端是会话内所有进程与用户交互的唯一通道:前台进程组独占终端的输入输出,后台进程组默认不能读取终端输入。

3. 包含多个进程组

会话的内部由一个或多个进程组组成,进程组是由一个或多个关联进程构成的集合。会话内的进程组分为两类:

  • 前台进程组:独占控制终端的输入输出,接收终端信号。
  • 后台进程组:不占用终端输入,输出默认打印到终端,无法接收终端信号。

3.2 进程组、终端文件与会话

1. 最外层:用户与控制终端

用户通过多个独立的控制终端(比如同时打开的两个 SSH 窗口)连接远程 Linux 环境 ------ 每个控制终端是用户与系统交互的 "入口"。

2. 控制终端 ↔ 终端文件:一一对应

每个控制终端会对应唯一的终端文件 (比如图中的 "终端文件 1" 对应/dev/pts/0,"终端文件 2" 对应/dev/pts/1):

  • 终端文件是 Linux 系统中表示终端的 "设备文件"(属于字符设备);

  • 控制终端的所有输入 / 输出,都会通过对应的终端文件传递到系统内部。

3. 终端文件 ↔ 会话:绑定关系

每个终端文件会绑定到一个独立的会话(图中的 "会话 1""会话 2"):

  • 会话是 Linux 中管理进程的逻辑单元,一个会话最多关联一个终端文件(即一个控制终端);

  • 反过来,一个终端文件也只会绑定到一个会话(不会被多个会话共享)。

4. 会话 ↔ 进程组:包含关系

每个会话内部可以包含多个进程组(图中每个会话下有 "进程组 1""进程组 2"):

这些进程组的标准流(stdin/stdout/stderr)会关联到会话绑定的终端文件(图中进程组指向终端文件的箭头,体现了这一继承关系)。

五、守护进程(精灵进程)

当我们的一个进程(作业)正在运行的时候用户关闭了控制终端此时会话内正在运行的进程(作业)会收到 SIGHUP 信号,默认行为是进程被终止------ 这是 Linux 终端与会话的信号机制决定的。

因为会话与终端文件的生命周期与其控制终端是强关联的,当控制终端终止时其创建的会话和终端文件也会一并销毁和回收。

假设我们需要在远程 Linux 服务器上运行一个日志收集程序,用来实时采集业务系统的日志并上传到存储服务器,此时会遇到两个致命问题:

  1. 终端不能关:一旦关闭 SSH 窗口(终端),内核会给日志收集程序发送SIGHUP信号,程序直接终止,日志收集中断;
  2. 会话依赖强:即使把程序放后台,只要终端会话销毁,程序依然会被终止;
  3. 用户退出影响:如果用户登出服务器,同样会触发会话销毁,程序停摆。

这种方式完全无法满足 "7x24 小时采集日志" 的需求 ------ 总不能让我们一直开着 SSH 窗口吧?

5.1 为什么需要守护进程

普通进程依赖终端会话运行,一旦关闭终端(如 SSH 断开),内核会发送 SIGHUP 信号终止进程;而守护进程彻底脱离终端和会话依赖,解决了 "程序需要长期运行但怕终端操作中断" 的核心问题。

典型应用场景:

  1. 系统核心服务:Nginx(Web 服务)、MySQL(数据库)、crond(定时任务)、sshd(SSH 服务)等,需 7x24 小时响应请求;
  2. 业务后台任务:日志采集、数据同步、消息队列消费、定时备份等,需持续运行不中断;
  3. 监控类程序:服务器状态监控、业务告警程序,需实时检测系统 / 业务状态。

5.2 手动创建守护进程

一个普通进程要变成守护进程,需完成 "脱离终端 - 独立会话 - 后台运行" 的核心步骤(Linux 下经典流程):

  1. fork子进程,父进程退出
  2. 子进程创建新会话,脱离原终端
  3. 重定向或关闭标准流
  4. 设置工作路径(可选**)**
cpp 复制代码
#define defaultworkpath "/"
#define Redirectpath "/dev/null"
class Daemon
{
    public:
    static bool Daemonprocess(bool change,bool closet)
    {
        //屏蔽信号
        signal(SIGPIPE,SIG_IGN);
        signal(SIGCHLD,SIG_IGN);

        //创建子进程
        pid_t id=fork();
        if(id>0)
        {
            //父进程
            exit(0);
        }
        //子进程
        //创建新会话
        setsid();

        if(change==true)
        {
            //更改进程的工作路径
            chdir(defaultworkpath);
        }

        if(closet==true)
        {
            close(0);
            close(1);
            close(2);
        }
        else
        {
            //否则将进程的标准输入,标准输出,标准错误重定向到dev/null中
            //1.打开/dev/null文件
            int fd=open(Redirectpath,O_RDWR);
            if(fd<0)
            {
                return false;
            }
            //2.将0,1,2重定向到/dev/null中
            dup2(fd,0);
            dup2(fd,1);
            dup2(fd,2);
            close(fd);
        }
        return true;
    }
};

为什么需要子进程创建新会话?

子进程通常调用setsid来创建新会话,但是调用setsid的进程必须要有一个前提就是不能是其所属进程组的组长进程,而普通进程(比如 Shell 中直接运行的程序)默认是进程组组长。

为什么需要重定向或关闭标准流?

因为当创建子进程后会默认继承父进程的文件描述符表,其中的标准流0,1,2会默认关联父进程所属终端的终端文件,当父进程的控制终端关闭终端文件也会随之销毁并回收。此时如果不关闭或这重定向会导致子进程的标准流都指向一个不存在的文件描述符引发错误。

5.3 守护进程的特征

三无:无终端、无前台、无交互

  • 无控制终端:不关联任何终端文件(TTY 列显示 ?),标准流(0/1/2)通常重定向到 /dev/null

  • 无前台运行状态:始终在后台运行,不会占用终端输入 / 输出;

  • 无用户交互依赖:不需要用户手动操作,启动后自动执行任务。

三独:独立会话、独立进程组、独立资源

  • 独立会话:通过 setsid() 创建新会话,脱离原终端会话,不受原会话的信号 / 销毁影响;

  • 独立进程组:是新进程组的组长,与原 Shell 进程组完全隔离;

  • 独立资源:工作目录通常切换到根目录(/),文件权限掩码重置,不依赖原会话的资源。

一稳:长期稳定运行

  • 生命周期长:随系统启动(或手动启动),除非主动终止 / 系统关机,否则持续运行;

  • 抗干扰性强:忽略终端相关信号(如 SIGHUP),不受用户登录 / 退出、终端关闭的影响;

相关推荐
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]iomap
linux·笔记·学习
两拆2 小时前
Linux(redhat7.9)安装KVM虚拟机
linux
Alex Cafu2 小时前
Linux网络编程2(HTTP 协议、IO 多路复用)
linux·c语言·网络·http
FOREVER-Q2 小时前
《Docker Compose 部署前后端分离项目实战:Nginx + Spring Boot(含完整踩坑记录)》
运维·docker·容器
广东大榕树信息科技有限公司2 小时前
当机房环境出现异常时,如何利用动环监控系统快速定位问题?
运维·网络·物联网·国产动环监控系统·动环监控系统
Trouvaille ~2 小时前
【C++篇】让错误被温柔对待(上):异常基础与核心机制
运维·开发语言·c++·后端·异常·基础入门·优雅编程
yBmZlQzJ2 小时前
第二篇:Linux服务器性能优化实战技巧(提升稳定性与效率)
linux·服务器·性能优化
戴西软件2 小时前
CAxWorks.VPG车辆工程仿真软件:打造新能源汽车安全的“数字防线“
android·大数据·运维·人工智能·安全·低代码·汽车
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之mlabel命令(实操篇)
linux·运维·服务器·前端·笔记