进程环境涉及到的主要内容包括:程序执行时,main函数如何被调用;命令行参数如何传递给新程序;典型的存储空间布局;如何分配另外的存储空间;如何使用环境变量;进程的终止方式;longjmp和setjmp函数以及它们与栈的交互等。
C程序总是从main函数开始执行。Main函数的原型是:
Int main(int agrc, char *argv[]);
其中,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组。
当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编译器设置的,而连接编辑器则由C编译器调用。启动例程从内存取得命令行参数和环境变量值,然后按上述方式调用main函数做好安排。
有8种方式使进程终止(termination),其中5种为正常终止,它们是:
² 从main返回
² 调用exit
² 调用_exit或_Exit
² 最后一个线程从启动例程返回
² 从最后一个线程调用pthread_exit
异常终止有3种方式:
² 调用aboart
² 接到一个信号
² 最后一个线程对取消请求做出响应
注意:内核使程序执行的唯一方法是调用exec函数。进程自愿终止的唯一方法是显示或隐式地(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。
当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。这是UNIX shell的一部分常规操作。
每个进程都接收到一个环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址。历史上,大多数UNIX系统支持main函数带3个参数,其中第3个参数就是环境表地址。但由于其未带来更多溢出,所以不使用第三个参数。通常用getenv和setenv函数来访问特定的环境变量,而不是用environ变量。但是如果要查看整个环境,则必须使用environ指针。
C程序一直由下列几部分组成:
² 正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器,C编译器和shell等)在存储器中也需有一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其指令。
² 初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。例如,C程序中任何函数之外的声明,使变量的初值放在初始化数据段中。
² 未初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序一个操作符,意思是“由符号开始的块”(block started by symbol),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。函数外的声明,使变量存放在非初始化数据段中。
² 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息(如某些机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时分配存储空间。通过以这种方式使用栈,C递归函数可以工作。递归函数每次调用自身时,就用一个新的栈帧,因此一次函数调用实例中的变量集不会影响另一次函数调用实例中的变量。
² 堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。
对于32位Intel x86处理器上的Linux,正文段从0x08048000单元开始,栈底则在0xC0000000之下开始(在这种特定结构中,栈从高地址向低地址方向增长)。堆顶和栈顶之间的未用虚地址空间很大。
未初始化的数据段的内容不存放在磁盘程序文件中。其原因是,内核在程序开始运行前将它们都设置为0。需要存放在磁盘程序文件中的段只有正文段和初始化数据段。
现在,大多数UNIX系统支持共享库。共享库使得可执行文件中不再需要包含公用的库函数,而只需要在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在改程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新连接编辑(假定参数的数目和类型都没有发生改变)。
在不同的系统中,程序可能使用不同的方法说明是否要使用共享库。比较典型的有cc和ld命令的选项。
ISO C说明了3个用于存储空间动态分配的函数
² Malloc,分配指定字节数的存储区。此存储区中的初始值不确定
² Calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位(bit)都初始化为0
² Realloc,增加或减少以前分配区的长度。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值不确定
这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何对象数据。
因为这3个alloc函数都返回通用指针void *,所以如果在程序中包含了#include
函数free释放ptr指向的存储空间。被释放的空间通常被送入可用存储池,以后,可在调用上述3个分配函数时在分配。
Realloc函数使我们可以增、减以前分配的存储区的长度(最常见的用法是增加该区)。如,如果先为一个数组分配存储空间,但运行一段时间后,发现空间不够用了,此时可调用realloc扩充相应存储空间。如果在该存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充,无需移动任何原先的内容,并返回与传给他相同的指针值。如果在原存储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的元素数组的内容复制到新分配的存储区。然后,释放原存储区,返回新分配区的指针。因为这种存储区可能会移动位置,所以不应当使任何指针指在该区中。
注意:realloc的最后一个参数是存储区的新长度,不是新旧存储区长度之差。作为一个特例,若ptr是一个空指针,则realloc的功能与malloc相同,用于分配一个指定长度为newsize的存储区。
大多数实现所分配的存储区空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等。这意味着,如果超过一个已分配区的尾端或者已分配区起始位置之前进行写操作,则会改写另一块的管理记录信息。这种类型的错误是灾难性的,但是因为这种错误不会很快的暴露出来,所以很难被发现。
其他可能产生的致命性错误是:释放一个已经释放了的块;调用free时所用的指针不是3个alloc函数的返回值等。若一个进程调用malloc函数,但却忘记调用free函数,那么该进程占用的存储空间就会连续增加,这被称为泄露(leakage)。但如果不调用free函数释放不再使用的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性能下降。
因为存储空间分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现版本。每次用这3个分配函数中的任意一个或free时,他们都进行附加的检错。
替代存储空间分配程序
有很多替代malloc和free的函数。某些系统已经提供存储空间分配函数的库。另一些系统只提供标准的存储空间分配程序。如需要,软件开发工程中可下载替代函数。下面是一些替代的库函数和库:libmalloc,vmalloc,quick-fit,jemalloc,TCMalloc,函数alloca等。
环境变量的字符串形式是:name = value
UNIX内核并不查看这些字符串,他们的解释完全取决于各个应用程序。通常在一个shell启动文件中设置环境变量以控制shell的动作。可以通过getenv函数取环境变量值,也可通过setenv设置环境变量值
环境表(指向实际name = value字符串的指针数组)和环境字符串常量通常存放在进程存储空间的顶部(栈之上)。删除一个字符串很简单——只要先在环境表中找到该指针,然后将所有后续指针都向环境表首部顺序移动一个位置。但是增加一个字符串或修改一个现有字符串就困难得多。环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向(向上)扩展;同时也不能移动在它之下的各栈帧,所以它也不能向低地址方向(向下)扩展。两者结合使得该空间的长度不能再增加。
² 如果修改一个现有name:
n 如果新value的长度小于或等于现有value的长度,则只要将新字符串复制到原字符串所用的空间中;
n 如果新value的长度大于原来长度,则必须调用malloc为新字符串分配空间,然后将新字符串复制到该空间中,接着使环境表中针对name的指针指向新分配区。
² 如果增加一个新的name,则操作就更加复杂。首先,必须调用malloc为name = value字符串分配空间,然后将该字符串复制到此空间中。
n 如果这是第一次增加一个新name,则必须调用malloc为新的指针分配空间。接着,将原来的环境表复制到新分配区,并将指向新name=value字符串的指针存放在该指针表的表尾,然后又将一个空指针存放在其后。最后使environ指向新指针表。如果原来的环境表位于栈顶之上(常见情况),那么必须将此表移植堆中。但是,此表中的大多数指针扔指向栈顶之上的各name=value字符串。
n 如果这不是第一次增加一个新name,则可知以前已调用malloc在堆中为环境表分配了空间,所以只要调用realloc,以分配比原空间多存放一个指针的空间。然后将指向新name=value字符串的指针存放在该表表尾,后面跟着一个空指针。
在C中,goto语句是不能跨越函数的,而执行这种类型跳转功能的函数是setjmp和longjmp。这两个函数对于处理发生在很深层嵌套函数调用中的出错情况非常有用。
非局部goto中的非局部指的是:不是由普通的C语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回当前函数调用路径上的某一个函数中。
每个进程都由一组资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改。对这两个函数的每一次调用都指定一个资源以及下一个指向下列结构的指针。
更改资源限制时,须遵循下列3条规则。
² 任何一个进程都可将一个软限制更改为小于或等于其硬件限制值。
² 任何一个进程都可降低限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆的
² 只有超级用户进程可以提高硬限制值
资源限制影响到调用进程并由子进程继承。这意味着,为了影响一个用户的所有后续进程,需要将资源限制的设置构造在shell中。C Shell中有内置limit命令(umask和chdir函数也必须是shell内置的)。