进程、会话与终端——一次真实的 Linux Session 解剖

控制终端由进程向 system 申请。一个 TTY 唯一对应一个 Session,一个 Session 可以创建多个 Process Group。进程组通常由时间上产生的第一个进程 的 PID 作为 PGID。一个不是进程组组长的进程可以通过 fork + setsid 脱离父进程,创立独立会话或成为守护进程。

scss 复制代码
会话 (Session) → 进程组 (Process Group) → 进程 (Process) → 线程 (Thread)

打个形象的比方:会话是一栋大楼,进程组是办公室组,进程是独立办公室(有自己的地址空间、文件描述符、堆空间),线程是办公室里干活的工人。


一、进程概念

概念词 释义 注释
TTY 控制终端设备(? 表示无终端) Ctrl+Alt+T 打开的 terminal 就是一个终端,每个 session 可以绑定一个 TTY
PR priority PR = 20 + NI(内核调度优先级)
NI nice 用户层可调节(-20 ~ 19)
PID 进程ID ---
PGID 进程组ID `cat file grep test` 中两个进程属于同一进程组
SID 会话ID 打开一个终端窗口就创建了一个会话,Shell 是 Session Leader
PPID 父进程ID ---

二、进程状态

状态 含义
R RUNNING/RUNNABLE --- 正在运行或等待调度
S sleep --- 可中断睡眠
D disk sleep --- 不可中断睡眠(等 I/O)
Z Zombie --- 子进程已死,父进程尚未回收
T stop --- 被 SIGSTOP 暂停
I IDLE --- 空闲内核线程

状态附加符号:

符号 含义
< 高优先级
N 低优先级
L 有内存页被锁,不能 swap
s 会话首进程(Session Leader)
l 多线程进程
+ 位于前台进程组

tpgid :内核在每个 TTY 结构体里记录了一个 tpgid(Terminal Process Group ID),STAT 里的 + 号就是内核通过比对进程组 ID 与 tpgid 是否一致来打上的标签。

三、会话分裂实战

以真实系统快照为例。下图展示了一次进程的"惊天大逃亡"------opencode agent 通过 fork+setsid 脱离父会话,创建独立会话的过程。

flowchart TD KERNEL["⚙️ 内核 Kernel"] S1["PID=1 systemd<br/>系统全局会话"] S2["PID=4012 systemd --user<br/>用户管理会话"] G["PID=214886 gnome-terminal-server<br/>图形界面组"] TTY["🖥️ TTY = /dev/pts/X"] KERNEL --> S1 --> S2 --> G G -->|"fork + open 申请控制终端"| TTY subgraph SESS_A["🔵 会话 A · 有终端 (SID=225227)"] SA["PID=225227 bash<br/>Session Leader A<br/>STAT=Ss · TTY=pts/3"] NODE["PID=225241 node-MainThread<br/>STAT=Sl+"] OC["PID=225250 .opencode<br/>STAT=Sl+"] SA --> NODE --> OC end TTY --> SA subgraph SESS_B["🟢 会话 B · 无终端 (SID=258975)"] SB["PID=258975 bash<br/>Session Leader B<br/>STAT=Ss · TTY=?<br/>stdin=/dev/null"] end OC -.->|"⚡ fork + setsid()<br/>创立新会话"| SB style KERNEL fill:#37474f,color:#fff style S1 fill:#455a64,color:#fff style S2 fill:#455a64,color:#fff style G fill:#546e7a,color:#fff style TTY fill:#ff8f00,color:#000 style SA fill:#1565c0,color:#fff style NODE fill:#1976d2,color:#fff style OC fill:#1e88e5,color:#fff style SB fill:#2e7d32,color:#fff style SESS_A fill:#e3f2fd,stroke:#1565c0 style SESS_B fill:#e8f5e9,stroke:#2e7d32

关键数据对比

属性 会话 A (终端 bash) 会话 B (opencode bash)
PID / SID 225227 / 225227 258975 / 258975
Session Leader
TTY pts/3 ? (无)
stdin /dev/pts/3 /dev/null
stdout socket socket
STAT Ss Ss
创立方式 gnome-terminal fork fork + setsid() 脱离旧会话

fork + setsid() 工作原理

  1. .opencode (PID=225250) 调用 fork() 生出子进程 bash
  2. 子进程立刻调用 setsid() → 创立全新会话,自任 session leader
  3. 新会话不再绑定任何控制终端 → TTY 变为 ?,stdin 变为 /dev/null
  4. 结果:关闭 gnome-terminal 不会波及 opencode 的 shell

💡 这就是守护进程 (daemon) 的标准脱壳流程:fork → setsid → 脱离终端 → 永生。

四、Electron 应用中的 fork + setsid

当你在 Electron 应用里启动后台服务时,底层发生了什么?

第一步:fork

主进程调用 fork(),在内核里复制出一个一模一样的子进程。子进程刚出生时继承了父亲的全部特征------同一个进程组、同一个会话、同一个终端。

第二步:setsid

子进程调用 setsid() 系统调用:

  • 断开父子关系:脱离父进程的进程组和会话
  • 成为新会话首领:创建全新会话,自任 Session Leader(SID = 自己的 PID)
  • 分离 TTY :主动丢弃从父进程继承的控制终端(TTY → ?

第三步:重定向与守护

把 stdin、stdout 全部重定向到 /dev/null。自此,它变成了一个隐形在系统后台的独立守护进程(Daemon)


五、实用指令速查

ps aux vs ps -ef

  • ps -ef :遵循 System V (SysV) 标准,注重进程的层级关系(父子进程)
  • ps aux :遵循 BSD 标准,注重进程对系统资源(CPU、内存)的占用

关键指标解读

PPID(父进程 ID) :排查恶意进程时,通过 ps -ef 找到 PPID 就能溯源到谁启动了它。

VSZ(虚拟内存大小) :进程总共申请的虚拟内存,包含未实际使用的、共享库等。即使机器只有 8G 物理内存,VSZ 也可以是 10G。

RSS(常驻内存大小) :进程当前实际占用的物理内存,不含 swap。含共享库的完整大小,多进程共享时会被重复计算。

通过 STAT 判断前台/后台

ps aux 的 TTY 列:

  • pts/0, pts/1 → SSH 或终端模拟器打开的虚拟终端
  • tty1 ~ tty6 → 服务器物理/虚拟控制台终端
  • ? → 已脱离终端(系统服务),永远不会有 + 状态

STAT 中的 + 是内核通过比对"进程组 ID"与"终端前台组 ID"是否一致来打上的标签。


本文基于 Ubuntu 24.04 + i7-13650HX + RTX 4060 真实环境编写。

相关推荐
daad7771 小时前
sitl_5760_io记录
linux·运维·服务器
枕星而眠1 小时前
【数据结构】树与二叉树基础知识点总结
数据结构·c++·后端·算法·运维开发
极光技术熊1 小时前
从零构建在线Excel:一个Java全栈工程师的实战记录
前端·后端
小谢小哥1 小时前
68-持续集成详解
java·后端·架构
foggyprojects1 小时前
列表里要带子表统计值时,为什么需要 QM 聚合型 JOIN
后端
用户925807911481 小时前
redission原理
java·后端
小旭95271 小时前
Spring Cloud 集成分布式日志 ELK+Swagger 接口文档实战
java·分布式·后端·elk·spring cloud
属鼠哥1 小时前
HDFS 短路读取:mmap 与 Unix Domain Socket 铸就的零拷贝艺术
后端
好好风格1 小时前
Scrapling:现代 Web 抓取,正在从“写选择器”走向“自适应”
linux·后端