1. 进程概念
1.1 进程的本质
核心定义
用户视角:程序的动态执行实例(如同时运行多个Chrome窗口即多个进程)。
内核视角:资源分配的最小实体单位,独享CPU时间片、内存空间和文件资源。
现代定义:
进程 = 内核数据结构(task_struct) + 程序代码和数据段
进程不仅是“程序的执行实例”或“正在执行的程序”,从内核角度看,它是分配系统资源(CPU时间、内存)的实体。更精确地说,进程 = 内核数据结构(task_struct) + 程序代码和数据。当程序被加载到内存时,操作系统为其创建一个task_struct实例,该结构体封装了进程的所有属性和状态信息。生动示例:想象一个C程序(如hello.c
)被编译执行时,操作系统会动态分配内存和CPU时间片,并将程序指令映射到虚拟地址空间,同时初始化task_struct来跟踪其状态,就像给每个运行的程序贴上一个“身份证”和“健康记录卡”。
示例:启动两个
vim
编辑不同文件时,系统创建两个独立进程,各自拥有独立的代码执行流和内存空间,互不干扰。我们先创建一个myprocess.c文件,然后死循环,每隔一秒打印
这里直接运行这个可执行程序,当我们把这个程序运行起来,其实就是一个进程
1.2 进程控制块 (PCB)
task_struct:Linux的PCB实现
存储位置:常驻内存(RAM),由内核动态管理。
关键字段分类(扩展版):
字段类别 具体内容 标识符 PID(进程ID)、PPID(父进程ID)、PGID(进程组ID) 状态 运行态(TASK_RUNNING)、睡眠态(TASK_INTERRUPTIBLE)、僵尸态(EXIT_ZOMBIE)等 内存指针 代码段( mm_struct->code_start
)、数据段(mm_struct->data_start
)指针上下文数据 保存暂停时的CPU寄存器值(eip, eax等),用于恢复执行 文件描述符表 记录打开的文件( files_struct
结构体)资源限制 最大文件打开数、CPU时间配额( struct rlimit
)
进程组织方式
// 内核源码示例(简化版)
struct task_struct {volatile long state; // 进程状态struct mm_struct *mm; // 内存管理结构体pid_t pid; // 进程IDstruct files_struct *files; // 打开文件表struct list_head tasks; // 双向链表指针// ... 其他字段
};
全局进程链表:内核通过
struct list_head tasks
将所有进程组成双向链表,头节点为init_task
(PID=1的init进程)。
1.3 查看进程的实战方法
1. /proc文件系统
动态虚拟文件系统:以目录形式暴露内核进程信息。
# 查看PID为1的进程信息 $ ls /proc/1 exe -> /usr/lib/systemd/systemd # 可执行文件链接 cwd # 当前工作目录 fd/ # 打开的文件描述符 status # 进程状态摘要
2. 命令行工具
# 显示进程树(含父子关系)
$ pstree -p
systemd(1)─┬─sshd(1234)───bash(5678)───vim(9012)└─crond(2345)# 动态监控进程
$ top -p 9012 # 监控PID 9012(vim进程)的资源占用
3. ps指令
一、ps
命令核心功能
作用:捕捉系统当前进程快照(非实时),用于:
查看进程状态(运行/睡眠/僵尸)
分析资源占用(CPU/内存)
定位问题进程
查看进程间关系
二、参数详解与使用场景
1. 基础查看
命令 | 作用 | 示例输出片段 |
---|---|---|
ps aux | 查看所有用户的所有进程 | USER PID %CPU %MEM VSZ RSS TTY... |
ps ajx | 显示进程树关系(含PPID/PGID) | PPID PID PGID SID TTY COMMAND... |
输出字段解析:
VSZ:虚拟内存大小 (KB)
RSS:实际物理内存 (KB)
TTY:关联终端(
?
表示无终端)STAT:进程状态(后文详解)
2. 进程状态(STAT)解码
状态码 | 含义 | 说明 |
---|---|---|
R | 运行中 (Running) | 正在执行或就绪状态 |
S | 可中断睡眠 (Sleeping) | 等待事件完成(如 I/O 操作) |
D | 不可中断睡眠 (Disk sleep) | 通常发生在磁盘 I/O,不可被信号中断 |
T | 暂停状态 (Stopped) | 被信号暂停(如 SIGSTOP ) |
Z | 僵尸进程 (Zombie) | 进程已终止,但父进程未回收 |
< | 高优先级进程 | 优先级高于默认值 |
N | 低优先级进程 | 优先级低于默认值 |
s | 会话领导者 (Session leader) | 控制终端的进程 |
+ | 前台进程组 (Foreground group) | 与终端交互的进程 |
示例:
Ss+
= 会话领导者 + 可中断睡眠 + 前台进程
3. 高级过滤与显示
参数组合 | 作用 | 示例应用场景 |
---|---|---|
ps -e | grep ssh | 查找特定进程 | 检查 SSH 服务是否运行 |
ps -fC nginx | 显示进程完整命令行 (-f ) + 按名称过滤 | 查看 Nginx 配置参数 |
ps -p 1234 -o pid,ppid,cmd | 自定义输出字段 | 查看指定进程的父子关系 |
ps --forest | 树形显示进程层级 | 分析进程派生关系 |
ps -eo pid,ppid,cmd --sort=-%mem | 按内存排序 | 找出内存消耗最大的进程 |
1.4 获取进程标识符(代码解析)
我们可以通过man手册来查看一下getpid
基本定义
进程ID(PID)
- 定义:进程ID(Process ID)是操作系统分配给每个进程的唯一标识符。它用于区分系统中的不同进程。
- 作用:
资源分配:PID是分配系统资源(如内存、CPU时间等)的重要依据。
进程控制:操作系统可以使用PID来对进程进行操作,例如启动、停止、暂停或终止进程。
唯一标识:每个进程在系统中都有一个唯一的PID,操作系统通过PID来管理进程。
示例:
cat
进程的PID为3538。
父进程ID(PPID)
定义:父进程ID(Parent Process ID)是指创建当前进程的进程的ID。每个进程都有一个父进程(除了初始进程)。
作用:
进程关系:PPID用于表示进程之间的父子关系。通过PPID,可以追踪进程的创建过程。
资源继承:子进程通常会继承父进程的资源(如文件描述符、环境变量等)。
进程管理:操作系统可以通过PPID来管理进程树结构,例如在父进程终止时,清理其子进程。
示例:
bash
进程(PPID=2686)创建了cat
进程(PID=3538),因此cat
的PPID为2686。
核心特性
(1) 父子进程关系
创建机制:父进程通过系统调用(如
fork()
)创建子进程。子进程继承父进程环境,但操作系统为其分配新PID,同时将其PPID设为父进程的PID。
代码示例(C++):pid_t t = fork(); // 创建子进程 if (t == 0) {// 子进程:getpid()返回自身PID,getppid()返回父进程PIDcout << "子进程 PID:" << getpid() << " PPID:" << getppid() << endl; } else {// 父进程:getpid()返回自身PIDcout << "父进程 PID:" << getpid() << endl; }
关系规则:
- 一个父进程可创建多个子进程,所有子进程共享同一PPID(即父进程PID)。
- 子进程退出后,父进程需回收其资源,否则可能产生僵尸进程。(至于什么是僵尸进程后面章节会讲)
(2) 特殊进程
示例:进程表中PID=1的进程PPID为-1。init
进程(PID=1):
系统启动的第一个进程(Linux中通常为systemd
),是所有用户进程的最终祖先,其PPID为0或-1(表示无父进程)。内核进程(PID=0):
管理内存交换等核心任务,无PPID。
注意:
数据类型本质:
pid_t
是一个带符号整数类型(signed int
),在 Linux 系统中被明确定义为 int
的别名。
设计目的:
提供进程 ID 的抽象表示,屏蔽不同操作系统(如 Linux/Windows)或硬件架构(32/64 位)的底层差异,增强代码可移植性。例如:- Linux 使用
pid_t
表示 PID,而 Windows 使用HANDLE
。 - 直接使用
int
可能导致平台兼容性问题。
- Linux 使用
进程id示例:
我们修改一下之前的代码
运行结果:
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288...
运行后我们可以发现可执行程序myprocess的pid是453288,我们还可以来验证一下
使用ps指令来查看:
ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep
ps ajx
:ps
是进程查看命令,用于显示当前系统中的进程快照(非动态更新)。- 选项
ajx
指定输出格式:a
显示所有用户进程,j
以作业控制格式显示,x
包括无终端的进程(如后台进程)。 - 示例输出:包含 PID(进程ID)、PPID(父进程ID)、COMMAND(命令名称)等列。
| head -1
:|
是管道符,将前一个命令的输出作为后一个命令的输入。head -1
只保留输出的第一行,即进程信息的表头(如PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
)。- 作用:确保后续进程信息有清晰的列名,便于阅读。
&&
:- 逻辑与运算符,表示只有前一个命令(
ps ajx | head -1
)成功执行(返回状态码 0)时,才执行后续命令。 - 这里用于分隔两个独立操作:先显示表头,再显示进程详情。
- 逻辑与运算符,表示只有前一个命令(
ps ajx | grep myprocess
:- 再次运行
ps ajx
获取所有进程信息。 grep myprocess
过滤出包含关键字 "myprocess" 的行(通常是目标进程的命令名称)。- 问题:
grep
命令自身在运行时也会被列为进程,且其命令中包含 "myprocess",因此会被错误地包含在结果中(例如输出grep --color=auto myprocess
)。
- 再次运行
| grep -v grep
:grep -v
表示反向过滤,排除包含指定关键字 "grep" 的行。- 作用:移除
grep myprocess
自身产生的进程条目,避免干扰。例如,如果未加此部分,输出会多出一行grep --color=auto myprocess
。
如果我们想杀掉这个进程可以使用快捷键CTRL + c(左边的Shell),或者在右边的Shell中使用
kill -9 [想要杀掉的pid]
至于kill这个指令的是如何杀掉这个进程的,我们在后面的信号章节也会讲到
父进程id示例:
再次修改一下代码
运行:
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
...
运行结果可看到进程pid为451964,父进程ppid为450425
同样也可以在验证一下
这里我们可以看到怎么这次的父进程ppid和上次的父进程ppid一样,都是450425.
我们可以多次运行看一下
父进程id一直不变,这是什么情况呢?
我们也可以用ps来查一下
我们可以看到原来我们的父进程是bash
因为 你每次都是在同一个交互式 Bash 会话里手动启动程序,所以:
Bash 是 Linux 默认的命令行解释器(Shell),用户通过终端输入命令时,Bash 会创建子进程来执行这些命令
父进程就是当前这个 Bash 进程;
Bash 进程的 PID 在你退出或关闭终端之前不会改变;
于是你看到的 PPID 始终就是 那个 Bash 的 PID(
-bash
或bash
)。
换句话说,只要你不关掉这个终端(或显式 exit
掉这个 Bash),它就是所有你手动启动命令的父进程,PPID 自然看起来“不变”。
1.5 fork() 机制深度解析
同样我们可以使用man手册来查一下
关键特性
一次调用,两次返回:
父进程返回子进程PID(>0)
子进程返回0
失败返回-1(如进程数超限)
写时拷贝(Copy-On-Write):
初始状态:父子进程共享同一物理内存。
修改触发:当任一进程尝试写入数据时,内核为该进程复制新内存页。
我们先来修改一下代码,浅尝一下fork
运行结果:
fork之后的代码被执行了两次,why?
fork: 如何呢?又能怎?
核心原理:一次调用,两次返回
当程序执行到 fork()
系统调用时,操作系统会创建一个与原进程(父进程)几乎完全相同的副本(子进程)。这个副本包括:
代码段的复制(共享只读)
数据段和堆栈的复制(写时拷贝)
程序计数器(PC)位置 - 指向
fork()
之后的下一条指令
#include <stdio.h>
#include <unistd.h>int main() {printf("父进程开始运行,pid:%d\n", getpid()); // 步骤1:父进程执行fork(); // 步骤2:分水岭!// ↓ 步骤3:此处开始有两个独立的执行流printf("进程开始运行,pid:%d\n", getpid()); // 步骤4:父子进程各执行一次return 0;
}
执行流程:
关键机制解析:
写时拷贝 (Copy-On-Write):
子进程创建时不立即复制物理内存
父子共享相同物理内存页(标记为只读)
当任一进程尝试写入内存时,触发缺页异常
内核再为该进程复制新的内存页
程序计数器继承:
子进程创建时复制父进程的CPU寄存器状态
包括指向
fork()
后下一条指令的EIP寄存器因此子进程从
fork()
返回处开始执行
返回值差异:
返回值 含义 执行进程 >0 子进程PID 父进程 0 成功创建标志 子进程 -1 创建失败 父进程
要是我们想让父子进程执行不同的代码逻辑,应该怎么做呢
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("父进程开始运行,pid:%d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){// childwhile(1){printf("我是一个子进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());sleep(1); }}else{// fatherwhile(1){printf("我是一个父进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());sleep(1); }}//printf("进程开始运行,pid:%d\n", getpid());//while(1)//{// printf("我是一个进程!我的pid:%d\n", getpid());// printf("我是一个进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());// sleep(1);//}return 0;
}
修改完代码我们来运行一下
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
父进程开始运行,pid:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425...
运行可以看到父进程myprocess可执行程序id为457987,父进程的父进程bash的id为450425,父进程的子进程id为457988。
1. 为什么fork要给父子进程返回不同的值?
这是为了在代码中区分父子进程的执行路径,让程序员能编写不同的逻辑分支:
设计考量:
父进程需要知道子进程ID:用于后续管理(等待、发送信号等)
子进程需要明确自身身份:避免递归创建进程
错误处理统一:只有父进程能处理fork失败
类比:就像双胞胎出生时获得不同的名字,虽然基因相同但身份不同
2. 为什么fork会"返回两次"?
实际上不是函数返回两次,而是创建了两个独立的执行流:
关键机制:
调用fork时,内核复制父进程的:
寄存器状态(包括程序计数器PC)
页表(通过写时拷贝)
文件描述符表
在返回用户空间前,内核修改:
父进程的EAX寄存器 = 子进程PID
子进程的EAX寄存器 = 0
两个进程从相同的代码位置继续执行:
父进程:从fork()调用后继续
子进程:"诞生"后的第一条指令就是fork()之后的代码
3. 为什么同一个变量既等于0又大于0?
核心原理:两个进程拥有独立的地址空间
int ret = fork(); // 这行代码在两个进程中都有!// 内存布局示意:
// 父进程内存空间:ret_addr = 0x1000, 值=457988(子进程PID)
// 子进程内存空间:ret_addr = 0x1000, 值=0
执行过程:
时间线 父进程(PID=457987) 子进程(PID=457988)
---------------------------------------------------------------T1 执行 fork() 系统调用T2 | 内核创建子进程T3 | 设置父进程返回值=457988T4 | 设置子进程返回值=0T5 从fork返回 ↓T6 ret = 457988 (>0) 从"诞生点"开始执行 ↓T7 执行 else 分支 ret = 0 (==0)T8 printf("Parent...") 执行 else if 分支T9 printf("Child...")
注意:
ret
不是同一个物理内存位置!父子进程有各自独立的变量副本。
技术本质:写时拷贝(COW)的作用
int global = 100; // 全局变量pid_t pid = fork();if (pid == 0) {global = 200; // 子进程修改
} else {sleep(1);printf("%d", global); // 父进程仍输出100
}
内存变化:
fork时:父子共享同一物理内存页(标记为只读)
子进程写global:触发页错误
内核复制该内存页给子进程
子进程在新页上修改值
父进程仍访问原内存页
总结要点
进程是资源容器:通过
task_struct
实现资源隔离与调度。fork()是进程分身术:通过写时拷贝高效复制,双返回值区分父子。