第七章〓Unix进程的环境? 7?1〓引言? 下一章将介绍进程控制原语,在此之前需先了解进程的环境。在本章中我们将说明 :当执行 一通程序时,其main函数是如何被调用的,命令行参数是如何传送给执行程序的; 典型的存 储器布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;进程终 止的不同 方式等。另外,我们也将说明longjmp和setjmp函数以及它们与栈的交互作用。在 本章结束 之前,我们将查看进程的资源限制。? 7?2〓main函数? 一道C程序总是从main函数开始执行。main函数的原型是:? int main (int argc,char * argv[]);? 其中,argc是命令行参数数,argv是指向参数的各个指针所构成的数组。在7?4节 中我们将 对命令行参数进行说明。? 在系统核起动一道C程序时(用一个exec函数,我们将在8?9节中说明exec函数), 在调用mai n前先调用一个特殊的起动例程。可执行程序文件将此起动例程指定为程序的起始 地址〖CD2 〗这是由连接编辑程序设置的,而连接编辑程序则由C编译程序(通常是cc)调用。 起动例程 从系统核取得命令行参数和环境变量值,然后为调用main函数作好安排。? 7?3〓进程终止? 有五种方式使进程终止:? 1?正常终止? (a)从main返回? (b)调用exit? (c)调用 迹茫模*常病絜xit? 2?异常终止? (a)调用abort(第十章)? (b)由一个信号终止(第十章)? 上节提及的起动例程是这样编写的,使得从main返回后即调用exit函数。如果将起 动例程以 C代码工表示(实际上该例程常常用汇编语言编写),则它调用main函数的形式可能 是:? exit(main(argc,argv));? exit和 迹茫模*常病絜xit函数? exit和 迹茫模*常病絜xit函数正常终止一个程序: 迹茫模*常病絜xit立即进入 系统核, exit则先执行一些清除处理(包括的用执行各终止处P2程序,关闭所有标准I/O流 等)? #include ? void exit(int ?status);?? #include ? void 迹茫模*常病絜xit (int ?status);?? 我们将在8?5节讨论这两个函数对其它进程,例如终止进程的父、子进程的影响。 ? 使用不同头文件的原因是:exit是由ANSIC说明的,而 迹茫模*常病絜xit则是由 Posix?1 说明的。? 由于历史原因,exit函数总是执行一个标准I/O库的清除关闭操作:对于所有打开 流调用fcl ose函数。回忆5?5节,这造成所有在缓存中的数据都被刷新(写到文件上)。? exit和 迹茫模*常病絜xit都带一个整型参数,我们称此为终止状态。大多数Uni x Shell都 提供检查一个 进程终止状态的方法。如果(a)若调用这些函数时不带终止状态,或(b)main执行了 一个无返 回值的return语句,或(c)main执行隐式返回,则该进程的终止状态是末定义的。 这就意味 着,下列经典性的C语言程序? #indude ? main ()? {? 〓〓printf ("hello,world \n");? }? 是不完整的,因为main函数没有使用return语句返回(隐式返回),它在返回到C的 起动例程 时并没有返回一个值(终止状态)。另外,若使用? return(0);? 或者? exit(0);? 则向启动执行此程序的进程(常常是一个shell进程)返回终止状态0。另外,main函 数的说明 实际上应当是:? int main(void)? 在下一章,我们针会了解到一个进程如何一道程序执行,如何等待执行该程序的进 程完成, 然后取得其终止状态。? 将main说明为返回一个整型以及用exit代替return,对某些C编译程序和Unix Lin t(1)程序 而言会产生不必要的警告信息。问题是这些编译程序并不了解在main中的exit与r eturn语句 的作用相同。警告信息可能是"control reaches end of nonvoid function。"(控 制到达 非void函数的结束处),避开这种警告信息的一种方法是:在main中使用return语 句而不是e xit。但是这样做的结果是使我们不能用Unix。? grep公用程序来找出一道程序中的所有exit调用。另外一个解决方法是将main说明 为返回vo id而不是int,然后的旧调用exit。这也避开了编译程序的警告,但从程序设计角 度看却并 不正确。在本章中,我们将main表示为返回一个整型,因为这是ANSIC和POSIX?1 所定义的 。我们将不理会编译程序不必要的警告。? atexit函数? 按照ANSIC的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用 。我们称 这些函数为终止处理程序,并用atexit函数来登记这些函数。? #include ? int atexit(void (??func)?(void));? Returns:0 if OK,nonzero on error返回:若成功为0,出错为非0? 其中,参数(*func)(void)是一个函数地址,当调用此函数时无需向它传送任何参 数,也不 期望它返回一个值。exit以登记这些函数的相反顺序调用它们。同一函数如若登记 多次,则 也被调用多次。? 终止处理程序这一机制是由ANSIC新引进的。SVR4和4?3+BSD都提供这种机制。系 统V的早期 版本和4?3BSD则都不提供此机制。? 按照ANSIC和POSIX?1,exit首先调用各终止处理程序,然后按需多次调用fclose ,关闭所 有打开流。图7?1摘要显示了一个C程序是如何起动的,以及它终止的各种方式。 ? P164?? 图7?1〓一个C程序是如何起动和终止的? 注意,系统核使一道程序执行的唯一方法是调用一种exec函数。一个进程自愿终止 的唯一方 法是显示或隐式地(调用exit)调用 迹茫模*常病絜xit。一个进程也可非自愿地由 一个信号 使其终止(在图7?1中没有显示)。? 实例? 程序7?1说明了如何使用atexit函数。执行程序7?1产生:? $ a?out? main is done? first exit handler? first exit handler? second exit handler? 注意,在main中我们没有调用exit,而是用了return语句。? P165?? 程序7?1〓终止处理程序的实例? 7?4〓命令行参数? 当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。这是Unix shell的一 部分常规操作。在前几章的很多实例中,我们已经看到了这一点。? 实例? 程序7?2将其所有命令行参数都回送到标准输出上(Unix echo(1)程序不回送第0个 参数。) 编译此程序,并将其可执行代码文件定名为echoarg,则? $ ?/echoarg arg1 TEST foo? argv[0]: ?/echoarg? argv[1]: arg1? argv[2]: TEST? argv[3]: foo?? P166? 程序7?2〓将所有命令行参数回送到标准输出? ANSIC和POIX?1都要求argv[argc]是一个空指针。这就使我们可以将参数处理循 环改写为 :? for(i=0;arg[i]!=NULL;i++)? 7?5〓环境表? 每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其 中每个指 针包含一个以null符结束的字符串的地址。全局变量environ则包含了该指针数组 的地址。 ? extern char **environ;? 例如:如果该环境包含了五个字符串,那么它看起来可能如图7?2中所示。? P166?? 图7?2〓由五个字符串组成的环境? 其中,在每个字符串的结束处都有一个null字符。我们称environ为环境指针,指 针数组为 环境表,其中各指针指向的字符串为环境字符串。? 按照惯例,环境由? name=value? 这样的字符串组成,这与图7?2中所示相同。大多数予定义名是完全由大写字母组 成的,但 这只是一个惯例。? 在历史上,大多数Unix系统对main函数提供了第三个参数,它就是环境表地址:?
int main(int argc,char *argv[],char *envp[]);? 因为ANSIC规定main函数只有两个参数,而且第三个参数与全局变量environ相比也 没有带来 更多益处,所以POSIX?1也规定应使用environ而不使用第三个参数。通常用gete nv和puten v函数(在7?9节中说明)来存取特定的环境变量,而不是用environ变量。但是,如 果要查看 整个环境,则必须使用environ指针。? 7?6〓C程序的存储空间 季知?nbsp; 由于历史原因,C程序一直由下列几部分组成:? ·正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使 是经常执 行的程序(文本编辑程序、编译程序、shell等)在存储器中也只需有一个副本。另 外,正文 段常常是只读的,以防止一道程序由于忌外子故而修改其自身的指令。? ·初始化数据段。通常将此段就称为数据段,它包含了程序中需赋初值的变量。例 如:C程 序中在任何函数之外的说明:? int maxcount=99;? 使此变量以初值存放在初始化数据段中。? ·非初始化数据段。通常将此段称为"bss"段,这一名称来源于早期汇编程序的一 个操作 符,意思是"block started by symbol"(由符号开始的块),在程序开始执行之前 ,系统 核将此段初始化为0。在函数外的说明? long sum[1000];? 使此变量存放在非初始化数据段中。? ·栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调 用时,其 返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新 被调用的 函数在栈上为其自动和临时变量分配存储空间。以这种方式使用栈,C函数可以递 归调用。 ? ·堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数 段顶和栈 底之间。? 图7?3显示了这些段的一种典型安排方式。这是一个程序的逻辑布局一虽然并不要 求一个具 体实现一定以这种方式安排其存储空间。尽管如此,这给出了一个我们便于作有关 说明的一 种典型安排。??? P168? 图7?3〓典型的存储器安排? 对于在VAX上的4?3+BSD,正文段在0号单位开始,栈顶则在O?x7fffffff之下开始 。在VAX 机器上,在堆顶和栈底之间未用的虚地址空间是很大的。? 从图7?3还勺⒁獾侥┏跏蓟??荻蔚哪谌莶⒉淮娣旁诖排坛绦蛭募?小P枰?娣?nbsp; 在磁盘程 序文件中的段只有正文段和初始化数据段。? size(1)命令报告正文段、数据段和bass段的长度(单位:字节)。例如:? $ 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列是分别以十进制和十六进制表示的总长度。? 7?7〓共享库? 现在,很多Unix系统支持共享库。Arnold[1986]说明了一个在系统V上共享库的 一个早期 实 现,Gingell等[1987]则说明了一个在SunOs上的另一个实现。共享库使得可 执行文件 中不再需要包含常用的库函数,而只需在所有进程都可存访的存储区中保存这种库 例程的一 个副本。在程序第一次执行或者第一次调用每个库函数时,用动起连接方法将程序 与共享库 函数相连接。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使 用该库的 程序重新连接编辑。(假定参数数和类型都没有发生改变。)? 不同的系统使用了不同的方法使程序可以说明是否要使用共享库。CC(1)和ld(1)命 令的选择 项是典型的。作为长度方面发生变化的例子,下列可执行文件(典型的hello?C程 序)先用无 共享库方式创建:? $ ls -1 a?out? -rwxrwxr-x〓1〓stevens〓〓104859 Aug〓2 14:25 a?out? $ size a?out? text〓data〓bass〓dec〓hex? 49152〓49152〓0〓98304〓18000? 如果我们再编译此程序使其使用共享库,则可执行文件的正文和数据段的长度都显 著减少; ? $ ls -1 a?out? -rwxrwxr-x〓1 stevens〓〓24576 Aug〓2 14:26 a?out? $ size a?out? text〓data〓bass〓dec〓hex? 8192〓8192〓0〓16384〓4000? 7?8〓存储器分配? ANSIC说明了三个用于存储空间动态分配的函数? 1? malloc。分配指定字节数的存储区。在此存储区中的初始值是不确定的。? 2? calloc。为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的 每一位(b it)都初始化为0。? 3? realloc。更改以前分配区的长度(增加或减少)。当增加长度时,可能需将以 前分配区 的内容移到另一个足够的大区域,而新增区域内的初始值则不确定。? #include ? void *malloc(size 迹茫模*常病健t—玸ize);?? void *calloc(size 迹茫模*常病健t—玭obj,?size 迹茫模*常病絫 ?size); ?? void *realloc(void ??ptr,?size 迹茫模*常病絫 ?newsize);?? All three return:nonnull pointer if OK,NULL on error? 三个函数返回:若成功为非空指针,出错为NULL? void free(void *?ptr)?? 这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。例如 ,在一个 特定的系统上,如果最苛刻的对齐要求是double,则对齐必须在8的倍数的地址单 元处,那 么这三个函数返回的指针都应这样对齐。? 回忆我们在1?6节中对类属void *指针和函数原型的讨论。因为这三个alloc函数 都返回类 属指针,如果在程序中包括了(包含了函数原型),那么当我们将这些 函数返回 的指针赋与一个不同类型的指针时,不需要作类型强制转换。? 函数free释放ptr所指向的存储空间。这种被释放的空间通常被送入可用存储区池 ,以后可 在调用分配函数时再分配。? realloc使我们可以增、减以前分配区的长度(最常见的用法是增加该区。)例如, 如果先分 配一个可容纳长度为512的数组的空间,然后在运行时填充它,然后又发现该数组 的长度还 不敷应用,则可调用realloc要求扩充该存储空间。如果在该存储区后有足够空间 可供扩充 ,则就在原存储区位置上向高地址方向扩充,并返回传送给它的同样的指针值。如 果在原存 储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的512个元 素数组的 内容复制到新分配到的存储区。因为这种存储区可能会移动位置,所以不应当使用 任何指针 指在该区中。练习4?18显示了在getcwd中如何使用realloc,以处理任何长度的路 程名。程 序15?27是使用realloc的另一个例子,用其可以避免使用编译时固定长度的数组 。? 注意,realloc的最后一个参数是存储区的newsize(新长度),不是新、旧长度之差 。作为 一 个特例,若ptr是一个空指针,则realloc的功能与malloc相同,分配一个指定长度 newsize 的存储区。? 此功能是由ANSIC新引进的。如果传送一个null指针,realloc的较老版本会失败。 较老版本 的这些例程允许我们realloc一块,该块是自上次malloc,realloc或calloc以来我 们所释放 的。这种技巧可回溯到Version7,它利用了malloc实现存储器紧缩的搜索策略。4 ?3+B5D仍 支持这一功能,而SVR4则不支持。这种功能不应再使用。? 这些分配例程通常是用sbrk(2)系统调用实现的。该系统调用扩充(或缩小)进程的 堆。(参见 图7?3)。malloc和fiee的一个样本实现请见kennighan和Ritchie[19 福福莸?nbsp; 8?7节。 ? 虽然sbrk可以扩充或缩小一个进程的存储空间,但是大多数malloc和fiee的实现都 不减小进 程的存储空间。释放的空间可供以后再分配,但将它们保持在malloc池中而不返回 给系统核 。? 应当了解的是,大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间 用来记录 管理信息〖CD2〗分配块的长度,指向下一个分配块的指针等等。这就意味着如果 写过了一 个已分配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但 是因为这 种错误小会很快就表露出来,所以也就很难发现。将指向分配块的指针向后移动也 可能会改写本块的管理信息。? 其它可能产生的致命性的错误是:释放一个已经释放了的块;调用fiee时所用的指 针不是三个alloc函数的返回值等。? 因为存储器分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现方法 。每次调用 这三个分配函数中的任一个或fiee时都进行附加的出错检验。在调用连接编辑程序 时指定一 个专用库,则在程序中就可使用这种版本的函数。也有公共可用的资源(例如由4? 3+BSD所 提供的),在对其进行编译时使用一个特殊标志就会使附加的运行时检查生效。?
因为存储空间分配程序的操作对某些应用程序的运行时性能是非常重要的,所以某 些系统提 供了附加能力。例如,SVR4提供了名为mallop函数,它使一个进程可以设置一些变 量,用它 们来控制存储空间分配程序的操作。还可使用另一个名为mallinfo的函数,以对存 储空间分 配程序的操作进行统计。请查看你所使用系统的malloc(3)手册实,弄清楚这些功 能是否可 用。? alloca函数? 还有一个函数也值得一提,这就是alloca。其调用序列与malloc相同,但是它是在 当前函数 的栈帧上分配存储空间,而不是在堆中。其优点是:当函数返回时,自动释放它所 使用的栈 帧,所以我们就不必再为释放空间而费心。其缺点是某些系统在函数已被调用后不 能增加栈 帧长度,于是也就不能支持alloca函数。尽管如此,很多软件包函是使用alloca函 数,也有 很多系统支持它。? 7?9〓环境变量? 如同前述,环境字符串的形式是:? name=vulue? Unix系统核并不关心这种字符串的意义,它们的解释完全取决于各个应用程序。例 如,Shel l使用了大量的环境变量。其中某一些在登录时自动设置(HOME,USER等),有些则 由用户设 置。我们通常在一个shell起动文件中设置环境变量以控制shell的动作。例如,若 我们设置 了环境变量MAILPATH,则它就告诉了Bourne shell和Korn shell到那里去查看邮件 。? ANSIC定义了一个函数getenv,可以用其取环境变量值,但是该标准又称环境的内 容是实现 定义的。? #include ? char *getenv(const char *?name);?? Returns:pointer to ?value? associated with ?name?,NULL if not found ? 返回:指向与name关联的value的指针,若未找到则为NULL? 注意,此函数返回一个指针,它指向name=value字符串中的value。我们总是应当 使用geter v从环境中取一个环境变量的值,而不是直接存取environ。? POSIX?1和XPG3定义了某些环境变量。图7?4列出了由这二个标准定义并受到SVR 4和4?3+B SD支持的环境变量。SVR4和4?3+BSD还使用了很多依赖于实现的环境变量。? FIPS 151-1要求一个登录shell一定要定义环境变量HOME和LOGNAME。?? P172?? 图7?4〓环境变量? 除了取环境变量值,有时也需要设置环境变量或者是改变现有变量的值,或者是增 加新的环 境变量。(在下一章将会了解到,我们能影响的是当前进程及其后生成的子进程的 环境,我 们不能影响父进程的环境,这通常是一个shell进程。尽管如此,修改环境表的能 力仍然是 很有用的。)不幸的是,并不是所有系统都支持这种能力。图7?5列出了由不同的 标准及实 现支持的各种函数。?? P173?? 图7?5〓对于各种环境表函数的支持? POSIX?1标准说明puterv和clearenv正被考虑加到POSIX?1的修订版中。? 在图7?5中,中间三个函数的原型是:? #include ? int putenv(const char *?str);?? int setenv(const char ??name,?const char ??value,?int ?rewrite); ?? Both return:0 if OK,nonzero on error两个函数返回:若成功为0,出错为非0?
void unsetenv(const char ?name);?? 这三个函数的操作是:? · putenv取形式为name=value的字符串,将其放到环境表中。如果name已经存在 ,则先删 除其原来的定义。? · setenv将name设置为value。如果在环境中name已经存在,那么(a)若rewrite非 0,则首 先删除其理存的定义;(b)若rewrite为0,则不删除其现存定义(name不设置为新值 ,而且也 不出错)。? · unsetenv删除name的定义。即使不存在这种定义也不算出错。? 这些函数在修改环境表时是如何进行操作的呢?回忆一下图7?3,其中,环境表(指 向实际na me=value字符串的指针数组)和环境字符串典型地存放在进程存储空间的顶部(栈之 上)。删 除一个字符串是简单的〖CD2〗只要先找到该指针,然后将所有后续指针都向下移 一个位置 ,但是增加一个字符串或修改一个现存的字符串就比较困难。在栈以上的空间因为 已处于进 程存储空间的顶部,所以无法扩充,即无法向上扩充,也无法向下扩充。? 1?如果修改一个现存的name:? (a)如果新value的长度少于或等于现存value的长度,则只要在原字符串所用空间 中写入新 字符串。? (b)如果新value的长度大于原长度,则必须调用malloc为新字符串分配空间,然后 将新字符 写入该空间中,然后使环境表中针对name的指针指向新分配区。? 2?如果要增加一个新的name,则操作就更加复杂。首先,调用malloc为name=val ue分配空 间,然后将该字符串写入此空间中。? (a)然后,如果这是第一次增加一个新name,则必须调用malloc为新的指针表分配 空间。将 原来的环境表复制到新分配区,并将指向新name=value的指针存在该指针表的表尾 ,然后又 将一个空指针存在其后。最后使environ指向新指针表。再看一下图7?3,如果原 来的环境 表位于栈顶之上(这是一种常见情况),那么我们必须将此表移至堆中。但是,此表 中的大多 数指针仍指向在栈顶之上的各name=value字符串。? (b)如果这不是第一次增加一个新name,则可知以前已调用malloc在堆中为环境表 分配了空 间,所以只要调用realloc,以分配比原空间多存放一个指针的空间。然后将该指 向新name= value字符串的指针存放在该表表尾,后面跟着一个空指针。? 7?10〓setjmp和longjmp函数? 在C中,可允许使用跳越函数的goto语句。而执行这种跳转功能的是函数setjmp和 longjmp。 这两个函数对发生在很深的嵌套函数调用中的出错情况进行处理是非常有用的。?
考虑一下程序7?3的骨干部分。其主循环是从标准输入读1行,然后调用do CD *常病絣 ine处理每一输 入行。该函数然后调用get 迹茫模*常病絫oken从该输入行中取下一个记号。一行 中的第一 个记号假定是 某种形式的一条命令,于是switch语句就实现命令选择。我们的程序只处理一条命 令,对此 命令调用cmd-add函数。?? P175?? 程序7?3〓进行命令处理的典型程序的骨干? 程序7?3在读命令,确定命令的类型,然后调用相应函数处理每一条命令这类程序 中是非常 典型的。图7?6显示了在调用cmd-add之后栈的大致使用情况。??? P176??? 图7?6〓在调用cmd-add后的各个栈帧? 自动变量的存储单元在每个函数的栈桢中。数组Line在main的栈帧员,整型cmd在 do-line的 栈帧中,整型token在cmd-add的栈帧中。? 正如上面说过,这种形式的栈等排是非常典型的,但并不要求非如此不可。栈并不 一定要向 低地址方向扩充。某些系统对栈并没有提供特殊的硬件支持,此时一个C实现可能 要用连接 表实现栈帧。? 在编写如程序7?3这样的程序2中经常会遇到的一个问题是,如何处理非致命性的 错误。例 如,若cmd-add函数发现一个错误,譬如说一个无效的数,那么它可能先打印一个 出错消息 ,然后希望忽略输入行的余下部分,返回main函数并读下一输入行。而单单用C语 言就比较 难做到这一点。(在本例中,cmd-add函数只比main低二个层次,在有些程序中往往 希望向高 五、六个层次处返回。)如果我们不得不以检查返回值的方法逐层返回,那就会变 得很麻烦 。? 解决这种问题的方法就是使用非局部跳转〖CD2〗setjmp和longjmp函数。"非局部 "表示区 不是在一个函数内的普通的C语言goto语句,而是在栈上跳过若干调用帧,返回到 当前函数 调用路径上的一个函数。? #include ? int setjmp(jmp 迹茫模*常病絙uf ?env);?? Returns:0 if called directly,nonzero if returning from a call to longjm p? 返回:若直接调用为0,若从longjmp返回则为非0? void longjmp(jmp 迹茫模*常病絙uf ?env,?int ?val?);? 在希望返回到的位置调用setjmp,在这本例中,此位置在main函数中。因为我们直 接调用该 函数,所以其返回值为0。setjmp的参数env是一个特殊类型jmp-buf。这一数据类 型是某种 形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。一般env变 量是个全 局变量,因为需在另一个函数中引用它。? 当检查到一个错误时,例如在cmd-add函数中,则以两个参数调用longjmp函数。第 一个就是 在调用setjmp时所用的env;第二个,val,是个非0值,它成为从setjmp处返回的 值。使用 第二个参数的原因是对于一个setjmp可以有多个longjmp。例如,可以在cmd-add中 以val为1 调用longjmp,也可在get 迹茫模*常病絫oken中以val为2调用longjmp。在main函 数中,se tjmp的返 回值就会是1或2,通过测试返回值就可判断是从cmd-add还是从get 迹茫模*常病?nbsp; token来 的longjmp。? 再回到程序实例中,图7?4中示出了经修改过的main和cmd-add函数(其它两个函数 ,do-lin e和get 迹茫模*常病絫oken未经更改)。? P177??? 程序7?4〓setjmp和longjmp实例?? 执行main时,调用setjmp,它将所需的信息记入变量jmpbuffer中并返回0。然后调 用do-lin e,它又调用end-add,假定在其中检测到一个错误。在cmd-add中调用longjmp之前 ,栈的形 式如图7?6中所示。但是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时保持不变。 ? 实例? 下面我们通过程序7?5说明在调用longjmp后,自动变量、寄存器变量和易失变量 的不同情 况。如果我们以不带优化和带优化对此程序分别进行编译,然后运它们,则得到的 结果是不 同的:? $ cc testjmp?c〓〓〓不进行任何优化的编译? $ a?out? in f1(1):count=97,val=98,sum=99? after longjmp:count=97,val=98,sum=99? $ cc -o testjmp?c? $ a?out? in f1(1):count=97,val=98,sum=99? after longjmp:count=2,val=3,sum=99? 注意,易失变量(sum)不受优化的影响,在longjmp之后的值,是它在调用f1时的值 。在我们 所使用的setjmp(3)手册页上说明存放在存储器中的变量将具有longjmp时的值,而 在CPU和 浮点寄存器中的变量则恢复为调用setjmp时的值。这确实就是我们在运行程序7? 5时所观察 到的值。不带优化时,所有这三个变量都存放在存储器中(亦即对val的寄存器存储 类被忽略 )。而带优化时,count和val都存放在寄存器中(即使count并末说明为register), volatile 变量则仍存放在存储器中。通过这一例子要理解的是,如果要编写一个使用非属部 跳转的可 移植程序,则必须使用volatile属性。??P179?? 程序7?5〓longjmp对自动,寄存器和易失变量的影响? 在第十章当我们讨论信号处理程序及sigsetjmp和siglongjmp时,我们会再回到se tjmp和lon gjmp函数。? 自动变量的潜在问题? 前面已经说明了处理栈帧的一般方式,与此相关我们现在可以分析一下自动变量的 一个潜在 出错情况。基本规则是说明自动变量的函数已经返回后,就不能再引用这些自动变 量。在整 个Unix手册中,关于这一点有很多警告。? 程序7?6是一个名为open-data的函数,它打开了一个标准I/O流,然后为该流设置 缓存。? ? P180? 程序7?6〓自动变量的不正确使用? 问题是:当open-data返回时,它在栈上所使用的空间将由下一个被调用的函数的 栈帧使用 。但是,标准I/O库函数将仍使用原先为databuf在栈上分配的存储空间作为该流的 缓存。这 就产生了冲突和混乱。为了改正这一问题,应在全局存储空间(或者是静态地(sta tic或exte rn),或者动态地(使用一种alloc函数)为数组databuf分配空间。? 7?11〓getrlimit和setrlimit函数? 每个进程都有一组资源限制,其中某一些可以用getrlimit和setrlimit函数查询和 更改。? #include ? #include ? int getrlimit(int ?resource?,struct rlimit *?rlptr);?? int setrlimit(int ?resource,?const struct rlimit ??rlptr);?? Both return: 0 if OK,nonzero on error两个函数返回:若成功为0,出错为非0 ? 对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针。? struct rlimit{? rlim 迹茫模*常病絫 rlim 迹茫模*常病絚ur;〓/* 软限制:当前限制 */? rlim 迹茫模*常病絫 rlim 迹茫模*常病絤ax;〓/* 硬限制:rlim 迹茫模*常?nbsp; 〗cur的 最大值 */? };? 这两个函数不属于POSIX?1,但SVR4和4?3+BSD提供它们。? SVR4在上面的结构中使用基本系统数据类型rlim 迹茫模*常病絫。其它系统则将 这两个成 员定义为整型或长整型。? 进程的资源限制通常是在系统初启时由0?#进程建立的,然后由后续进程继承。 在SVR4中 ,系统默认值可以查看文件/etc/conf/cf?d/mtune?在4?3+BSD中,系统默认值 分散在多 个头文件中。? 在更改资源限制时,须遵循下列三条规则:? 1?任何一个进程都可将一个软限制更改为小于或等于其硬限制。? 2?任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降 低,对普 通用户而言是不可逆反的。? 3?只有超级用户可以提高硬限制。? 一个无限量的限制由常数RLIM 迹茫模*常病絀NFINITY指定。? 这两个函数的resource参数取下列值之一。注意并非所有资源限制都受到SVR4和4 ?3+BSD的 支持。? RLMIT 迹茫模*常病紺ORE〓(SVR4及4?3+BSD)core文件的最大字节数,若其值为 0则阻止 创建core文件。? RLIMIT 迹茫模*常病紺PU〓(SVR4及4?3+BSD)CPU时间的最大量值(秒),当超过此 软限止时 ,向该进程发送SIGXCPU信号。? RLIMIT 迹茫模*常病紻ATA〓(SVR4及4?3+BSD)数据段的最大字节长度。这是图7 ?3中初始 化数据、非初始化数据以及堆的总和。? RLIMIT 迹茫模*常病紽SIZE〓(SVR4及4?3+BSD)可以创建的一个文件的最大字节 长度。当 超过此软限制时,则向该进程发送SIGFSZ信号。? RLIMIT 迹茫模*常病組EMLOCK〓(4?3+BSD)锁定在存储器地址空间(尚末实现)。 ? RLIMIT 迹茫模*常病絅OFILE〓(SVR4)每个进程最多打开的文件数。更改此限制将 影响到sy sconf函数在参数-sc-OPEN-MAX中返回的值(2?5?4节)。见程序2?3。? RLIMIT 迹茫模*常病絅PROC〓(4?3+BSD)每个实际用户ID所拥有的最大子进程数 。更改此 限制将影响到sysconf函数在参数 迹茫模*常病絊C 迹茫模*常病紺HILD 迹茫模?nbsp; ?2〗MA X中返回的值(2?5?4节)。? RLIMIT 迹茫模*常病絆FILE〓(4?3+BSD)与SVR4的RLIMIT 迹茫模*常病絅OFILE 相同。? RLIMIT 迹茫模*常病絉SS〓(4?3+BSD)最大驻内存集字节长度(RSS)。如果物理 存储器供 子应求,则系统核将从进程处取回超过RSS的部分。? RLIMIT 迹茫模*常病絊TACK〓(SVR4及4?3+BSD)栈的最大字节长度。见图7?3。 ? RLIMIT 迹茫模*常病絍MEM〓(SVR4)可映照地址空间的最大字节长度。这影响到 mmap函数( 12?9节)。? 资源限制影响到调用进程并由其子进程继承。这就意味着为了影响一个用户的所有 后续进程 ,需将资源限制设置构造在shell之中。确实,Bourne Shell和Kornshell具有内部 ulimit命 令,CShell具有内部limit命令。(umask和chdir也必须是shell内部的)。? 较早的Bourme Shell,例如由贝克莱提供的一种,不支持ulimit命令。? 较新的KornShell的ulimit命令具有-H和-s选择项,以分别检查和修改硬和软的限 制,但它 们尚末编写入文档。? 实例? 程序7?7打印由系统支持的对所有资源的当前软限制和硬限制。为了在SVR4和4? 3+BSD之下 运行此程序,我们必须条件编译不同的资源名。? P183??? 程序7?7〓打印当前资源限制?? 注意,我们在doit宏中使用了新的ANSIC字符串创建算符(#),以便为每个资源名产 生字符串 值。例如:? doit(RLIMIT 迹茫模*常病紺ORE);? 这将由C予处理程序扩展为:? pr 迹茫模*常病絣imits("RLIMIT 迹茫模*常病紺ORE",RLIMIT 迹茫模*常病紺 ORE);? 在SVR4下运行此程序,得到:? $ a?out? RLIMIT 迹茫模*常病紺ORE〓1048576〓1048576? RLIMIT 迹茫模*常病紺PU〓(infinite)〓(infinite)? RLIMIT DATA〓16777216〓16777216? RLIMIT 迹茫模*常病紽SIZE〓2097152〓2097152? RLIMIT 迹茫模*常病絅OFILE〓64〓1024? RLIMIT 迹茫模*常病絊TACK〓16777216〓16777216? RLIMIT 迹茫模*常病絍MEM〓16777216〓16777216? 在4?3+BSD下运行此程序,得到:? $ a?out? RLIMIT 迹茫模*常病紺ORE〓(infinite)〓(infinite)? RLIMIT 迹茫模*常病紺PU〓(infinite)〓(infinite)? RLIMIT 迹茫模*常病紻ATA〓8388608〓16777216? RLIMIT 迹茫模*常病紽SIZE〓(infinite)〓(infinite)? RLIMIT 迹茫模*常病組EMLOCK〓(infinite)〓(infinite)? RLIMIT 迹茫模*常病絆FILE〓64〓(infinite)? RLIMIT 迹茫模*常病絅PROC〓40〓(infinite)? RLIMIT 迹茫模*常病絉SS〓27070464〓27070464? RLIMIT 迹茫模*常病絊TACK〓524288〓16777216? 在我们介绍了信号机构后,练习10?11将继续讨论资源限制。? 7?12〓摘要? 理解在Unix环境中C程序的环境是理解Unix进程控制特征的先决条件。在本章中, 我们说明 了一个进程是如何起动和终止的,如何向其传递传数表和环境。虽然这两者都不是 由系统核 进行解释的,但系统核起到了从exec的调用者将这两者传递给新进程的作用。? 我们在本章中也说明了C程序的典型存储器布局,以及一个进程如何动态地分配和 释放存储 器。详细地了解用于维护环境的一些函数是值得的,因为它们涉及存储器分配。本 章也介绍
了setjmp和longjmp函数,它们提供了一种在进程内非本地转移的方法。最后我们 介绍了SVR4和4?3+BSD提供的资源限制功能。〖LM〗
| | |