收尾
进程终止:子进程通过exit()或_exit()终止,父进程通过wait()或waitpid()等待子进程终止,并获取其退出状态。?其实可以考虑在另一篇博文中来写
fork函数讲解
fork函数概述
fork()
是 Linux 中用于创建新进程的系统调用。当一个进程调用 fork()
时,系统会创建一个与原进程几乎完全相同的子进程。
新的子进程在有相关写操作时,会复制父进程的资源(即写时复制的概念)。
父进程的PID和子进程的PID是不同的。
父进程和子进程会从 fork()
调用的返回值处开始继续执行,但返回值在父进程和子进程中是不同的。
fork函数的工作原理和流程
其具体工作原理和流程如下:
当某个进程调用fork()
创建子进程后,系统会创建一个与原进程几乎完全相同的子进程,然后父进程和子进程会从fork()
返回的地方继续执行。
不过两个进程得到的fork()
的返回值不一样:
- 对于父进程,它得到的
fork()
的返回值是子进程PID; - 对于子进程,它得到的
fork()
的返回值是0;
即:
如果 fork()
成功,它会返回两次:在父进程中返回子进程的 PID,在子进程中返回 0。
如果 fork()
失败(如资源不足),返回 -1
,并设置 errno
以说明错误。
子进程的特点
- 父子进程的几乎相同:子进程是父进程的副本,它们俩几平是相同的,除了
fork()
的返回值和 PID。 - 资源复制:父子进程共享文件描述符、内存映射等资源,但会使用“写时复制”(Copy-On-Write, COW)技术优化内存使用。这意味着在父子进程开始执行时,内存不会立即复制,而是在修改内存时才复制。
- 进程独立性:父进程和子进程是独立的,执行过程中互不影响,但它们会共享某些资源(如打开的文件)。
进程与线程的区别
fork()
创建的子进程在某些方面与线程(特别是线程的创建和管理)有相似之处,但它们在操作系统级别的实现和资源管理上有一些关键的区别。
- 进程(Process)
- 进程是操作系统分配资源的基本单位。每个进程都有独立的地址空间、文件描述符、栈、堆等资源。
- 进程之间是独立的,它们不会共享内存空间(除非显式使用共享内存或其他进程间通信机制),每个进程有自己的 PID 和独立的资源。
- 进程的创建(通过
fork()
)是相对重的操作,需要操作系统为子进程分配新的资源(如内存)。
- 线程(Thread)
- 线程是进程内部的执行单元,同一个进程中的多个线程共享进程的资源(如内存、文件描述符等),它们共享相同的地址空间。
- 线程之间是轻量级的,因为它们共享进程的资源,而不需要像进程那样拥有完全独立的资源。
- 线程的创建通常比进程轻量,操作系统管理线程的开销较小,线程之间可以很容易地进行共享数据和通信(如使用互斥锁、条件变量等)。
二者的关键差异
-
地址空间和资源:
- 进程:子进程有独立的地址空间。父进程与子进程之间没有直接共享内存,除非使用共享内存(
mmap()
)或其他进程间通信(IPC)方式。 - 线程:线程在同一进程内共享内存、文件描述符等资源。线程间的通信非常高效,因为它们共享相同的地址空间。
- 进程:子进程有独立的地址空间。父进程与子进程之间没有直接共享内存,除非使用共享内存(
-
创建开销:
- 进程:通过
fork()
创建子进程时,操作系统需要为新进程分配独立的资源(如内存空间)。这使得进程创建的开销相对较大。 - 线程:创建线程时,操作系统只需要分配线程控制块(TCB)等较小的资源,不需要分配独立的内存空间,因此线程创建比进程轻量。
- 进程:通过
-
执行独立性:
- 进程:父进程与子进程相互独立,父进程的退出不会影响子进程,反之亦然。它们各自拥有独立的控制流。
- 线程:同一进程中的多个线程共享控制流,互相协作。线程的退出会影响到进程的状态,甚至可能导致整个进程退出。
-
调度和切换:
- 进程:操作系统调度时,进程之间的切换需要保存和恢复更多的状态,因为每个进程有独立的地址空间。
- 线程:线程之间的切换较轻量,操作系统只需要保存和恢复少量状态(如寄存器、栈指针等),因为线程共享地址空间。
fork()
创建的子线程和线程的相似性和差异
相似性:
- 并行执行:无论是
fork()
创建的子进程还是线程,它们都可以并行执行(多核 CPU 上); - 并发性:父进程与子进程之间的调度是并发的,线程间的调度也是如此。
差异:
- 资源分配:
fork()
创建的子进程拥有独立的资源(如地址空间、PID),而线程共享进程的资源。 - 进程控制:父进程与子进程是完全独立的,退出父进程不会直接影响子进程;线程则不同,进程退出时会导致所有线程结束。
何时选择使用进程,何时使用线程?
- 如果需要完全隔离的执行环境,或者需要实现进程间的严格隔离,应该选择进程(使用
fork()
)。例如,fork()
在服务器中常用于创建多个独立的工作进程,每个进程可以处理独立的任务。 - 如果需要多个轻量级的并发任务,并且共享资源是必须的,应该选择线程。线程通常适用于需要大量并发操作且共享内存的场景,例如 Web 服务器的请求处理。
小结
虽然子进程和线程在某些方面表现得有点相似——例如它们可以并行执行,但它们本质上是不同的:子进程具有独立的资源和地址空间,而线程则是共享同一进程的资源。fork()
创建的子进程是“重型”的,而线程则是“轻型”的。因此,子进程和线程虽然在并发执行上有相似之处,但它们的实现和适用场景有很大区别。
fork函数使用的简单例子
下面是一个简单的示例,展示如何使用 fork()
创建一个子进程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid = fork(); // 创建新进程if (pid == -1) {// 错误处理:fork()失败perror("fork failed");return 1;} else if (pid > 0) {// 父进程printf("This is the parent process, PID: %d, child PID: %d\n", getpid(), pid);sleep(2); // 父进程睡眠 2 秒,确保子进程有时间执行,否则有可能出现子进程还没执行完父进程就执行完的情况} else {// 子进程printf("This is the child process, PID: %d, parent PID: %d\n", getpid(), getppid());}return 0;
}
代码解释:
当 fork()
被调用时,操作系统会在内核中创建一个新进程,子进程会复制父进程的大部分资源(包括内存、文件描述符等)。然后父进程和子进程都会从 fork()
语句的下一行代码开始继续执行,但它们的执行路径有所不同:
- 父进程:父进程得到的
fork()
返回值是子进程的 PID(大于 0 的值),父进程会执行属于它的if条件分支代码块。 - 子进程:子进程得到的
fork()
返回值的值是 0,子进程会执行属于它的if分支条件代码块。
由于父进程和子进程得到的fork()
的返回值不一样,即各自的pid变量的值不一样,所以在后续的代码中各自执行了不同的条件分支。
在Linux开发板上测试上面这个例子
代码文件复制到Ubuntu中
交叉编译
运行下面的命令进行交叉编译
cd /home/book/mycode/C0034_fork
arm-buildroot-linux-gnueabihf-gcc -o fort_test fort_test.c
复制到NFS目录中,备用。
在开发板上测试
打开串口终端→打开开发板→挂载网络文件系统
执行下面的代码运行测试程序
/mnt/fork_test/fort_test
运行结果如下:
可见,符合我们的预期,所以测试成功。
子进程的应用场景
子进程的创建通常是为了实现进程间的隔离、并行化或者并发处理,以及提升程序的响应性和资源管理。虽然有时线程可能更合适,但子进程在某些场景下非常重要。下面我将列举一些典型的子进程应用场景,帮助你更好地理解它们的使用场景。
注意:要理解下面的应用场景关键要明白子进程与父进程为何能执行不同的代码,其实在实际代码中就是通过判断fork的返回值来进行的,通过前面的例子我们已经知道fork()函数对父进程和子进程的返回值不一样嘛。
1. 服务器模型中的进程池
- 场景:Web 服务器(如 Apache)或数据库服务器常常需要处理大量并发请求。这时,创建一个子进程来处理每一个请求,可以保证每个请求都在独立的进程中运行,互不干扰。
- 原因:使用子进程可以实现进程间隔离,每个请求都在独立的进程中执行,即使其中一个请求出现崩溃或问题,也不会影响其他请求。父进程只负责接收和分发任务,而实际处理任务的工作交给多个子进程。
- 举例:
- Web 服务器:比如 Apache 使用子进程处理不同的客户端请求,这样即使一个子进程崩溃,其他请求仍然能继续处理。
- 数据库服务:一些数据库管理系统通过子进程处理不同的查询,确保查询之间不会互相干扰。
2. 任务调度与并行处理
- 场景:当某个程序需要同时进行多个独立的计算任务时,创建子进程可以将这些任务分配给不同的进程,使它们并行执行,从而提高执行效率。
- 原因:多核 CPU 环境下,父进程可以通过
fork()
创建多个子进程,这样每个子进程都可以在不同的 CPU 核心上并行执行任务,从而提高计算效率。 - 举例:
- 科学计算:例如,处理大规模数据分析时,可以将数据分成多个块,每个子进程处理一个数据块,最后将结果合并。
- 视频处理:在进行视频转码或图片渲染时,创建多个子进程来并行处理不同帧或不同部分的图像。
3. 资源隔离与安全性
- 场景:一些需要高安全性的应用会将不同的功能模块分离成不同的进程,这样即使某个进程被攻击或崩溃,其他进程的运行不会受到影响。
- 原因:子进程的隔离性使得每个进程拥有自己的内存空间和资源,避免了进程间的干扰。这对于处理敏感信息或者确保系统稳定性和安全性至关重要。
- 举例:
- 浏览器:现代浏览器会为每个标签页、插件或扩展创建不同的进程,这样一个标签页崩溃不会导致整个浏览器崩溃。
- 操作系统安全:如一些操作系统使用子进程来执行高权限操作,避免给系统带来潜在风险。
4. 守护进程(Daemon)
- 场景:守护进程是一种长期在后台运行的进程,通常在系统启动时启动,并在系统关闭时停止。守护进程需要创建一个子进程来执行具体的工作。
- 原因:守护进程通常不与用户直接交互,它们负责监视某些任务或提供某些服务。通过创建子进程,守护进程可以独立处理各种工作,保持系统的高效和稳定。
- 举例:
- 系统守护进程:例如
cron
进程定期运行任务,sshd
进程监听远程连接。 - 文件服务器:一个守护进程可以监听文件的变化并对文件进行同步或备份。
- 系统守护进程:例如
5. 进程控制与协作
- 场景:父进程与子进程之间的协作。父进程创建子进程后,可以与子进程进行通信,通过管道、消息队列等机制协作完成任务。
- 原因:父子进程之间可以通过进程间通信(IPC)进行数据传递,子进程执行任务的结果会影响父进程的执行。父进程和子进程之间的关系通常是控制与执行的关系。
- 举例:
- 编译工具:在某些构建系统(如
make
)中,父进程通过创建子进程来执行不同的编译任务,最后将结果汇总。 - 日志管理:父进程可以创建多个子进程来处理日志文件,每个子进程独立地处理不同的日志任务,最后通过 IPC 汇总结果。
- 编译工具:在某些构建系统(如
6. 多重任务并发处理
- 场景:当一个应用程序需要并发执行多个不同的任务时,可以通过
fork()
创建多个子进程,分配任务给不同的子进程。 - 原因:这种方式适合处理需要大量并行工作的任务,例如并行搜索、并行计算等。
- 举例:
- 多线程下载管理器:如果一个文件很大,可以通过
fork()
创建多个子进程并行下载文件的不同部分,最后合并成一个完整的文件。 - 分布式计算:某些分布式计算任务会将计算工作分配给多个进程来并行处理,提高整体计算速度。
- 多线程下载管理器:如果一个文件很大,可以通过
7.小结
子进程通常用于以下几种情况:
- 进程隔离和独立执行:避免不同任务间的干扰,提高系统稳定性。
- 并行化计算:充分利用多核 CPU 提高计算效率。
- 长期运行的守护进程:独立执行后台任务。
- 提高安全性:隔离不同的任务和数据,防止安全问题扩散。
子进程的优势通常体现在资源隔离和任务并行化上,尤其在需要并发处理多个独立任务或确保系统高可靠性和稳定性时,使用子进程可以大大提高系统性能和安全性。
父进程等待子进程结束后自己再结束
原因分析
当 fork() 创建子进程后,子进程是独立的进程,和父进程共享部分资源,但运行是相互独立的。
如果父进程提前结束,子进程仍然可以继续执行,直到它自己终止或被系统杀死。
如果父进程退出,而子进程仍在运行,那么子进程会成为孤儿进程。
孤儿进程会被 init 进程(PID=1) 领养,init 进程会负责等待它结束,回收它的资源。
但通常我们还是会让父进程等待子进程结束后自己再结束,原因如下:
1. 防止子进程变成僵尸进程(Zombie Process)
- 问题:
- 当子进程结束时,它的退出状态信息会保留在系统的进程表中,直到父进程使用
wait()
或waitpid()
读取该信息。 - 如果父进程没有处理这个信息,子进程的进程表项不会被释放,导致僵尸进程(Zombie Process)。
- 如果系统中存在大量僵尸进程,会占用进程表,可能导致新进程无法创建。
- 当子进程结束时,它的退出状态信息会保留在系统的进程表中,直到父进程使用
2. 确保父进程在子进程完成任务后再退出
- 有时候子进程的任务和父进程相关,比如:
- 子进程负责处理数据,父进程等待结果。
- 子进程执行重要任务,父进程需要等它完成才能继续。
3. 避免创建大量孤儿进程
- 虽然孤儿进程会被
init
进程接管,但不是所有情况下都希望这样:- 如果有多个子进程,可能会导致 进程管理变得混乱。
- 如果子进程运行时间较长,可能会造成 不必要的资源占用。
- 更好的方法:
- 直接让父进程
wait()
等待子进程。 - 或者 让子进程调用
setsid()
使自己变成独立的进程组。
- 直接让父进程
4. 让父进程控制子进程的执行
- 例如:
- 父进程动态分配任务给子进程。
- 父进程需要获取子进程的退出状态来决定下一步操作。
5 最佳实践
- 如果子进程需要完成某些任务,父进程应该
wait()
等待,避免进程管理混乱。 - 如果不关心子进程的状态,但不想产生僵尸进程,可以用
SIGCHLD
信号处理:
这样,子进程退出后,系统会自动回收它们的资源。signal(SIGCHLD, SIG_IGN);
使用wait让父进程等待子进程的示例代码
下面是一个简单的 C 代码示例,演示 Linux 父进程如何等待子进程终止:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork(); // 创建子进程if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) {// 子进程执行的代码printf("Child process (PID: %d) is running...\n", getpid());sleep(2); // 模拟子进程执行任务printf("Child process (PID: %d) is exiting...\n", getpid());exit(0);} else {// 父进程执行的代码printf("Parent process (PID: %d) is waiting for child (PID: %d)...\n", getpid(), pid);int status;waitpid(pid, &status, 0); // 等待子进程结束if (WIFEXITED(status)) {printf("Child process exited with status %d\n", WEXITSTATUS(status));}printf("Parent process is exiting...\n");}return 0;
}
代码说明:
fork()
创建一个子进程,返回两次:- 在父进程中返回子进程的 PID。
- 在子进程中返回 0。
- 子进程执行自己的任务,
sleep(2)
模拟耗时操作,然后退出。 - 父进程调用
waitpid()
等待子进程结束,并获取其退出状态。 WIFEXITED(status)
判断子进程是否正常退出,WEXITSTATUS(status)
获取其退出码。