Linux/Unix系统调用fork()学习

fork() 是一个用于创建新进程的系统调用。在 Unix 和类 Unix 操作系统中,fork() 会创建一个与当前进程几乎完全相同的新进程。新进程被称为子进程,原进程称为父进程。

1、fork()系统调用的说明

(1)头文件

fork()系统调用声明在<unistd.h>文件中;

(2)函数声明

pid_t fork(void);//pid_t为int类型,进行了重载

(3)函数返回值说明

子进程创建失败,返回值-1;

子进程创建成功:

a)父进程中,此函数返回值为子进程的id;

b)子进程中,此函数返回值为0;

1.1、代码示例

#include <stdio.h>
#include <unistd.h>

int main()
{
        pid_t pid = fork();

        if (pid < 0) {
                printf("fork fail\n");
        } else if (pid == 0) {
                printf("this is a child process pid is %d, parent process pid is %d\n", getpid(), getppid());
        } else {
                printf("this is a parent process with pid %d and child pid %d\n", getpid(), pid);
        }
        return 0;
}

1.2、相关系统调用

pid_t getpid();  // 获取当前进程的 pid 值。
pid_t getppid(); //获取当前进程的父进程 pid 值。

2、fork系统调用涉及的关键技术

2.1、写时复制技术(写时拷贝技术)

写时拷贝(Copy-On-Write, COW)是一种优化技术,用于在进程创建时延迟复制内存页,直到它们被修改,这种技术可以有效地减少内存使用和提高性能。

在使用 fork 系统调用时,操作系统通常会使用写时拷贝来处理父进程和子进程的内存空间。以下是一个简单的示例,展示了如何使用 fork 和写时拷贝的概念。

#include <stdio.h>
#include <unistd.h>

// 全局变量,内存映射在数据段
unsigned int gtestVal = 2024;

int main()
{
        pid_t pid = fork();
        // 局部变量,内存在栈空间
        unsigned int testVal = 1314;

        if (pid < 0) {
                printf("fork fail\n");
        } else if (pid == 0) {
                printf("this is a child process pid is %d, parent process pid is %d\n", getpid(), getppid());
                printf("gtestVal in child process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in child process is %d, addr 0x%x\n", testVal, &testVal);
        } else {
                printf("this is a parent process with pid %d and child pid %d\n", getpid(), pid);
                printf("gtestVal in parent process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in parent process is %d, addr 0x%x\n", testVal, &testVal);
        }
        return 0;
}

执行结果:

this is a parent process with pid 13355 and child pid 13356
gtestVal in parent process is 2024, addr 0xd2d48010
testVal in parent process is 1314, addr 0x692eb8e0
this is a child process pid is 13356, parent process pid is 13355
gtestVal in child process is 2024, addr 0xd2d48010
testVal in child process is 1314, addr 0x692eb8e0

通过上述执行结果可以看出,只创建子进程,父子进程均未执行写操作时,此时子进程是复用父进程的内存空间,这里的内存空间主要是指虚拟内存空间和物理内存空间的复用;

2.1.1、父进程尝试执行写操作

#include <stdio.h>
#include <unistd.h>

// 全局变量,内存映射在数据段
unsigned int gtestVal = 2024;

int main()
{
        pid_t pid = fork();
        // 局部变量,内存在栈空间
        unsigned int testVal = 1314;

        if (pid < 0) {
                printf("fork fail\n");
        } else if (pid == 0) {
                printf("this is a child process pid is %d, parent process pid is %d\n", getpid(), getppid());
                printf("gtestVal in child process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in child process is %d, addr 0x%x\n", testVal, &testVal);
        } else {
                // 父进程执行写操作
                gtestVal = 4202;
                testVal = 4131;
                printf("this is a parent process with pid %d and child pid %d\n", getpid(), pid);
                printf("gtestVal in parent process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in parent process is %d, addr 0x%x\n", testVal, &testVal);
        }
        return 0;
}

执行结果:

this is a parent process with pid 14261 and child pid 14262
gtestVal in parent process is 4202, addr 0xbbe8b010
testVal in parent process is 4131, addr 0xa4e900e0
this is a child process pid is 14262, parent process pid is 14261
gtestVal in child process is 2024, addr 0xbbe8b010
testVal in child process is 1314, addr 0xa4e900e0

可以看到,此时gtestVal和testVal的值,在父子进程中的值不再一样,但是他们的地址还是一样的,这个主要是因为,这里打印的地址实际上是虚拟地址,并非物理地址;也就是说,fork()出来的子进程实际和父进程的虚拟空间地址是一样的,如果未执行写时拷贝操作,那么两者物理空间也是一样的(实际上此时,并没有给子进程分配实际的物理地址);当父子进程中任意一个执行写操作时,就会为对应的进程重新分配对应段的物理地址。

2.1.2、子进程尝试写操作

#include <stdio.h>
#include <unistd.h>

// 全局变量,内存映射在数据段
unsigned int gtestVal = 2024;

int main()
{
        pid_t pid = fork();
        // 局部变量,内存在栈空间
        unsigned int testVal = 1314;

        if (pid < 0) {
                printf("fork fail\n");
        } else if (pid == 0) {
                // 子进程写操作
                gtestVal = 5566;
                testVal = 7788;
                printf("this is a child process pid is %d, parent process pid is %d\n", getpid(), getppid());
                printf("gtestVal in child process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in child process is %d, addr 0x%x\n", testVal, &testVal);
        } else {
                printf("this is a parent process with pid %d and child pid %d\n", getpid(), pid);
                printf("gtestVal in parent process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in parent process is %d, addr 0x%x\n", testVal, &testVal);
        }
        return 0;
}

执行结果:

this is a parent process with pid 14970 and child pid 14971
gtestVal in parent process is 2024, addr 0x8b192010
testVal in parent process is 1314, addr 0xe32fa8a0
this is a child process pid is 14971, parent process pid is 14970
gtestVal in child process is 5566, addr 0x8b192010
testVal in child process is 7788, addr 0xe32fa8a0

2.1.3、父进程调用wait()

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

// 全局变量,内存映射在数据段
unsigned int gtestVal = 2024;
int wait_status = 0;

int main()
{
        pid_t pid = fork();
        // 局部变量,内存在栈空间
        unsigned int testVal = 1314;

        if (pid < 0) {
                printf("fork fail\n");
        } else if (pid == 0) {
                // 子进程写操作
                gtestVal = 5566;
                testVal = 7788;
                printf("this is a child process pid is %d, parent process pid is %d\n", getpid(), getppid());
                printf("gtestVal in child process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in child process is %d, addr 0x%x\n", testVal, &testVal);
        } else {
                wait(&wait_status);
                printf("this is a parent process with pid %d and child pid %d\n", getpid(), pid);
                printf("gtestVal in parent process is %d, addr 0x%x\n", gtestVal, &gtestVal);
                printf("testVal in parent process is %d, addr 0x%x\n", testVal, &testVal);
        }
        return 0;
}

执行结果:

this is a child process pid is 19108, parent process pid is 19107
gtestVal in child process is 5566, addr 0xc2f70010
testVal in child process is 7788, addr 0xbd4afc60
this is a parent process with pid 19107 and child pid 19108
gtestVal in parent process is 2024, addr 0xc2f70010
testVal in parent process is 1314, addr 0xbd4afc60

2.1.4、写时复制技术总结

(1)当fork()出来的父子进程,均为执行写操作时,此时父子进程内存空间大致如下:

(2)当fork()出来的父子进程,任意一个执行执行写操作时,此时父子进程内存空间大致如下:

子进程是父进程的副本,拥有相同的代码段、数据段和堆栈段,但它们有各自的进程ID和内存空间。

2.2、进程控制块(PCB)

fork()调用成功时,操作系统会为新创建的子进程分配一个新的 PCB。

2.2.1、什么是PCB

进程控制块(Process Control Block, PCB) 是操作系统中用于管理进程的重要数据结构。它包含了与进程相关的所有信息,操作系统通过 PCB 来跟踪和控制进程的状态。

PCB的主要功能:

进程管理:操作系统通过 PCB 来管理进程的创建、调度和终止。
上下文切换:在进行上下文切换时,操作系统会保存当前进程的 PCB 信息,并加载下一个进程的 PCB 信息,以恢复其执行状态。
资源分配:通过 PCB,操作系统能够跟踪每个进程的资源使用情况,确保资源的合理分配。

2.2.2、PCB中包含的关键字段

(1)进程标识符(PID):
唯一标识一个进程的编号。

(2)进程状态:
表示进程当前的状态,如就绪、运行、等待、终止等。

(3)程序计数器(PC):
指向进程下一条将要执行的指令的地址。

(4)寄存器:
包含进程在执行时的寄存器状态,包括通用寄存器、堆栈指针、基址寄存器等。

(5)内存管理信息:
包括进程的页表、段表、内存限制等信息,用于管理进程的虚拟内存。

(6)调度信息:
包含调度优先级、调度队列指针等信息,用于进程调度。

(7)I/O状态信息:
记录进程所使用的I/O设备、打开的文件描述符等信息。

(8)父进程和子进程信息:
包含父进程的 PID 和子进程的列表,用于维护进程之间的关系。

(9)信号处理信息:
包含进程的信号处理器和信号屏蔽字的信息。

(10)资源使用信息:
记录进程使用的系统资源,如 CPU 时间、内存使用量等。

2.3、进程间通信(IPC)

父进程和子进程之间可能需要进行通信,常用的 IPC 技术包括管道、消息队列、共享内存等。

关于进程间通信,往上学习资料比较多,不再单独赘述,有兴趣的可以自行学习。

2.4、调度算法

在使用 fork() 创建新进程后,操作系统需要使用调度算法来管理父进程和子进程的执行;以下是一些常见的调度算法,它们可能会在 fork() 后的进程调度中被使用:

(1)先来先服务(FCFS, First-Come, First-Served):
按照进程到达就绪队列的顺序进行调度。第一个到达的进程会被优先执行,直到完成。

(2)短作业优先(SJF, Shortest Job First):
优先调度预计执行时间最短的进程。虽然可以减少平均等待时间,但可能导致长作业的“饥饿”现象。

(3)时间片轮转(RR, Round Robin):
每个进程被分配一个固定的时间片,进程在时间片用完后被挂起,调度器将 CPU 分配给下一个进程。这种方法适合时间共享系统。

(4)优先级调度(Priority Scheduling):
每个进程被分配一个优先级,调度器优先执行优先级高的进程。可以是静态优先级或动态优先级。

(5)多级队列调度(Multilevel Queue Scheduling):
将进程分为多个队列,每个队列使用不同的调度算法。不同类型的进程(如交互式和批处理)可以被分配到不同的队列。

(6)多级反馈队列(Multilevel Feedback Queue):
结合了多级队列和时间片轮转的特点,允许进程在不同的队列之间移动,以适应其行为和需求。

(7)完全公平调度(CFS, Completely Fair Scheduler):
旨在为每个进程提供公平的 CPU 时间,基于进程的虚拟运行时间进行调度,确保每个进程都能获得相应的 CPU 时间。

注:关于调度算法(策略)主要是操作系统内部的行为,本文目前也只是简单知道有哪些调度算法,关于每一种调度算法的深层原理掌握不深,不再做过多赘述。

2.5、fork()使用中的注意事项

fork使用过程中,要注意不要出现僵尸进程或者孤儿进程;

2.5.1、僵尸进程

僵尸进程(Zombie Process) 是指已经完成执行但仍然在进程表中保留其信息的进程。它的状态是“终止”,但由于其父进程尚未调用 wait() 或 waitpid() 来读取其退出状态,因此它仍然占用系统资源。

僵尸进程的特点:
(1)状态:
僵尸进程的状态为“Z”,表示它已经终止,但仍然存在于进程表中。

(2)资源占用:
僵尸进程不再占用 CPU 时间和内存,但仍然占用进程表项和一些系统资源。

(3)父进程:
僵尸进程的存在是因为其父进程没有及时处理其退出状态。父进程可以通过调用 wait() 或 waitpid() 来获取子进程的退出状态,从而清除僵尸进程。

(4)清理:
一旦父进程处理了子进程的退出状态,僵尸进程将被从进程表中移除,释放相关资源。

注:父进程应及时调用 wait() 或 waitpid() 来处理子进程的退出状态,避免产生僵尸进程。

2.5.2、孤儿进程

孤儿进程(Orphan Process)是指在父进程终止后仍然存在的进程,操作系统通过将其父进程更改为 init 进程来管理这些孤儿进程,确保它们不会造成资源浪费。

避免出现孤儿进程:在编写程序时,可以通过确保父进程在子进程完成之前不会意外终止,或者使用适当的进程管理策略来避免孤儿进程的产生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只特立独行的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值