1、引言
这一章主要学些的是当执行程序时,main函数是如何被调用的,命令行参数是如何传递给执行程序的,典型的存储器分布是什么样式的,如何分配另外的存储空间,进程如何使用环境变量,进程终止的不同方式等另外还有longjmp和setjmp函数以及他们与栈的交互作用。
2、main函数
C程序总是从main函数开始执行,main函数原型如下:
int main (int argc,char* argv[]);
其中argc是命令行参数的数目,argv是指向各个参数的各个指针构成的数组,当内核启动C程序时,在调用main之前先调用一个特殊的启动例程,可执行文件将此启动例程指定为程序的起始地址,启动例程从内核取得命令行参数和环境变量值然后为调用main函数做好安排。
3、进程终止
有五种方式使进程终止:
(1)正常终止
(a)从main返回。
(b)调用exit。
(c)调用_exit。
(2)异常终止
(a)调用abort。
(b)由一个信号终止。
3.1 exit和_exit函数
exit和_exit函数都用于正常终止一个程序:_exit立即进入内核,exit则先执行一些清除工作(包括调用执行各终止处理程序,关闭标准的IO流等)然后进入内核。
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
exit函数总是执行一个标准IO库的清除关闭操作,对所有打开流调用fclose函数,这造成缓存中的数据都被刷新,写到文件上。exit和_exit都带有一个整形参数,称之为终止状态,大多数UNIX shell都提供检查一个进程中止状态的方法。如果(a)若调用这些函数不带中止状态,或者(b)main函数执行了一个无返回值的return语句。或者(c)main执行隐式返回,则该进程的终止状态是未定义的。这也就意味着下面这个程序其实是不完整的:
因为main函数没有使用return(隐式返回)另外如果使用return(0)或者exit(0)则像执行程序的进程返回中止状态0。
3.2 atexit函数
按照ANSI C的规定,一个进程可以登记32个函数,这些函数由exit自动调用,称这些函数为终止处理程序并使用atexit函数来登记这些函数。
#include <stdlib.h>
int atexit(void (*function)(void));
其中atexit的参数是一个函数地址,当调用此函数时,无需向它传递任何参数,也不希望它返回一个值。exit以登记这些函数的相反顺序调用它们,同一个函数若被登记多次则也被调用多次。exit首先调用各种终止处理程序,然后按需调用多次fclose,关闭所有打开的流。
上图显示了一个C程序是如何启动的,以及各种终止方式。内核使程序执行的唯一方式是调用一个exec函数,进程自愿终止的唯一方式是显式或隐式的调用_exit,进程也可以非自愿的由一个信号使其终止。
4、命令行参数
当执行一个程序时,调用exec的进程可将命令行参数传递给新程序。可以使用如下程序将命令行参数回送到标准输出。
#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);
}
5、环境表
每个程序都接收到一个环境表,与参数表一样,环境表也是一个字符指针数组,其中每个指针都包含一个以null结束的字符串的地址,全局变量environ则包含了该指针数组的地址,如下图,该环境包含五个字符串,
其中每个字符串的结束处都有一个null字符,我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。
6、C程序的存储空间布局
这里在CSAPP中已经讨论过,不再赘述。
7、共享库
共享库使得可执行文件不再需要包含常用的库函数,而只需要在所有进程都可存取的存储区中保存这种库例程的一个副本,程序第一次调用某个库函数,用动态链接的方法将程序与共享库函数相连接,这样减少了每个可执行文件的长度,但是增加了一些运行时间开销,共享库的另一个优点是库函数新版本代替老版本时不需要对使用该库的程序重新连接编译。共享库其实就是动态链接。
8、存储器分配
ANSI C说明了三个用于存储空间动态分配的函数。
(1)malloc,分配指定字节数的存储区,此存储区初始值不确定。
(2)calloc,为指定长度的对象,分配能容纳其指定个数的存储空间,该空间中的每一位都初始化为0。
(3)realloc,更改以前分配区的长度,当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值不确定。
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size)
void *realloc(void *ptr, size_t size);
这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。函数free释放ptr指向的存储空间,被释放的空间通常被送入可用存储池,以后可在调用分配函数时再分配。还有一个alloca函数,其调用序列与malloc相同,但是它是在当前函数的栈帧上分配空间,而不是在堆中。其优点是,当函数返回时,自动释放她所使用的栈帧。
9、环境变量
环境字符串的形式是如下:
name=value
但是内核并不关心这种字符串的意义,它们的解释完全取决于各个应用程序。
ANSI C定义了一个函数getenv,可以用其环境变量值,但是该标准又称环境的内容是由实现定义的。
#include <stdlib.h>
char *getenv(const char *name);
函数返回一个指针,指向name=value字符串中的value,我们应当使用getenv从环境中取一个环境变量的值,而不是直接存取environ。除了取环境变量值,有时也需要设置环境变量或者改变现有变量的值,或者增加新的环境变量。
#include <stdlib.h>
int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
putenv函数,取形式为name=value的字符串,将其放到环境表中,如果name已经存在,则删除原来的定义。
setenv函数,将neme设置为value,如果环境中name已经存在,那么(a)如果overwrite非0,则首先删除其现存的定义。(b)ruo rewrite为0,则不删除现存定义。
unsetenv删除name的定义,即使不存在这样的定义也不算出错。
10、setjmp和longjmp函数
在C中不允许使用跳跃函数的goto语句,而执行这种跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在很深的嵌套函数调用中的出错情况非常有用。
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
在希望返回的位置调用setjmp,因为直接调用该函数,所以其返回值为0,setjmp的参数env是一个特殊类型,jmp_buf,这一数据类型是某种形式的数组,其中存放调用longjmp时能恢复栈状态的所有信息,一般env是个全局变量,因为需要从另一个函数中引用它。当检查到一个错误时,则以两个参数调用longjmp,第一个参数就是刚刚调用setjmp的env,第二个val,是个非0值,它成为从setjmp返回的值,使用第二个参数的原因是,对于一个setjmp可以有多个longjmp。
自动、寄存器和易失变量
在执行longjmp之后,自动变量和寄存器变量的状态会怎么样,标准会说它们的值是不确定的,如果你有一个自动变量,又不想使其值滚回,可以定义为volatile属性。说明为全局或静态变量的值在执行longjmp时保持不变。如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性。
自动变量的潜在问题
自动变量的函数返回后,就不能再引用这些自动变量。这些自动变量是在栈上的,函数结束这个栈就会被下一个调用的函数的栈帧使用。禁止返回值是局部变量的引用或值局部变量的指针。
11、getrlimit和setrlimit函数
每个进程都有一组资源限制,其中某一些可以用getrlimit和setrlimit函数查询和更改。
#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
对这两个函数的每一次调用都制定一个资源以及指向一个rlimit结构的指针。在更改资源限制时,必须遵循下列三条规则:
(1)任何一个进程都可以将一个软限制更改为小于或等于其硬限制。
(2)任何一个进程都可以降低其硬限制值,但它必须大于或等于其软限制值,这种降低对普通用户是不可逆反的。
(3)只有超级用户可以提高硬限制。
这两个函数的resource参数取下列之值之一:
RLIMIT_CORE core文件最大字节数,若其值为0则组织创建core文件。
RLIMIT_CPU CPU时间的最大量值(秒),当超过这个软限制时,向该进程发送SIGXCPU信号。
RLIMIT_DATA 数据段的最大字节长度,也就是初始化数据、非初始化数据以及堆的总和
RLIMIT_FSIZE 可以创建的文件的最大字节长度,当超过此软限制时,则向该进程发送SIGXFSZ信号。
RLIMIT_MEMLOCK 锁定在存储器地址空间(尚未实现)。
RLIMIT_NOFILE 每个进程能打开的最多文件数。更改此限制将影响到sysconf函数在参数_SC_OPEN_MAX中返回的值。
RLIMIT_NPROC 每个实际用户ID所拥有的最大子进程数。更改此限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值。
RLIMIT_OFILE 与RLIMIT_NOFILE相同。
RLIMIT_RSS 最大驻内存集字节长度。如果物理存储器供不应求,则内核将从进程处取回超过RSS的部分。
RLIMIT_STACK 栈的最大字节长度。
RLIMIT_VMEM 可映射地址空间的最大字节长度,这将影响mmap函数。
资源限制影响到调用进程并由其子进程继承,这意味着为了影响一个用户的所有后续进程需要将资源限制设置构造在shell中。
12、总结
理解UNIX环境中C程序的环境是理解UNIX进程控制特征的先决条件,本章说明了一个进程是如何启动和终止的,如何向其传递参数表的环境,虽然这两者都不是由内核进行解释的,但是内核也到了从exec的调用者将这两者传递给新进程的作用。本章也说明了C的典型存储器的布局,以及一个进程如何动态分配和释放存储器,并且了解了一些维护环境的函数,因为它们涉及存储器的分配。