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程序是如何启动的,以及它可以终止的各种方式:

图片1

注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式地或隐式地(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。

 

4、环境表

每个进程都会有一张环境表,它是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:

extern char **environ;

如下图所示:

图片1

通常用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分配空间。

posted @ 2014-04-23 14:28  sheshiji  阅读(154)  评论(0编辑  收藏  举报