这是《UNIX环境高级编程》第7章内容,这篇文章记录进程所需要的环境。

4 进程环境

程序加载到内存,运行起来后就成为了进程。就像人活着需要生活环境(衣食住行的环境)一样,进程也需要运行环境,进程所需要的环境如下:启动代码、环境变量、进程的内存空间布局、库等,下面分别说这四点。

4.1 进程环境1:启动代码

启动代码就是启动程序的代码。所有高级语言的程序,都有自己的启动代码。C程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个C程序都已运行。启动代码由编译器提供,其都是用汇编写的,为什么都是汇编写的呢?因为还没有程序内存空间之前,高级语言无法运行,所以启动代码都是汇编写的。

4.1.1 启动代码做的两件事

对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构。 高级语言程序在运行时,函数调用需要“栈”,启动代码就需要在c内存空间上建立“栈”,说白了就是从c内存空间中划出一段空间,然后以“栈”的形式来进行管理。留下相应库接口 如果程序使用的是动态库的话,编译时,动态库代码并不会被直接编译到程序中,只会留下相应的接口,程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。

4.1.2 进程的终止方式

进程的正常终止

进程主动调用终止函数/返回关键字结束,就是正常终止。有三种方式:

方式1:main调用return关键字结束 return关键字的作用是返回上一级函数,如果main函数的子函数调用return,返回的上一级是main函数。如果main函数调用return,main函数所返回的上一级是启动代码。方式2:程序任何位置调用exit函数结束 程序任意位置调用exit函数,程序返回到启动代码。Exit函数原型如下:

#include

void exit(int status);

看到stdlib.h这个头文件,我们就知道exit函数是一个c库函数。main函数调用return将返回值返回给启动代码后,启动代码又会调用exit(返回值),将返回值返回。 一般函数错误时候,会调用exit函数。

方式3:程序任何位置调用_exit函数结束 _exit是一个系统函数(系统API),而exit是c库函数,exit就是调用_exit来实现的。所以,exit是_exit的封装,比_exit多了很多功能。 _exit函数原型:

#include

void _exit(int status);

总结这三种方式: return,exit,_exit都可以终止进程,但是尽量使用return和exit, 这三种方式的执行过程如下: Return: return(0) —————> exit(0) ————> _exit(0) ———> Linux OS ** exit** exit(0) ————————> _exit(0) ————> Linux OS ** _exit** _exit(0) ————>Linux OS

进程正常终止时候,注册进程终止函数

在写程序时候,可以使用atexit函数注册进程终止处理函数。函数原型如下:

#include

int atexit(void (*function)(void));

显然,这是一个C库函数。其功能是: 注册(登记)进程终止处理函数,参数就是被登记“进程终止函数”的地址。当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的一些扫尾处理。

代码演示:

#include

#include

void process_deal1(void)

{

printf("void process_deal1(void)\n");

}

void process_deal2(void)

{

printf("void process_deal2(void)\n");

}

int main()

{

atexit(process_deal1);

atexit(process_deal2);

while(1);

return 0;

}

1)从这个实例中可以看出,进程终止处理函数的注册顺序和调用顺序刚好相反。 为什么顺序刚好相反呢?调用atexit注册时,会将“进程终止处理函数”的函数地址压入进程栈中,当进程正常终止时,又会自动从栈中取出函数地址,并执行这个函数,实现进程的扫尾操作。栈的特点是先进后出,先压栈的后调用,所以调用顺序刚好和注册顺序相反。 2)在Linux下,调用atexit最多可以允许登记32个终止处理函数。 3)同一个函数如果被登记多次,自然也会被调用多次。 4)在两种情况下,登记的进程终止处理函数不会被调用; (a)异常终止,不会调用 (b)直接调用_exit来正常终止时,不会调用注册的进程终止函数。 换句话说,只有使用return和exit来正常终止时,才会调用。

登记“进程终止处理函数”有什么意义?

有时,代码正常终止时候,需要做一些扫码工作,比如保存链表中的数据等操作。如果不使用进程终止处理函数,这个操作有点困难,因为进程有时候可能是因为某个函数调用失败,然后在函数出错处理时调用exit(-1)终止的,但是你又无法预估哪一个函数会出错,并在出错时调用相应的函数实现链表数据的保存,那怎么办呢?这个时候就可以注册进程终止处理函数来实现了,因为进程终止时,会自动的调用终止处理函数来实现进程的扫尾处理,比如将链表数据保存到文件中。

#include

#include

void process_deal1(void)

{

printf("void process_deal1(void)--save list\n");

}

void process_deal2(void)

{

printf("void process_deal2(void)\n");

}

void fun1()

{

exit(-1);

}

int main()

{

atexit(process_deal1);

atexit(process_deal2);

fun1(); // 程序正常终止

return 0;

}

进程的异常终止

有些情况下,进程不是因为return、exit和_exit函数而终止的,而是被强行发送了一个信号,这个信号将进程给无条件终止了,这就是异常终止。在命令行下按下ctrl + c, 就是在向正在运行的进程发送终止信号,这个信号将异常终止程序。

4.1.3 进程启动到正常终止的全过程

上图中,为什么调用exit会刷新io缓冲区 ? 标准IO的库缓存的缓冲有三种,无缓冲、行缓冲、全缓冲。标准输出(printf)的库缓存就是行缓冲的,在缓存中积压的数据,直到出现以下情况时,才会刷新输出,否则就一直积压着。 **1)**遇到\n时就刷新输出,\n表示这是一行,就好比句号表示一句话一样。 **2)**库缓存中数据满了,也会自动刷新输出,这就好比盆里的水满了溢出一样。不过一般来说,数据不可能多到能够把缓存装满的。 **3)**调用标准fflush函数,主动刷新数据 **4)**调用fclose关闭标准输出时,会自动调用fflush刷洗数据; exit会调用fclose关闭所有的标准io,关闭时会自动调用fflush来刷新数据。

4.2 进程环境2:环境变量表

先说下什么是环境变量?环境变量就是字符串,环境变量 = 环境变量名 + 环境变量数据,比如在windows下设置的环境变量如下图所示,其作用是**:专门记录各种可执行程序所在的路径。** 环境变量存放在“环境变量表”里面。每个进程都有一张环境变量表,最原始的“环境变量表”都被保存在了“环境变量文件”中。通过修改环境变量文件,实现了“环境变量”数据的永久保存。我们通过图形界面设置、修改windows“环境变量”时,修改、设置的内容,都会被永久保存到“环境变量文件”中。 当我们在linux命令行下面,执行自己的./a.out程序时候,查找的谁的环境变量表呢? 查找的是“命令行窗口进程”的“环境变量表”。 为什么只有重新打开“命令行窗口”后, 新设置的“环境变量”才生效? 因为新设置的环境变量,只是被保存到了windows的环境变量文件中,但是之前所打开的“命令行窗口”进程的“环境变量表”还没有得到更新,只有当重新打开后,才能更新。

4.3 进程环境3:进程的内存布局

编译好的C程序,需要加载到内存中,然后才能运行。所以,启动代码需要在内存中开辟好进程需要的空间给程序时候,有虚拟内存的OS,进程的空间布局在虚拟内存上,CPU执行的虚拟内存的地址。进程的内存布局如下:

4.4 进程环境4:库

当我们写程序时候,也需要库,比如使用printf函数,就是标准库中的函数。又比如使用别人写的好用的库,所以库也是程序必须可少的,有库的支持,我们可以更方便的写程序。

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。