Unix之进程环境(七)

本文主体内容来自《UNIX环境高级编程第三版》。

本章中将学习:当程序执行时,其main函数是如何被调用的;命令行参数是如何传递给新程序的;典型的存储空间布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;进程的各种不同终止方式等。另外,还将说明longjmp和setjmp函数以及它们与栈的交互作用。本章结束之前,还将查看进程的资源限制。


main函数

C程序总是从main函数开始执行。main函数的原型是:int main(int argc, char *argv[]);

其中,argc 是命令行参数的数目,argv 是指向参数的各个指针所构成的数组。

当内核执行C程序时,在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。


进程终止

有8种方式使进程终止(termination),其中5种为正常终止,它们是:

(1)从 main 返回;

(2)调用 exit

(3)调用 _exit_Exit

(4)最后一个线程从其启动例程返回;

(5)从最后一个线程调用 pthread_exit

异常终止有3种方式,它们是:

(6)调用 abort

(7)接到一个信号;

(8)最后一个线程对取消请求做出响应。

上节提及的启动例程是这样编写的,使得从main返回后立即调用 exit 函数。如果将启动例程以C代码形式表示(实际上该例程常常用汇编语言编写),则它调用main函数的形式可能是:exit(main(argc, argv));

退出函数

3个函数用于正常终止一个程序:_exit_Exit 立即进入内核,exit则先执行一些清理处理,然后返回内核。

1
2
3
4
5
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

由于历史原因,exit 函数总是执行一个标准 I/O 库的清理关闭操作:对于所有打开流调用 fclose 函数。这造成输出缓冲中的所有数据都被冲洗(写到文件上)。

3个退出函数都带一个整型参数,称为终止状态(或退出状态,exit status)。大多数UNIX系统shell都提供检查进程终止状态的方法。如果(a)调用这些函数时不带终止状态,或(b)main执行了一个无返回值的return语句,或(c)main没有声明返回类型为整型,则该进程的终止状态是未定义的。但是,若main的返回类型是整型,并且main执行到最后一条语句时返回(隐式返回),那么该进程的终止状态是0。

main函数返回一个整型值与用该值调用exit是等价的。于是在main函数中exit(0);等价于return(0);

函数atexit

按照ISO C的规定,一个进程可以登记多至32个函数,这些函数将由 exit 自动调用。我们称这些函数为终止处理程序(exit handler),并调用 atexit函数来登记这些函数。

1
2
3
#include <stdlib.h>
int atexit(void (*func)(void));
// 返回值:若成功,返回0;若出错,返回非0

其中,atexit 的参数是一个函数地址,当调用此函数时无需向它传递任何参数,也不期望它返回一个值。exit 调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,也会被调用多次。

根据ISO C和POSIX.1,exit首先调用各终止处理程序,然后关闭(通过fclose)所有打开流。POSIX.1扩展了ISO C标准,它说明,如若程序调用exec函数族中的任一函数,则将清除所有已安装的终止处理程序。下图显示了一个C程序是如何启动的,以及它终止的各种方式。

img

注意,内核使程序执行的唯一方法是调用一个 exec 函数。进程自愿终止的唯一方法是显式或隐式地(通过调用 exit)调用_exit_Exit。进程也可非自愿地由一个信号使其终止。


命令行参数

当执行一个程序时,调用 exec的进程可将命令行参数传递给该新程序。


环境表

每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量 environ 则包含了该指针数组的地址:extern char **environ;

例如,如果该环境包含5个字符串,那么它看起来如下图所示。其中,每个字符串的结尾处都显式地有一个null字节。我们称 environ 为环境指针(environment pointer),指针数组为环境表,其中各指针指向的字符串为环境字符串。

img

按照惯例,环境由 name = value 这样的字符串组成。大多数预定义名完全由大写字母组成,但这只是一个惯例。


C程序的存储空间布局

历史沿袭至今,C程序一直由下列几部分组成:

  • 正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器、C编译器和shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其指令。

  • 初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。例如, C程序中任何函数之外的声明:int maxcount = 99;

  • 未初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序一个操作符,意思是“由符号开始的块”(block started by symbol),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。函数外的声明: long sum[1000];使此变量存放在非初始化数据段中。

  • 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息(如某些机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C递归函数可以工作。递归函数每次调用自身时,就用一个新的栈帧,因此一次函数调用实例中的变量集不会影响另一次函数调用实例中的变量。

  • 堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。

下图显示了这些段的一种典型安排方式。这是程序的逻辑布局,虽然并不要求一个具体实现一定以这种方式安排其存储空间,但这是一种我们便于说明的典型安排。

img

未初始化数据段的内容并不存放在磁盘程序文件中。其原因是,内核在程序开始运行前将它们都设置为 0。需要存放在磁盘程序文件中的段只有正文段和初始化数据段。

size 命令报告正文段、数据段和bss段的长度(以字节为单位)。例如:

1
2
3
4
size /usr/bin/cc /bin/sh
text  data   bss   dec  hex  filename
346919  3576  6680  357175  57337  /usr/bin/cc
102134  1776  11272  115182  1c1ee  /bin/sh

第4列和第5列是分别以十进制和十六进制表示的3段总长度。


共享库

共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新编译连接(假定参数的数目和类型都没有发生改变)。


存储空间分配

ISO C说明了3个用于存储空间动态分配的函数。

(1)malloc,分配指定字节数的存储区。此存储区中的初始值不确定。

(2)calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位(bit)都初始化为0。

(3)realloc,增加或减少以前分配区的长度。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定。

1
2
3
4
5
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
// 3个函数返回值:若成功,返回非空指针;若出错,返回NULL

注意,realloc 的最后一个参数是存储区的新长度,不是新、旧存储区长度之差。作为一个特例,若 ptr 是一个空指针,则 realloc 的功能与 malloc 相同,用于分配一个指定长度为newsize的存储区。


环境变量

如同前述,环境字符串的形式是:name=value

UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序。例如,shell使用了大量的环境变量。其中某一些在登录时自动设置(如HOME、USER等),有些则由用户设置。我们通常在一个shell启动文件中设置环境变量以控制shell的动作。

ISO C定义了一个函数getenv,可以用其取环境变量值,但是该标准又称环境的内容是由实现定义的。

1
2
3
#include <stdlib.h>
char *getenv(const char *name);
// 返回值:指向与name关联的value的指针;若未找到,返回NULL

注意,此函数返回一个指针,它指向 name=value 字符串中的value。

下图列出了由Single UNIX Specification定义的环境变量。注意,ISO C没有定义任何环境变量。

img

除了获取环境变量值,有时也需要设置环境变量。我们可能希望改变现有变量的值,或者是增加新的环境变量。(我们能影响的只是当前进程及其后生成和调用的任何子进程的环境,但不能影响父进程的环境,这通常是一个shell进程。尽管如此,修改环境表的能力仍然是很有用的。)下图列出了由不同的标准及实现支持的各种函数。

img

clearenv 被用来删除环境表中的所有项。中间3个函数的原型是:

1
2
3
4
5
6
7
#include <stdlib.h>
int putenv(char *str);
// 函数返回值:若成功,返回0;若出错,返回非0

int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
// 两个函数返回值:若成功,返回0;若出错,返回−1

这3个函数的操作如下。

  • putenv 取形式为 name=value 的字符串,将其放到环境表中。如果 name 已经存在,则先删除其原来的定义。

  • setenvname 设置为 value。如果在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则不删除其现有定义(name不设置为新的value,而且也不出错)。

  • unsetenv删除name的定义。即使不存在这种定义也不算出错。


函数setjmp和longjmp

在C中,goto语句是不能跨越函数的,而执行这种类型跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在很深层嵌套函数调用中的出错情况是非常有用的。解决这种问题的方法就是使用非局部goto——setjmplongjmp 函数。非局部指的是,这不是由普通的C语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。

1
2
3
4
#include <setjmp.h>
int setjmp(jmp_buf env);
// 返回值:若直接调用,返回0;若从longjmp返回,则为非0
void longjmp(jmp_buf env, int val);

在希望返回到的位置调用 setjmpsetjmp 参数 env 的类型是一个特殊类型 jmp_buf。这一数据类型是某种形式的数组,其中存放在调用 longjmp 时能用来恢复栈状态的所有信息。因为需在另一个函数中引用 env 变量,所以通常将 env 变量定义为全局变量。


函数getrlimit和setrlimit

每个进程都有一组资源限制,其中一些可以用 getrlimitsetrlimit 函数查询和更改。

1
2
3
4
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
// 两个函数返回值:若成功,返回0;若出错,返回非0

进程的资源限制通常是在系统初始化时由0进程建立的,然后由后续进程继承。

对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针。

1
2
3
4
struct rlimit {
rlim_t rlim_cur; /* soft limit: current limit */
rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */
};

在更改资源限制时,须遵循下列3条规则。

(1)任何一个进程都可将一个软限制值更改为小于或等于其硬限制值。

(2)任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆的。

(3)只有超级用户进程可以提高硬限制值。

常量 RLIM_INFINITY 指定了一个无限量的限制。

这两个函数的 resource 参数取下列值之一。

img

RLIMIT_AS 进程总的可用存储空间的最大长度(字节)。这影响到 sbrk 函数和 mmap 函数。

RLIMIT_CORE core 文件的最大字节数,若其值为0则阻止创建core文件。

RLIMIT_CPU CPU时间的最大量值(秒),当超过此软限制时,向该进程发送 SIGXCPU 信号。

RLIMIT_DATA 数据段的最大字节长度。这是初始化数据、非初始以及堆的总和。

RLIMIT_FSIZE 可以创建的文件的最大字节长度。当超过此软限制时,则向该进程发送 SIGXFSZ 信号。

RLIMIT_MEMLOCK 一个进程使用 mlock 能够锁定在存储空间中的最大字节长度。

RLIMIT_MSGQUEUE 进程为POSIX消息队列可分配的最大存储字节数。

RLIMIT_NICE 为了影响进程的调度优先级,nice 值可设置的最大限制。

RLIMIT_NOFILE 每个进程能打开的最多文件数。

RLIMIT_NPROC 每个实际用户 ID 可拥有的最大子进程数。

RLIMIT_NPTS 用户可同时打开的伪终端的最大数量。

RLIMIT_RSS 最大驻内存集字节长度(resident set size in bytes,RSS)。如果可用的物理存储器非常少,则内核将从进程处取回超过RSS的部分。

RLIMIT_SBSIZE 在任一给定时刻,一个用户可以占用的套接字缓冲区的最大长度(字节)。

RLIMIT_SIGPENDING 一个进程可排队的信号最大数量。这个限制是 sigqueue 函数实施的。

RLIMIT_STACK 栈的最大字节长度。

RLIMIT_SWAP 用户可消耗的交换空间的最大字节数

RLIMIT_VMEM 这是 RLIMIT_AS 的同义词。

资源限制影响到调用进程并由其子进程继承。这就意味着,为了影响一个用户的所有后续进程,需将资源限制的设置构造在shell之中。