07.进程环境
当执行程序时,其main函数是如何被调用的;命令行参数是如何传送给执行程序的;典型的存储器布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;各种不同的进程终止方式等。另外,还将说明longjmp和setjmp函数以及它们与栈的交互作用。
1、main函数
C程序总是从main函数开始执行。
当内核执行一个C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址---这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和设置变量值,然后为按上述方式调用main函数做好安排。
2、进程终止
有5种正常进程终止:
①从main函数返回;
②调用exit;
③调用_exit或_Exit;
④最后一个线程从其启动例程返回;
⑤最后一个线程调用pthread_exit;
有3中异常终止:
①调用abort;
②接到一个信号终止;
③最后一个线程对取消请求(pthread_cancel)作出响应;
有三个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理(包括调用执行各终止处理程序,关闭所有标准I/O流(非文件描述符)等),然后进入内核。
由于历史原因,exit函数总是执行一个标准I/O库的清理关闭操作:为所有打开流调用fclose函数,这会造成所有缓冲的输出数据都被冲洗(写到文件上)。
未定义的终止状态
#include <stdlib.h> void exit(int status); #include <unistd.h> void _exit(int status); #include <stdlib.h> void _Exit(int status);
三个exit函数都带一个参数,称之为终止状态,如果
a、若调用这些函数时不带终止状态;
b、main执行了一个无返回值的return语句;
c、main没有声明返回类型为整型;
则该进程的终止状态为未定义的。
main函数返回一整型值与用该值调用exit是等价的,于是在main函数中
exit(0);
等价于
return 0;
3、aexit函数
int atexit(void (*function)(void));
一个进程可以调用atext登记多达32个函数,这些函数将由exit自动调用,我们称这些函数为终止处理函数。
exit调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,则也会被调用多次。
下图为一个C程序是如何启动的,以及它可以终止的各种方式:
注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式地或隐式地(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。
4、环境表
每个进程都会有一张环境表,它是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:
extern char **environ;
如下图所示:
通常用getenv和putenv函数来访问特定的环境变量,而不是用environ变量。但是,如果要查看整个环境,则必须使用environ指针。
5、C程序的存储空间布局
a、正文段(代码段)。这是由CPU执行的机器指令部分。正文段是可共享的,即使是频繁执行的程序(如文本编辑器、C编译器和shell等)在存储器中也只需一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其自身的命令。
b、初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。如,C程序中出现在任何函数之外的声明:
int maxcount = 99;
使此变量带有其初值存放在初始化数据段中。
c、非初始化数据段。通常将此段称为bss段,这一名称来源于汇编运算符,意思是"block started by symbol"(由符号开始的块),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。出现在任何函数外的C声明
long sum[1000];
使此变量放在非初始化数据段中。
d、栈。
e、堆。
程序分为下面的段:
text,data(initialized),bss,stack,heap
text和data需要存入可执行文件,bss的数据在程序载入时由内核清0,因此不需保存。所以有初值的全局变量和static变量在data区,未赋初值的在bss段,函数的局部变量和参数在stack中,动态分配的在heap中。
未初始化数据段的内容并不存放在磁盘上的可执行文件中。其原因是,内核在程序开始运行前将它们都设置为0。需要存放在程序文件中的段只有正文段和初始化数据段。
6、setjmp和longjmp函数
两个函数属于非局部goto,非局部指的是,这不是由普通C语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。
#include <setjmp.h> int setjmp(jmp_buf env); void longjmp(jmp_buf env,int val);
setjmp返回值:若直接调用则返回0,若从longjmp调用返回则返回longjmp的第二个参数值。
longjmp函数使用第二个参数的原因是对于一个setjmp可以有多个longjmp,setjmp可以通过测试longjmp的第二个参数值可判断造成返回的longjmp是在哪个函数中。
例子:
static jmp_buf buf; void second() { printf("second.\n"); longjmp(buf,1); } void first() { second(); printf("first.\n"); } int main() { if(!setjmp(buf)) { first(); } else { printf("main.\n"); } return 0; }
结果:
second. main.
注意到虽然first()子程序被调用,"first"不可能被打印。"main"被打印,因为条件语句if(!setjmp(buf))被执行第二次。
使用setjmp和longjmp要注意以下几点:
①setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,如果在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可预测,很容易导致程序崩溃而退出;
②longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放 (函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。
如果你有一个自动变量,而不想使其值回滚,则可定义其具有volatile属性,声明为全局或静态变量的值在执行longjmp时保持不变。
自动变量的潜在危险
如:
FILE *open_data(void) { FILE *fp; char databuf[1024]; if((fp = fopen(DATAFILE,"r")) == NULL) return NULL; if(setvbuf(fp,databuf,_IOLBF,1024) != 0) return NULL; return fp; }
问题是:当open_data返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。但是,标准I/O库函数仍将使用其流缓冲区的存储空间。这就产生了冲突和混乱。为了校正这一问题,应在全局存储空间静态地(如static或extern)或者动态地(使用一种alloc函数)为数组databuf分配空间。