进程环境
一. main()函数
当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。
二.进程终止
8种方式使进程终止(termination),其中5种为正常终止:
(1)从main返回;
(2)调用exit;
(3)调用_exit或_Exit;
(4)最后一个线程从其启动例程返回;
(5)最后一个线程调用pthread_exit。
异常终止有3种方式,它们是:
(6)调用abort;
(7)接到一个信号并终止;
(8)最后一个线程对取消请求做出响应。
上一节中的启动例程是这样编写的,使得从main返回后立即调用exit函数。若启动例程以C代码形式表示,则其调用main函数的形式可能是: exit( main(argc,argv));
1.exit函数
三个函数用于正常终止一个程序:
由ISO C说明:
#include<stdlib.h>
(1)voidexit(int status);
(2)void_Exit(int status);
由POSIX.1说明:
#include<unistd.h>
(3)void_exit(int status);
exit执行时先执行一些清理处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。
由于历史原因,exit函数总是执行一个标准I/O库的清理关闭操作:为所有打开流调用fclose函数。这会造成所有缓冲的输出数据都倍冲洗(写到文件上)。
三个exit函数都带的一个整型参数,称之为终止状态(退出状态,exit status)。大多数UNIX Shell都提供检查进程终止状态的方法。若:
(a)若调用这些函数时不带终止状态,或
(b)main执行了一个无返回值的return语句,或
(c)main没有声明返回类型为整型;
则该进程的终止状态是未定义的。若main的返回类型是整型,并且main执行到最后一条语句时返回(隐式返回),则该进程的终止状态为0。
main函数返回一整型值与用该值调用exit是等价的。于是在main函数中:
exit(0); 等价于 return (0);
2.atexit函数
按照ISO C规定,一个进程可以登记多达32个函数,这些函数由exit自动调用。这写函数被称为终止处理函数(exit handler),并调用atexit函数来登记这些函数。
#include<stdlib.h>
intatexit(void (*func)(void));
exit调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,则也会调用多次。
ISOC要求系统至少应该支持32个终止处理程序。要确定一个平台支持的最大终止处理程序数量,可以使用sysconf函数。
根据ISO C和POSIX.1,exit首先调用各终止处理函数,然后按需多次调用fclose,关闭所有打开的流。POSIX.1扩展了ISO C标准,指定若程序调用exec函数族中的任一函数,则将清楚所有已安装的终止处理程序。
用户函数->调用exit(不返回)->exit函数--->调用终止处理函数--->返回exit函数--->调用_exit或_Exit--->内核
注意:内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式(通过调用exit)调用_exit或_Exit。进程也可非资源地由一个信号使其终止。
三.环境表
每个程序都会接收到一张环境表。环境表与参数表一样是一个字符指针数组,其中每个指针包含一个以null结束的C字符串地址。全局变量environ则包含了该指针数组的地址:
externchar **environ;
我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。环境由name=value这样的字符串组成。
HOME=/home/sar\0
PATH=:/bin:/usr/bin\0
SHELL=/bin/bash\0
USER=sar\0
LOGNAME=sar\0
大多数预定义的名字完全由大写字母组成,但这仅仅是一个惯例。通常用getenv和putenv函数来访问特定的环境变量,而不是用environ变量。但是,如果要察看整个环境,则必须使用environ指针。
历史上,大多数UNIX系统支持main函数带有3个参数,其中第3个参数就是环境表的地址: char *envp[]
由于ISO C规定main函数只能有两个参数,而且第三个参数与全局变量environ相比也没有带来益处,所以POSIX.1也规定应使用environ而不是用第3个参数。
四. C程序的存储空间布局
从历史上讲,C程序由下面几部分组成:
(1)正文段:text
(2)初始化数据段:data
(3)非初始化数据段:bss
(4)栈。
(5)堆。
1.正文段。是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器、shell)在存储器中也只需有一个副本,正文段常为只读,以防止程序由于意外而修改其自身的指令。
2.初始化数据段。包含了程序中需要明确地赋初值的变量。
如int maxCount=99;
3.非初始化数据段。通常称之为bss段。在程序开始执行之前,内核将此段中的数据初始化为0或者空指针。出现在任何函数外的C声明
longsum[1000];
使此变量存放在非初始化数据段中。
4.栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次调用函数时,其返回地址以及调用者的环境信息(如某些及机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,可以递归调用C函数。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量。
5.堆。通常在堆中进行动态存储分配。由于历史惯例,堆位于非初始化数据段和栈之间。
注意1:bss段的内容并不存放在磁盘上的程序文件中。其原因是,内核在程序开始运行前将它们都设置为0,需要存放在程序文件中的段只有text段和data段。
注意2:a.out还有若干其他类型的段,比如包含符号表的段,包含调试信息的段以及包含动态共享库链接表的段等等。这写部分并不装载到进程执行的程序映像中。
五. 共享库
共享库使得可执行文件中不再需要包含公用的库例程,而之需要在所有进程都可引用的存储区中维护这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数的时候,用动态链接方式将程序与共享库函数相链接。在不同的系统中,程序可能使用不同的方法说明是否要使用共享库。典型的有cc和ld命令的选项。
共享库优点:
1.减少了每个可执行文件的长度;
2.库函数的版本更新,无需对使用该库的程序重新连接编译。
共享库缺点:
1.程序首次执行时,增加运行时间开销。
六. 存储器分配
ISOC说明了用于存储空间动态分配的函数。
(1)malloc。分配制定字节数的存储区。此存储去中的初始值不确定。
(2)calloc。为制定数量具指定长度的对象分配存储空间。该空间中每一位都初始化为0。
(3)realloc。更改以前分配区的长度(+或者-)。增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值不确定。
#include<stdlib.h>
void*malloc(size_t size);
void*calloc(size_t size nobj, size_t size);
void*realloc(void *ptr, size_t newsize);
3个函数返回值:若成功则返回非空指针,若出错则返回NULL
因为这3个alloc函数都返回通用指针void*,所以如果在程序中包括了#include<stdlib.h>(以获得函数原型),那么当我们将这些函数返回的指针赋予一个不同类型的指针时,就不需要显示地执行类型强制转换。
voidfree(void *ptr);
函数free释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,可再被分配。
realloc函数可以使我们增、减以前分配区的长度。
(1)如果在该存储区后有足够的空间可功扩充,则可在原存储区位置上向高地址方向扩充,无需移动任何原先的内容,并且返回传送给它的同样的指针值。
(2)如果在原存储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现有的元素的内容复制到新分配的存储区。然后,释放原存储区,返回新分配区的指针。
注意:realloc最后一个参数是存储区的新长度,不是新旧存储区的长度之差。若ptr是空指针,则realloc功能与malloc相同。
这些存储空间动态分配函数通常用sbrk(2)系统调用实现。该系统调用扩充(或缩小)进程的堆。大多数malloc和free的实现都不减少进程的存储空间。释放的空间可供以后再分配,但通常将它们保持在malloc池中而不返回给内核。
特别注意:大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。
如果超过一个已分配区的尾端进行写操作,则会重写后一个块的管理记录。这种类型的错误是灾难性的,但该种错误不会很快暴露,所以也很难发现。同样的,在已分配区起始位置之前进行写操作会重写本块的管理记录。
在动态分配的缓冲区前后进行写操作,破坏的可能不仅仅是该区的管理记录信息。很可能用于其他动态分配的对象。
其他致命性错误:
(1)释放一个已经释放的块;
(2)调用free时所用的指针不是这3个alloc函数的返回值。
(3)若一个进程调用malloc函数,却忘记调用free函数释放,则该进程占用的存储器就会连续增加,这被称为泄露(leakage)。