进程
一、进程与线程核心概念
线程是应用层概念,Linux内核中所有调度实体称为任务(task) 。部分任务拥有独立完整资源,部分任务共享一套资源,线程本质是轻量级进程。如下图所示
上图中:
- 左边是一个含有单个线程的进程,它拥有自己的一套完整的资源。
- 右边是一个含有两条线程的进程,线程彼此间共享进程内的资源。
由此可见,线程是一种轻量级进程,提供一种高效的任务处理方式。
核心定义
- 线程(任务):系统任务调度的最小单位。
- 进程:系统分配资源的最小单位。
核心区别(同一任务需求下)
- 资源消耗:进程消耗更大,线程共享同一进程的资源。
- 独立性:进程内存隔绝,稳定且相互影响小;线程共享资源,单个线程异常可能导致整个进程崩溃。
- 协作难度:进程间协作需复杂数据调度;线程共享资源,无需额外处理数据传输。
- 调度处理:两者均需解决同步与互斥问题。
- 同步:并发任务间有明确先后顺序。
- 互斥:并发任务间不能同时执行某一操作。
二、进程基本概念
程序与进程的区别
- 程序:编译后生成的ELF格式文件(如a.out、demo),存储于硬盘的文件,(静态的“剧本”)。
- 进程:程序的代码和数据被加载到内存中运行的过程,(动态 按照剧本开始表演)。
进程的加载过程
Linux中ELF格式程序执行时,会将代码段、数据段等必要资源拷贝到内存,系统同时分配栈、堆等内存空间,使程序成为动态实体。
在Linux中,程序文件的格式都是ELF,这些文件在被执行的瞬间,就被载入内存,所谓的载入内存,如上图所示,就是将数据段、代码段这些运行时必要的资源拷贝到内存,另外系统会再分配相应的栈、堆等内存空间给这个进程,使之成为一个动态的实体。
三、进程的组织方式
在Linux系统中,除了系统的初始进程之外,其余所有进程都是通过从一个父进程(parent)复刻(fork)而来的,有点像人类社会,每个个体都是由亲代父母繁衍而来。
因此,在整个Linux系统中,所有的进程都起源于相同的初始进程,它们之间形成一棵倒置的进程树,就像家族族谱,可以使用命令pstree查看这些进程的关系:
可以看到,最开始的系统进程叫 systemd、init ,这个进程的诞生比较特别,其身份信息在系统启动前就已经存在于系统分区之中(内存),在系统启动时直接复制到内存。而其余的进程,从上述pstree命令的执行效果可见,都是系统初始进程的直接或间接的后代进程。
四、进程的复刻(fork)
fork() 是进程创建的核心机制,类比“细胞分裂”,父进程复制自身资源生成子进程。
一个进程复刻一个子进程的时候,会将自身几乎所有的资源复制一份
父子进程相同的属性
在创建之初完全一样
- 实际UID/GID、有效UID/GID。
- 所有环境变量。
- 进程组ID、会话ID。
- 当前工作路径。
- 已打开的文件(父进程打开文件后创建子进程,两者均保持打开状态)。
- 信号响应函数。
- 完整内存空间(栈、堆、数据段、代码段、标准IO缓冲区等)。
父子进程不同的属性
- 进程号(PID):唯一标识,父子进程PID不同。
- 记录锁:父进程对文件的加锁不会被子进程继承。
- 挂起的信号:“悬而未决”的信号不会被子进程继承。
五、进程的状态
进程是动态实体,存在多种状态转换,核心状态及转换条件如下:
解析:
- 所有进程(除了系统初始进程systemd之外),都有一个父进程。
- 父进程通过调用fork()函数,将自身复制一份形成一个子进程。
- 新创建的子进程拥有与父进程一样的执行代码、内存空间(父子进程的内存空间的内容是一致的,但分属不同的区域各自独立)等信息,并处于就绪态(TASK_RUNNING)。
- 当进程退出时(不管是主动退出还是被动退出),进入僵尸态(EXIT_ZOMBIE),僵尸态下的进程无法运行,也无法被调度,但其所占据的系统资源未被释放。僵尸态是进程的必经状态,编程过程中不能避免僵尸态,但要避免进程长时间处于僵尸态。
- 僵尸态进程要等待其父进程对其资源进程回收后,才能变成死亡态(EXIT_DEAD),死亡态的进程所有占据的系统资源可以被系统随时回收。
完整状态流转
- 创建态:父进程调用 fork() 函数,子进程生成。
- 就绪态(TASK_RUNNING):子进程拥有完整资源,等待CPU调度。
- 执行态(TASK_RUNNING):进程获得CPU,正在处理数据。
- 睡眠态/挂起态:
- 可中断睡眠(TASK_INTERRUPTIBLE):等待资源(键盘输入、网络消息等),资源可用时唤醒。
- 不可中断睡眠(TASK_UNINTERRUPTIBLE):等待核心资源,不可被信号唤醒。
- 暂停态:
- TASK_STOPPED:收到 SIGSTOP/SIGTSTP 信号,被动暂停。
- TASK_TRACED:被调试工具跟踪暂停。
- 僵尸态(EXIT_ZOMBIE):进程退出(主动return/exit或被动被信号杀死),资源未释放。
- 死亡态(EXIT_DEAD):父进程调用 wait()/waitpid() 回收资源,进程彻底终止。
关键说明
- 僵尸态是进程必经状态,无法避免,但需避免长时间处于该状态(否则占用系统资源)。
- 只有父进程回收资源后,僵尸态进程才能转为死亡态,释放所有资源。
简化三态模型
| 状态 | 触发条件 | 转换条件 |
|---|---|---|
| 运行态 | 进程获得CPU调度 | 时间片耗尽、被高优先级进程抢占、进入等待态 |
| 就绪态 | 进程创建完成、等待资源就绪、暂停后恢复 | 获得CPU调度进入运行态 |
| 等待态 | 等待资源(IO、网络)、收到暂停信号 | 资源可用、收到继续信号(SIGCONT)进入就绪态 |
六、拓展知识(实用进阶)
1. 进程控制常用系统调用
- exec系列函数:fork() 后,子进程可通过 exec() 加载新程序(替换原有代码段、数据段),实现“创建进程并执行新任务”。
- 常用函数:execl()、execv()、execlp()、execvp(),区别在于参数传递方式和是否搜索PATH路径。
- 示例:子进程通过
execl("/bin/ls", "ls", "-l", NULL)执行ls -l命令。
- exit/_exit/Exit:进程退出函数。
- exit():刷新标准IO缓冲区后终止进程(用户空间缓冲区)。
- _exit():直接终止进程,不刷新缓冲区(内核直接回收资源)。
- Exit():stdlib库封装函数,效果同exit()。
- wait/waitpid:父进程回收子进程资源,避免子进程成为僵尸进程。
- wait():阻塞等待任意子进程退出,返回退出子进程的PID。
- waitpid():可指定等待的子进程PID(如
-1表示任意子进程),支持非阻塞模式(通过WNOHANG参数)。 - 示例:
waitpid(-1, &status, WNOHANG)非阻塞回收所有子进程。
2. 进程查看与控制命令
| 命令 | 功能描述 | 常用参数 | 示例输出片段 |
|---|---|---|---|
pstree |
以树状图展示进程关系 | -p 显示PID,-u 显示用户 |
init(1)─┬─bash(100) |
ps |
查看当前进程状态 | ps -ef 显示所有进程,ps aux 显示详细信息 |
UID PID PPID C STIME TTY TIME CMD |
top |
实时监控进程资源占用(CPU、内存) | P 按CPU排序,M 按内存排序 |
%Cpu(s): 2.0 us, 1.0 sy, 0.0 ni |
kill |
向进程发送信号(终止、暂停、恢复) | kill -9 PID 强制终止,kill -19 PID 暂停 |
kill -9 1234 强制杀死PID=1234的进程 |
jobs |
查看当前终端后台运行的进程 | jobs -l 显示PID |
[1]+ Running ./demo & |
fg/bg |
将后台进程调至前台/后台继续运行 | fg %1 调第一个后台进程到前台 |
fg %1 将 jobs 中序号1的进程调至前台 |
3. 线程补充知识(Linux下实现)
- Linux内核无“线程”概念,线程本质是“共享资源的进程”,通过
clone()系统调用创建(fork() 是clone()的特殊情况,clone(SIGCHLD, 0)等价于 fork())。 - 线程共享的资源:进程地址空间(代码段、数据段、堆)、文件描述符表、信号处理函数、进程组ID/会话ID、当前工作目录。
- 线程独立的资源:线程ID(TID,内核中用
pid_t表示)、线程私有栈(默认8MB,可通过pthread_attr_setstacksize调整)、寄存器上下文(CPU寄存器值)、调度优先级(sched_getscheduler/sched_setscheduler调整)。 - 常用线程库:POSIX线程库(pthread),核心函数:
pthread_create(&tid, NULL, func, arg):创建线程,func为线程执行函数。pthread_join(tid, &retval):阻塞等待线程结束,回收线程资源(类似进程的wait())。pthread_mutex_init(&mutex, NULL):初始化互斥锁,解决线程间资源竞争。pthread_cond_init(&cond, NULL):初始化条件变量,解决线程间同步(如“生产者-消费者”模型)。
4. 进程间通信(IPC)方式(补充进程协作方案)
由于进程内存隔绝,需通过特定方式实现通信,常用方式对比:
| IPC方式 | 特点 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 管道(pipe) | 半双工,父子进程间通信 | 简单的父子进程数据传递(如shell管道) | 实现简单,无需额外资源 | 仅支持亲缘进程,半双工 |
| 命名管道(FIFO) | 全双工,无亲缘关系进程可通信 | 不同进程间的稳定数据交互 | 支持任意进程,全双工 | 需通过文件系统创建,有IO开销 |
| 消息队列(message queue) | 按消息类型收发,非阻塞 | 多进程间异步通信(如服务器-客户端) | 无需轮询,按类型筛选消息 | 消息大小有限制,内核资源占用 |
| 共享内存(shared memory) | 进程共享物理内存,最快IPC方式 | 高频、大数据量传输(如数据库) | 速度最快,无数据拷贝 | 需配合信号量解决同步问题 |
| 信号量(semaphore) | 用于同步与互斥,不传递数据 | 控制多进程对共享资源的访问 | 轻量级,解决竞争问题 | 不传递数据,需配合其他IPC使用 |
| 套接字(socket) | 支持跨主机、全双工通信,基于TCP/UDP协议 | 网络通信(如客户端-服务器模型、跨主机进程交互) | 通用性强,支持本地/网络通信,协议成熟 | 网络环境下有延迟开销,需处理协议细节(如粘包、超时重传) |
5. 僵尸进程的危害与解决方法
(1)僵尸进程的危害
- 占用系统资源:僵尸进程虽不运行,但仍占用PID(进程号)、PCB(进程控制块)等内核资源,PID总数有限(Linux默认最大PID为32768),大量僵尸进程会导致新进程无法创建。
- 内存泄漏风险:PCB长期占用内核内存,若父进程长期不回收,会造成内核内存泄漏。
(2)核心解决方法
-
父进程主动回收(wait/waitpid)
父进程在fork()后,通过wait()或waitpid()阻塞/非阻塞回收子进程资源。
示例代码(非阻塞回收):#include <sys/wait.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程:执行任务后退出 printf("Child process exit\n"); return 0; } else if (pid > 0) { // 父进程:非阻塞回收子进程 int status; while (waitpid(-1, &status, WNOHANG) > 0) { if (WIFEXITED(status)) { printf("Child exited with code: %d\n", WEXITSTATUS(status)); } } // 父进程继续执行其他任务 sleep(10); } return 0; } -
利用SIGCHLD信号自动回收
子进程退出时,内核会向父进程发送SIGCHLD信号,父进程可注册信号处理函数,在函数中调用waitpid()回收资源,避免阻塞主逻辑。
示例代码:#include <signal.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> // SIGCHLD信号处理函数 void sigchld_handler(int sig) { int status; // 循环回收所有退出的子进程(避免漏收) while (waitpid(-1, &status, WNOHANG) > 0); } int main() { // 注册信号处理函数 signal(SIGCHLD, sigchld_handler); pid_t pid = fork(); if (pid == 0) { printf("Child process exit\n"); return 0; } else if (pid > 0) { // 父进程无需阻塞,继续执行其他任务 sleep(10); } return 0; } -
双进程模型(父进程退出,init接管子进程)
父进程fork()后立即退出,子进程成为“孤儿进程”,由系统初始进程(init/systemd,PID=1)接管,init会自动回收孤儿进程的资源,避免僵尸态。
适用场景:后台服务进程(如守护进程)。
6. Linux进程调度策略
Linux内核通过调度器(Scheduler)分配CPU资源,不同进程对应不同调度策略,核心策略如下:
| 调度策略 | 适用进程类型 | 核心逻辑 | 优先级范围(数值越小优先级越高) |
|---|---|---|---|
| CFS(完全公平调度) | 普通非实时进程(默认策略) | 按进程“虚拟运行时间”分配CPU,确保各进程公平占用 | 静态优先级(nice值):-20~19(默认0) |
| SCHED_FIFO(先进先出) | 实时进程(对响应时间要求高) | 高优先级进程一旦获得CPU,一直运行到主动放弃或被更高优先级进程抢占 | 实时优先级:1~99 |
| SCHED_RR(时间片轮转) | 实时进程(需公平分配CPU的实时任务) | 同优先级进程按时间片轮转,时间片耗尽后放回就绪队列 | 实时优先级:1~99 |
关键说明
- 普通进程通过
nice命令调整静态优先级:nice -n 5 ./demo(将demo进程的nice值设为5,优先级降低)。 - 实时进程需通过
pthread_setschedparam(线程)或setpriority(进程)设置实时优先级,且需root权限。
7. 线程安全与互斥锁实践
线程共享进程资源,若多个线程同时操作“临界资源”(如全局变量、共享内存),会导致数据不一致(“竞态条件”),需通过互斥锁(Mutex)保证线程安全。
(1)互斥锁使用步骤(POSIX线程库)
- 初始化互斥锁:
pthread_mutex_init(&mutex, NULL)(默认属性)。 - 加锁:
pthread_mutex_lock(&mutex)(阻塞等待,直到获取锁);或pthread_mutex_trylock(&mutex)(非阻塞,获取失败立即返回)。 - 操作临界资源:访问/修改共享数据(如全局变量计数)。
- 解锁:
pthread_mutex_unlock(&mutex)(释放锁,让其他线程获取)。 - 销毁互斥锁:
pthread_mutex_destroy(&mutex)(进程/线程退出前释放资源)。
(2)示例代码(线程安全的计数器)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define THREAD_NUM 3 // 线程数量
int count = 0; // 临界资源(全局计数器)
pthread_mutex_t mutex; // 互斥锁
// 线程执行函数:累加计数器
void* count_task(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < 10000; i++) {
// 加锁:进入临界区
pthread_mutex_lock(&mutex);
count++;
// 解锁:退出临界区
pthread_mutex_unlock(&mutex);
}
printf("Thread %d finished, current count: %d\n", thread_id, count);
return NULL;
}
int main() {
pthread_t tid[THREAD_NUM];
int thread_ids[THREAD_NUM] = {1, 2, 3};
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建3个线程
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&tid[i], NULL, count_task, &thread_ids[i]);
}
// 等待所有线程结束
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(tid[i], NULL);
}
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Final count: %d\n", count); // 预期输出:30000
return 0;
}
(3)避免死锁的核心原则
死锁是指多个线程互相等待对方持有的锁,导致永久阻塞,避免方法:
- 顺序加锁:多个线程按固定顺序获取多把锁(如先锁A再锁B)。
- 超时加锁:使用
pthread_mutex_timedlock设置超时时间,超时后释放已持有的锁。 - 避免嵌套锁:尽量减少锁的嵌套使用,单个临界区只使用一把锁。
七、常见问题解答(FAQ)
1. 为什么fork()会返回两次?
fork() 是“复制进程”的系统调用,调用时会先复制父进程资源生成子进程,然后:
- 父进程中:fork() 返回子进程的PID(大于0),用于后续管理子进程。
- 子进程中:fork() 返回0,用于标识自身是子进程(子进程可通过getppid()获取父进程PID)。
本质是“一次系统调用,两次返回”,因父子进程在不同地址空间独立执行。
2. 线程和进程如何选择?
| 场景 | 选择进程 | 选择线程 |
|---|---|---|
| 资源隔离需求 | 高(如多服务进程,一个崩溃不影响其他) | 低(共享进程资源,一个线程崩溃影响整个进程) |
| 通信效率需求 | 低(需IPC机制,有开销) | 高(直接共享内存,无需额外通信) |
| 并发数量需求 | 低(进程资源消耗大,支持并发数少) | 高(线程轻量,支持上万级并发) |
| 开发复杂度 | 低(无需处理线程安全问题) | 高(需解决互斥、同步、死锁等问题) |
3. 为什么Linux中没有“真正的线程”?
Linux内核设计中,线程本质是“共享资源的进程”,通过clone()系统调用创建时,指定共享的资源(如地址空间、文件描述符),而非单独设计“线程”数据结构。这种实现方式简化了内核逻辑(复用进程调度机制),同时保证了线程的轻量级特性,与Windows的“内核线程”实现思路不同,但功能上完全等价。
八、总结
- 核心关系:进程是资源分配单位,线程是调度单位;线程共享进程资源,进程间资源隔绝。
- 进程生命周期:创建(fork())→ 就绪→ 执行→ 等待/暂停→ 僵尸→ 死亡(wait()回收),核心是避免僵尸进程长期占用资源。
- 实用工具:通过
pstree/ps/top查看进程,kill/fg/bg控制进程,wait/exec系统调用编程。 - 进阶重点:IPC通信方式选择、线程安全(互斥锁)、进程调度策略,需结合实际场景灵活应用。

浙公网安备 33010602011771号