前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux系统编程】五、进程创建 -- fork()

【Linux系统编程】五、进程创建 -- fork()

作者头像
利刃大大
发布2025-02-09 22:08:39
发布2025-02-09 22:08:39
10000
代码可运行
举报
文章被收录于专栏:csdn文章搬运
运行总次数:0
代码可运行

前言

现阶段我们知道进程创建有如下两种方式,其实包括在以后的学习中这两种方式也是最常见的:

  • 命令行启动命令 (程序、指令等)
  • 通过程序自身 fork() 后产生的子进程

Ⅰ. 重温fork函数

一、fork()的概念

​ 在 linuxfork函数 是非常重要的 系统函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。之前我们通过查 man 手册,知道了运用 fork() 函数,并且调用它需要包含 头文件 <unistd.h>

​ 如下是函数的声明和返回值:

代码语言:javascript
代码运行次数:0
复制
#include <unistd.h> 
pid_t fork(void); // 返回值:子进程中返回0,父进程返回子进程id,出错返回-1

​ 但是之前我们一直有个问题没有解决,那么就是**为什么一个函数一个返回两个值?**有了之前的进程概念的知识,这里我们就可以来解释一下这个现象了!

二、如何理解fork()有两个返回值

父进程 fork,子进程是以父进程为模板,简单地说就是子进程的大部分属性和属性值是拷贝父进程的,而小部分是指子进程的调度时间要重置、子进程的 pidppid 以及兄弟的要重置。其中上面的 PCB、地址空间、页表都在内核里由操作系统维护的,这也就意味着我们只需要调用操作系统提供的接口 fork,而具体工作细节由操作系统完成

​ 那其中 为什么 fork() 给父进程返回 子进程的pid,给子进程返回 0 呢???因为我们知道父进程和子进程的关系就是一对多的关系,每个子进程只能有一个孩子,而每个父进程可以有多个子进程,这个 返回值的意义就是为了标识它们的关系

​ 我们还要知道 fork() 函数是在用户空间中被我们调用的,但是其实现是在内核空间中由操作系统实现的

fork() 的实现思路,大致如下

1、给子进程分配新的内存块和内核数据结构(PCB、进程地址空间、页表等,并构建对应的映射关系);

2、将父进程的部分数据结构内容拷贝至父进程;

3、把子进程添加到系统进程列表中;

4、fork 返回,调度器开始调度。

​ 从上图我们知道 既然在 fork 函数 return 之前,就已经有了父子两个进程,父子两个执行流分别执行,所以会给父进程返回子进程的PID,给子进程返回0,失败则返回-1

​ 让我们看看下面的代码程序:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
int main()    
{    
    pid_t pid;    
    printf("Before: pid is %d\n", getpid());    
    if((pid = fork()) == -1) // 判断是否返回的是失败值
    {
        perror("fork()");
        exit(1);
    }
    printf("After:pid is %d, fork return %d\n", getpid(), pid);    
    sleep(1);    
    return 0;    
}    

调用结果:
[liren@VM-8-2-centos process]$ ./mypro 
Before: pid is 20052
After:pid is 20052, fork return 20053
After:pid is 20053, fork return 0

​ 从上述结果可以看到原来只有父进程一个也就是 20052,但是 fork 之后又产生了子进程 20053

​ 🔴 注意fork 之后,谁先执行完全由调度器决定

调度器是CPU中央处理器的管理员,主要负责完成做两件事情:

  1. 选择某些就绪进程来执行
  2. 打断某些执行的进程让它们变为就绪状态。

​ 利用 fork 返回值的这个特性,我们可以用变量 id 接收返回值,根据 fork 返回值不同让父子进程执行不同的代码,这个我们之前也讲过啦,简单过一下就好!

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <unistd.h>
int grobal_val = 100;
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("子进程:pid = %d,ppid = %d | grobal_val = %d, &grobal_val = %p\n",getpid(), getppid(), grobal_val, &grobal_val);
    }
    else if(id > 0)
    {
        printf("父进程:pid = %d,ppid = %d | grobal_val = %d, &grobal_val = %p\n", getpid(), getppid(), grobal_val, &grobal_val);
        sleep(1);
    }
    else 
    {
        printf("fork error\n");
        return 1;
    }
    return 0;
}

调用结果:
[liren@VM-8-2-centos process]$ ./mypro 
父进程:pid = 18199,ppid = 16903 | grobal_val = 100, &grobal_val = 0x60105c
子进程:pid = 18200,ppid = 18199 | grobal_val = 100, &grobal_val = 0x60105c

Ⅱ.fork的常规用法

  • 一个父进程希望复制自己,使得 父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 让一个进程执行一个不同的程序。例如子进程从 fork 返回后,调用 exec 函数。(这个会在进程替换中学习)

Ⅲ. fork调用失败的原因

fork 是操作系统级别的接口,所以失败的原因一定是系统级别的原因。

  • 系统中已经存在太多的进程了
  • 实际用户创建的进程超过了限制

这段代码是测试你的用户能跑好多个进程,但是不建议跑。因为跑了之后就会影响 bash,会导致系统出错!代码如下:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <unistd.h>
int main()
{
    int cnt = 0;
    while(1)
    {
        int ret = fork();
        if(ret < 0)
        {
            printf("fork error!, cnt: %d\n", cnt);
            break;
        }
        else if(ret == 0)
        {
            // 子进程不断循环
            while(1) 
            {
                printf("子进程:pid = %d,ppid = %d\n",getpid(), getppid());
                sleep(1);
            }
                
        }
        // 父进程不断循环不断产生子进程
        cnt++;
    }
    return 0;
}

调用结果:
子进程:pid = 1706,ppid = 27353
子进程:pid = 32351,ppid = 27353
子进程:pid = 32348,ppid = 27353
子进程:pid = 32347,ppid = 27353
子进程:pid = 32331,ppid = 27353
............
^C
[liren@VM-8-2-centos process]$ 

解决方法:

  1. 用命令 kill -9 -1 将进程全部杀死
  2. 重新增加一个用户使用

Ⅳ. 写时拷贝

当父子代码只读时,父子的代码和数据是共享的。但是任意一方试图写入时,便以写时拷贝的方式各自一份副本。

​ 写时拷贝是一种机制或者策略,好比打仗时的敌退我打,敌进我撤,它根据实时情况来完成既定规则。同理写时拷贝是根据父和子谁先写入的实时情况来完成拷贝的,它是一种延时操作的策略。

​ 具体步骤其实我们在讲进程地址空间的时候已经讲过了,细节见下图:

​ 这里要强调的是这里的写时拷贝是针对数据的写时拷贝,这里留一个疑问 ~~ 代码会发生类似的写时拷贝的问题吗?答案是会的,后面我们讲进程程序替换时候会讲到!

为什么存在写时拷贝❓❓❓

  • 写时拷贝是为了保证父子进程的独立性
  • 节省内存和系统资源,提高 fork 的效率,减少 fork 失败的概率

​ 父子进程创建时,所有数据直接各自拷贝一份不行吗 ???很明显,不使用写时拷贝也可以保证父子进程的独立性,为啥还要费劲使用写时拷贝。其根本原因是:

  1. 所有的数据,父进程和子进程并不是都必须写入数据,有可能它们仅仅需要读取,而此时的各自拷贝是没有意义的,而且会浪费内存和系统资源。
  2. fork 时,创建数据结构,如果还要将数据拷贝一份,那么 fork 的效率一定会降低。
  3. fork 本质就是向系统申请更多的内存资源,资源申请多了,fork 有可能就会失败。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Ⅰ. 重温fork函数
    • 一、fork()的概念
    • 二、如何理解fork()有两个返回值
  • Ⅱ.fork的常规用法
  • Ⅲ. fork调用失败的原因
  • Ⅳ. 写时拷贝
    • 为什么存在写时拷贝❓❓❓
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档