我们知道C程序总是从main函数开始执行,main函数的原型如下:
int main(int argc, char *argv[]);
其中int是main函数的类型,虽然旧的编译器使用void定义,或者不声明main的类型也可以编译,但是那是不好的做法,根据 ISO C和POSIX.1 的定义都应该将main显式声明为 int 类型的。argc是命令行参数的数目,argv是指向参数的各个指针构成的数组。与众面向对象语言不同,C需要显式的 argc 传递参数的个数,因为仅凭 argv 不能确定其大小。
当内核执行C程序时,在调用 main 前先调用了一个特殊的启动例程,可执行程序文件将此例程指定为程序的起始地址–这是有连接编辑器设置的,而连接编辑器则由C编译器调用,启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排1。
进程终止进程有多种退出运行的方式,最常用的是从main函数返回,或者main函数执行到结束。所有的进程终止的方式总结如下,其中前5种正常终止,后三种是异常中止:
从main返回
调用 exit()
调用 _exit() 或者 ——Exit()
最后一个线程从其启动例程返回
最后一个线程调用 pthread_exit
调用 abort
接到一个信号
最后一个线程对取消请求作出响应
退出函数 exit 也是很常用的, _exit 和 _Exit 则不太常用,它们之间的区别是 exit 会先执行一些清理操作,比如对所有打开的文件调用 fclose 函数,刷新输出缓冲等,然后在如内核,_exit 和 _Exit 则是立即进入内核的。还有一个区别是它们包含在不同的头文件中,exit 和 _Exit 包含在中, _exit 包含在中(因为前两者是ISO C说明的,而后者是POSIX.1说明的)。
这些终止函数都是用一个整型的状态码作为参数,称为终止状态(exit status)。C99 规定没有显示调用return而main执行到最后一个语句时返回,那么进程的终止状态是0,在之前的标准这种情况是为定义,所以返回值可能是随机的。我们的应该以C99为标准2。
atexit按照 ISO C的规定,一个进程可以登记最多32个(具体实现可能更多)由exit自动调用的函数,这些函数称为终止处理程序,调用atexit函数来登记这些函数。#include int atexit(void (*func)(void)); 还记得之前介绍的函数指针吗,atexit函数的参数的类型就是一个函数指针(函数地址),其返回值和参数都是 void 。注意:exit调用这些登记了的函数的顺序与它们登记的顺序相反,同一函数若登记多次,则会被调用多次。
一个C程序的启动和终止流程:
可以看出内核使程序执行的唯一方法是调用一个exec函数。
命令行参数命令行参数其实我们之前已经使用过了,基本了解了,对于Java和Go中的命令行参数方式也有所了解:Java通过一个String数组获取命令行参数,而Go则通过设置 flag 可以非常方便地获取特定的参数。
C的命令行参数保存字啊 main函数的第二个参数,char **argv或者 (char *argv[])中,通过第一个参数 argc 获得参数的个数。如果要想Go那样获取特定形式的参数则需要自己对 argv 数组进行一些处理。
在ISO C 和 POSIX.1 中,都要求 argv[argc] 是一个空指针(这由C启动例程保证),所以对argv的遍历也可以不借助 argc 的值。
for (int i=0; iargv[i] != NULL; i++) { ...}
环境变量每个程序都自动接受(获得)一张环境表,环境表也是一个字符指针数组(字符串数组),全局变量environ包含了该指针数组的地址。定义为:extern char **environ;。要想在代码中使用这个数组,需要前面的声明,否则 environ 是一个非定义的符号。
按照惯例,环境由name=value这样形式的字符串组成,大多数预定义名完全由大写字母组成,但是不保证全部是这样。
ISO C定义了一些函数对环境变量进行读写相关的操作:
#include char *getenv(const char *name);int putenv(char *str);// rewrite非0则覆盖已存在的定义,0则不删除现有定义int setenv(const char *name, const char *value, int rewrite); int unsetenv(const char *name);
C程序的存储空间布局典型的C程序的内存布局如下图所示:
上图说明:
文本段(Text Segment),保存CPU将要执行的机器指令。文本段是可共享的,所以某个程序多次执行时,对应的文本段只需要在内存中存有一份拷贝。文本段是只读的(read-only),防止程序的指令被修改。
已初始化数据段(initialized data segment),保存程序中被初始化的全局变量(定义在任何函数之外)。例如:int maxcount = 99; 全局变量变量maxcount被保存在初始化数据段。
未初始化数据段(uninitialized data segment),也被称为BSS(block started by symbol),这个段中的数据在程序执行之前被内核初始化为0或者null。;例如定义一个全局变量(定义在任何函数之外),long sum[1000]; 该变量保存在未初始化数据段中。
栈(Stack):存储临时变量,函数相关信息。当一个函数被调用时,返回地址、调用者相关信息(如寄存器信息)会被保存在栈中。该被调用的函数会在栈上分配一部分空间保存它的临时变量。函数的递归调用也是应用这个原理。每一次函数调用自己,都会保存当前函数的信息,然后再栈上开辟一个新的空间用于保存该次函数的信息,和以前的函数并没有影响。
堆(Heap):动态内存分配位置。堆的位置位于未初始化数据段和栈的中间3。
存储空间分布ISO C说明了3个用于存储空间动态分配的函数:
malloc分配指定字节数的存储区,存储区的初始值不确定
calloc为指定数量,指定长度的对象分配存储空间,该空间中,每一位都初始化为0
realloc增加或减少以前分配区的长度,当增加长度时,可能将以前分配的内容移到另一个足够大的区域以便在尾端增加存储区,新增的存储区的初始值不确定
它们的函数声明如下:
#include void *malloc(size_t size);void *calloc(size_t nobj, size_t size);void *realloc(void *ptr, size_t newsize);void free(void *ptr);
关于它们返回值的赋值有一个要注意的地方,参见这里。要注意 realloc函数的第二个参数是存储区的新长度,而不是新旧存储区的长度之差。
这些存储区分配函数通常用sbrk系统调用实现,该系统调用扩充(或缩小)进程的堆,虽然 sbrk 可以扩充或者缩小进程的存储空间,但是大多数 malloc 和 free的实现都不减少进程的存储空间,释放的空间可供以后再分配,但是将它们保持在 malloc 池中,而不是返还给内核。
大多数动态分配函数的实现实际分配的空间比所请求的要大一些,额外的空间用于记录管理信息,比如分配块的长度,指向下一个分配块的指针等。如果在超多分配去尾端或者在已分配区开始位置之前进行写操作,会修改另一块的管理记录信息,这导致的错误是灾难性的,可恶的是这中错误很难发现。在动态分配的缓冲区的前或后进行写操作,破坏的可能不仅仅是该区的管理记录信息,这些区域可能用于其它动态分配的对象,这些对象因此可能被破坏,而且很难追查到原因。
另一个导致致命错误的是:释放一个已经释放的块,或者free的参数的指针不是由上面的函数分配的对象。对一个对象调用 free 后,这个指针的值实际上没有改变,它仍然在作用域内,如果该指针指向的地址被重新分配了,对它再进行free就会导致预料之外的行为。
如果一个分配的区域没有调用free则会导致进程占用的存储空间越来越大,导致泄漏(即时是在调用函数中分配的空间,没有free的话在函数调用结束也不会自动释放)。