关于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-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n