【进程管理】认识系统调用函数fork

news2024/11/16 21:32:44

目录

前言

一.查看进程的pid

二.父子进程

三.查看进程的第二种方式

四.代码创建进程——fork

1.fork的功能

2.fork的返回值

3.fork代码的一般写法

 五.对于fork的理解

1.fork干了些什么?

2.fork为什么给子进程返回0,给父进程返回子进程的pid?

3.fork之后父子进程谁先运行?

4.fork为什么会有两个返回值?

5.一个变量怎么可能会有两个值?


前言

本篇文章会以fork函数为切入点,引入父子进程的概念,并研究fork函数的返回值,带你了解创建进程时的诸多细节。

友情提示:本篇文章的所有操作都是基于Linux操作系统的,想要验证文中的操作请使用LInux环境

一.查看进程的pid

介绍一个LInux命令

ps  -axj

ps查看进程,-axj查看所有进程的详细信息

显示这么一大片,这些都是启动起来的进程。第一行的标签栏有一个pid,这个pid(process id)就是用来标识一个进程的,操作系统会给每一个启动起来的进程一个编号,类似于每个学生有自己的学号。

再来认识一个系统调用函数getpid,大家可以用man手册查一下这个函数的用法

man getpid

这个函数的功能就是返回当前进程的pid,需要包含<unistd> 和 <sys/types.h>这两个头文件。

(友情提示:这是LInux的系统调用函数哈,不是C标准库里的,Windows下的返回pid的函数是什么有兴趣自行查找。)

下面我写了这样一段C语言代码

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h>      
      
int main()      
{      
    while (1)      
    {      
        printf("我是一个进程,我的pid是%d\n", getpid());      
        sleep(1);      
    }      
    return 0;      
}                           

 我们将它运行起来,getpid函数返回的结果是18573

再重新打开一个窗口,查找一下mycode对应的进程

 我们发现mycode进程的pid果然是18573,但下面一行是个什么玩意呢?那个其实是grep指令,在我们用grep,由此可见指令也就是一个可执行程序。

pid就是用来标识一个进程的,我们也可以用这个pid来杀死这个进程

kill -9 18573

kill指令还有很多用法,例如暂停进程 ,后续会讲到。

二.父子进程

首先认识一个系统调用函数getppid,它的作用是返回当前进程的ppid(parent process id)

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
    while (1)    
    {    
        printf("我是一个进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());                                
        sleep(1);    
    }    
    return 0;    
}    

先后两次启动mycode程序,发现pid变了,但是ppid没有变。ppid是什么呢?是父进程的pid。啊?父进程又是个什么玩意儿?且听我慢慢道来。

在LInux中创建进程的方式有两种:

  1. 命令行中直接启动进程--手动启动
  2. 通过代码来创建进程

而启动进程,本质就是创建进程,一般是通过父进程创建的。

诶?不对吧,以前不是说进程是由操作系统创建的吗?操作系统首先要把程序加载到内存中,然后为其创建PCB,这个进程就被操作系统管理起来了。

这两种说法都是对的,创建一个进程就得创建一个PCB,但你别忘了PCB是一个对象,是一个结构体,里面有很多字段要填充的,那操作系统怎么知道要怎么初始化呢?所以一般情况下都是以父进程PCB为模版,很多属性内容直接赋给子进程,所以子进程是由父进程创建的说法是没错的。

所以,进程关系中注定会有一种关系,叫做父子关系

回到上面的现象,父进程的pid一直没变,那么这个父进程到底是谁呢?这个进程就是bash(命令行解释器)。所以,当我们登录LInux系统后,我们在命令行中敲的所有指令,点斜杆运行的所有程序,这些进程的父进程都是bash

三.查看进程的第二种方式

前面我们已经学会了用ps指令来查看进程,下面再扩展一个方法:

先把程序跑起来,记住它的pid,5304

在另一个窗口进入/proc目录,查看里面有些什么

 看到这些蓝色的数字心里大概有了猜想,这些是不是启动起来进程呢?猜对了!!!/proc目录是一个动态的目录,里面是操作系统的内核数据结构信息,也就是内存中的信息,是会实时变动的,它并不像普通目录占用磁盘空间。

我们进入5304这个目录,然后就会看到这些乌泱泱的一大片

主要讲两个东西,一个cwd,一个exe。

exe的文件属性是l(链接文件),这个路径就是这个进程在磁盘上对应的可执行程序啊!进程并没有忘记它是从哪来的,也就是说通过进程的PCB实际上是可以找到在磁盘上找到对应的可执行程序的。如果在这个进程运行时,你把可执行程序删了,它也能检测到。

cwd的文件属性也是l(链接文件),这不就是可执行程序所在的目录吗?这个目录叫做工作目录,也叫当前目录。进程运行时,如果遇到要创建文件的函数,例如fopen,如果你指定了路径,他就在你给的路径下创建,如果你没有指定,那么就在这个工作目录下创建。Linux下的工作目录默认就是可执行程序所在目录,而Windows默认是在源文件所在目录。所以我们在VS上写代码,文件就默认创建在.c或.cpp目录下。

这个工作目录是可以修改的,用到chdir这个系统调用函数即可,传入一个字符串,即你要设定的工作目录。

四.代码创建进程——fork

目前为止,我们通常是用手动的方式来启动一个进程。我们该如何理解启动进程这种行为呢?

启动一个进程,本质就是系统多了一个进程,操作系统要管理的进程就多了一个。

进程=可执行程序+task_struct对象(内核对象),创建一个进程,就是系统中要申请内存,保存当前进程的可执行程序+task_struct对象,并将task_struct对象添加到进程列表中。

接下来介绍如何用fork这个系统调用函数来创建进程。

1.fork的功能

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
    printf("我是一个进程,我的pid是%d\n", getpid());//这个函数只执行了一次    
    
    //创建子进程
    fork();    
    
    printf("我是一个进程,我的pid是%d, 我的父进程pid是%d\n", getpid(), getppid());    
    sleep(1);    
    return 0;                                                                                                       
}    

看执行结果: 

诶?奇了怪了,两条printf语句,怎么打印了三句话。这正是fork造成的

小结:fork会创建一个子进程,只有父进程执行fork之前的代码,fork之后,父子进程都要执行后续代码

emmm,匪夷所思,是不是打破你的认知,先把问题搁一边,接着往下看。

2.fork的返回值

man手册是这样说的:

大概就是说,如果创建子进程失败了就会返回-1,如果创建成功,在父进程中会返回子进程的pid,在子进程中会返回0

…………

什么鬼?一个函数怎么可能有两个返回值?简直是危言耸听!!!话不多说,来验证一下。

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
    printf("我是一个进程,我的pid是%d\n", getpid());//这个函数只执行了一次    
    
    pid_t id = fork();    
    
    printf("我是一个进程,我的pid是%d, 我的父进程pid是%d, fork return id: %d\n", getpid(), getppid(), id);                   
    sleep(1);    
    return 0;    
}    

执行结果如下:

确实是这样的,系统层面的代码和语言层面有很大的差异,请先按捺住疑惑,接着往下看。

3.fork代码的一般写法

先抛出两个问题:我们为什么要创建子进程?我们创建子进程是为了让子进程和我父进程做一样的事吗?

我们之前写的都是单进程代码,但当我们使用fork之后,这就已经是一个多进程代码了。上面的问题换句话说,你为什么要编写多进程代码?

原因就是我们想让子进程协助父进程完成一些工作,这些工作是单进程完成不了的。

例如我们下载视频时,我想让它下载,那么单进程就OK了。但我想一边下载,一边播放,那么就可以让一个进程去下载,一个进程去播放,这样的话父子进程就能完成不同的工作,满足用户的需求。

所以我们创建子进程,就是为了让父子进程执行不一样的代码。

你怎么保证父子进程执行不同的代码呢?可以通过判断fork的返回值来判断当前是父还是子,让后让它他们执行不同的代码片段,通常用if else语句来分流。

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

int main()
{
    printf("我是一个进程,我的pid是%d\n", getpid());//这个函数只执行了一次

    pid_t id = fork();
    if (id == -1)
    {
        return -1;
    }
    else if (id == 0)
    {
        while (1)
        {
            printf("我是子进程,我的pid是%d, ppid是%d, 我正在执行下载任务\n", getpid(), getppid());
            sleep(1);
        }
    }
    else                                                                                                                     
    {
        while (1)
        {
            printf("我是父进程,我的pid是%d, ppid是%d, 我正在执行播放任务\n", getpid(), getppid());
            sleep(1);
       }
    }
    return 0;
}

 五.对于fork的理解

看完fork的用法后,大家可能会存在以下的疑问:

  1. fork干了什么事情?
  2. 为什么fork会有两个返回值?
  3. 为什么fork的两个返回值,会给父进程返回子进程的pid,给子进程返回0?
  4. 如何理解同一个变量,会有不同的值?
  5. fork之后,父子进程谁先运行?

1.fork干了些什么?

fork创建子进程,系统会多一个子进程:

1.以父进程为模版,为子进程创建PCB

2.但是fork创建的子进程,是没有代码和数据的,只能暂时共享父进程的代码和数据,所以fork之后父子进程会执行一样的代码

等一下!子进程会执行fork之后的代码,那fork之前呢?子进程看不到fork之前的代码吗?答案是都能看到。那么为什么子进程不从头开始执行呢?

还记得我们以前学语言的时候吗?我们被老师告知,程序是从上到下按顺序执行的。这是因为当一个进程运行时,CPU内有一个寄存器eip,这个寄存器中保存着当前正在执行的指令的下一条指令(代码就是一条条指令),当执行完一个指令时,它会自动更新到下一个。

因为父进程已经执行fork完毕,eip指向的是fork后续的代码,而eip也会被子进程继承!!!

2.fork为什么给子进程返回0,给父进程返回子进程的pid?

(注意这个问题不是说为什么fork有两个返回值,我们先接受有两个返回值的事实。)

一个父进程可能有多个子进程,而子进程只有一个父进程。如果父进程要管理子进程,就要标识子进程的唯一性,而子进程要访问父进程则不需要

例如一个父亲有三个儿子,父亲喊:”儿子,过来一下!“。三个儿子蒙了,到底是喊的谁呢?所以父亲只能喊名字,”张三,你过来一下!“ 老大张三就知道是在喊他了。但是任意一个儿子要找父亲直接喊”爸爸,你过来一下“。这样父亲就明白是在喊他,儿子不用喊父亲的名字。

3.fork之后父子进程谁先运行?

创建完成子进程,只是一个开始,创建完成子进程之后,系统的其它进程,父进程,和子进程,接下来是要被调度执行的。

当父子进程的PCB都被创建并在运行队列中排队的时候,哪一个进程的PCB先被选择调度,哪个进程就先运行。

换句话说,父子进程哪个先运行我们用户是不确定的,这个是由各自PCB中的调度信息(时间片,优先级)+ 调度算法决定的,简单地说,由操作系统决定

4.fork为什么会有两个返回值?

如果一个函数已经执行到return了,它的核心工作做完了吗?答案是肯定的。

而fork就是一个函数,它执行了以下的工作:

前面说过,父子进程都要执行fork之后的代码。那么return语句是代码吗?肯定是的。而在return之前进程创建进程的工作已经完成了,所以fork会return两次!!!实际上操作系统是通过一些寄存器做到的,一个寄存器接收一个返回值。

5.一个变量怎么可能会有两个值?

在代码中,我们用id这个变量来接受fork的返回值,怎么在子进程中是0,在父进程中又是别的值了呢?

在解释这个问题之前,先做个铺垫。

如果父进程被杀掉,子进程还在吗?子进程被杀掉,父进程还在吗?大家可以通过kill -9指令杀掉进程,然后用ps指令查看,来验证一下。我直接给出结论,进程之间运行的时候,是有独立性的,无论什么关系

进程的独立性,首先表现在有各自的PCB,其次进程之间不会互相影响。

但是前面说过子进程会共享父进程的代码和数据,怎么不会相互影响呢?

第一,代码本身是只读的,不会影响。如果父进程被杀掉,本来要释放代码,但是发现子进程也在用这份代码,所以代码留着,只释放自己的PCB,所以子进程还能运行。又因为父子进程都不会修改代码,所以互不影响。
但是,数据父子是会修改的呀!例如有一个全局变量,父进程里把它从1修改成0,然后子进程里有一段逻辑:如果这个变量是0就直接退出,那么父进程不就影响子进程了吗?

所以,得到的结论就是,代码共享,数据各个进程必须想办法各自私有一份!

那么操作系统是如何保证进程的数据私有呢?理论上来说,在创建子进程时,直接把父进程的数据拷贝一份,让子进程PCB指向新的数据,这样肯定是能行的。但有可能父进程里有100个变量,但子进程只需要修改2个变量的值,剩下的98个变量只需读一下,或者压根用不到,那么直接拷贝岂不是血亏?

所以操作系统采用一种写时拷贝的技术,如果不修改数据我就让父子共享,否则就把要修改的变量给子进程单独拷贝一份。

fork的返回值用变量id接收,那么fork返回值给id的本质就是写入,而id是父进程创建的变量,里面保存的就是数据,所以fork返回的时候发生了写时拷贝,所以同一个变量会有不同的值

可能讲到这里还是不太明白,为什么同一个变量有两个值。写时拷贝就写时拷贝呗,可我用的是同一个变量呀,难道两个进程中的变量id不是同一个吗?


int main()
{
    printf("我是一个进程,我的pid是%d\n", getpid());//这个函数只执行了一次

    pid_t id = fork();
    if (id == -1)
    {
        return -1;
    }
    else if (id == 0)
    {
        while (1)    
        {    
            printf("我是子进程,我的pid是%d, ppid是%d, &id=%p, 我正在执行下载任务\n", getpid(), getppid(), &id);    
            sleep(1);    
        }    
    }    
    else                                                                                                                      
    {    
        while (1)    
        {    
            printf("我是父进程,我的pid是%d, ppid是%d &id=%p, 我正在执行播放任务\n", getpid(), getppid(), &id);    
            sleep(1);    
        }
    }
    return 0;
}

结果证明,id的地址竟然是一样的,真是令人匪夷所思,如果是一样的,怎么可能同一个地址会有不同的值呢?

这个问题暂时无法解答,后面讲到进程地址空间的知识再回过头来解答这个问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1065767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据与Hadoop入门理论

一、大数据的3种数据类型 1、结构化数据 可定义&#xff0c;有类型、格式、结构的强制约束 如&#xff1a;RDBMS&#xff08;关系型数据库管理系统&#xff09; 2、非结构化数据 没有规律没有数据约束可言&#xff0c;很复杂难以解析 如&#xff1a;文本文件&#xff0c;视…

第86步 时间序列建模实战:Transformer回归建模

基于WIN10的64位系统演示 一、写在前面 这一期&#xff0c;我们介绍Transformer回归。 同样&#xff0c;这里使用这个数据&#xff1a; 《PLoS One》2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever with Renal Sy…

Casdoor系统static任意文件读取漏洞

文章目录 Casdoor系统static任意文件读取漏洞复现0x01 前言0x02 漏洞描述0x03 影响平台0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 Casdoor系统static任意文件读取漏洞复现 0x01 前言 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由…

郁金香2021年游戏辅助技术中级班(七)

郁金香2021年游戏辅助技术中级班&#xff08;七&#xff09; 058-C,C写代码HOOK分析封包数据格式A059-C,C写代码HOOK分析封包数据格式B-detours劫持060-C,C写代码HOOK分析封包数据格式C-过滤和格式化061-C,C写代码HOOK分析封包数据格式D-写入配置文件062-C,C写代码HOOK分析封包…

容器运行elasticsearch安装ik分词非root权限安装报错问题

有些应用默认不允许root用户运行&#xff0c;来确保应用的安全性&#xff0c;这也会导致我们使用docker run后一些操作问题&#xff0c;用es安装ik分词器举例&#xff08;es版本8.9.0&#xff0c;analysis-ik版本8.9.0&#xff09; 1. 容器启动elasticsearch 如挂载方式&…

第二证券:A股“业绩底”已现?两大板块被看好

9月&#xff0c;A股商场经历了一段明显的缩量下跌&#xff0c;成交量持续萎缩&#xff0c;增量资金不足&#xff0c;商场“痛感”激烈。跟着国庆十一长假结束&#xff0c;2023年四季度正式敞开&#xff0c;大都分析人士和私募安排都认为&#xff0c;国内经济预期转好将为A股商场…

蓝桥杯每日一题2023.10.7

跑步锻炼 - 蓝桥云课 (lanqiao.cn) 题目描述 题目分析 简单枚举&#xff0c;对于2的情况特判即可 #include<bits/stdc.h> using namespace std; int num, ans, flag; int m[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; bool is_ren(int n) {if((n %…

3d渲染农场全面升级,好用的渲染平台值得了解

什么是渲染农场&#xff1f; 渲染农场是专门从事 3D 渲染的大型机器集合&#xff0c;称为渲染节点&#xff0c;这些机器组合在一起执行一项任务&#xff08;渲染 3D 帧和动画&#xff09;。通过将渲染工作分配给数百台机器&#xff0c;可以显着减少渲染时间&#xff0c;从而使…

STM32F030在使用内部参考电压 (VREFINT)时与STM32G070的区别

背景&#xff1a; 之前使用过STM32G070的内部参考电压来提升ADC采集的准确度&#xff08;STM32使用内部参考电压提高ADC采集准确度&#xff09;&#xff0c;所以本次使用STM32F030的芯片时直接把之前G070的代码拿过来用了&#xff0c;但是出现了问题。 查找资料发现两者不同&am…

go语言判断管道是否关闭的误区

前言 本文是探讨的是"在Go语言中&#xff0c;我们是否可以使用读取管道时的第二个返回值来判断管道是否关闭?" 样例 在Go语言中&#xff0c;我们是否可以使用读取管道时的第二个返回值来判断管道是否关闭? 可以看下面的代码 package mainimport "fmt"…

步进电机S曲线驱动模块

一、电路 带有CAN及485接收&#xff0c;三个光耦接口&#xff0c;TMC2660电机驱动芯片&#xff0c;stm32f103的主控芯片 二、协议 一般来说&#xff0c;板子之间的通信协议格式通常为&#xff1a; 内容 帧头 长度 类型1 类型2 Data 校验 帧尾 字节数 1 1 1 1 N 2 1 帧头为0xB…

二叉树--二叉树最大深度

文章前言&#xff1a;如果有小白同学还是对于二叉树不太清楚&#xff0c;作者推荐&#xff1a;二叉树的初步认识_加瓦不加班的博客-CSDN博客 二叉树最大深度-力扣 104 题 &#xff08;不知道“后序遍历”的小白同学&#xff0c;请先看&#xff1a;二叉树的初步认识_加瓦不加班…

组件的挂载和渲染

React的挂载和渲染 React的生命周期中包括三个主要的阶段&#xff1a;挂载、渲染以及卸载。 很多小伙伴包括我自己可能对挂载和渲染的概念比较模糊&#xff0c;今天这篇文章主要的目的是为了解答我们的这个小疑惑~ 这张图是从其他地方搬运过来的&#xff0c;这张图中描述的主…

直播间自动点赞第一章:MouseEvent 实现根据坐标X,Y自动点击浏览器的效果

最终项目 制作一个自动点赞的浏览器插件&#xff0c;可以根据用户指定一个浏览器区域&#xff0c;进行自动化点击&#xff0c;其中可以设置参数&#xff1a;点击频率、指定区域。 本章节效果 指定了一块区域&#xff0c;进行点击&#xff0c;这边是模拟直播间实现自动化点击 …

【Java 进阶篇】JDBC数据库连接池Druid工具类详解

在Java应用程序中&#xff0c;数据库连接是一种重要的资源&#xff0c;因为每次创建和销毁数据库连接都会产生开销&#xff0c;降低了系统性能。为了高效地管理数据库连接&#xff0c;降低资源消耗&#xff0c;常常使用数据库连接池。Druid是一个功能强大的数据库连接池&#x…

成品短视频App源码:10个最热门的功能模块详解

作为成品短视频App源码领域的专家&#xff0c;我将为您揭开成品短视频App的神秘面纱。在这篇文章中&#xff0c;我将详细介绍这热门应用背后的10个最受欢迎的功能模块。无论您是想开发一款创新的短视频App&#xff0c;还是寻找适合您业务的现成解决方案&#xff0c;本文都会为您…

递归

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析&#xff08;3&#xff09; 目录 &#x1f449;&#x1f3fb;汉诺塔 &#x1f449;&…

C#Winform新建工程

C#Winform新建工程 选择创建新项目 搜索窗体 填写工程名称和位置

第四课 递归、分治

文章目录 第四课 递归、分治lc78.子集--中等题目描述代码展示 lc77.组合--中等题目描述代码展示 lc46.全排列--中等题目描述代码展示 lc47.全排列II--中等题目描述代码展示 lc226.翻转二叉树--简单题目描述代码展示 lc98.验证二叉搜索树--中等题目描述代码展示 lc104.二叉树的最…

八、Thymeleaf链接表达式

链接表达式使用&#xff20;符开头&#xff0c;用于描述一个URL&#xff0c;url可以是相对的&#xff0c;也可以是绝对的。当为相对路径时&#xff0c;此表达式用于在指定的URI前拼接项目的根路径&#xff0c;相当于request.getContextPath()。当为绝对路径时&#xff0c;路径按…