一、进程的概念与特征
1.1 进程与程序的区别
- 程序:存储在硬盘上的静态数据指令集合(如可执行文件),本身不占用CPU和内存资源。
- 进程:程序被加载到内存后形成的动态实体,需要占用内存空间,并在CPU上执行指令。进程具有生命周期(启动→运行→消亡),且同一程序可对应多个进程实例。
1.2 进程是资源分配的最小单位
Linux为每个进程分配独立的04GB虚拟内存空间**(32位系统),其中用户空间(03GB)各进程相互隔离,内核空间(3~4GB)为所有进程共享。进程拥有独立的进程控制块(PCB)、地址空间、文件描述符表等资源,因此进程间默认无法直接通信,需借助IPC机制**(管道、信号、共享内存等)进行数据交换。
二、进程的创建
2.1 fork() 系统调用
fork() 是创建新进程的核心接口,通过复制父进程的用户空间数据来生成子进程。
函数原型:
#include <unistd.h>
pid_t fork(void);
行为:
- 子进程完整拷贝父进程0~3GB用户空间数据(代码段、数据段、堆、栈)。
- 内核空间部分拷贝(PCB块、文件描述符表等),但PID和PPID(父进程ID)不拷贝,子进程获得新的PID,PPID设置为父进程的PID。
- 父子进程共享3~4GB内核空间,但用户空间互相独立。
返回值:
- 在父进程中返回子进程的PID(>0)。
- 在子进程中返回0。
- 失败返回-1。
执行模型:调用一次fork,返回两次。父进程和子进程从fork调用返回处开始并发执行,执行顺序由调度器决定,无法预先假设。
辅助函数:
- getpid():获取当前进程的PID。
- getppid():获取当前进程的父进程PID。
三、进程调度与状态
3.1 调度策略
Linux内核采用抢占式多任务调度,宏观上多个进程可以“同时”执行,微观上通过时间片轮询实现并发。调度算法包括:
- 时间片轮询(公平分配CPU时间)
- 先来先服务
- 优先级调度(高优先级优先)
- 短作业优先(缩短平均等待时间)
3.2 进程状态模型
操作系统经典的三态模型(运行、就绪、阻塞)在Linux中被扩展为更细粒度的状态:
| R | 运行态 | 正在执行(用户态或内核态)或被调度器选中但尚未获得CPU |
| S | 可中断睡眠态 | 等待资源(如I/O),可被信号唤醒 |
| D | 不可中断睡眠态 | 等待资源,但不会被信号打断(通常用于磁盘I/O) |
| T | 暂停态 | 因收到SIGSTOP等信号而被挂起 |
| Z | 僵尸态 | 进程已退出,但资源未被父进程回收 |
| X | 结束态 | 进程彻底结束,资源已回收(通常不可见) |
3.3 进程状态迁移
典型迁移路径:就绪(R)→ 运行(R)→ 睡眠(S/D)→ 就绪(R)→ 运行 → 退出 → 僵尸(Z)→ 结束(X)。父进程需通过wait或waitpid回收僵尸态子进程,否则子进程会残留占用资源。
四、进程管理常用命令
Linux提供多组命令用于查看和控制进程:
| ps -aux / ps -ef | 显示所有进程的PID、PPID、状态、内存/CPU占用等 |
| top | 动态实时显示进程资源占用情况 |
| pstree -sp <pid> | 以树形结构显示进程创建关系 |
| kill -<信号> <pid> | 向进程发送信号(如 kill -9 强制结束) |
| jobs / fg / bg | 管理后台任务(shell作业控制) |
管道与grep:ps -aux | grep a.out 用于筛选特定进程。
五、进程退出与资源回收
5.1 进程退出的方式
- 主函数 return 或调用 exit()(标准C库函数,刷新缓冲区后退出)。
- 调用 _exit() / _Exit()(直接系统调用,不刷新缓冲区)。
- 被信号(如 SIGKILL)强制终止。
原型:
#include <stdlib.h>
void exit(int status); // status=0表示正常退出,非0表示异常
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
5.2 僵尸进程与孤儿进程
- 僵尸进程(Zombie):进程已退出,但其父进程未调用wait/waitpid回收其资源,导致进程表项仍保留。僵尸进程占用少量内存(PCB),大量堆积可耗尽系统资源。
- 孤儿进程(Orphan):父进程先于子进程结束。此时子进程被**init进程(PID=1)**收养,待其结束后由init回收资源,因此孤儿进程不会成为僵尸。
5.3 回收接口:wait 与 waitpid
wait(阻塞等待任一子进程结束):
#include <sys/wait.h>
pid_t wait(int *wstatus);
成功返回回收的子进程PID,失败返回-1。参数可接收退出状态,传入NULL表示不关心状态。
waitpid(更灵活,可指定特定进程或非阻塞):
pid_t waitpid(pid_t pid, int *status, int options);
- pid > 0:等待指定PID的子进程。
- pid = -1:等待任意子进程(同wait)。
- pid = 0:等待同一进程组内的任意子进程。
- pid < -1:等待指定进程组内的任意子进程。
- options = 0:阻塞等待。
- options = WNOHANG:非阻塞,若无子进程结束则立即返回0。
注意:使用非阻塞模式时需配合轮询(循环检查)以避免忙等。
六、exec 函数族:在当前进程执行外部程序
exec系列函数用于替换当前进程的代码段和数据段,执行一个新的可执行文件,而不创建新进程(PID不变)。调用成功后,原进程的后续代码不再执行。
常用函数:
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* NULL */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
- l vs v:参数传递方式(列表或数组)。
- p:使用PATH环境变量查找可执行文件。
- 返回 -1 表示执行失败,成功则不返回。
典型用法(在子进程中加载新程序):
pid_t pid = fork();
if (pid == 0) { // 子进程
execlp("ls", "ls", "-l", NULL);
perror("execlp"); // 只有失败才执行
exit(1);
}
七、进程间通信概述
由于进程间用户空间隔离,直接通信被禁止。Linux提供多种IPC机制:
- 管道(Pipe/FIFO)
- 信号(Signal)
- 共享内存(Shared Memory)
- 消息队列(Message Queue)
- 信号量(Semaphore)
- 套接字(Socket)
这些通信方式均需借助内核空间作为中介。
