关于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

相关文章

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Linux下MySQL数据库定时备份脚本与Crontab配置教学

《Linux下MySQL数据库定时备份脚本与Crontab配置教学》在生产环境中,数据库是核心资产之一,定期备份数据库可以有效防止意外数据丢失,本文将分享一份MySQL定时备份脚本,并讲解如何通过cr... 目录备份脚本详解脚本功能说明授权与可执行权限使用 Crontab 定时执行编辑 Crontab添加定

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

linux系统上安装JDK8全过程

《linux系统上安装JDK8全过程》文章介绍安装JDK的必要性及Linux下JDK8的安装步骤,包括卸载旧版本、下载解压、配置环境变量等,强调开发需JDK,运行可选JRE,现JDK已集成JRE... 目录为什么要安装jdk?1.查看linux系统是否有自带的jdk:2.下载jdk压缩包2.解压3.配置环境

Linux搭建ftp服务器的步骤

《Linux搭建ftp服务器的步骤》本文给大家分享Linux搭建ftp服务器的步骤,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录ftp搭建1:下载vsftpd工具2:下载客户端工具3:进入配置文件目录vsftpd.conf配置文件4:

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

Spring创建Bean的八种主要方式详解

《Spring创建Bean的八种主要方式详解》Spring(尤其是SpringBoot)提供了多种方式来让容器创建和管理Bean,@Component、@Configuration+@Bean、@En... 目录引言一、Spring 创建 Bean 的 8 种主要方式1. @Component 及其衍生注解

Linux实现查看某一端口是否开放

《Linux实现查看某一端口是否开放》文章介绍了三种检查端口6379是否开放的方法:通过lsof查看进程占用,用netstat区分TCP/UDP监听状态,以及用telnet测试远程连接可达性... 目录1、使用lsof 命令来查看端口是否开放2、使用netstat 命令来查看端口是否开放3、使用telnet