【Linux系统】进程的影分身之术:fork系统调用与getpid的探秘

Linux系列


文章目录


前言

在上篇文章中我们介绍了Linux进程的概念及操作系统对进程的管理,并且了解了进程标识符,PID和PPID的基本概念,接下来这篇文章将帮助我们更进一步的理解操作系统进程管理和多进程的概念。


一、基础知识

PID是操作系统分配给进程的唯一标识符,类似于我们的身份证。

PPID是进程所对应的父进程的标识符,类似于它父亲的身份证。

ps ajx查看进程的详细信息,使用这个指令可以查看进程的标识符(PID、PPID).

二、系统调用接口getpid、getppid

这两个系统调用接口我们可以通过man手册进行查找:

我们可以从man手册中得到以下信息:

  • 两个函数都在#include<unistd.h>头文件中,并且返回值都是pid_t(本质为有符号整数类型,经typedef重定义了)。.

  • getpid()函数它返回调用进程的PID.

  • getppid()函数返回调用进程的父进程的PID.

接下来我们使用下面代码进行测试:

c 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   while(1)
  6   {
  7     pid_t a=getpid();
  8     pid_t parent=getppid();
  9     printf("当前进程的PID是:%d\n",a);
 10     printf("当前进程父进程的PID是:%d\n",parent);                                                                                 
 11     sleep(1);
 12   }
 13   return 0;
 14 }

我们让程序一直执行并调用上面两个函数获取当前进程的PID和PPID,并且休眠1秒再次执行。

可以看到程序确实打印出来了结果,接下来我们使用指令进行验证:

ps ajx | head -1&&ps ajx | grep test

都学到这啦,这句指令相信大家一定可以看懂

图中可以看到指令的执行结果和程序的执行结果相同。

那么这个父进程又是谁呢?我们通过指令搜索它,看看它的庐山真面目:
ps ajx | head -1&&ps ajx | grep 9265

通过搜索父进程的PID找到它。

可以看到我们通过PID搜索出来的进程,竟然是我们的bash命令行,这是命令行解释器(bash命令行)在我们让它执行./test时,它就生成了一个子进程,让子进程去指行./test命令,而自己继续去当命令行解释器,等待我们的下一个指令,这样就算它创建的子进程在执行指令时崩溃了也不会对它造成影响,可谓是独善其身。其实在我们进入xshell时操作系统就会给我们创建一个bash进程,而我们的所有指令都是由bash进程的子进程执行的,而进程之间又是独立的,所以这样就将bash进程很好的保护起来了。

三、frok()创建子进程

接下来我们学习如何创建我们自己的子进程,首先我们先来了解一下这个函数。

我们可以从man手册中得到以下信息:

  • fork() 函数是包含在#include<unistd.h>头文件中的,返回值是pid_t类型的数据。
  • 如果成功创建子进程,则给父进程返回子进程的PID,给子进程返回0
  • 如果创建失败则返回-1

既然你说子进程创建成功的时候会给父子进程返回不同的值,那么我们就来测试一下:

c 复制代码
  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 int main()  
  4 {  
  5   pid_t num=fork();  
  6   if(num==0)  
  7   {  
  8     printf("这是子进程\n");  
  9   }  
 10   else if(num>0)  
 11   {  
 12     printf("这是父进程\n");                                                                                                       
 13   }  
 14   else{  
 15     printf("创建失败\n");  
 16   }                                                                                                                           
 17   return 0;                                                                                                                   
 18 }         

从程序执行结果可以看出,确实进入了不同的if语句。

看到这里你可能会产生如下几个问题:

  1. 为什么创建成功后fork()要给子进程返回0,给父进程返回自己子进程的PID?
  2. 一个函数是如何做到返回两次的?我们该如何理解?
  3. 一个变量怎么会有不同的内容?如何理解?
  4. fork()究竟在干什么?又干了什么?

接下来我们就通过解答这四个问题,深入认识一下fork()函数。

首先来看一下下面这段代码:

c 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   printf("Hello Linux\n");
  6   fork();
  7   printf("Hello fork\n");                                                                                                         
  8   return 0;                                                                                                             
  9 }                                                                                                                       
~                                                

从程序运行结果我们可以看出,调用fork()之前打印函数执行了一次,调用fork()之后打印函数被执行了两次,有了上面这些铺垫,我们就可以来回答上面四个问题了。

问题一:

首先大家要知道我们在调用fork()创建子进程的目的是什么?为了让父子进程执行不同的代码!!那么如何让他们执行对应的代码呢,或者说如何区分父子进程呢?返回不同的返回值,这样我们就可以使用不同的返回值进行分流操作(往往使用if语句的不同判断条件进行分流操做),那么问题又来了,给子进程返回0可以理解,为什么要给父进程返回子进程的PID呢?一个子进程只能有一个父进程,但是一个父进程可以有多个子进程,为了将这些子进程区分开,必须使用一种方法将他们表示出来,而子进程的PID就是识别他们的唯一标识。

问题二:

对于fork()函数来说,它可以给我们创建一个子进程,那么当fork()函数执行到return语句之前,也就是说fork()函数的主逻辑已经执行完了,那么子进程一定也被创建出来了,而子进程被创建出来后,后面的代码又是父子进程共享的,所以它可以有两个返回值。

问题三:
要解决这里的问题,我们需要知道fork()究竟干了什么,建议大家先看问题四,等我将问题四介绍到一定程度,我们自然理解了。

从上面两处代码以及问题四,我们都可以体会到,在fork()函数执行后子进程和父进程公用一块代码,所以:

c 复制代码
  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 int main()  
  4 {  
  5   pid_t num=fork();  
  6   if(num==0)  
  7   {  
  8     printf("这是子进程\n");  
  9   }  
 10   else if(num>0)  
 11   {  
 12     printf("这是父进程\n");                                                                                                       
 13   }  
 14   else{  
 15     printf("创建失败\n");  
 16   }                                                                                                                           
 17   return 0;                                                                                                                   
 18 }         

fork()执行结束后,对num来说它属于父进程的数据,此时pid_t num=fork()子进程对父进程的数据进行修改,就会发生写时拷贝,开辟空间用来存子进程修改后的num值。

至于一个变量名为什么可以表示两个不同的值,在这里是解释不清的,等到进程地址空间时,我们在来介绍。

问题四:

在进程概念篇我们介绍了:进程=内核PCB结构体对象+代码和数据

同样的这里创建出的子进程,也要满足上面的要求,那么他是如何来完成的呢?

在调用fork()函数进行分流时,操作系统根据当前进程的PCB结构体创建一个子进程的PCB结构体对象,当然会对新创建的属性进行修改。这时子进程的PCB结构体对象有了,那代码和数据呢?对于代码我们可以体会到,一个运行中的程序,它的代码是无法被修改的(Windows下在使用VS时,会深刻感受到),所以父子进程可以共用同一块代码(if语句分流就可以执行不同的代码块),但是对于数据,如:返回值、变量值,都是可以修改的,这也就意味着,我们不可以让父子进程共用,同一块数据。难道我们要将数据也拷贝一份吗?内存资源是非常有限的,如果我们将代码拷贝一份,而子进程仅仅是执行了打印、和循环等行为,并没有对数据做出修改,那这块空间不是浪费了吗,所以这里使用的是:如果子进程没有对父进程的数据修改就不给他开辟空间,如果修改了,就只对他修改的数据,开辟空间存放修改后的值,这种方式我们称为--------------**写时拷贝。**现在我们再来理解问题三,就很清楚了。

相关推荐
小羊在奋斗3 小时前
【Linux网络】NAT技术、DNS系统、五种IO模型
linux·网络·智能路由器
jiarg4 小时前
linux 内网下载 yum 依赖问题
linux·运维·服务器
yi个名字5 小时前
Linux第一课
linux·运维·服务器
Kurbaneli5 小时前
深入理解 C 语言函数的定义
linux·c语言·ubuntu
Archer1945 小时前
C语言——链表
c语言·开发语言·链表
夜晚中的人海5 小时前
【C语言】------ 实现扫雷游戏
android·c语言·游戏
菜鸟xy..5 小时前
linux 基本命令教程,巡查脚本,kali镜像
linux·运维·服务器·kali镜像·巡查脚本·nmtui
暴躁的小胡!!!5 小时前
Linux权限维持之协议后门(七)
linux·运维·服务器·网络·安全
面会菜.5 小时前
C语言(队列)
c语言·开发语言