在学习进程之前,先来了解下进程的执行环境。
main函数
进程总是从main函数开始执行的,我们编程时,程序运行也是从main函数运行的,它的原型如下:
int main(int argc, char *[]argv);
argc是命令行参数的数目,argv是指镇数组,即指向指针的指针,可以写代码测试一下:
#include<stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i=0; i<argc; i++)
printf("argv[%d]:%s\n",i,argv[i]);
exit(0);
}
输出:
linux:/home # ./test this is a test program
argv[0]:./test
argv[1]:this
argv[2]:is
argv[3]:a
argv[4]:test
argv[5]:program
在调用main函数之前,内核先调用一个特殊的启动例程,它从内核取得命令行参数和环境变量的值,可执行程序文件将此启动例程指定为程序的起始地址。内核使用exec函数来启动C程序。
进程终止
有5种方式可以终止进程
(1)正常终止
(a)从main函数返回
(b)调用exit
(c)调用_exit
(2)异常终止
(a)调用abort
(b)由一个信号终止
在上面的正常终止中有exit和_exit,区别在于:_exit调用后立即进入内核;而exit则会先执行一些清除工作(包括调用执行各种终止处理程序,关闭所有标准I/O流等),然后在进入内核。它们定义如下:
#include<stdlib.h>
void exit(int status);
#include<unistd.h>
void _exit(int status);
使用了不同的头文件,因为exit是右ANSI C说明的,而_exit是由POSIX.1说明的。
atexit函数
我们可以在进程中登记一些函数(最多32个),这些函数由exit调用。这样的函数叫做终止处理程序(exit handler),有atexit函数来登记。这点看起来有点像类的析构函数。
#include<stdlib.h>
int atexit(void (*func)(void));
其参数是一个函数地址。exit调用顺序与登记顺序相反,先登记后调用(像栈),同一个函数可以被登记多次。
环境表
进程的运行都有一个环境,环境的信息存储在环境表里面。环境表是一个字符指针数组,其中每个指针包含一个以NULL结束的字符串的地址。全局变量environ包含了该指针数组的地址:
extern char **environ;
程序的存储空间布局
C程序由以下几部分组成:
正文段:也叫代码段,是CPU执行的机器指令。正文段一般可以共享的,且是只读的。
初始化数据段:也叫做数据段,包含程序中已经初始化的全局变量和静态变量。
非初始化数据段:也叫做bss段,包含未初始化的全局变量和静态变量,在程序执行之前有内核初始化为0。不在代码中存储器初始值,可以减小代码的大小。
栈:自动存储变量以及每次函数调用时所需保存的信息都放在此段中。每次函数调用时,其返回地址、以及寄存器中的变量都放在栈中。新被调的函数在栈上为其自动和临时变量分配存储空间。
堆:进行动态分配的变量。需要手动释放。堆位于未初始化数据段的顶和栈底之间。
存储器的分配:
可以参考这里
setjmp和longjmp
在C语言中,可以使用goto语言(虽然不建议使用),但是goto语句不能跳跃函数。执行这个跳转功能的函数时setjmp和longjmp。这两个函数对于处理发生在很深的嵌套调用中的出错情况非常好用。递归调用返回时只能返回到上一层函数,这两个函数可以使其跳出递归调用。
UNIX进程的环境