线程和进程区别
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|
| 定义 | 最小的资源分配单元。独立运行的程序实例,拥有自己的内存空间和系统资源 | 最小的执行单元。进程中的执行单元,多个线程共享同一进程的资源 |
| 资源管理 | 独立地址空间、独立文件描述符、独立堆和栈 | 共享进程的地址空间、文件描述符和堆,但有独立的栈 |
| 创建与切换 | fork() 创建进程,开销较大;进程切换涉及完整上下文切换,速度较慢 | pthread_create() 创建线程,开销小;线程切换仅涉及线程上下文,速度较快 |
| 通信方式 | 进程间通信(IPC):管道、消息队列、共享内存、套接字等,通信开销较大 | 共享进程内存,可直接访问全局变量,通信开销小,但需要同步控制 |
| 并发性 | 进程独立运行,一个进程崩溃不会影响其他进程,适用于多核并行计算 | 线程共享资源,一个线程崩溃可能导致整个进程崩溃,适用于 I/O 密集型任务 |
| 切换开销 | 高,涉及进程上下文切换 | 低,线程切换只需保存和恢复少量寄存器 |
| 适用场景 | 适用于独立运行的程序,如数据库、后台服务、分布式计算 | 适用于需要频繁数据交互的任务,如 Web 服务器、多线程计算 |
进程间通信(IPC)方式对比表
| 通信方式 | 优点 | 缺点 | 适用场景 | 示例 API |
|---|
| 无名管道(Pipe) | - 简单易用,适用于父子进程通信 - 速度较快,基于内存传输 | - 仅支持 单向通信 - 只能用于 具有亲缘关系(父子进程) - 数据是 字节流,需要解析 | 进程间的简单数据传输 | int pipe(int pipefd[2]) |
| 命名管道(FIFO) | - 允许 无亲缘关系进程 通信 - 仍然基于内存,通信速度较快 | - 半双工通信(单向传输) - 需要 文件路径 来命名管道 - 读取数据后数据被清除 | 不同进程间的数据传输,通过文件系统传输 | mkfifo(const char pathname, mode_t mode);读写 open("fifo", O_WRONLY); |
| 消息队列(Message Queue) | - 多对多 进程通信,支持 消息结构化存储 - 不会像管道一样丢失数据(数据持久化) - 可设置优先级,提高消息处理顺序控制 | - 消息长度有限(受系统限制,如 msgmax) - 读取时需遍历队列,效率可能受影响 - 需要 显式创建和管理 消息队列 | 任务调度、事件通知,如多进程日志系统 | msgget(), msgsnd(), msgrcv()(Linux) |
| 共享内存(Shared Memory) | - 最快的 IPC 方式(进程直接访问内存,无需内核干预) - 适合大数据共享(如图像处理) - 多个进程可直接读写共享数据 | - 需要 同步机制(如信号量或互斥锁) 防止数据竞争 - 进程 意外退出 可能导致数据不一致 | 需要 高效数据交换 的场景,如视频流处理、数据库缓存 | shmget(), shmat(), shmdt()(Linux) |
| 信号量(Semaphore) | - 适用于 进程同步,可控制资源访问 - 支持进程间和线程间 资源共享控制 | - 仅用于 同步,不能传输数据 - 使用复杂,需手动维护信号量状态 | 进程同步、控制 临界区资源访问,如数据库连接管理 | semget(), semop(), semctl()(Linux) |
| 信号(Signal) | - 适用于 异步事件通知 - 轻量级,不需要额外的资源 - 进程可自定义信号处理函数 | - 只能传递简单信息(信号编号) - 可能会被 忽略或屏蔽 - 信号的处理顺序 不确定 | 进程控制,如终止、暂停、恢复进程(kill 命令) | kill(), signal(), sigaction() |
- 管道(Pipe)/命名管道(FIFO) 适用于简单的进程间数据流传输(如 Shell 命令)。
- 消息队列(Message Queue) 适用于 事件驱动 的任务调度,但数据量受限。
- 共享内存(Shared Memory) 是 最快 的进程通信方式,适用于 大数据传输,但需要同步机制防止冲突。
- 信号量(Semaphore) 用于 同步进程,信号(Signal) 用于 事件通知。
僵尸进程(Zombie Process)和孤儿进程(Orphan Process)的预防与处理
1. 僵尸进程(Zombie Process)
- 子进程执行完毕,但 父进程没有调用
wait() 或 waitpid() 处理它的退出状态,导致子进程的 PCB(进程控制块) 仍然保留在系统进程表中,成为“僵尸”状态。 - 僵尸进程不会占用 CPU 或内存,但会占用进程表项(PID),如果大量僵尸进程堆积,可能导致 系统无法创建新进程。
预防和处理僵尸进程**
| 方法 | 具体操作 | 优缺点 |
|---|
1. wait() 或 waitpid() | 父进程在适当的时间调用 wait() 回收子进程资源 | 优点:简单直接,避免产生僵尸进程- 缺点:如果父进程没有及时调用,仍可能短时间出现僵尸进程 |
2. 设置信号处理函数(SIGCHLD) | 在父进程中捕获 SIGCHLD 信号,并在信号处理函数中调用 waitpid(-1, NULL, WNOHANG); 回收任意子进程 | 优点:适用于多个子进程,避免 wait() 阻塞父进程- 缺点:信号处理函数可能受到其他信号干扰,导致某些子进程未被正确回收 |
3. 让子进程自己 exit() 退出 | 不建议使用,因为子进程自己退出不会清除其 PCB,必须由父进程 wait() | - 缺点:不能真正解决僵尸进程问题 |
4.pthread_detach(tid); | 让线程自动释放资源,不需要 pthread_join() | 优点: - 避免僵尸线程,无需手动回收 - 适用于短时任务或守护线程 缺点: - 线程结束后无法获取返回值 - 不能回收已变成僵尸的线程 |
5.pthread_join(tid, NULL); | 等待线程结束,回收线程资源 | 优点: - 确保线程正常结束并释放资源 - 可获取线程返回值 缺点: - 需要手动调用,否则可能导致资源泄漏 - 若线程未结束,pthread_join() 会阻塞主线程 |
2. 孤儿进程(Orphan Process)
定义:父进程先于子进程退出,子进程成为 孤儿进程,此时 init 进程(PID=1)会接管它,并回收其资源。
孤儿进程的影响:
- 通常不会造成严重问题,因为
init 进程会自动 wait() 清理它们。 - 但在某些情况下,如子进程仍然占用关键资源或进行 I/O 操作,可能导致 资源泄露 或 输出错误。
如何预防和处理孤儿进程
| 方法 | 具体操作 | 优缺点 |
|---|
1. 让父进程 wait() 回收子进程 | 在父进程退出前,等待所有子进程执行完毕 | - 优点:保证子进程完成后再退出- 缺点:如果子进程运行时间较长,父进程会一直等待 |
| 2. 让子进程以守护进程(Daemon)方式运行 | 子进程 调用 setsid() 让自己成为新的会话组长,独立运行 | - 优点:适用于长期运行的后台任务,如 Web 服务器、日志收集等- 缺点:不适用于短生命周期进程 |
3. 使用 prctl(PR_SET_CHILD_SUBREAPER, 1);(Linux) | 让某个进程(如守护进程)接管孤儿进程,而不是 init 进程 | - 优点:适用于复杂的多进程管理- 缺点:仅适用于 Linux |
| 4. 父进程提前通知子进程 | 在退出前,通知子进程先退出,如发送 SIGTERM 信号 | - 优点:保证父进程退出时不留下孤儿进程- 缺点:子进程必须支持信号处理 |
代码示例:让子进程成为守护进程,避免孤儿进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
void daemonize() {
pid_t pid = fork();
if (pid > 0) {
exit(0); // 让父进程退出,子进程成为孤儿进程
} else if (pid < 0) {
perror("fork failed");
exit(1);
}
// 创建新会话
setsid();
// 更改工作目录
chdir("/");
// 关闭标准输入、输出和错误流
fclose(stdin);
fclose(stdout);
fclose(stderr);
}
int main() {
daemonize(); // 让进程成为守护进程
while (1) {
sleep(5);
printf("守护进程运行中...\n"); // 由于标准输出关闭,实际不会打印
}
return 0;
}
总结
| 问题 | 定义 | 影响 | 预防与处理 |
|---|
| 僵尸进程 | 子进程退出但父进程未 wait(),导致进程表项未释放 | 占用 PID 资源,可能导致新进程无法创建 | - wait() 或 waitpid() 处理子进程 - 绑定 SIGCHLD 信号处理函数 - prctl(PR_SET_CHILD_SUBREAPER, 1)(Linux) |
| 孤儿进程 | 父进程先于子进程退出,子进程由 init 进程接管 | 影响较小,但可能导致 资源泄露 | - 让父进程 wait() 子进程 - 让子进程调用 setsid() 变为守护进程 - 让父进程提前通知子进程退出 |
通常,孤儿进程不需要特别处理,因为 init 进程会自动回收,而 僵尸进程需要特别关注,防止系统资源耗尽。