C程序总是从main函数开始执行
当内核起动C程序时(使用一个exec函数,8.9节将说明exec函数),在调用main前先调用一个特殊的起动例程。可执行程序文件将此起动例程指定为程序的起始地址——这是由连接编辑程序设置的,而连接编辑程序则由C编译程序(通常是cc)调用。起动例程从内核取得命令行参数和环境变量值,然后为调用main函数作好安排。
进程终止
有五种方式使进程终止:(1)正常终止:
(a)从main返回。
(b)调用exit。
(c)调用_exit。
(2)异常终止:
(a)调用abort(见第10章)。
(b)由一个信号终止(见第10章)。
上节提及的起动例程是这样编写的,使得从main返回后立即调用exit函数。如果将起动例程以C代码形式表示(实际上该例程常常用汇编语言编写),则它调用main函数的形式可能是:
exit(main(argc,argv));
exit和_exit函数用于正常终止一个程序:_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。
#include<stdlib.h>
voidexit(int status);
#include<unistd.h>
void_exit(int status);
使用不同头文件的原因是:exit是由ANSIC说明的,而_exit则是由POSIX.1说明的。
将main说明为返回一个整型以及用exit代替return,对某些C编译程序和UNIXlint(1)程序而言会产生不必要的警告信息,因为这些编译程序并不了解main中的exit与return语句的作用相同。警告信息可能是“controlreachesendofnonvoidfunction(控制到达非void函数的结束处)”。避开这种警告信息的一种方法是:在main中使用return语句而不是exit。但是这样做的结果是不能用UNIX的grep公用程序来找出程序中所有的exit调用。另外一个解决方法是将main说明为返回void而不是int,然后仍旧调用exit。这也避开了编译程序的警告,但从程序设计角度看却并不正确。本章将main表示为返回一个整型,因为这是ANSIC和POSIX.1所定义的。我们将不理会编译程序不必要的警告。
按照ANSI C的规定,一个进程可以登记多至3 2个函数,这些函数将由e x i t自动调用。我们称这些函数为终止处理程序(exit handler),并用a t e x i t函数来登记这些函数。texit的参数是一个函数地址,当调用此函数时无需向它传送任何参数,也不期望它返回一个值。exit以登记这些函数的相反顺序调用它们。同一函数如若登记多次,则也被调用多次。
注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(调用exit)调用_exit。进程也可非自愿地由一个信号使其终止(图7-1中没有显示)。
命令行参数
当执行一个程序时,调用e x e c的进程可将命令行参数传递给该新程序。这是UNIX shell的一部分常规操作
#include <stdio.h>
int main(int argc, char* argv[])
{
int i;
for(i=0; argv[i]!=NULL; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
ANSIC和POSIX.1都要求argv[argc]是一个空指针。这就使我们可以将参数处理循环改写为:
for(i=0;argv[i]!=NULL;i++)
环境表
每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个
指针包含一个以n u l l结束的字符串的地址。全局变量e n v i r o n则包含了该指针数组的地址。
extern char **environ;
其中,每个字符串的结束处都有一个n u l l字符。我们称e n v i r o n为环境指针,指针数组为环境表,
其中各指针指向的字符串为环境字符串。
按照惯例,环境由:
n a m e = v a l u e
这样的字符串组成,这与图7 - 2中所示相同。大多数预定义名完全由大写字母组成,但这只是
一个惯例。
在历史上,大多数UNIX系统对main函数提供了第三个参数,它就是环境表地址:
intmain(intargc,char*argv[],chare*nvp[]);
因为ANSIC规定main函数只有两个参数,而且第三个参数与全局变量environ相比也没有带来更多益处,所以POSIX.1也规定应使用environ而不使用第三个参数。通常用getenv和putenv函数(7.9节将说明)来存取特定的环境变量,而不是用environ变量。但是,如果要查看整个环境,则必须使用environ指针。
C程序的存储空间布局
由于历史原因,C程序一直由下列几部分组成:
•正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常执行的程序(如文本编辑程序、C编译程序、shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。
•初始化数据段。通常将此段称为数据段,它包含了程序中需赋初值的变量。例如,C程
序中任何函数之外的说明:
intmaxcount=99;
使此变量以初值存放在初始化数据段中。
•非初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序的一个操作符,意思是“blockstartedbysymbol(由符号开始的块)”,在程序开始执行之前,内核将此段初始化为0。函数外的说明:
longsum[1000];
使此变量存放在非初始化数据段中。
•栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。
•堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段顶和栈底之间。
图1显示了这些段的一种典型安排方式。
这是程序的逻辑布局—虽然并不要求一个具体实现一定以这种方式安排其存储空间。尽管如
此,这给出了一个我们便于作有关说明的一种典型安排。
对于VAX上的4.3+BSD,正文段从0位置开始,栈顶则在0x7fffffff之下开始。在VAX机器上,
堆顶和栈底之间未用的虚地址空间很大。从图7-3还可注意到末初始化数据段的内容并不存放在磁盘程序文件中。需要存放在磁盘程序文件中的段只有正文段和初始化数据段。
size(1)命令报告正文段、数据段和bss段的长
度(单位:字节)。例如:
guhui@ubuntu:~/unix$ size a.out /bin/bash
text data bss dec hexfilename
1266 560 8 1834 72aa.out
978305 36536 235121038353 fd811/bin/bash
存储器分配
ANSIC说明了三个用于存储空间动态分配的函数。
(1)malloc。分配指定字节数的存储区。此存储区中的初始值不确定。
(2)calloc。为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的每一位
(bit)都初始化为0。
(3)realloc。更改以前分配区的长度(增加或减少)。当增加长度时,可能需将以前分配区的
内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
#include<stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj,size_t size);
void *realloc(void ptr*,size_t newsize);
三个函数返回:若成功则为非空指针,若出错则为NULL
void free(voidpt*r)
这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。例如,在一个特定的系统上,如果最苛刻的对齐要求是double,则对齐必须在8的倍数的地址单元处,那么这三个函数返回的指针都应这样对齐。
因为这三个alloc函数都返回类属指针,如果在程序中包括了<stdlib.h>(包含了函数原型),那么当我们将这些函数返回的指针赋与一个不同类型的指针时,不需要作类型强制转换。函数free释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后可在调用分配函数时再分配。
realloc使我们可以增、减以前分配区的长度(最常见的用法是增加该区)。例如,如果先分配一个可容纳长度为512的数组的空间,并在运行时填充它,但又发现空间不够,则可调用realloc扩充该存储空间。如果在该存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充,并返回传送给它的同样的指针值。如果在原存储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的512个元素数组的内容复制到新分配的存储区。因为这种存储区可能会移动位置,所以不应当使用任何指针指在该区中。习题4.18显示了在etcwd中如何使用realloc,以处理任何长度的路径名。程序15-27是使用realloc的另一个例子,用其可以避免使用编译时固定长度的数组。注意,realloc的最后一个参数是存储区的newsize(新长度),不是新、旧长度之差。作为一个特例,若ptr是一个空指针,则realloc的功能与malloc相同,用于分配一个指定长度newsize的
存储区。
这些分配例程通常通过sbrk(2)系统调用实现。该系统调用扩充(或缩小)进程的堆(见图7-3)。
malloc和free的一个样本实现请见Kernighan和Ritchie[1988]的8.7节。虽然sbrk可以扩充或缩小一个进程的存储空间,但是大多数malloc和free的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将它们保持在malloc池中而不返回给内核。
应当注意的是,大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。这就意味着如果写过一个已分配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。将指向分配块的指针向后移动也可能会改写本块的管理信息。
其他可能产生的致命性的错误是:释放一个已经释放了的块;调用free时所用的指针不是三个alloc函数的返回值等。因为存储器分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现方法。每次调用这三个分配函数中的任意一个或free时都进行附加的出错检验。在调用连接编辑程序时指定一个专用库,则在程序中就可使用这种版本的函数。此外还有公共可用的资源(例如由4.3+BSD所提供的),在对其进行编译时使用一个特殊标志就会使附加的运行时间检查生效。因为存储空间分配程序的操作对某些应用程序的运行时间性能非常重要,所以某些系统提供了附加能力。例如,SVR4提供了名为mallopt的函数,它使进程可以设置一些变量,并用它们来控制存储空间分配程序的操作。还可使用另一个名为mallinfo的函数,以对存储空间分配程序的操作进行统计。请查看所使用系统的malloc(3)手册页,弄清楚这些功能是否可用。