Linux 进程学习笔记:从概念到实战
重点:
fork之后会创建和父进程一模一样的子进程,子进程会拷贝父进程的文本段,数据段,堆区,栈区,二者的虚拟地址是相同的,但通过MMU分配到不同的物理地址
fork之后,父子两个进程都活着,并且会执行同样的程序(fork以下的程序),如果只想让父程序运行的话,需要先用exit释放子进程,否则子进程会同样进行与父进程一样的程序
1. 什么是进程
进程是 Linux 系统中正在运行的程序实例,是操作系统进行资源分配和调度的基本单位。简单来说,程序是静态的、存放在磁盘上的文件,而进程是程序被加载到内存并执行后的动态实体。
每个进程都有独立的地址空间、文件描述符、环境变量和运行状态。Linux 通过进程控制块(PCB,即 task_struct)来管理每个进程,其中记录了进程的 PID、状态、优先级、内存指针、打开的文件等信息。
2. 进程与线程的区别
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程可以包含多个线程,它们共享进程的地址空间和资源,但各自拥有独立的栈和寄存器上下文。
- 资源开销:创建进程需要分配独立的地址空间,开销较大;创建线程只需分配栈空间,开销较小。
- 通信方式:进程间通信(IPC)需要借助管道、消息队列、共享内存等机制;同一进程内的线程可直接读写共享内存。
- 独立性:进程之间相互独立,一个进程崩溃通常不影响其他进程;线程共享进程资源,一个线程出错可能导致整个进程退出。
3. 进程的状态
Linux 进程在生命周期中会经历多种状态,可以通过 ps 命令查看进程当前状态。常见状态如下:
| 状态码 | 状态名称 | 说明 |
|---|---|---|
| R | 运行态 | 进程正在运行或处于运行队列中,等待 CPU 调度。 |
| S | 可中断睡眠态 | 进程正在等待某个事件或资源,可被信号唤醒。 |
| D | 不可中断睡眠态 | 进程通常在内核态等待 I/O 完成,不能响应信号。 |
| T | 停止态 | 进程被暂停,可通过 SIGCONT 信号恢复运行。 |
| Z | 僵尸态 | 进程已结束,但父进程尚未回收其退出状态。 |
4. 进程的创建与退出
在 Linux 中,进程主要通过 fork() 和 exec 系列函数创建。下面是一个简单的 C 语言示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程 PID: %d\n", getpid());
} else {
// 父进程
printf("父进程 PID: %d,子进程 PID: %d\n", getpid(), pid);
wait(NULL); // 等待子进程结束,避免产生僵尸进程
}
return 0;
}
编译运行后,可以看到父进程和子进程分别打印各自的 PID。子进程通过 exit() 退出,父进程通过 wait() 回收子进程资源。
4.1 fork() 详解
fork() 是 Linux 中创建进程的核心系统调用,调用一次却会返回两次:在父进程中返回子进程的 PID(大于 0),在子进程中返回 0。如果创建失败则返回 -1。fork 采用写时复制(Copy-on-Write)技术,子进程刚创建时与父进程共享同一份物理内存,只有某一方修改数据时才真正复制,因此创建开销较小。
fork 之后,父子进程的执行顺序是不确定的,由内核调度器决定。父进程和子进程拥有独立的地址空间,互不影响,但会继承父进程的文件描述符、环境变量、信号处理方式等属性。
4.2 PID 与 PPID
每个进程都有唯一的进程标识符 PID(Process ID),用于在内核中唯一标识一个进程。PID 由内核分配,通常从 1 开始递增,并在进程退出后可能被复用。进程还可以通过 getpid() 获取自身 PID,通过 getppid() 获取父进程 PID(PPID)。
在 Linux 中,PID 为 1 的进程是 init 或 systemd,它是所有进程的祖先。当某个进程的父进程先退出时,该进程会成为孤儿进程,并被 init 进程收养。
4.3 exit() 与退出状态
exit() 用于终止当前进程,并返回一个退出状态码给父进程。退出码 0 通常表示正常退出,非 0 表示异常或错误。进程退出时,内核会释放其占用的内存、文件描述符等资源,但进程控制块(PCB)会保留,直到父进程调用 wait() 或 waitpid() 回收。
如果父进程没有回收子进程的退出状态,子进程就会变成僵尸进程(Zombie)。僵尸进程不占用 CPU 和内存,但会占用一个 PID 和 PCB 条目,长期积累会耗尽系统进程表。因此父进程应及时调用 wait() 回收子进程,避免产生僵尸进程。
4.4 exec 系列函数
exec 系列函数(如 execl、execv、execvp 等)用于在当前进程中加载并执行一个新的程序。exec 不会创建新进程,而是用新程序替换当前进程的代码段、数据段和堆栈,但 PID 保持不变。常见的组合是 fork 后立即调用 exec,让子进程执行新的程序。
exec 调用成功后不会返回;只有调用失败时才会返回 -1。因此 exec 之后通常紧跟错误处理代码。
5. 进程的查看与管理
Linux 提供了丰富的命令用于查看和管理进程,日常使用频率较高的有:
ps:查看当前终端或系统中的进程快照,常用组合ps -ef或ps aux。top:动态查看系统进程和资源占用情况,按 CPU 或内存排序。htop:top 的增强版,支持交互式操作,显示更友好。kill:向进程发送信号,默认发送 SIGTERM(15),强制结束用kill -9。pstree:以树状结构展示进程之间的父子关系。
6. 进程间通信(IPC)
进程之间相互独立,若需要交换数据,必须借助内核提供的通信机制。常用的 IPC 方式包括:
- 管道(Pipe):用于父子进程或兄弟进程之间的单向数据传输,分为匿名管道和命名管道(FIFO)。
- 消息队列:内核维护的消息链表,进程可以向队列写入或读取消息,支持双向通信。
- 共享内存:多个进程映射同一块物理内存,读写速度最快,但需要配合信号量解决同步问题。
- 信号量:用于进程间的同步与互斥,常与共享内存搭配使用。
- 套接字(Socket):不仅支持本机进程通信,还支持跨主机的网络通信。
7. 守护进程
守护进程(Daemon)是长期运行在后台、不依赖终端会话的特殊进程,常用于提供系统服务,如 sshd、nginx、crond 等。编写守护进程通常需要以下步骤:
- 调用
fork()创建子进程,父进程退出,使子进程成为孤儿进程并被 init 收养。 - 调用
setsid()创建新的会话,脱离控制终端。 - 修改工作目录为根目录
/,避免占用可卸载的文件系统。 - 重设文件权限掩码
umask(0),确保创建文件时权限可控。 - 关闭标准输入、输出和错误文件描述符,避免占用终端资源。
8. 总结
Linux 进程是系统编程的核心概念,理解进程的状态转换、创建退出机制以及进程间通信方式,是深入学习多进程编程和系统调优的基础。建议在掌握上述内容后,动手编写 fork、管道和共享内存的示例程序,通过实际运行观察进程行为,加深理解。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)