翼度科技»论坛 云主机 LINUX 查看内容

[apue] 进程环境那些事儿

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
main 函数与进程终止

众所周知,main 函数为 unix like 系统上可执行文件的"入口",然而这个入口并不是指链接器设置的程序起始地址,后者通常是一个启动例程,它从内核取得命令行参数和环境变量值后,为调用 main 函数做好安排。main 函数原型为:
  1. int main (int argc, char *argv[]);
复制代码
这是 ISO C 和 POSIX.1 指义的,当然还存在下面几种不太标准的 main 原型:
  1. void main (int argc, char *argv[]);
  2. void main (void);
  3. int main (void);
复制代码
不带 argc & argv 参数的表示不打算接受命令行参数;void 返回值的表示不打算返回一个结束状态。
进程的结束状态码与 main 的返回值关系如下:

  • main 声明为 int 类型返回值

    • main 结束前执行了 return x 语句:x
    • main 结束前执行了无参数 return 语句:未定义 (warning: ‘return’ with no value, in function returning non-void)
    • main 结束前执行了 exit(x) 函数:x
    • main 结束前未执行以上语句:未定义 (warning: control reaches end of non-void function)
    • main 结束前未执行以上语句 [-std=c99]:0

  • main 声明为 void 类型返回值 (warning: return type of ‘main’ is not ‘int’)

    • main 结束前执行了 return x 语句:未定义 (warning: ‘return’ with a value, in function returning void)
    • main 结束前执行了无参数 return 语句:未定义 
    • main 结束前执行了 exit(x) 函数:x
    • main 结束前未执行以上语句:未定义

测试机为 CentOS 7.9,gcc 版本 4.8.5,每一项的 warning 信息就是基于这两个版本测得。未定义的场景中,均返回 25 这个魔数。
开了 -std=c99 后大部分场景没有改善,仅 main 返回值被声明为 int 类型且在结束前没有调用任何 return 或 exit 时 (第 1 项第 4 小项) 发生了显著变化:从未定义变为返回 0。
进程有 8 种终止方式,其中 5 种为正常终止:

  • 从 main 返回 (无论是否有返回值)
  • 调用 exit
  • 调用 _exit 或 _Exit
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用 pthread_exit
另有 3 种为异常终止:

  • 调用 abort
  • 接到一个信号并终止
  • 最后一个线程对取消请求做出响应
下面重点看一下 3 个 exit 函数:
  1. #include <unistd.h>
  2. void _exit(int status);
  3. #include <stdlib.h>
  4. void exit(int status);
  5. void _Exit(int status);
复制代码
声明差别不大,_exit 与 _Exit 分别是 POSIX.1 与 ISO C 的标准,不过可以将它们视为等价,都直接进入内核。exit 则在它们的基础上做了一些清理工作,主要包含以下几个方面:

  • 清理线程局部存储 (TLS) 信息
  • 按顺序调用注册的终止处理程序
  • 为所有标准 I/O 库打开的流调用 fclose 函数,这会 flush 缓冲的输出数据
关于标准 I/O 库,请参考之前写的这篇文章:《[apue] 标准 I/O 库那些事儿》。
有了上面的铺垫,可以这样理解可执行程序的启动例程与 main 之间的关系:
  1. ...
  2. exit (main (argc, argv));
复制代码
 即 main 的返回值是直接传递给 exit 的 status 参数作为进程结束状态的。
atexit

关于终止处理程序,一般通过 atexit 函数进行注册:
  1. #include <stdlib.h>
  2. int atexit(void (*function)(void));
复制代码
这里的 function 参数就是希望在 exit 时被调用的清理程序,关于终止处理程序,有下面几点需要注意:

  • 调用次数有上限,通过 sysconf (_SC_ATEXIT_MAX) 查询 (实测为 2147483647, 即 INT_MAX)
  • FILO,先注册的后被调用,类似于堆栈,而非队列
  • 调用次数等于注册次数,同一清理程序可多次注册,注册几次调用几次
  • 执行 exec 函数族执行另一个程序的时候,自动清空 atexit 注册的清理程序
  • 在清理程序中调用 exit "无效",如果调用 _exit 或 _Exit,会导致程序直接退出,后续清理程序不再被调用
  • 进程异常终止时清理程序不会被调用
下面这个例子验证了调用次数与 FILO 特性:
  1. #include "../apue.h"
  2. void do_dirty_work ()
  3. {
  4.   printf ("doing dirty works!\n");
  5. }
  6. void bye ()
  7. {
  8.   printf ("bye, forks~\n");
  9. }
  10. void times ()
  11. {
  12.   static int counter = 32;
  13.   printf ("times %d\n", counter--);
  14. }
  15. int main ()
  16. {
  17.   int ret = 0;
  18.   ret = atexit (do_dirty_work);
  19.   if (ret != 0)
  20.     err_sys ("atexit");
  21.   ret = atexit (bye);
  22.   if (ret != 0)
  23.     err_sys ("bye1");
  24.   ret = atexit (bye);
  25.   if (ret != 0)
  26.     err_sys ("bye2");
  27.   for (int i=0; i<32; i++)
  28.   {
  29.     ret = atexit (times);
  30.     if (ret != 0)
  31.       err_sys ("times");
  32.   }
  33.   printf ("main is done!\n");
  34.   return 0;
  35. }
复制代码
可见 exit 并非没有生效,一个合理的推断是:第二次进入 exit 后,继续处理之前没处理完的清理程序,使得输出看起来就像"没生效"一样。真正的 _exit 是被第二次进入 bye 的那个 exit 所调用,对程序稍加改动来看个明白:
  1. $ ./atexit
  2. main is done!
  3. times 32
  4. times 31
  5. times 30
  6. times 29
  7. times 28
  8. times 27
  9. times 26
  10. times 25
  11. times 24
  12. times 23
  13. times 22
  14. times 21
  15. times 20
  16. times 19
  17. times 18
  18. times 17
  19. times 16
  20. times 15
  21. times 14
  22. times 13
  23. times 12
  24. times 11
  25. times 10
  26. times 9
  27. times 8
  28. times 7
  29. times 6
  30. times 5
  31. times 4
  32. times 3
  33. times 2
  34. times 1
  35. bye, forks~
  36. bye, forks~
  37. doing dirty works!
复制代码
为了便于区别,这里给的初始值为 10,每调用一次 bye,exit_status 递增 1,如果最后进程结束状态码为 10 就证明是第一次 exit 结束了进程,否则就是第二次。
  1. void bye ()
  2. {
  3.   printf ("bye, forks~\n");
  4.   exit (2);  // no effect
  5.   printf ("after exit (2)\n");
  6. }
复制代码
结论已经非常明显,之前的猜测成立!如此就可以合理的推断 exit 调用清理程序后,会将其从 FILO 结构中移除,从而避免再次调用,进而引发无限循环。
下面试试 _exit 的效果:
  1. $ ./atexit
  2. main is done!
  3. times 32
  4. ...
  5. times 1
  6. bye, forks~
  7. bye, forks~
  8. doing dirty works!
  9. > echo $?
  10. 2
复制代码
改为 _exit 后输出发生了截断:
  1. int exit_status = 10;
  2. void bye ()
  3. {
  4.   printf ("bye, forks~\n");
  5.   exit (exit_status++);  // no effect
  6.   printf ("after exit (%d)\n", exit_status-1);
  7. }
复制代码
进入 bye 处理程序后进程就终止了,后续的处理程序不再调用。检查进程结束状态码为 3,正好是 _exit  的 status 参数。
将上面 exit 和 _exit 全都打开后,_exit 反而不起作用了:
  1. $ ./atexit
  2. main is done!
  3. times 32
  4. ...
  5. times 1
  6. bye, forks~
  7. bye, forks~
  8. doing dirty works!
  9. > echo $?
  10. 11
复制代码
经过上面的分析,想必读者已经知道了答案,正确的做法是将 _exit 放在 exit 前面,这样才能避免进入 exit 之后不再返回,从而被忽略。
最后再试一种场景,就是在处理器中继续调用 atexit 注册新的处理器,观察新的处理器是否能被调用,参考下面这个例子:
  1. void bye ()
  2. {
  3.   printf ("bye, forks~\n");
  4.   _exit (3);  // quit and no other atexit function running anymore !
  5.   printf ("after _exit (3)\n");
  6. }
复制代码
先注册处理器 bye,在其被回调时再注册处理器 do_dirty_work,结果是两个处理器都能被回调:
  1. $ ./atexit
  2. main is done!
  3. times 32
  4. ...
  5. times 1
  6. bye, forks~
  7. $ echo $?
  8. 3
复制代码
如果注册的处理器形成循环会如何?参考下面的例子:
  1. void bye ()
  2. {
  3.   printf ("bye, forks~\n");
  4.   exit (2);  // no effect
  5.   printf ("after exit (2)\n");
  6.   _exit (3);  // no effect
  7.   printf ("after _exit (3)\n");
  8. }
复制代码
在 do_dirty_work 中再次注册 bye 作为处理器,重新编译后运行,发现程序果然陷入了死循环:
  1. $ ./atexit
  2. main is done!
  3. times 32
  4. ...
  5. times 1
  6. bye, forks~
  7. $ echo $?
  8. 3bye, forks~doing dirty works!bye, forks~doing dirty works!bye, forks~doing dirty works!bye, forks~doing dirty works!bye, forks~...doing dirty works!bye, forks~doing dirty works!bye, forks~doing dirty works!bye, forks~doing dirty works!bye, forks~doing dirty works!^C
复制代码
直到输入 Ctrl+C  才能退出,看起来 atexit 并不能检测这种情况,需要程序员自己避免调用环的形成,好在这种场景并不多见。
命令行参数与环境变量

ISO C 与 POSIX.1 都要求 argv[argc] 参数为 NULL,因此下面两种遍历命令行参数的方式是等价的:
  1. $ ./atexit_term
  2. main is done!
  3. bye, forks~
  4. doing dirty works!
复制代码
dec/hex 列分别是三者加总后的十进制与十六进制长度。示例中 layout_s 是静态链接版本,可见使用共享库的动态链接在各个段的尺寸上都有明显缩减。
堆分配

栈的增长主要依赖函数调用层次的增加;堆的增长主要依赖以下存储器分配函数:
  1. #include "../apue.h"
  2. extern void bye ();
  3. void do_dirty_work ()
  4. {
  5.   printf ("doing dirty works!\n");
  6.   int ret = atexit (bye);
  7.   if (ret != 0)
  8.       err_sys ("bye2");
  9. }
  10. void bye ()
  11. {
  12.   printf ("bye, forks~\n");
  13.   int ret = atexit (do_dirty_work);
  14.   if (ret != 0)
  15.       err_sys ("do_dirty_work");
  16. }
  17. int main ()
  18. {
  19.   int ret = 0;
  20.   ret = atexit (bye);
  21.   if (ret != 0)
  22.     err_sys ("bye");
  23.   printf ("main is done!\n");
  24.   return 0;
  25. }
复制代码
其中:

  • malloc 分配 size 长度的存储区
  • calloc 分配 nmemb*size 长度的存储区
  • realloc 可更改以前分配区到 size 长度 (增加或减小)
对于新增的存储区

  • calloc 初始值为 0
  • malloc 和 realloc 初始值不确定
对于 realloc,新旧地址之间的关系:

  • 当存储区减小时,新旧地址保持一致
  • 当存储区增加时

    • 原存储区后有足够的空间时,新旧地址保持一致
    • 原存储区后没有足够的空间,新旧地址不同,会先分配足够大的空间,复制数据,再释放原存储区

realloc(NULL, size) 等价于 malloc(size)。
sbrk

这些分配例程通常用 sbrk 系统调用来扩充进程的堆:
  1. $ ./atexit_term
  2. main is done!
  3. bye, forks~
  4. doing dirty works!
  5. bye, forks~
  6. doing dirty works!
  7. bye, forks~
  8. doing dirty works!
  9. bye, forks~
  10. doing dirty works!
  11. bye, forks~
  12. doing dirty works!
  13. bye, forks~
  14. ...
  15. doing dirty works!
  16. bye, forks~
  17. doing dirty works!
  18. bye, forks~
  19. doing dirty works!
  20. bye, forks~
  21. doing dirty works!
  22. bye, forks~
  23. doing dirty works!^C
复制代码
这通常是通过调用 program break 的位置来实现的,参考 man 这段说明:
  1. int i;
  2. for (i=0; i<argc; ++ i)
  3. ...
  4. for (i=0; argv[i]!=NULL; ++i)
  5. ...
复制代码
program break 就是 bss 段的结尾,参考上图应该就是堆底。
sbrk 也可以减小堆大小,不过大多数 malloc 和 free 的实现都不减小进程的存储空间,释放的空间可供以后再分配,但通常将它们保持在 malloc 池中而不返回给内核。
环境变量空间的变更

有上面内容的铺垫,就可以回顾下上一节中增删改环境变量对存储空间的影响了:

  • 删除环境变量,之后的变量前移填补删除后的空位
  • 修改环境变量

    • 新值长度小于等于旧值,在原字符串空间中写入新值
    • 新值长度大于旧值,在堆上分配新字符串空间并赋值,更新环境变量表中的指针使之指向新分配的字符串

  • 新增环境变量

    • 第一次新增环境变量,在堆上分配新的环境变量表,将原来的环境变量"复制"到新分配的环境变量表中,然后把新增的环境变量字符串放在表尾,再新增一个空指针放在最后,最后使用 environ 变量指向新分配的环境变量表,基本上就是将环境变量从栈顶搬到了堆中,不过大多数环境变量仍指向栈顶中分配的字符串而已
    • 非第一次新增,使用 realloc 重新分配 environ 变量,以容纳新增加的环境变量

环境变量空间改变如此复杂,主要是因为它的大小被栈顶限制死了,没有办法扩容,当增加环境变量数目时,只能从栈顶搬到堆中。
下面的程序演示了这一过程:
  1. int main (int argc, char* argv[], char* envp[]);
复制代码
程序比较简单,依次执行以下操作:add HOME -> add LOGNAME -> remove PATH -> add DISAPPEAR -> add ADDISION,每次操作后都打印整个环境变量表,以观察 environ 和各个环境变量的变化:
  1. extern char **environ;
复制代码
启动后先打印整个环境变量表,大概有 30 个环境变量。
  1. name=value
复制代码
设置 HOME 变量,虽然新值长度小于旧值,这里仍然为新值在堆上分配了空间,看起来 linux 上的实现偷懒了。
  1. PATH=/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin
复制代码
设置 LOGNAME 变量,新值长度大于旧值,这里没有悬念的在堆上进行了分配。
  1. #include <stdlib.h>
  2. [ISO C/POSIX.1] char *getenv(const char *name);
  3. [POSIX.1]       int setenv(const char *name, const char *value, int overwrite);
  4. [POSIX.1]       int unsetenv(const char *name);
  5. [XSI]           int putenv(char *string);
  6. [linux]         int clearenv(void);
复制代码
删除 PATH 变量,这一步主要验证再次新增环境变量时,会不会重复利用已删除的空位,到目前为止 environ 指针地址 (0x7fff15e16468) 没有发生变化,仍位于栈顶之上。
  1. #include "../apue.h"
  2. int data1 = 2;
  3. int data2 = 3;
  4. int data3;
  5. int data4;
  6. int main (int argc, char *argv[])
  7. {
  8.   char buf1[1024] = { 0 };
  9.   char buf2[1024] = { 0 };
  10.   char *buf3 = malloc(1024);
  11.   char *buf4 = malloc(1024);
  12.   printf ("onstack %p, %p\n",
  13.     buf1,
  14.     buf2);
  15.   extern char ** environ;
  16.   printf ("env %p\n", environ);
  17.   printf ("arg %p\n", argv);
  18.   printf ("onheap %p, %p\n",
  19.     buf3,
  20.     buf4);
  21.   free (buf3);
  22.   free (buf4);
  23.   printf ("on bss %p, %p\n",
  24.     &data3,
  25.     &data4);
  26.   printf ("on init %p, %p\n",
  27.     &data1,
  28.     &data2);
  29.   printf ("on code %p\n", main);
  30.   return 0;
  31. }
复制代码
增加 DISAPPEAR 变量,没有悬念的在堆上分配了空间,最大的变化在于 environ 指针变了!从栈顶之上移动了到了堆中 (0x16fc0d0),看起来之前删除 PATH 变量腾空的位置没有利用上。
  1. $ ./layout
  2. onstack 0x7ffe31b752a0, 0x7ffe31b74ea0
  3. env 0x7ffe31b757b8
  4. arg 0x7ffe31b757a8
  5. onheap 0x1984010, 0x1984420
  6. on bss 0x6066b8, 0x6066bc
  7. on init 0x606224, 0x606228
  8. on code 0x40179d
复制代码
增加 ADDISION 变量,仍然在堆上分配空间,而且 environ 指针地址 (0x16fc0d0) 没有发生变化,看起来仍有足够的空间让 realloc 分配。
指令跳转 (setjmp & longjmp)

说到指令跳转,第一印象就是 goto。由于程序的执行本质是一条条机器代码的执行,有些指令本身自带跳转属性,像函数调用 (call)、函数返回 (return) 、switch-case 都是某种形式的指令跳转,goto 则将这种能力公布给了开发者,然而下面的两个限制导致它在实际应用上的推广受阻:

  • 只能在函数内部跳转,无法跨越函数栈
  • 滥用 goto 导致代码逻辑不清晰、后期维护困难
setjmp & longjmp 完美的解决了上述 goto 的缺点,支持跨函数栈的跳转、且使用上更不易被滥用,也被称为非局部 goto。
它的跳转逻辑和现代 C++ 的异常机制已经非常相似了,区别是后者加入了对栈上对象析构函数的自动调用等更多的内容。
先来看函数原型:
  1. $ size ./layout ./layout_s /bin/sh
  2.    text           data            bss            dec            hex        filename
  3.   20073           2152             80          22305           5721        ./layout
  4. 802535           7292          11120         820947          c86d3        ./layout_s
  5. 905942          36000          22920         964862          eb8fe        /bin/sh
复制代码
书上给的例子就不错,这里找到另外一个更简单的例子:
  1. #include <stdlib.h>
  2. void *malloc(size_t size);
  3. void *calloc(size_t nmemb, size_t size);
  4. void *realloc(void *ptr, size_t size);
  5. void free(void *ptr);
复制代码
编译运行这个 demo,输出如下:
  1. #include <unistd.h>
  2. void *sbrk(intptr_t increment);
复制代码
对于没有接触过非局部 goto 的人来说还是比较直观的。
compiler explorer

这里推荐一个在线的 c++ 编译器 compiler explorer,对于没有 Linux 环境的人来说非常友好,下面是编译运行上述 demo 的过程:

可以看到这个工具非常强大,可以:

  • 选择编译语言
  • 选择编译器
  • 选择编译模式 (是否开启 Vim)
  • 修改编译链接选项
  • 查看反汇编
  • 查看预处理结果
  • 查看运行输出
  • 更改窗口布局
有兴趣的读者可以自行探索。
回归代码,注意 longjmp 第二个参数,这个不是随便给的,它将作为跳转后 setjmp 的返回值,要与初始化时返回的 0 有一些区别,另外允许任意多个 longjmp 跳向同一个 jmp_buf 实例,这种情况下,通过指定不同的 val 参数也能区别出跳转源,是不是想的很周到?
longjmp 跳转时,当前所在的函数栈到 setjmp 之间的栈将被回收,依附之上的自动变量将不复存在,但是跳转目的地所在的栈帧还是存在的,此外还有不在当前栈上的全局变量、静态变量等等也是存在的。
变量值回退

虽然没读过 setjmp & longjmp 的源码,但原理应该就是存储和恢复函数栈 (各种寄存器),那这些未被撤销的变量,是恢复到 setjmp 时的状态,还是保留最后的状态呢?对上面的例子稍加修改来进行一番考察:
  1. DESCRIPTION
  2.        brk()  and  sbrk() change the location of the program break, which defines the end of the process's data segment (i.e., the pro‐
  3.        gram break is the first location after the end of the uninitialized data segment).  Increasing the program break has the  effect
  4.        of allocating memory to the process; decreasing the break deallocates memory.
复制代码
在原来的基础上添加了几种类型的变量:

  • globaval:全局变量
  • autoval:main 栈上自动变量
  • regival:main 栈上寄存器变量
  • valaval:main 栈上易失变量
  • statval:main 栈上静态变量
并分别在 call_jmp 内部和 longjmp 后 (第二次从 setjmp 返回) 时打印它们的值:
  1. $  ./jumpvarin call_jmp ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99#include <unistd.h>
  2. void *sbrk(intptr_t increment);in main ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99
复制代码
在没开优化的情况下,各个变量都最新的状态,没有发生值回退现象,添加 -O 编译选项: 
  1. $ ./envpos
  2. base 0x7fff15e16468
  3. [0x7fff15e17488]  XDG_SESSION_ID=318004
  4. [0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
  5. [0x7fff15e174c1]  SHELL=/bin/bash
  6. [0x7fff15e174d1]  TERM=xterm-256color
  7. [0x7fff15e174e5]  HISTSIZE=1000
  8. [0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
  9. [0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
  10. [0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
  11. [0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
  12. [0x7fff15e1757e]  SSH_TTY=/dev/pts/6
  13. [0x7fff15e17591]  USER=yunhai01
  14. [0x7fff15e17c57]  TMOUT=0
  15. [0x7fff15e17c5f]  PATH=/home/yunh/.BCloud/bin:/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/home/users/yunhai01/tools/bin:/home/users/yunhai01/project/android-ndk-r20:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin
  16. [0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
  17. [0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
  18. [0x7fff15e17e30]  LANG=en_US.UTF-8
  19. [0x7fff15e17e41]  HISTCONTROL=ignoredups
  20. [0x7fff15e17e58]  HOME=/home/users/yunhai01
  21. [0x7fff15e17e72]  SHLVL=2
  22. [0x7fff15e17e7a]  GTAGSFORCECPP=1
  23. [0x7fff15e17e8a]  LOGNAME=yunhai01
  24. [0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
  25. [0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
  26. [0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
  27. [0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
  28. [0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
  29. [0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
  30. [0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
  31. [0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
  32. [0x7fff15e17fe4]  _=./envpos
复制代码
再次运行:
  1. $ ./jumpvarin call_jmp ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99#include <unistd.h>
  2. void *sbrk(intptr_t increment);in main ():globval = 95,autoval = 2,regival = 3,volaval = 98,statval = 99
复制代码
这次 autoval 和 regival 的值发生了回退。加优化选项后为提高程序运行效率,这些变量的值从内存提升到了寄存器,从而导致恢复 main 堆栈时被一并恢复了。这里有几个值得注意的点:

  • 声明为 register 的 regival 在未开启优化前编译器并没有遵循指令将其放置在寄存器,再一次证实了 register 关键字只是建议而非强制
  • 开优化后,栈上的自动变量也被放置在了寄存器中
  • 即使开优化,volatile 关键字声明的变量也不存在于寄存器中
所以最终的结论是:如果不想栈上的变量受 setjmp & longjmp 影响发生值回退,最好将它们声明为 volatile
这里出于好奇,也使用 compiler explorer 运行了一把,结果没加优化的第一次运行输出就不一样:
 

主要区别在于 regival 会回退,将 compiler explorer 中的 gcc 版本降到和我本地一样的 4.8.5 后输出就一致了,因此主要区别在于编译器版本。
这一方面展示了 compiler explorer 强大的切换编译器版本的能力,另一方面也显示高版本 gcc 版本器倾向于"相信"用户提供的 register 关键字。
最后在 compiler explorer 中增加 -O 编译器参数,会得到和之前一样的结果:

资源限制 (getrlimit & setrlimit)

进程对系统资源的请求并不是没有上限的,使用 getrlimit 和 setrlimit 查询或更改它们:
  1. after set LOGNAME:
  2. base 0x7fff15e16468
  3. [0x7fff15e17488]  XDG_SESSION_ID=318004
  4. [0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
  5. [0x7fff15e174c1]  SHELL=/bin/bash
  6. [0x7fff15e174d1]  TERM=xterm-256color
  7. [0x7fff15e174e5]  HISTSIZE=1000
  8. [0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
  9. [0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
  10. [0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
  11. [0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
  12. [0x7fff15e1757e]  SSH_TTY=/dev/pts/6
  13. [0x7fff15e17591]  USER=yunhai01
  14. [0x7fff15e17c57]  TMOUT=0
  15. [0x7fff15e17c5f]  PATH=/home/yunh/.BCloud/bin:/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/home/users/yunhai01/tools/bin:/home/users/yunhai01/project/android-ndk-r20:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin
  16. [0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
  17. [0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
  18. [0x7fff15e17e30]  LANG=en_US.UTF-8
  19. [0x7fff15e17e41]  HISTCONTROL=ignoredups
  20. [0x16fc010]  HOME=ME
  21. [0x7fff15e17e72]  SHLVL=2
  22. [0x7fff15e17e7a]  GTAGSFORCECPP=1
  23. [0x16fc060]  LOGNAME=this is a very very long user name
  24. [0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
  25. [0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
  26. [0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
  27. [0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
  28. [0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
  29. [0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
  30. [0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
  31. [0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
  32. [0x7fff15e17fe4]  _=./envpos
复制代码
resource 指定了限制的类型,rlim 则包含了资源限制的信息,主要包含两个成员:

  • rlim_cur:软限制值,当前生效的限制值
  • rlim_max:硬限制值,大于等于软限制值,软限制值的提升上限

    • 任何用户可以降低硬限制值,只有超级用户可以提升硬限制值
    • 每次降低的硬限制值必需大于等于软限制值

RLIM_INFINITY 表示无限量:
  1. after unset PATH:
  2. base 0x7fff15e16468
  3. [0x7fff15e17488]  XDG_SESSION_ID=318004
  4. [0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
  5. [0x7fff15e174c1]  SHELL=/bin/bash
  6. [0x7fff15e174d1]  TERM=xterm-256color
  7. [0x7fff15e174e5]  HISTSIZE=1000
  8. [0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
  9. [0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
  10. [0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
  11. [0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
  12. [0x7fff15e1757e]  SSH_TTY=/dev/pts/6
  13. [0x7fff15e17591]  USER=yunhai01
  14. [0x7fff15e17c57]  TMOUT=0
  15. [0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
  16. [0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
  17. [0x7fff15e17e30]  LANG=en_US.UTF-8
  18. [0x7fff15e17e41]  HISTCONTROL=ignoredups
  19. [0x16fc010]  HOME=ME
  20. [0x7fff15e17e72]  SHLVL=2
  21. [0x7fff15e17e7a]  GTAGSFORCECPP=1
  22. [0x16fc060]  LOGNAME=this is a very very long user name
  23. [0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
  24. [0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
  25. [0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
  26. [0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
  27. [0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
  28. [0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
  29. [0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
  30. [0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
  31. [0x7fff15e17fe4]  _=./envpos
复制代码
可以指定的资源限制类型及在本地环境上的软硬限制值列表如下:
resource 含义软限制硬限制
RLIMIT_AS进程可用存储区的最大字节长度,会影响 sbrk & mmap 函数,非 Linux 平台也命名为 RLIMIT_VMEMinfiniteinfinite
RLIMIT_CORE崩溃转储文件的最大字节数,0 表示阻止创建,生成的 core 文件大于限制值时会被截断0infinite
RLIMIT_CPUCPU 的最大量值,单位秒,超过软限制时,向进程发送 SIGXCPU 信号;超过硬限制时,向进程发送 SIGKILL 信号infiniteinfinite
RLIMIT_DATA数据段的最大字节长度,是 init + bss + heap 的总长度,即除栈、环境变量、命令行参数外的内存总长度infiniteinfinite
RLIMIT_FSIZE可以创建的文件的最大字节长度,当超过软限制时,向进程发送 SIGXFSZ 信号,若信号被捕获,则 write 返回 EBIG 错误infiniteinfinite
RLIMIT_LOCKS一个进程可持有的文件锁的最大数量 (仅 Linux 支持)infiniteinfinite
RLMIT_MEMLOCK一个进程使用 mlock 能够锁定在存储器中的最大字节长度,当超过软限制时,mlock 返回 ENOMEM 错误6553665536
RLIMIT_NOFILE每个进程能打开的最大文件数,当超过软限制时,open 返回 EMFILE 错误,更改软限制会影响 sysconf (_SC_OPEN_MAX) 返回的值10244096
RLIMIT_NPROC每个实际用户 ID 可拥有的最大进程数,当超过软限制时,fork 返回 EAGAGIN 错误,更改软限制会影响 sysconf (_SC_CHILD_MAX) 返回的值409663459
RLIMIT_RSS最大驻内存集的字节长度 (resident set size in bytes),如果物理内存不足,内核将从进程处取回超过 RSS 的部分infiniteinfinite
RLMIT_SBSIZE用户任意给定时刻可以占用的套接字缓冲区的最大字节长度 (仅 FreeBSD 支持)n/an/a
RLMIT_STACK栈的最大字节长度8388608infinite
限制值获取的 demo 就直接用书上提供的,感兴趣的读者可以查看原书,这里就不再列出了。
进程的资源限制通常是在系统初始化时由进程 0 建立的,然后由每个后续进程继承,对于其中非 RLIM_INFINITY 限制值的,进程终其一生无法提升限制值 (超级用户进程除外)。
shell 也提供相应的内置命令 (一般为 ulimit) 来修改默认的限制值,在启动命令前设置各种限制值才能在新进程中生效,在 CentOS 上使用 -a 选项可以查看所有的限制值:
  1. after set DISAPPEAR:
  2. base 0x16fc0d0
  3. [0x7fff15e17488]  XDG_SESSION_ID=318004
  4. [0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
  5. [0x7fff15e174c1]  SHELL=/bin/bash
  6. [0x7fff15e174d1]  TERM=xterm-256color
  7. [0x7fff15e174e5]  HISTSIZE=1000
  8. [0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
  9. [0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
  10. [0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
  11. [0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
  12. [0x7fff15e1757e]  SSH_TTY=/dev/pts/6
  13. [0x7fff15e17591]  USER=yunhai01
  14. [0x7fff15e17c57]  TMOUT=0
  15. [0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
  16. [0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
  17. [0x7fff15e17e30]  LANG=en_US.UTF-8
  18. [0x7fff15e17e41]  HISTCONTROL=ignoredups
  19. [0x16fc010]  HOME=ME
  20. [0x7fff15e17e72]  SHLVL=2
  21. [0x7fff15e17e7a]  GTAGSFORCECPP=1
  22. [0x16fc060]  LOGNAME=this is a very very long user name
  23. [0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
  24. [0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
  25. [0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
  26. [0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
  27. [0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
  28. [0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
  29. [0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
  30. [0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
  31. [0x7fff15e17fe4]  _=./envpos
  32. [0x16fc1e0]  DISAPPEAR=not exist before
复制代码
大部分限制值与调用接口的 demo 打印的一致,但是单位可能和接口不同,使用时需要注意。
下面大体按上表的顺序对各个限制类型分别施加资源限制,观察程序的行为是否和预期一致。
RLIMIT_AS (RLIMIT_VMEM)
  1. after set ADDISION:
  2. base 0x16fc0d0
  3. [0x7fff15e17488]  XDG_SESSION_ID=318004
  4. [0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
  5. [0x7fff15e174c1]  SHELL=/bin/bash
  6. [0x7fff15e174d1]  TERM=xterm-256color
  7. [0x7fff15e174e5]  HISTSIZE=1000
  8. [0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
  9. [0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
  10. [0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
  11. [0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
  12. [0x7fff15e1757e]  SSH_TTY=/dev/pts/6
  13. [0x7fff15e17591]  USER=yunhai01
  14. [0x7fff15e17c57]  TMOUT=0
  15. [0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
  16. [0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
  17. [0x7fff15e17e30]  LANG=en_US.UTF-8
  18. [0x7fff15e17e41]  HISTCONTROL=ignoredups
  19. [0x16fc010]  HOME=ME
  20. [0x7fff15e17e72]  SHLVL=2
  21. [0x7fff15e17e7a]  GTAGSFORCECPP=1
  22. [0x16fc060]  LOGNAME=this is a very very long user name
  23. [0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
  24. [0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
  25. [0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
  26. [0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
  27. [0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
  28. [0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
  29. [0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
  30. [0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
  31. [0x7fff15e17fe4]  _=./envpos
  32. [0x16fc1e0]  DISAPPEAR=not exist before
  33. [0x16fc240]  ADDISION=addision adding
复制代码
设置进程内存软限制 1M ,然后分配 1M 的堆内存:
  1. #include <setjmp.h>
  2. int setjmp(jmp_buf env);
  3. void longjmp(jmp_buf env, int val);
复制代码
果然内存超限失败了。
RLIMIT_DATA

例子同上,只需将 RLIMIT_AS 修改为 RLIMIT_DATA 即可,输出也一致。
毕竟 RLIMIT_DATA 所包含的三个段 (init / bss / heap) 中有堆内存,通过分配堆内存肯定是会挤占这部分限制的。
RLIMIT_CORE
  1. #include <setjmp.h>
  2. #include <stdio.h>
  3. static jmp_buf g_jmpbuf;
  4. void exception_jmp()
  5. {
  6.     printf ("throw_exception_jmp start.\n");
  7.     longjmp(g_jmpbuf, 1);
  8.     printf ("throw_exception_jmp end.\n");
  9. }
  10. void call_jmp()
  11. {
  12.     exception_jmp();
  13. }
  14. int main(int argc, char *argv[])
  15. {
  16.     /* using setjmp and longjmp */
  17.     if (setjmp(g_jmpbuf) == 0)
  18.     {
  19.         call_jmp();
  20.     }
  21.     else
  22.     {
  23.         printf ("catch exception via setimp-longjmp.\n");
  24.     }
  25.     return 0;
  26. }
复制代码
设置崩溃转储文件软限制为 1K,在遭遇空指针崩溃后,能正常生成 core 文件:
  1. throw_exception_jmp start.
  2. catch exception via setimp-longjmp.
复制代码
文件大小未超过 1K。当然前提是需要通过 ulimit -c 指定一个大于 1K 的数值 (非 root 用户),否则在 setrlimit 时会报错:
  1. #include <setjmp.h>
  2. #include <stdio.h>
  3. static jmp_buf g_jmpbuf;
  4. static int globval;
  5. void exception_jmp()
  6. {
  7.     printf ("throw_exception_jmp start.\n");
  8.     longjmp(g_jmpbuf, 1);
  9.     printf ("throw_exception_jmp end.\n");
  10. }
  11. void call_jmp(int i, int j, int k, int l)
  12. {
  13.     printf ("in call_jmp (): \n"
  14.         "globval = %d,\n"
  15.         "autoval = %d,\n"
  16.         "regival = %d,\n"
  17.         "volaval = %d,\n"
  18.         "statval = %d\n\n",
  19.         globval,
  20.         i,
  21.         j,
  22.         k,
  23.         l);
  24.     exception_jmp();
  25. }
  26. int main(int argc, char *argv[])
  27. {
  28.     int autoval;
  29.     register int regival;
  30.     volatile int volaval;
  31.     static int statval;
  32.     globval = 1;
  33.     autoval = 2;
  34.     regival = 3;
  35.     volaval = 4;
  36.     statval = 5;
  37.     /* using setjmp and longjmp */
  38.     if (setjmp(g_jmpbuf) == 0)
  39.     {
  40.         /*
  41.         * Change variables after setjmp, but before longjmp
  42.         */
  43.         globval = 95;  
  44.         autoval = 96;
  45.         regival = 97;
  46.         volaval = 98;
  47.         statval = 99;
  48.         call_jmp(autoval, regival, volaval, statval);
  49.     }
  50.     else
  51.     {
  52.         printf ("catch exception via setimp-longjmp.\n");
  53.         printf ("in main (): \n"
  54.         "globval = %d,\n"
  55.         "autoval = %d,\n"
  56.         "regival = %d,\n"
  57.         "volaval = %d,\n"
  58.         "statval = %d\n\n",
  59.         globval,
  60.         autoval,
  61.         regival,
  62.         volaval,
  63.         statval);
  64.     }
  65.     return 0;
  66. }
复制代码
另外生成的 core 文件应该是被截断了,通过 gdb 加载过程日志可以判断:
  1. $  ./jumpvar
  2. in call_jmp ():
  3. globval = 95,
  4. autoval = 96,
  5. regival = 97,
  6. volaval = 98,
  7. statval = 99
  8. throw_exception_jmp start.
  9. catch exception via setimp-longjmp.
  10. in main ():
  11. globval = 95,
  12. autoval = 96,
  13. regival = 97,
  14. volaval = 98,
  15. statval = 99
复制代码
因此也是不能用的。最后补充一点,设置 core 文件的最小尺寸必需大于 1,否则不会生成任何 core 文件。
RLIMIT_CPU
  1. ...
  2. jumpvar: jumpvar.o apue.o
  3.         gcc -Wall -g $^ -o $@
  4. jumpvar.o: jumpvar.c ../apue.h
  5.         gcc -Wall -g -c $< -o $@ -std=c99
  6.    
  7. jumpvar_opt: jumpvar_opt.o apue.o
  8.         gcc -Wall -g $^ -o $@
  9. jumpvar_opt.o: jumpvar.c ../apue.h
  10.         gcc -Wall -g -c $< -o $@ -std=c99 -O
  11. ...
复制代码
设置了 CPU 软限制为 1 秒,硬限制为 5 秒,且捕获 SIGXCPU 信号,之后进入一个计算死循环,不停消耗 CPU 时间:
  1. $ ./jumpvar
  2. in call_jmp ():
  3. globval = 95,
  4. autoval = 96,
  5. regival = 97,
  6. volaval = 98,
  7. statval = 99
  8. throw_exception_jmp start.
  9. catch exception via setimp-longjmp.
  10. in main ():
  11. globval = 95,
  12. autoval = 2,
  13. regival = 3,
  14. volaval = 98,
  15. statval = 99
复制代码
日志几乎是一秒输出一行,第 5 秒时达到 CPU 硬限制,进程被强制杀死。
RLIMIT_FSIZE
  1. #include <sys/resource.h>
  2. // struct rlimit {
  3. //     rlim_t rlim_cur;  /* Soft limit */
  4. //     rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
  5. // };
  6. int getrlimit(int resource, struct rlimit *rlim);
  7. int setrlimit(int resource, const struct rlimit *rlim);
复制代码
设置最大写入文件字节数软限制 1K,捕获 SIGXFZE 信号后打开 core.tmp 文件不停写入,每次写入 32 字节直到失败:
  1. # define RLIM_INFINITY ((__rlim_t) -1)
复制代码
写满 1K 后收到了 SIGXFSZ 信号,捕获信号避免了进程 abort,不过 write 返回了 EBIG 错误。
这里需要注意不应使用 fopen/fclose/fwrite 来进行测试,因标准 I/O 库的缓存机制,导致写入的字节数大于实际落盘的字节数,从而得不到准确的限制值。
RLMIT_MEMLOCK

[code]#include "../apue.h"#include #include int main (int argc, char *argv[]){  int ret = 0;  struct rlimit lmt = { 0 };  lmt.rlim_cur = 32 * 1024;  lmt.rlim_max = 64 * 1024;  ret = setrlimit (RLIMIT_MEMLOCK, &lmt);  if (ret == -1)    err_sys ("set rlimit memlock failed");    char *ptr = malloc (32 * 1024);  if (ptr == NULL)    err_sys ("malloc failed");  printf ("alloc 32K success!\n");#define BLOCK_NUM 32  for (int i=0; i

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具