【linux进程控制(一)】进程创建&退出-->fork&退出码详谈

🎬 个人主页:HABuo

📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、重谈fork

[📖1.1 为什么要有两个返回值](#📖1.1 为什么要有两个返回值)

[📖1.2 两个返回值怎么做到的](#📖1.2 两个返回值怎么做到的)

[📖1.3 同一个id为什么具有两个值](#📖1.3 同一个id为什么具有两个值)

📚二、进程退出

[📖2.1 从main函数return返回](#📖2.1 从main函数return返回)

[📖2.2 使用exit&_exit终止进程](#📖2.2 使用exit&_exit终止进程)

[📖2.3 进程异常终止的场景](#📖2.3 进程异常终止的场景)

📚三、总结


前言

前几篇博客我们认识了进程的一些基础概念,从这篇博客开始我们进入进程控制板块,这个板块包含,进程创建&退出、进程等待、进程程序替换 ,这篇博客我们来了解进程创建&退出

本章重点:

本篇文章我们将重新认识fork函数 ,并着重讲解进程退出时的三种场景,以及常见的退出方法,并对比C库函数exit和系统调用函数_exit的区别与联系,最后我们将使用信号来模拟一些非正常退出的情况


📚一、重谈fork

【Linux进程(一)】进程深入剖析-->进程概念&PCB的底层理解这篇博客中,我们fork了一个子进程,发现了同一段代码if else同时运行的场景,我们当时很震惊,为什么?我当时仅仅是解释了,fork之后有两个执行流。今天呢我们就来详细地拆解一下:

  • fork有两个返回值怎么做到的?
  • 为啥fork的返回值,给父进程返回子进程pid,给子进程返回0?
  • fork之后,父子进程谁先运行?
  • 如何理解同一个id有不同的值?让if else同时执行

这些问题的答案会接下来的内容一一给出

fork()函数信息:

📖1.1 为什么要有两个返回值

我们知道fork有两个执行流,即父子进程,那子进程被创建出来难道就是再走一遍父进程的代码?显然不是,因而fork的作用就是让父子进程执行各自的代码,实现它们各自的功能,如下所示:

cpp 复制代码
int id = fork();
if(id==0)
{
	执行子进程的专有代码
}
else
{
	执行父进程的专有代码
}

我们将代码填充:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("我是一个进程,我的id是: %d,我的父进程id是: %d\n", getpid(), getppid());
    pid_t id = fork();
    if(id > 0){
        while(1){
             printf("我是一个父进程,我的id是: %d\n", getpid());
             sleep(1);
        }
    }
    else if(id == 0){
        while(1){
             printf("我是一个子进程,我的id是: %d\n", getpid());
             sleep(2);
        }
    }
    else
         printf("创建子进程失败\n");
    return 0;
}

因而我们可以回答一个问题就是:为啥fork的返回值,给父进程返回子进程pid,给子进程返回0?

子进程千千万,而父进程却只有一个,也就是说,一个父进程可以有多个子进程,但是一个子进程却只能有一个父进程,所以子进程获取父进程id很简单getppid,但是父进程获取子进程就需要系统告诉我,因而fork的返回值返回给父进程的就是子进程的id

📖1.2 两个返回值怎么做到的

现在我们来回答两个返回值问题,上面我们介绍的是为什么要有两个返回值,现在我们回答怎么做到的,C/C++函数只能有一个返回值,然而这里的fork函数既然也是C函数,为什么会有两个返回值呢?请看以下的解释:

首先,前面进程地址空间中我们知道了,创建一个子进程,它的PCB以及进程地址空间,还有页表都会按照父进程的拷贝一份,且fork之后,父子进程都会执行代码的本质是它们都被内存调度了而当一个函数执行到return时,它的核心工作才算执行完成,于是我们可以想象一下fork函数内部的一些代码信息:

可以发现,在fork函数return之前,就已经创建了子进程,并且将子进程放入调度队列中运行了,所以当子进程在调度队列时,它和父进程就已经分流了而不是真正在fork函数return之后才分流的并且创建完子进程后代码是共享的很明显return也是一句代码,所以父子进程都会执行return语句,fork函数有两个返回值

📖1.3 同一个id为什么具有两个值

这就用到上篇博客所学到的知识了,即写时拷贝,因为进程具有独立性,那么当子进程被创建出来,那么和父进程就属于不同的进程,虽然我的PCB、进程地址空间、页表、映射等等拷贝于你,但是一旦两方任何一方发生改变,那么操作系统就会在内存中新创建一个空间,将改变的值覆盖到这个空间中,然后改变页表的映射关系,即发生写时拷贝

具体请看:

【Linux进程(五)】进程地址空间深入剖析-->虚拟地址、物理地址、逻辑地址的区分

小结一下:

对于fork我们现在知道以下几点即可,其它的在以后的学习过程中慢慢补充:

  • fork之后有两个执行流:父子进程
  • fork返回值:父进程返回的是子进程id,子进程返回的是0,小于0是创建失败
  • 两个返回值的原因:fork函数内部已经存在了两个进程,即最后一个return语句将被执行两次
  • 同一个id不同的值原因:发生了写时拷贝!

📚二、进程退出

首先进程有三个退出场景:

  • 代码执行完,结果正确
  • 代码执行完,结果不正确
  • 代码异常终止了

第一、二种退出场景都是属于正常退出的范围,正常退出又有以下常见的方法:

  • 从main函数返回
  • 调用exit终止进程
  • 调用_exit终止进程

第三种退出场景是异常退出,异常退出往往是信号导致的,我们熟悉的一个信号有:

  • CTRL+c,信号终止进程
  • kill -9

我们着重讲解进程退出的前两种场景即围绕上述的常见方法展开,第三种场景将在以后的博客中详细介绍。

📖2.1 从main函数return返回

我们之前写C/C++代码时总会在写了int main后写return 0,但是程序只能return 0吗?答案是肯定不是!

先给出两个结论:

  • 结论一:
  1. 非main函数执行到return语句时代表此函数执行完毕!

  2. main函数执行到return语句时代表此进程执行完毕!

  • 结论二:
  1. 程序正常执行完毕并且结果****正确时返回0

  2. 程序正常执行完毕但结果不正确****时返回非0

有一个问题浮现在我们眼前,既然结果不正确的返回值是非0,但是非0有很多值:1,2,3,4,5等等,它们分别有什么含义呢?

它可以将错误码转换为错误字符串

cpp 复制代码
int main()    
{    
    int i=0;    
    for(i=0;i<200;i++)                                                                                                                                                  
    {    
        printf("[%d]: %s\n",i,strerror(i));    
    }    
    return 0;    
}

134号错误以后,就是未知错误了

查看最近进程的退出码:

bash 复制代码
使用指令: echo $?
cpp 复制代码
int main()
{
	return 1;
}

首先要知道**$?:永远记录最近一个进程在命令行中执行完毕时对应的退出码**

那1我们知道,为啥第二次是0了,因为第二次的退出码是上次echo进程的退出码!

📖2.2 使用exit&_exit终止进程

exit的参数即为错误码,和main函数的return值是一个意思

exit函数和return的区别:

  • return只有在main中使用时才代表此进程退出

  • exit函数在程序任一地方使用都可以直接退出属于该exit函数的进程,并且返回错误码

系统调用接口_exit():

它和exit一样都是终止进程,并且_exit的参数也代表错误码

那么它们两个有什么区别呢?

我们分别运行以下代码:

cpp 复制代码
代码一:
	printf("我是谁,我在哪?");
	sleep(1); 
	exit(10);
代码二:
	printf("我是谁,我在哪?");
	sleep(1); 
	_exit(10);

代码1:

代码2:

我们知道printf打印的数据如果不使用\n换行的话,数据会被存储到缓冲区里,暂时不会打印出来,然而使用exit函数结束进程后,缓冲区的数据被打印出来了,所以可以得出结论exit函数会帮助我们刷新缓冲区的数据,然而_exit函数不会 。且_exit是系统调用,exit是库函数,那么exit就是_exit的封装,由此我们可以得出缓冲区压根就不在操作系统内,也就是说缓冲区不由操作系统来维护,而是用户级的缓冲区

📖2.3 进程异常终止的场景

前面无论是return或者是exit/_exit都是进程正常退出的情况,还有一种就是进程碰到异常导致的退出,例如下面的情况:

cpp 复制代码
//野指针的问题
int* pa = NULL;
*pa = 10;

//发生除0错误
int a = 0;
int b = 19;
a = b / a;

此时运行程序后,程序会退出这时候再去使用指令:echo $? 就没有意义了!这是因为一般而言,退出码对应的return语句还没执行到就已经崩溃了!


📚三、总结

本篇博客我们重新认识了fork,并且将进程退出的内容也进行了介绍。

小结以下:

fork:

  • fork之后有两个执行流:父子进程
  • fork返回值:父进程返回的是子进程id,子进程返回的是0,小于0是创建失败
  • 两个返回值的原因:fork函数内部已经存在了两个进程,即最后一个return语句将被执行两次
  • 同一个id不同的值原因:发生了写时拷贝!

进程退出:

  • 退出码的作用:0表示正常,!0表示出现异常,特定的数字表示特定的错误
  • return/exit/_exit的区别:return在函数内表示该函数执行结束,main函数内表示该进程执行结束。exit/_exit表示所属的进程执行结束,具体的数字表示退出码就是多少。exit/_exit的区别就是刷不刷新缓冲区!
  • 其余小知识点:$?、strerror。

经历前面几篇抽象的内容之后,是不是发现现在的知识点简直小菜一碟!确实!相对简单点,大家稍加记忆即可!

相关推荐
EndingCoder2 小时前
高级类型:联合类型和类型别名
linux·服务器·前端·ubuntu·typescript
2301_765715142 小时前
Linux中组合使用多个命令的技巧与实现
linux·运维·chrome
十六年开源服务商2 小时前
WordPress运维服务中的内容营销策略
java·运维·spring
Tandy12356_2 小时前
手写TCP/IP协议栈——TCP数据接收
c语言·网络·网络协议·tcp/ip·计算机网络
想唱rap2 小时前
MySQL内置函数
linux·运维·服务器·数据库·c++·mysql
CQ_YM2 小时前
SQLite3 数据库与网页html
c语言·数据库·sqlite·html
玖釉-2 小时前
[Vulkan 学习之路] 10 - 掌握 SPIR-V:编写你的第一个着色器 (Shader Modules)
c++·windows·图形渲染
Jet_582 小时前
Ubuntu 桌面版 Wireshark 抓包权限不足问题解决指南
linux·ubuntu·wireshark
wit_yuan2 小时前
openbmc 支持mctp over pcie(三)(支持作为endpoint)
linux·服务器·嵌入式硬件