当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址—这是由链接编辑器设置的,而链接编辑器由C编译器调用。启动例程从内核取得命令行参数和环境变量值,而后按上述方式调用main函数作好安排。shell
5种正常终止 数组
3种异常终止缓存
无论进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭全部打开描述符,释放他所使用的存储器等。
对上述任意一种终止情形,咱们都但愿终止进程可以通知其父进程它是如何终止的。对于e x i t和_ e x i t,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止状况,内核(不是进程自己)产生一个指示其异常终止缘由的终止状态( termination status) 。在任意一种状况下,该终止进程的父进程都能用 w a i t或w a i t p i d函数(在下一节说明)取得其终止状态。(退出状态是传给exit/_exit的参数,或main返回值。在最后调用_exit时内核将其退出状态转为终止状态,若是子进程正常终止那父进程能够获取子进程的退出状态)。
在说明fork函数时,子进程是在父进程调用fork后生成的。子进程将其终止状态返回给父进程。可是若是父进程在子进程以前终止,该如何?回答是:对于父进程已经终止的全部进程,他们的父进程都改变为init进程。咱们称这些进程由init进程收养。其操做过程大概是:在一个进程终止时,内核诸葛检查全部活动进程,以判断他是不是正要终止进程的子进程,若是是,则该进程的父进程的ID更改成1(init进程的ID)。这种处理方法保证了每个进程都有一个父进程。
若是子进程在父进程以前终止,那么父进程如何在作相应检查时获得子进程的终止状态呢?若是子进程彻底消失了,父进程在最终准备好检查子进程是否终止时是没法得到他的终止状态的。内核为每一个终止子进程保存了必定量的信息。因此当终止进程的父进程调用wait或waitpid时,能够获得这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核能够释放终止进程所使用的全部存储区,关闭其全部打开文件。在UNIX术语中,一切已经终止、可是其父进程还没有对其进行善后处理的进程被称为僵尸进程。==若是一个长期运行的程序,它fork了不少子进程,那么除非父进程取得子进程的终止状态,否则子这些进程终止后就会变成僵尸进程。 编辑器
一个由init进程收养的进程最终终止时会发生什么?他会不会变成一个僵尸进程,不会的,由于init被编写成不管什么时候只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中塞满了僵尸进程。当说起“一个init的子进程”时,这指的是init直接产生的进程,也多是其父进程已经终止,由init收养的过程。函数
启动例程是这样编写的,使得从main返回后当即调用exit函数,若是将启动历程以C代码形式表示(此例程一般用汇编编写),则它调用main函数的形式多是优化
exit(main(argc, argv));
exit和_exit函数用于正常终止一个程序:_exit当即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭全部标准IO流等),而后进入内核。spa
#include <stdlib.h> void exit(int status); #include <unistd.h> void _exit(int statu);
因为历史缘由,exit函数老是执行一个标准IO库的清除关闭操做:对全部打开的流调用fclose函数,这会形成缓存中的全部数据都被刷新(写入到文件上)。命令行
按照ANSI C的规定,一个进程能够登记多至32个的终止处理函数(exit handler),这些函数将由exit自动调用。可用atexit函数来注册这些函数。线程
#include <stdlib.h> int atexit(void (*func)(void)); //返回值:成功为0,出错非0
atexit的参数是一个无参数而且无返回的函数的指针。exit以注册这些函数的相反顺序调用它们。若是一个函数被注册屡次那会被调用屡次。 根据ANSI C和POSIX.1 exit首先调用各终止处理程序,而后按需屡次调用fclose。下图显示了一个C程序是如何启动的,以及它终止的各类方式。指针
内核使程序执行的惟一方式是调用一个exec函数。
每一个程序都接收到一张环境表。与参数表同样,环境表也是一个字符指针数组,其中每一个指针包含一个以null结束的字符串的地址。全局变量environ则包含了该指针数组的地址。 extern char **environ;
若是该环境包含五个字符串,那么它们看起来以下图:
其中每一个字符串的结束处都有一个null字符。咱们称environ为环境指针。指针数组为环境表,其中各指针指向的字符串为环境字符串。一般使用getenv和putenv函数来存取特定的环境变量,而不是用environ变量。可是若是要查看整个环境则必须使用environ指针。
int maxcount = 99;
使此变量以初值存放在初始化数据段中。long sum[1000]
使此变量存放在非初始化数据段中。 由编译器自动分配释放管理。局部变量及每次函数调用时返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。新被调用的函数在栈上为其自动和临时变量分配存储空间。经过以这种方式使用栈,C函数能够递归调用。递归函数每次调用自身时,就使用一个新的栈帧,所以一个函数调用实例中的变量集不会影响另外一个函数调用实例中的变量。
a.局部变量
b.函数调用时返回地址
c.调用者的环境信息(例如某些机器寄存器)
从图中能够看到未初始化数据段的内容并不存放在磁盘程序文件中。须要存放在磁盘程序文件中的段只有正文段和初始化数据段。size命令报告正文段、数据段、和bss段的长度:
$ size /bin/cc /bin/sh text data bss dec hex 81920 16384 664 98968 18298 /bin/cc 90112 16384 0 106496 1a000 /bin/sh //第4列和第5列分别以十进制和十六进制表示的总长度。
Linux中能够借助brk或mmap函数从用户空间中申请连续内存。
共享库使得可执行文件中再也不须要包含经常使用的库函数,而只需在全部进程均可存取的存储区中保存这种库例程的一个副本。程序第一次执行的时候或第一次调用某个库函数的时候,用动态连接方法将程序与共享库函数相连接,这减小了每一个可执行文件的长度,但增长了一些运行时间开销。另外一个优势就是能够用库函数的新版原本替换老版本而无需对该库的程序从新连接编译。
不一样的系统使用不一样的方法说明程序是否须要使用共享库。比较典型的有cc和ld命令的可选项。
ANSI C说明了三个存储空间动态分配的函数
malloc。分配指定字节数的存储区。此存储区中的初始值不肯定。 (2) calloc。在内存中动态地分配nobj个长度为size的连续空间。该空间中的每一位都初始化为0。 (3) realloc。更改之前分配区的长度(增长或减小)。当增长长度时,可能须要将之前分配区的内容移到另外一个足够大的区域,并且新增区域内的初始值不肯定。
#include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nboj, size_t size); void *realloc(void *ptr, size_t newsize); //三个函数返回:成功返回为非空指针,出错为NULL void free(void *ptr);
这三个分配函数返回的指针必定是适当对齐的,使其能够用于任何数据对象。在一个特定的系统上,若是最苛刻的对齐要求是double,则对齐必须在8的倍数的地址单元处,那么这三个函数返回的指针都应这样对齐。 free函数释放的空间一般被送入可用存储区池,之后可在调用分配函数时再调用。 realloc若是在原存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充。并返回传给它的一样的指针值。若是在原存储区后没有足够的空间则realloc分配一个足够大的存储区,将现存的内容复制到新分配的存储区中。由于这种存储区会移动位置因此不该使任何指针指到该区。 realloc的最后一个参数是存储区的newsize而不是新旧长度之差。若是ptr是空指针,则realloc功能与malloc相同。用于分配一个制定长度newsize的存储区。
这些分配例程一般经过sbrk系统调用实现。该系统调用扩充或缩小进程的堆。
虽然sbrk能够扩充或缩小一个进程的存储空间,可是大多数malloc和free的实现都不减少进程的存储空间而是将它们保存在malloc池中而不返回给内核。
大多数实现所分配的存储空间比所要求的要大,额外的空间用来记录管理信息--分配块的长度,指向下一个分配块的指针等等。这就意味着若是写过一个已分配区的尾端,则会改写后一块的管理信息。将指向分配块的指针向后移动可能也会改写本块的管理信息。
其余可能出现的错误:释放一个已经释放了的块;调用free所用的指针不是三个alloc函数的返回值等。
alloca函数是在当前函数的栈帧上分配存储空间。优势是:当函数返回时自动释放它所使用的栈帧,缺点是:某些系统在函数已经被调用后不能增长栈帧长度,因而也就不能支持alloca函数。
ANSI C定义了一个函数getenv,能够用其取环境变量值,可是该标准又称环境的内容是由实现定义
#include <stdlib.h> char *getenv(const char *name); //返回值:指向与name关联的value的指针,未找到则返回NULL
POSIX.1和XPG3定义了某些环境变量。下表列出了由这两个标准定义并受到SVR4和4.3+BSD支持的环境变量。
除了取环境变量值,有时也须要设置环境变量,或者是改变现有变量的值,或者是增长新的环境变量。可是不是全部系统都支持这些操做。下表列出了不一样的标准及实现支持的各类函数:
#include <stdlib.h> int putenv(const char *str); int setenv(const char *name, const char *value, int rewrite); //两个函数返回:成功为0,失败非0. void unsetenv(const char *name);
这三个函数的操做是:
环境表和环境字符串典型的存放在进程存储空间的顶部(栈之上)。删除一个字符串很简单--只要找到该指针,而后将全部后续指针都向下移一个位置。可是增长一个字符串或修改一个现存的字符串就比较困难。栈以上的空间由于已处于进程存储空间的顶部因此没法扩充,即没法向上扩充也没法向下扩充。
1.若是修改一个现存的name:
(a) 若是新value的长度少于或等于value的长度,则只要在原字符串所用空间中写入新字符串。
(b) 若是新value的长度大于原长度,则必须调用malloc为新字符串分配空间,而后将新字符写入该空间中,而后使环境表中针对name的指针指向新分配区。
2.若是要增长一个新的name,则操做更为复杂。首先调用malloc为name=value分配空间而后将该字符串写入该空间。而后:
(a) 若是这是第一次增长一个新name,则必须调用malloc为新的指针表分配空间。将原来的环境表复制到新分配区。并将指向新name=value的指针存在该指针表的表尾,而后又将一个空指针存在其后。最后使environ指针指向新指针表。再看上一节中的内存分配图,若是原来的环境表位于栈顶之上(这是常见状况)那么必须将此表移至堆中。可是此表中的大多数指针仍指向栈顶之上的个name=value字符串。
(b) 若是这不是第一次增长一个新name,则可知之前调用malloc在堆中为环境表分匹配了空间,因此只要调用realloc,以分配比原来空间多一个指针的空间。而后将该指向新name=value字符串的指针存放在该表表尾,后面跟着一个空指针。
在C中不容许使用跳跃函数的goto语句。而执行这种跳转功能的是非局部跳转函数setjmp和longjmp。非局部表示这不是子啊一个函数内的普通的C语言goto语句,而是在栈上跳过若干调用栈,返回到当前函数调用路径上的一个函数中。
#include <setjmp.h> int setjmp(jmp_buf env); //返回值:直接调用则为0,若从longjmp返回则为非0 void longjmp(jmp_buf env, int val);
在但愿返回到的位置调用setjmp,由于咱们直接调用该函数因此其返回值为0。setjmp的参数env是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用恢复栈状态的全部信息。通常,env变量是个全局变量,由于须要从另外一个函数中引用它。
当检查到一个错误时,则调用longjmp函数,第一个参数就是在调用setjmp时所用的env,第二个val是个非0值,它成为从setjmp处返回的值。使用第二个参数的缘由是对于一个setjmp能够有多个longjmp。
执行main时,调用setjmp,它将所需的信息记入变量jmpbuffer中返回0。而后调用do_line,它又调用cm_add,假定在其中检测到一个错误。在cmd_add中调用longjmp以前,栈的形式如图所示
可是longjmp使栈回到执行main函数时的状况,也就是抛弃了cmd_add和do_line的栈帧。调用longjmp形成main中setjmp的返回。可是,这一次的返回值是1(longjmp的第二个参数)。
在main函数中,自动变量和寄存器变量的状态如何?当longjmp返回到main函数时,这些变量的值是否能恢复到之前调用setjmp时的值(即滚回原先值),或者这些变量的值保持为调用do_line时的值(do_line调用cmd_add,cmd_add又调用longjmp)?大多数实现并不滚回这些自动变量和寄存器变量的值,而全部标准则说它们的值是不肯定的。若是有一个自动变量而又不想使其数值滚回能够定义其为具备volatile属性。说明为全局和静态变量的值在执行longjmp时保持不变
#include <setjmp.h> static void f1(int, int, int); static void f2(void); static jmp_buf jmpbuffer; int main(void) { int count; register int val; volatile int sum; count 2; val = 3; sum = 4; if (setjmp(jmpbuffer) != 0) { printf("after longjmp: count = %d, val = %d, sum = %d\n", count, val, sum); exit(0); } count = 97; val = 98; sum = 99; f1(count, val, sum); } static void f1(int i, int j, int k) { printf("in f1():count = %d, val = %d, sum = %d\n", i, j, k); f2(); } static void f2(void) { longjmp(jmpbuffer, 1); }
若是以不带优化和带优化对此程序分别进行编译,而后运行它们获得的结果是不一样的:
易失变量不受优化的影响,在longjmp以后的值,是它在调用f1时的值。存放在存储器中的变量将具备longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。不进行优化时全部这三个变量都存放在存储器中(会忽略val寄存器存储优化)。而进行优化时,count和val都存放在寄存器中。sum因为加了volatile限定符(该限定符修饰表示告诉编译器不要对这个变量进行优化)因此不会放到寄存器中。
自动变量问题
一个open_data的函数,它打开了一个标准IO流,而后为该流设置缓存
FILE *open_data(void) { FILE *fp; char databuf[BUFSIZ]; /* setvbuf设置的标准IO缓存 */ if ((fp = fopen(DATAFILE, "r")) == NULL) { return (NULL); } if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0) { return (NULL); } return (fp); }
当open_data返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。可是,标准IO函数仍然使用原先在栈上分配的存储空间做为流的缓存。这就产生了问题。为了改正这个问题应该在全局空间静态的(如static或extern),或者动态的为数组分配空间(malloc在堆上分配)。
#include <sys/time.h> #inlcude <sys/resource.h> int getrlimit(int resource, struct rlimit *rlptr); int setrlimit(int resource, const struct rlimit *rlptr); //返回值:成功为0,出错非0. struct rlimit { rlimi_t rlim_cur; /* soft limit: current limit */ rlimi_t rlim_max; /* hard limit:maximum vlaue for rlim_cur */ };
每一个进程都有一组资源限制,其中一些能够用getrlimit和setrlimit函数查询和修改。
对这两个函数的每一次调用都指定一个资源以及一下指向下列结构的指针。
在更改资源限制时,须遵循下列三条规则:
一个无限量的限制一般由常数RLIM_INFINITY指定。
这两个函数的resource参数取下列值之一。并不是全部资源限制都受到SVR4和4.3+BSD的支持。
资源限制将影响到调用进程并由其子进程继承。这就意味着为了影响一个用户的全部后续进程,需将资源限制设置构造在shell中。