关于Linux下的进程创建与终止(进程篇 - 涉及写时拷贝,fork函数)

2024-04-07 13:12

本文主要是介绍关于Linux下的进程创建与终止(进程篇 - 涉及写时拷贝,fork函数),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

创建进程

写时拷贝

fork函数

进程终止

进程终止时,操作系统都做了什么?

进程终止的常见方式有哪些?

如何使用代码终止掉一个进程?


创建进程

写时拷贝

在了解下面的内容之前,我们需要先聊一聊写时拷贝这一概念。

什么是写时拷贝呢?

        通常,父进程与子进程的代码是共享的,父进程与子进程不再进行写入时,数据也是共享的,当任意一方试图写入的时候,便会以写时拷贝的方式各自生成一份副本,这项技术被称为写时拷贝。

示例:

为什么会存在写实拷贝这一项技术呢?

        一般而言,创建子进程,子进程是必须要独立出去的,因为进程是具有独立性的,理论上,子进程也必须要有自己的代码以及数据。

        可是,fork创建子进程,我们并没有加载的过程,这也就意味着,子进程并没有自己的代码与数据! 所以,子进程必须 “使用” 父进程的代码与数据。

        代码:都是不可以被写的,进程只有读取的权限,所以说父子进程共享代码,没有问题。但是

        数据:是可以被父进程或者子进程更改的,所以从这一方面来说,必须分离。

对于数据来说:

        创建进程的时候,就直接进行拷贝分离,吗???可是拷贝子进程根本不会用到的空间,或者拷贝到子进程只会读取但是不会写入的空间,无疑增大了内存的负担。那么什么样的数据子进程会进行写入呢???系统根本无法提前预知哪些数据会被写入。

        所以,系统选择了写实拷贝的技术,来实现父进程与子进程的数据分离,完成了进程独立性的技术保障。子进程或者父进程需要进行写入的时候,系统再进行分配空间,必要的时候拷贝原数据。这是高效使用内存的一种表现。

也可以参考程序地址空间这一概念

fork函数

linux fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

注:fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

示例1:

示例2:

fork函数用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
  • 一个进程要执行一个不同的程序

fork函数调用失败原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

fork的两个返回值是什么?

给父进程返回子进程的pid,给子进程返回0

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

因为在实现fork这个函数的时候,在这个函数内部,return返回之前,fork函数就已经开始生效了,这也就意味着return会被执行两次,一次是原本的父进程,一次是fork在函数创建子进程的返回值。

示例:在实现fork函数的内部,就已经是两个执行流了

一个变量怎么可能同时保存不同的值?

pid_t id = fork();

因为return会被执行两次。
return的本质,不就是对id进行写入吗!
此时发生了写时拷贝!,所以父子进程各自其实在物理内存中,有属于自己的变量空间!只不过在用户层用同一个变量(虚拟地址!)来标识了。也可以参考程序地址空间这一概念

问题:fork之后,是调用fork函数这一行之后的代码开始与父进程共享?还是父进程所有的代码都与子进程共享??

注:pc:程序计数器(当前正在执行代码的下一行代码的地址)

示例:

  • 1.我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址
  • 2.因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始哦!),就要要求CPU必须随时记录下,当前进程执行的位置,所以,CPU内有对应的寄存器数据这就是进程的上下文数据),用来记录当前进程的执行位置!
  • 3.寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的!
  • 创建的时候,要不要给子进程  ---  进程的上下文数据?  答案是需要的。
    所以,虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码!!
     


进程终止

进程终止时,操作系统都做了什么?

谈到进程终止,首先我们需要明白,创建进程时,操作系统都做了些什么?

        创建进程 ---> 操作系统要管理进程 ---> 创建内核数据结构test_struct ---> 创建对应的地址空间mm_struct ---> 创建页表,构建映射关系 ---> 在一定程度上,将该进程对应的代码和数据加载至内存

        所以,进程终止,就是操作系统释放进程申请的相关数据结构和对应的数据与代码(本质上就是释放内存资源,也可能是CPU或者是磁盘等等,但是只会占很少的一部分,更多的还是内存)。

进程终止的常见方式有哪些?

情况示例:

a.代码跑完,结果正确

b.代码跑完,结果不正确

c.代码没有跑完,程序崩溃

老样子,谈上述前两种情况之前,先来聊一聊main函数的返回值。

        好像从我们开始学习写代码的时候,老师就告诉我们,main函数的最后一行要写 return 0;可是为什么?mian函数返回的意义是什么?return 0的含义是什么? return 其他的值可不可以?

        答案是这样的,main函数返回的意义是返回给上一级进程,用来评判该进程的执行结果;return 0代表的是进程的退出码;可以return其他值,并不总是0;这里0标识 sucesss,非0标识运行结果不正确。

        非0值有很多,不同的非0值就可以标识不同的错误原因,这样在程序结束之后,当结果不正确时,方便我们快速定位原因。

示例:Linux下可以通过strerror函数来获取系统的错误信息。

strerror#include<string>char* strerror(int errnum);

示例1: 

Linux下的运行结果:

另外,Linux下,我们也可以通过指令,来获取进程的退出码

echo $? : 获取最近一个进程执行完毕的退出码

示例2:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>int sum(int top)
{int s = 0;int i = 0;for(i = 1; i <= top; i++){s += i;}return s;
}int main()
{int ret = 0;int res = sum(100);if(res != 5050){//代码运行结果不正确ret = 1;}return ret;
}

运行结果:

当我们故意调整一下逻辑:

int sum(int top)
{int s = 0;int i = 0;for(i = 1; i < top; i++){s += i;}return s;
}

运行结果:

        因为函数是我们自己实现的,所以我们可以对返回值做判断,来确定程序运行的结果是正确的还是不正确的,进而设置main函数的返回值

示例3:

示例4:这里使用kill指令发送9号信号杀掉11111进程,但是系统此时是没有11111进程的

按道理来说这里的错误码应该是3啊。怎么会是1呢??? --- 因为这里的退出码叫自定义退出码,是可以自定义设置的

所以:我们自己可以使用这些退出码和含义,但是,如果你想自己定义,也可以自己设计一套退出方案!

关于情况c.代码没有跑完,程序崩溃

示例:这是经典的野指针错误

int main()
{printf("hello world\n");printf("hello world\n");printf("hello world\n");int *p = NULL;*p = 12345; // 野指针printf("hello world\n");printf("hello world\n");return 0;
}

输出结果:段错误

程序崩溃的时候,退出码无意义。一般而言退出码对应的return语句,没有被执行,即使被执行,我们也不会关心,因为我们更想知道,程序为什么会崩溃?
 

如何使用代码终止掉一个进程?

  • 1. return语句,就是终止进程的 !
  • 2. exit函数在代码的任何地方调用,都表示直接终止进程
     

注意: main函数内的return是终止进程,main函数调用其他函数,其他函数内部的return不是终止进程,而是return返回

        main函数内的 exit函数 是终止进程,main函数调用其他函数,其他函数内部的 exit函数 依旧会直接终止进程

示例1: 

int main()
{printf("hello world\n");printf("hello world\n");printf("hello world\n");return 1;printf("hello world\n");printf("hello world\n");return 0;
}

输出:

示例2:

int main()
{printf("hello world\n");printf("hello world\n");printf("hello world\n");exit(111);printf("hello world\n");printf("hello world\n");return 0;
}

输出:

示例3:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>int sum(int top)
{int s = 0;int i = 0;for(i = 1; i < top; i++){s += i;}exit(222);return s;
}int main()
{printf("hello world\n");printf("hello world\n");printf("hello world\n");exit(111);printf("hello world\n");printf("hello world\n");return 0;
}

输出:

深入了解

_exit函数 :Linux下的系统接口

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然 status int ,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 $? 发现返回值是255

exit函数 :c语言库函数

NAMEexit - cause normal process terminationSYNOPSIS#include <stdlib.h>void exit(int status);
exit 最后也会调用 exit, 但在调用 exit 之前,还做了其他工作:
  • 1. 执行用户通过 atexit或on_exit定义的清理函数。
  • 2. 关闭所有打开的流,所有的缓存数据均被写入
  • 3. 调用_exit

二者区别:

c语言库–会把缓冲区的内容打印在显示器

Linux系统接口–不会把缓冲区内容打印在显示器

return 退出
return 是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。

这篇关于关于Linux下的进程创建与终止(进程篇 - 涉及写时拷贝,fork函数)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/882692

相关文章

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

Linux ls命令操作详解

《Linuxls命令操作详解》通过ls命令,我们可以查看指定目录下的文件和子目录,并结合不同的选项获取详细的文件信息,如权限、大小、修改时间等,:本文主要介绍Linuxls命令详解,需要的朋友可... 目录1. 命令简介2. 命令的基本语法和用法2.1 语法格式2.2 使用示例2.2.1 列出当前目录下的文

MySQL中FIND_IN_SET函数与INSTR函数用法解析

《MySQL中FIND_IN_SET函数与INSTR函数用法解析》:本文主要介绍MySQL中FIND_IN_SET函数与INSTR函数用法解析,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一... 目录一、功能定义与语法1、FIND_IN_SET函数2、INSTR函数二、本质区别对比三、实际场景案例分

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1