文章目录
- 引言
- fork() 函数概述
- 父子进程
- fork函数
- fork() 的常见问题
- fork() 的优势与限制
- 结论
命为志存。 —— 朱熹
引言
《Linux系统编程篇》——基础篇首页传送门
本节我们正式进入Linux的进程代码编写。
fork()
是 Unix 系统中一个重要的系统调用,用于创建一个新的进程。它是创建进程的核心函数之一,其特性和用法在进程管理中至关重要。本文将带你深入了解 fork()
函数的使用、返回值、常见问题及其在操作系统中的重要性。
fork() 函数概述
在 Linux 中,所有的进程都是通过 fork() 派生而来。当一个进程调用 fork() 时,操作系统会复制当前进程的上下文,为它创建一个几乎完全相同的副本,这个副本被称为子进程,而原始进程被称为父进程。
这也是人们口中的父子进程的概念
父子进程
进程a->fork->进程b
- 我们称作进程a是进程b的父进程,反之进程b则是进程a的子进程
fork函数
我们在命令终端,可以轻易的使用man
手册来看,这个函数的作用返回值等,以及这个函数所需要的头文件是什么。
man 2 fork
fork()
是一个在Unix系统中可用的系统调用函数,用于创建一个新的进程。
fork()
函数的原型如下:
#include <unistd.h>
pid_t fork(void);
fork()
函数在调用时会复制当前进程创建一个新的子进程。子进程将几乎完全复制父进程的状态,包括代码、数据、打开的文件等。父进程和子进程之间的唯一区别是进程 ID(PID)
和返回值。
- 如果
fork()
返回 -1,则表示创建子进程失败。 - 如果
fork()
返回 0,则表示当前代码运行在子进程中。 - 如果
fork()
返回一个正整数,表示当前代码运行在父进程中,并且返回的值是子进程的PID
。
好,接着我们写一段调用fork
函数的一个小demo
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
printf("pid = %d\n",pid);
return 0;
}
编译运行后可以看到我的pid被打印了两次,也就是说我的printf函数被执行了两次。
结合我们提前整理的资料来看,使用fork之后返回的pid,一个是3403,一个是0,那么输出是0的这个应该是子进程打印的,那么另一个则是我的父进程打印的,这一区别使父子进程能够在相同的代码段内执行不同的操作。
例如,父进程可以根据 fork() 的返回值来监控子进程的运行状态,而子进程则可以独立执行任务。
我们升级这个程序继续观察一下。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("********* test log 01 **********\n");
pid = fork();
printf("pid = %d\n",pid);
if(pid == 0){
printf("this is child\n");
while(1){usleep(500);}
}else{
printf("this is father\n");
while(1){usleep(500);}
}
return 0;
}
代码解释:
当 fork()
被调用时,系统会创建一个新进程。
代码中,pid
是 fork()
的返回值,如果 pid
== 0 表示这是子进程,否则就是父进程。
父进程可以通过 wait()
函数等待子进程执行完成。
这次在fork
之前加了打印,可见在调用fork之前,我们的代码还是单进程再跑的。在fork
之后也做了相关的判断,看到这里我想大家应该掌握了进程的奥秘了。
于是我们总结一下,写一个进程小框架,方便日后便捷使用。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
// 创建子进程失败
perror("fork failed");
} else if (pid == 0) {
// 子进程代码
printf("Hello from child process!\n");
} else {
// 父进程代码
printf("Hello from parent process!\n");
printf("Child process ID: %d\n", pid);
}
return 0;
}
到了这里恭喜你,学会了进程,剩下的一些骚操作就是在此基础上不断的扩展延伸,以及迭代。
但是请注意
当一个进程调用fork()创建一个子进程时,子进程会复制父进程的内存空间,包括代码段、数据段、堆和栈等。这意味着在刚刚创建时,父进程和子进程的内存是一样的,但在后续的运行过程中,它们的内存是相互独立的。
现代操作系统中为了提高效率,通常使用写时复制(Copy-On-Write,COW )技术。
写时复制(Copy-On-Write,COW )
这意味着在 fork() 后,父子进程共享相同的内存页,直到其中一个进程试图修改这些页时,操作系统才会真正复制这些页。这样可以显著减少内存的占用,优化性能。
fork() 的常见问题
- 僵尸进程(Zombie Process):如果父进程在子进程结束后未调用 wait() 或 waitpid() 函数读取子进程的状态信息,子进程将成为僵尸进程。这会占用系统资源,导致系统无法创建新的进程。
- 多次 fork():如果不慎多次调用 fork(),可能会产生大量子进程,消耗系统资源,甚至造成“fork 炸弹”(即创建太多进程导致系统资源耗尽,系统崩溃)。
- COW 效率:尽管写时复制提高了效率,但在高负载情况下,COW 的性能可能会受限。
Copy-On-Write(COW)是一种内存管理技术,通常用于优化进程之间的内存共享和复制操作。在 COW 机制中,当多个进程共享同一块内存时,只有在其中一个进程尝试修改这块内存时,系统才会执行复制操作,确保每个进程都能看到自己的独立副本,从而实现了延迟复制的效果。
COW 的效率主要体现在以下几个方面:
- 节省内存开销:COW 允许多个进程共享同一块内存,避免了不必要的内存复制。只有在必要的时候才会进行复制,从而节省内存开销。
- 减少复制时间:由于只有在写入操作时才会执行复制,因此在大多数情况下,COW 可以减少复制所需的时间,提高了操作的效率。
- 提高性能:通过延迟复制操作,COW 可以提高程序的性能,特别是在需要频繁复制内存数据的情况下,避免了不必要的复制开销。
fork() 的优势与限制
- 优势:fork() 是创建子进程的简单方式,适合多任务并行处理的应用场景。写时复制机制降低了资源消耗,提升了系统效率。
- 限制:在资源受限的嵌入式系统或实时系统中,频繁使用 fork() 可能导致性能下降。此外,不恰当的 fork() 使用可能引发僵尸进程或系统资源耗尽等问题。
结论
fork()
是 Linux 系统中关键的系统调用,理解它的行为和特性是掌握 Linux 进程管理的关键一步。掌握 fork()
的用法和注意事项,可以帮助开发者设计出高效、健壮的多进程应用。在实际开发中,fork()
通常与 exec
、wait
等函数配合使用,以满足多任务系统的要求。