这是《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 <stdlib.h>
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 <unistd.h>
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 <stdlib.h>
int atexit(void (*function)(void));
显然,这是一个C库函数。其功能是:
注册(登记)进程终止处理函数,参数就是被登记“进程终止函数”的地址。当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的一些扫尾处理。
- 代码演示:
#include <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
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函数,就是标准库中的函数。又比如使用别人写的好用的库,所以库也是程序必须可少的,有库的支持,我们可以更方便的写程序。