• 进程环境+进程终止


    进程环境

    程序和进程的区别

    程序

    存放在硬盘上的、静态的机器指令文件-------静态就是没有运行的意思

    进程

    将硬盘上的程序代码拷贝到内存中,正在被cpu执行的程序

    运行的程序:cpu从第一条指令开始,一条一条的执行程序中的所有指令代码

    进程所需的运行环境

    启动代码、环境变量、c程序的内存空间布局、库等

    1. 启动代码

      就是启动程序的代码,其实所有高级语言编写的程序,都有启动代码

      c程序都是从main函数开始运行的,其实main也是需要被C程序的启动代码调用的

    2. 环境变量

      进程在运行的过程中需要用到环境变量,而环境变量存放在了环境表中,也就是环境变量表

    3. c程序的内存空间结构

      • c程序运行时,是运行在内存上的

      • 需要在内存上开辟出一块空间给c程序,然后将C代码会被从硬盘拷贝到内存空间上运行

      • 这段空间必须布局为c程序运行所需的空间结构,c程序才能运行

      所以,c程序的内存空间结构也是必须要的“进程环境”

      比如程序在调用函数时需要用到“栈”这个东西,那么就必须在内存空间中构建出“栈”,也就是说以栈的形式去管理这块内存空间

      内存空间是需要布局的,比如那些内存是代码区,哪些内存是数据区

    4. 库是也是非常重要的进程环境

      c程序(进程)运行时,都是需要库的支持的

      没有库的支持,程序基本做不了太过复杂的事

    理解进程环境后回答如下问题

    与进程环境相关的API

    • main函数是被谁调用的
    • main函数的返回值返回给了谁
    • main函数的参数有什么用
    • 什么是进程的环境变量表
    • 什么是程序的内存空间,程序的内存空间为什么要进行结构的布局

    启动代码

    所有高级语言的程序,都有自己的启动代码

    C程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个C程序都已运行

                              .......
    						     |
    							 |
    						   子函数2
    							 |
    							 |
    				           子函数1  
    							 |
    							 |
    						  main函数
    							 | 
    							 |
    						  启动代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    高级语言程序 = 启动代码 + 自己代码

    启动代码由谁提供

    启动代码一般(99%)都是由编译器提供的

    一般有两种提供方式

    1. 源码形式

      以源码形式提供时,编译器会将启动代码的源文件自己程序的源文件一起编译

      • 像开发单片机这种没有OS的计算机的C程序时,启动代码一般是源码形式提供的
      				       	编译
                启动代码.c  ——————————> ***.o   
                                              \
      										 \
      										  \  链接
      											 ————>  可执行文件(可执行程序)
                                                  /
      			            编译  			  /
        我的程序 ***.c ...  ——————————> ***.o ... / 
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    2. 二级制的.o(目标文件)形式

      直接以.o形式提供时,省去了我自己对“启动代码”的编译

                                   启动代码
                                ***.o ***.o   
                                         \
      									\
      					                 \  链接
      									   ————>  可执行文件(可执行程序)
      									  /
      					 编译             /
      	我的程序 ***.c... ————> ***.o .../
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      一般来说,如果开发的程序是运行在OS上时,那么编译器一般是以.o形式来提供启动代码

    gcc就是以.o形式提供的,基于OS运行的程序的启动代码

    gcc时加一个-v选项,查看gcc编译链接的详细情况时,可以看到有很多.o,这些.o就是gcc提供的启动代码

    • gcc -v 文件名.c

    • 在编译的详细信息里面,有很多的事先就被编译好的.o文件,这些.o文件就是用来生成启动代码的

    启动代码做了什么

    启动代码基本由汇编语言编写

    启动代码做了两件重要的事:

    1. 对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构

      函数调用需要“栈”,启动代码就需要在c内存空间上建立“栈”

      从c内存空间中划出一段空间,然后以“栈”的形式来进行管理

      • uboot的最开始的汇编代码就是启动代码,其中有一段汇编代码就是用来建立栈空间
    2. 留下相应库接口,方便与库进行链接

      裸机只能调用静态库,但是有os时候,大部分用动态库

    为什么启动代码基本用汇编语言来写

    ​ 在程序的内存空间结构还没有布局起来之前,高级语言程序还无法运行,此时只能使用汇编,当利用汇编编写的启动代码将高级语言的内存空间结构建立起来后,自然就可以运行c/c++等高级语言的程序了。

    比如C语言无法用C语言去写启动代码,比如C语言函数调用需要栈,但是启动代码需要布局栈结构。

    ​ 如果程序使用的是动态库的话,编译时,动态库代码并不会被直接编译到程序中,只会留下相应的接口,程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。

    裸机上程序是怎么运行起来的

    分为裸机和操作系统两种情况:

    1. 裸机的情况

      裸机,就是没有OS

      1. 内存和硬盘一体式

        51单片机-----没有单独的内存和单独硬盘,使用的是内存和硬盘功能二合一的norflash

        norflash既是内存也是硬盘

        • 因为norflash的访问速度很快,因此cpu能够直接从norflash上读取指令并执行,此时norflash就是一个内存
        • 因为norflash能够永久保存数据,设备关电后,数据依然存在,所以将程序下载到norflash后,关机重上电,程序依然还能运行

      2. 内存和硬盘分开式

    为什么传统的计算机,硬盘和内存都是分开的?

    • 像单片机这种内存和硬盘二合一的norflash形式,由于造价太高,只适合于单片机这种小容量的设备使用

    • 1)传统的内存:cpu能够快速访问,但是掉电丢失数据
      2)传统的硬盘:可以容量做的很大,掉电不丢失数据,但是访问速度很慢

      所以二者相互配合使用,同样能起到与norflash一样的二合一的效果,而且容量更高,单位造价更低

    像内存和硬盘分开的这种情况,一般都要运行OS的,很少直接以裸机方式来来使用

    裸机而且内存和硬盘分开运行程序的方式
    1. 直接将下载到内存中,然后运行
      • 直接将pc上编译好的代码下载到内存
      • 但是如果断电了,那么内存的数据就丢失了
    2. 先下载硬盘永久保存,开机时自动从硬盘中将代码拷贝到内存上,然后运行
      • 将pc上编译好的程序下载到开发板硬盘上后,程序会被永久的保存起来
      • 开机运行时,将硬盘上的代码拷贝到内存上(拷贝的过程通过代码实现)
      • pc指向内存中程序第一条指令,然后整个程序就运行起来了

    有OS时候程序的运行

    有OS的计算机,基本都是内存和硬盘分开式的情况

    原因:计算机资源相对丰富,有能力跑OS,如果不上系统直接裸机使用的话,这其实是在浪费计算机资源

    OS也是一个程序,而且是一个很大的裸机程序,因为操作系统直接运行在硬件上

    • OS的代码也是被永久的存放在了硬盘上,开机上电后,启动程序开始启动OS
    • 启动时会将OS代码从硬盘拷贝到内存上,然后就运行起来了

    重点:介绍基于OS运行的应用程序,是怎么在OS的支持下运行起来的

    !!!!有操作系统时,启动程序:

    ​ ※启动后OS调用加载器exec函数,这个加载器加载程序,然后会将硬盘上的代码自动拷贝到内存上,加载的过程就是拷贝的过程,但是只会拷贝当前运行的代码而不是全部的代码,然后让PC指向第一条指令,cpu取指令执行,那么整个程序就运行起来了。

    进程的终止方式

    进程终止方式有两种,一种是正常终止,另一种是异常终止

    进程主动调用终止函数/返回关键字所实现的结束,就是正常终止

    • main调用return关键字结束
    • 程序任何位置调用exit函数结束
    • 程序任何位置调用_exit函数结束
    1. main函数调用return关键字,实现正常终止
    • return关键字的作用是返回上一级函数

      1. 如果main函数的子函数调用return的话,返回的上一级是main函数

      2. 如果main函数调用return的话,main函数所返回的上一级是启动代码

      main函数调用return有两种方式:

      1. 显式调用(return 返回值)

        返回值的意义

        • 返回值标记的了进程的终止状态
        • return 0:正常结束
          return -1:代表了某种操作失败
          return -2:代表了另一种的操作失败
        • 使用:echo $? 查看返回值

    如果main函数没有调用任何子函数,而且return又没有返回任何值的话,默认返回-1

    1. 隐式调用
      • 不明写出return,当main函数中的最后一句代码执行完毕后,会默认的调用return返回
      • 不过隐式return时,默认返回0。
    exit函数

    在程序中任何位置调用exit都有效

    不管是在main函数中调用,还是在main的子函数中调用,甚至是在子函数的子函数中调用都是有效的

    其实,main函数调用return返回到启动代码后,启动代码也是调用exit函数来实现正常终止的

    #include 
    //这个参数就是返回值(进程终止状态)
    void exit(int status);
    
    • 1
    • 2
    • 3

    一般使用该函数进行报错处理

    _exit()

    _exit是一个系统函数(系统API),而exit是c库函数

    • exit就是调用_exit来实现的
    • exit对_exit封装后,exit额外还做了好些事情
    #include 
    void _exit(int status);
    
    • 1
    • 2

    裸机时:

    • 只能调用return返回,因为没有OS时,不支持exit和_exit函数
    • main的return到启动代码后,返回动作到启动代码就截止了
    1:      main函数            启动代码          
    		return(0)  ——————>  exit(0)  ————————> _exit(0) ————> Linux OS
    2:		 exit		 
    		 exit(0)  ————————> _exit(0) ————————> Linux OS
    3:		_exit
    		_exit(0) ————————>Linux OS
    ----------有OS时,不管是采用哪种方式实现正常终止,返回值都会被返回给OS
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    异常终止

    ​ 进程不是因为return、exit和_exit函数而终止的,而是被强行发送了一个信号,这个信号将进程给无条件终止了,这就是异常终止

    1. 自杀—自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死,杀人凶器是信号

    2. 他杀—由别人发一个信号,将其杀死,杀人凶器也是信号

      程序死循环—ctrl+c结束进程

      其实向终端输入ctrl+c时,就是在向我的进程发送某个信号,然后这个信号将我的程序给异常终止

    #include 
    void abort(void);
    
    • 1
    • 2


    atexit()

    #include 
    //进程终止处理函数
    //参数就是被登记“进程终止函数”的地址
    /*
    当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的一些扫尾处理
    ------强调的是正常终止,不是异常终止
    */
    //返回值 
    //函数调用成功返回0,失败返回非零值,不会进行错误号设置。
    int atexit(void (*function)(void));
    //function为void (*)(void)的函数指针类型
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在第三手册查到了,说明是个库函数

    #include
    #include
    void process_deal(void){
        printf("deal process!!\n");
    }
    int main(){
        atexit(process_deal);
        printf("having used function()\n");
        return 0;
    }
    /*执行结果:
    having used function()
    deal process!!
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    #include
    #include
    void process_deal1(void){
        printf("deal1 process!!\n");
    }
    void process_deal2(void){
        printf("deal2 process!!\n");
    }
    int main(){
        atexit(process_deal1);
        atexit(process_deal2);
        printf("having used function()\n");
        return 0;
    }
    /*执行结果:
    having used function()
    deal2 process!!
    deal1 process!!
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ​ 调用atexit注册时,会将“进程终止处理函数”的函数地址压入进程栈中,当进程正常终止时,又会自动从栈中取出函数地址,并执行这个函数,实现进程的扫尾操作。

    • 调用顺序刚好和注册顺序相反
    • 在Linux下,调用atexit最多可以允许登记32个终止处理函数
    • 同一个函数如果被登记多次,自然也会被调用多次
    • 只有使用return和exit来正常终止时,才会调用
    • 异常终止和_exit不会调用该进程终止处理函数
    该函数的意义

    有时候需要在进程正常终止时,做一些扫尾操作

    • 不管什么位置或者什么时候做正常终止,该函数都会做扫尾工作,一般在main之前使用

    比如将链表数据保存到文件中

    如果不使用进程终止处理函数的话,这个操作有点困难

    • 因为进程有时候可能是因为某个函数调用失败,然后在函数出错处理时调用exit(-1)终止的,但是你又无法预估哪一个函数会出错,并在出错时调用相应的函数实现链表数据的保存.
    • 这个时候就可以注册进程终止处理函数来实现了,因为进程终止时,会自动的调用终止处理函数来实现进程的扫尾处理--------将链表数据保存到文件中

  • 相关阅读:
    【Shell】Shell脚本入门
    Vuex的5个核心属性是什么?
    Redis 数据类型
    2023_Spark_实验十:RDD基础算子操作
    RabbitMQ--基础--06--界面说明
    查找:基本概念
    Python GUI_Tinkter学习笔记
    js基础笔记学习250事件委派1
    Sentinel核心算法设计与实现
    8.gec6818开发板通过并发多线程实现电子相册 智能家居 小游戏三合一完整项目
  • 原文地址:https://blog.csdn.net/weixin_47173597/article/details/127720838