当前位置: 首页 > article >正文

【Linux系统】进程的创建与程序计数器(PC指针)




在这里插入图片描述



我们下面文章通过讲解如何使用 fork 函数创建一个进程,同时讲解如何使用及解释使用过程中的一些疑问


一、fork 函数


1、fork 函数的作用和概念

​ 进程创建主要通过 fork 函数实现。调用fork()函数后,操作系统会创建一个新的进程,这个新进程几乎完全复制了调用fork()的进程(父进程)的状态,包括代码、数据段、堆、栈等。同时,子进程拥有自己独立的进程ID(PID),也表示着一个全新进程的产生。

在底层,子进程实际上继承父进程的整个虚拟地址空间,同时在初始阶段,子进程会和父进程共享大部分资源(代码、全局数据(环境变量表)等等),这个共享机制是通过写时拷贝实现的。

(如果需要,可以通过这篇博客,了解虚拟地址空间:【Linux系统】程序虚拟地址空间(重要)-CSDN博客)


2、fork 函数的使用

函数原型:

pid_t fork(void);

头文件:

#include <unistd.h>

返回值:

  • 父进程中:返回子进程的进程ID (PID),即返回值大于零。
  • 子进程中:返回 0。
  • 出错时:返回 -1,并设置全局变量 errno 以指示错误类型


示例代码

以下的示例演示如何使用 fork() 创建子进程:因为 fork 函数的返回值有三种类型,因此下面代码中有三层 if/else 结构。

为了演示,我将父子进程的 if/else 逻辑放到死循环中,同时一个打印语句加一个 sleep(1) 休眠一秒

#include <stdio.h>        
#include <unistd.h>         // 包含 fork() 和 getpid() 等函数的头文件
#include <sys/types.h>      // 包含 pid_t 类型的定义
#include <stdlib.h>         // 包含 exit() 函数的头文件

int main() {

    // 调用 fork() 创建子进程
    pid_t pid = fork();
    
    // (1) 如果 fork() 返回 -1,表示创建子进程失败
    if (pid < 0) 
    {  
        fprintf(stderr, "Fork failed\n");  // 将错误信息输出到标准错误流
        return 1;                          // 返回非零值表示程序异常退出
    } 




    while(1)
    {
		// (2) 如果 fork() 返回 0,表示当前代码在子进程中执行
        if (pid == 0)  
        {  
            printf("我是子进程, PID = %d, PPID = %d\n", getpid(), getppid());
            sleep(1);
            // 输出子进程的 PID 和父进程的 PID
            // getpid() 获取当前进程的 PID, getppid() 获取当前进程的父进程的 PID
        } 
        // (3) 如果 fork() 返回一个正整数,表示当前代码在父进程中执行
        else  
        {
            printf("我是父进程, PID = %d\n", getpid());
            sleep(1);
            // 输出父进程的 PID
        }
    }

    return 0; 
}


结果演示:

如图,在死循环中,父子进程的打印语句轮流打印:

在这里插入图片描述



3、为什么 fork 函数会同时返回两个值??(清晰解释)


疑问点:

正常来说,一个函数一次只能执行一条 return 语句,只要执行了return 语句,该函数就会退出,并将返回值返回给调用这个函数的 “父”函数中,这个过程中,只可能有一个返回值,其中fork 函数调用失败返回 -1 ,这个属于函数调用失败返回错误,这里不讨论这个情况。我们要关注,为什么 fork 函数创建成功后会同时返回两个值,然后匹配进入不同的 if/else 语句中执行不同的打印代码?

这里还有一个疑惑点:返回两个不同的值就算了,你见过同一个代码中的 if else 同时执行的吗???

答疑解惑:

其实,当 fork() 被调用时,操作系统会创建一个新的进程(子进程),这个新进程几乎是父进程的一个副本。 实际上fork() 是在两个不同的进程中返回,又由于父子进程共享同一份代码,父子两个进程根据返回值的不同在代码中匹配进入不同的 if/else


详细解释过程

  1. 父进程调用 fork()
    • 父进程执行到 pid = fork(); 这一行时,操作系统内核接管并创建子进程。
  2. 子进程创建
    • 操作系统内核为子进程分配新的PCB,并复制父进程的状态:这一步可以看作,父进程产生了一个分身,这个分身和父进程几乎一摸一样(子进程几乎继承了父进程的所有资源)。
    • 子进程和父进程共享使用同一份代码
    • 操作系统内核设置子进程的返回值为0,设置父进程的返回值为子进程的PID。
  3. 恢复执行
    • 操作系统内核恢复父进程和子进程的执行,从 fork() 之后的下一条指令开始执行。
    • 因为父子进程共享同一份代码,这里可以看作同一份代码在 fork() 语句之后,就开始用两个人同时使用同一份代码,这两个人拥有的数值不同(不同返回值),在之后的代码中就进入不同的 if/else 逻辑中,执行着不同的代码。
  4. 父进程和子进程的执行
    • 父进程:
      • pid 被设置为子进程的 PID。
      • 执行 else 分支的代码,输出父进程的PID和子进程的PID。
      • 正常退出,返回0。
    • 子进程:
      • pid 被设置为 0。
      • 执行 else if (pid == 0) 分支的代码,输出子进程的PID和父进程的PID。
      • 正常退出,返回0。


清晰的图示及讲解

如下图,红色线指的是父子进程各自的执行流:运行这份代码的过程 流程

在这里插入图片描述

在父进程没有执行 fork 函数之前,父进程独享这份代码,并不断执行 before代码 ,这部分代码是父进程单独执行的。

随后,当父进程调用 fork() 时,操作系统内核接管并开始创建子进程。当子进程一旦被创建出来就会开始运行。(这里其实是子进程进入CPU的运行队列中,被调度到CPU中运行:这里暂时不用理解这个底层原理)

因为之前说过子进程相当于父进程的分身,会和父进程共享大部分资源,而代码一般都会直接共享,子进程也就从 fork 函数之后的语句开始运行了。

而在 fork 函数内部有着两部分代码:1、创建子进程;2、返回语句 return

fork 函数内部,当子进程创建成功后,子进程就已经开始共享父进程的代码了,包括 fork 函数内部的 return 语句!!!

操作系统内核会设置子进程的返回值为 0,设置父进程的返回值为子进程的 PID。

父子进程拿着不同的返回值,执行着“同一句 return” ,返回不同的值,

虽然 fork() 只执行了一次,但它在两个不同的进程中分别返回不同的值。

因此证明了在我们代码中,fork 语句为什么同时返回了两个返回值。




4、为什么子进程执行流会从 fork 之后开始?(底层原理)


理解为什么子进程的执行流会从 fork 之后开始,需要从操作系统的底层机制入手。让我们详细解析这一过程。

以下面这段代码为例讲解:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main() {
    pid_t pid;

    // before 代码:fork之前的代码
    printf("Before fork()\n");

    // 调用 fork() 创建子进程
    pid = fork();

    if (pid < 0) {
        // fork() 失败
        fprintf(stderr, "Fork failed\n");
        return 1;  // 返回 1 表示程序异常退出
    } 
    else if (pid == 0) {
        // 子进程
        printf("子进程: PID = %d, PPID = %d\n", getpid(), getppid());
        return 0;  // 子进程正常退出
    } 
    else {
        // 父进程
        printf("父进程: PID = %d, Child PID = %d\n", getpid(), pid);
        return 0;  // 父进程正常退出
    }
}


  1. 父进程执行 before 代码
    • fork 之前的代码,父进程独享运行:父进程执行 printf("Before fork()\n");,输出 “Before fork()”。
  2. 父进程调用 fork()
    • 父进程执行 pid = fork();,操作系统内核接管并创建子进程:操作系统内核为子进程分配新的PCB,并复制父进程的状态。。
  3. 子进程创建
    • 程序计数器(PC)被设置为 fork() 调用之后的下一条指令的地址,即 if (pid < 0) 这一行。
  4. 返回值的设置
    • 操作系统内核设置父进程的 pid 为子进程的PID。
    • 操作系统内核设置子进程的 pid 为0。
  5. 恢复执行
    • 操作系统内核恢复父进程的执行,从 if (pid < 0) 这一行开始。
    • 操作系统内核将子进程加入到就绪队列中,等待CPU调度。当子进程被调度到CPU上执行时,它也会从 if (pid < 0) 这一行开始。
  6. 父进程和子进程的执行
    • 父进程:
      • pid 被设置为子进程的PID。
      • 执行 else 分支的代码,输出 “Parent process: PID = [父进程的PID], Child PID = [子进程的PID]”。
      • 正常退出,返回0。
    • 子进程:
      • pid 被设置为0。
      • 执行 else if (pid == 0) 分支的代码,输出 “Child process: PID = [子进程的PID], PPID = [父进程的PID]”。
      • 正常退出,返回0。

重点在 程序计数器(PC)的设置

程序计数器(PC)是进程属性之一(可以看作一个变量),作用是存储着该进程下一条指令的地址,目的是告诉进程下一步该执行哪一句指令,进程就会通过程序计数器中记录的指令地址找到并执行这句指令(当然运行一段代码过程中,程序计数器会不断更新下一条指令地址,以推进进程运行)

在创建子进程时,操作系统内核将子进程的程序计数器(PC)设置为 fork() 调用之后的下一条指令的地址。这意味着子进程从 fork() 之后的代码开始执行:即 if (pid < 0) 这一行语句。

这就说明了为什么子进程执行流会从 fork 之后开始



5、fork 常规用法


1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

简单来说:就是父进程用于获取你用户的需求,子进程用来执行这需求,分工不同

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main() {
    pid_t pid;

    // 父进程等待客户端请求
    while (1) {
        printf("Waiting for client request...\n");

        // 调用 fork() 创建子进程
        pid = fork();

        if (pid < 0) {
            // fork() 失败
            fprintf(stderr, "Fork failed\n");
            return 1;
        } 
        else if (pid == 0) {
            // 子进程
            printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid());
            // 子进程处理客户端请求
            handle_client_request();
            return 0;  // 子进程处理完请求后退出
        } 
        else {
            // 父进程
            printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
            // 父进程继续等待新的客户端请求
        }
    }

    return 0;
}

void handle_client_request() {
    // 模拟处理客户端请求
    printf("Handling client request...\n");
    sleep(5);  // 模拟处理时间
}

2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

常见的应用场景包括 shell 命令的执行:

实际上,我们向 shell 命令行窗口中输入命令,其中 shell 会创建子进程,通过 exec 系列函数进行进程替换,来执行用户输入的命令。

这其中涉及到【进程替换】的知识,后面章节会进行讲解



6、fork 调用失败的原因


系统中有太多的进程

实际用户的进程数超过了限制



http://www.kler.cn/news/367007.html

相关文章:

  • 基于JAVASE的题
  • Spring 配置文件动态读取pom.xml中的属性
  • Java-图书管理系统
  • 文本预处理操作简述
  • 【微服务】Java 对接飞书多维表格使用详解
  • C++ (7) 内存管理:掌握魔法能量的流动
  • windows DLL技术-DLL的更新和安全性
  • C++研发笔记8——C语言程序设计初阶学习笔记6
  • 028_Comma_Separated_List_in_Matlab中的逗号分割列表
  • electron 中 app 的 getName、setName 方法
  • react hook应用详解+diff 理解 + 父子组件渲染
  • 【论文阅读】2022 TChecker Precise Static Inter-Procedural Analysis for Detecting
  • Git_GitLab
  • 如何评估Mechanize和Poltergeist爬虫的效率和可靠性?
  • 解决 Spring Boot项目 CPU 尖刺问题
  • Vue学习笔记(二)
  • Docker快速上手教程:MacOS系统【安装/配置/使用/原理】全链路速通
  • avue-crud组件,输入框回车搜索问题
  • Oracle OCP认证考试考点详解082系列04
  • Redis 目录
  • 触想全新一代AIoT工控主板CX-3576上市热销
  • Spring Boot 整合 Kafka 详解
  • springboot-mybatisplus操作集锦(上)
  • 十分钟Linux中的epoll机制
  • 深入理解Linux内核网络(三):内核发送网络包
  • 【读书笔记·VLSI电路设计方法解密】问题25:为什么时钟如此重要