|
main 函数与进程终止
众所周知,main 函数为 unix like 系统上可执行文件的"入口",然而这个入口并不是指链接器设置的程序起始地址,后者通常是一个启动例程,它从内核取得命令行参数和环境变量值后,为调用 main 函数做好安排。main 函数原型为:- int main (int argc, char *argv[]);
复制代码 这是 ISO C 和 POSIX.1 指义的,当然还存在下面几种不太标准的 main 原型:- void main (int argc, char *argv[]);
- void main (void);
- 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 函数:- #include <unistd.h>
- void _exit(int status);
- #include <stdlib.h>
- void exit(int status);
- void _Exit(int status);
复制代码 声明差别不大,_exit 与 _Exit 分别是 POSIX.1 与 ISO C 的标准,不过可以将它们视为等价,都直接进入内核。exit 则在它们的基础上做了一些清理工作,主要包含以下几个方面:
- 清理线程局部存储 (TLS) 信息
- 按顺序调用注册的终止处理程序
- 为所有标准 I/O 库打开的流调用 fclose 函数,这会 flush 缓冲的输出数据
关于标准 I/O 库,请参考之前写的这篇文章:《[apue] 标准 I/O 库那些事儿》。
有了上面的铺垫,可以这样理解可执行程序的启动例程与 main 之间的关系:- ...
- exit (main (argc, argv));
复制代码 即 main 的返回值是直接传递给 exit 的 status 参数作为进程结束状态的。
atexit
关于终止处理程序,一般通过 atexit 函数进行注册:- #include <stdlib.h>
- int atexit(void (*function)(void));
复制代码 这里的 function 参数就是希望在 exit 时被调用的清理程序,关于终止处理程序,有下面几点需要注意:
- 调用次数有上限,通过 sysconf (_SC_ATEXIT_MAX) 查询 (实测为 2147483647, 即 INT_MAX)
- FILO,先注册的后被调用,类似于堆栈,而非队列
- 调用次数等于注册次数,同一清理程序可多次注册,注册几次调用几次
- 执行 exec 函数族执行另一个程序的时候,自动清空 atexit 注册的清理程序
- 在清理程序中调用 exit "无效",如果调用 _exit 或 _Exit,会导致程序直接退出,后续清理程序不再被调用
- 进程异常终止时清理程序不会被调用
下面这个例子验证了调用次数与 FILO 特性:- #include "../apue.h"
- void do_dirty_work ()
- {
- printf ("doing dirty works!\n");
- }
- void bye ()
- {
- printf ("bye, forks~\n");
- }
- void times ()
- {
- static int counter = 32;
- printf ("times %d\n", counter--);
- }
- int main ()
- {
- int ret = 0;
- ret = atexit (do_dirty_work);
- if (ret != 0)
- err_sys ("atexit");
- ret = atexit (bye);
- if (ret != 0)
- err_sys ("bye1");
- ret = atexit (bye);
- if (ret != 0)
- err_sys ("bye2");
- for (int i=0; i<32; i++)
- {
- ret = atexit (times);
- if (ret != 0)
- err_sys ("times");
- }
- printf ("main is done!\n");
- return 0;
- }
复制代码 可见 exit 并非没有生效,一个合理的推断是:第二次进入 exit 后,继续处理之前没处理完的清理程序,使得输出看起来就像"没生效"一样。真正的 _exit 是被第二次进入 bye 的那个 exit 所调用,对程序稍加改动来看个明白:- $ ./atexit
- main is done!
- times 32
- times 31
- times 30
- times 29
- times 28
- times 27
- times 26
- times 25
- times 24
- times 23
- times 22
- times 21
- times 20
- times 19
- times 18
- times 17
- times 16
- times 15
- times 14
- times 13
- times 12
- times 11
- times 10
- times 9
- times 8
- times 7
- times 6
- times 5
- times 4
- times 3
- times 2
- times 1
- bye, forks~
- bye, forks~
- doing dirty works!
复制代码 为了便于区别,这里给的初始值为 10,每调用一次 bye,exit_status 递增 1,如果最后进程结束状态码为 10 就证明是第一次 exit 结束了进程,否则就是第二次。- void bye ()
- {
- printf ("bye, forks~\n");
- exit (2); // no effect
- printf ("after exit (2)\n");
- }
复制代码 结论已经非常明显,之前的猜测成立!如此就可以合理的推断 exit 调用清理程序后,会将其从 FILO 结构中移除,从而避免再次调用,进而引发无限循环。
下面试试 _exit 的效果:- $ ./atexit
- main is done!
- times 32
- ...
- times 1
- bye, forks~
- bye, forks~
- doing dirty works!
- > echo $?
- 2
复制代码 改为 _exit 后输出发生了截断:- int exit_status = 10;
- void bye ()
- {
- printf ("bye, forks~\n");
- exit (exit_status++); // no effect
- printf ("after exit (%d)\n", exit_status-1);
- }
复制代码 进入 bye 处理程序后进程就终止了,后续的处理程序不再调用。检查进程结束状态码为 3,正好是 _exit 的 status 参数。
将上面 exit 和 _exit 全都打开后,_exit 反而不起作用了:- $ ./atexit
- main is done!
- times 32
- ...
- times 1
- bye, forks~
- bye, forks~
- doing dirty works!
- > echo $?
- 11
复制代码 经过上面的分析,想必读者已经知道了答案,正确的做法是将 _exit 放在 exit 前面,这样才能避免进入 exit 之后不再返回,从而被忽略。
最后再试一种场景,就是在处理器中继续调用 atexit 注册新的处理器,观察新的处理器是否能被调用,参考下面这个例子:- void bye ()
- {
- printf ("bye, forks~\n");
- _exit (3); // quit and no other atexit function running anymore !
- printf ("after _exit (3)\n");
- }
复制代码 先注册处理器 bye,在其被回调时再注册处理器 do_dirty_work,结果是两个处理器都能被回调:- $ ./atexit
- main is done!
- times 32
- ...
- times 1
- bye, forks~
- $ echo $?
- 3
复制代码 如果注册的处理器形成循环会如何?参考下面的例子:- void bye ()
- {
- printf ("bye, forks~\n");
- exit (2); // no effect
- printf ("after exit (2)\n");
- _exit (3); // no effect
- printf ("after _exit (3)\n");
- }
复制代码 在 do_dirty_work 中再次注册 bye 作为处理器,重新编译后运行,发现程序果然陷入了死循环:- $ ./atexit
- main is done!
- times 32
- ...
- times 1
- bye, forks~
- $ echo $?
- 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,因此下面两种遍历命令行参数的方式是等价的:- $ ./atexit_term
- main is done!
- bye, forks~
- doing dirty works!
复制代码 dec/hex 列分别是三者加总后的十进制与十六进制长度。示例中 layout_s 是静态链接版本,可见使用共享库的动态链接在各个段的尺寸上都有明显缩减。
堆分配
栈的增长主要依赖函数调用层次的增加;堆的增长主要依赖以下存储器分配函数:- #include "../apue.h"
- extern void bye ();
- void do_dirty_work ()
- {
- printf ("doing dirty works!\n");
- int ret = atexit (bye);
- if (ret != 0)
- err_sys ("bye2");
- }
- void bye ()
- {
- printf ("bye, forks~\n");
- int ret = atexit (do_dirty_work);
- if (ret != 0)
- err_sys ("do_dirty_work");
- }
- int main ()
- {
- int ret = 0;
- ret = atexit (bye);
- if (ret != 0)
- err_sys ("bye");
- printf ("main is done!\n");
- return 0;
- }
复制代码 其中:
- malloc 分配 size 长度的存储区
- calloc 分配 nmemb*size 长度的存储区
- realloc 可更改以前分配区到 size 长度 (增加或减小)
对于新增的存储区
- calloc 初始值为 0
- malloc 和 realloc 初始值不确定
对于 realloc,新旧地址之间的关系:
- 当存储区减小时,新旧地址保持一致
- 当存储区增加时
- 原存储区后有足够的空间时,新旧地址保持一致
- 原存储区后没有足够的空间,新旧地址不同,会先分配足够大的空间,复制数据,再释放原存储区
realloc(NULL, size) 等价于 malloc(size)。
sbrk
这些分配例程通常用 sbrk 系统调用来扩充进程的堆:- $ ./atexit_term
- main is done!
- 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!
- bye, forks~
- doing dirty works!
- bye, forks~
- doing dirty works!^C
复制代码 这通常是通过调用 program break 的位置来实现的,参考 man 这段说明:- int i;
- for (i=0; i<argc; ++ i)
- ...
- for (i=0; argv[i]!=NULL; ++i)
- ...
复制代码 program break 就是 bss 段的结尾,参考上图应该就是堆底。
sbrk 也可以减小堆大小,不过大多数 malloc 和 free 的实现都不减小进程的存储空间,释放的空间可供以后再分配,但通常将它们保持在 malloc 池中而不返回给内核。
环境变量空间的变更
有上面内容的铺垫,就可以回顾下上一节中增删改环境变量对存储空间的影响了:
- 删除环境变量,之后的变量前移填补删除后的空位
- 修改环境变量
- 新值长度小于等于旧值,在原字符串空间中写入新值
- 新值长度大于旧值,在堆上分配新字符串空间并赋值,更新环境变量表中的指针使之指向新分配的字符串
- 新增环境变量
- 第一次新增环境变量,在堆上分配新的环境变量表,将原来的环境变量"复制"到新分配的环境变量表中,然后把新增的环境变量字符串放在表尾,再新增一个空指针放在最后,最后使用 environ 变量指向新分配的环境变量表,基本上就是将环境变量从栈顶搬到了堆中,不过大多数环境变量仍指向栈顶中分配的字符串而已
- 非第一次新增,使用 realloc 重新分配 environ 变量,以容纳新增加的环境变量
环境变量空间改变如此复杂,主要是因为它的大小被栈顶限制死了,没有办法扩容,当增加环境变量数目时,只能从栈顶搬到堆中。
下面的程序演示了这一过程:- int main (int argc, char* argv[], char* envp[]);
复制代码 程序比较简单,依次执行以下操作:add HOME -> add LOGNAME -> remove PATH -> add DISAPPEAR -> add ADDISION,每次操作后都打印整个环境变量表,以观察 environ 和各个环境变量的变化:启动后先打印整个环境变量表,大概有 30 个环境变量。设置 HOME 变量,虽然新值长度小于旧值,这里仍然为新值在堆上分配了空间,看起来 linux 上的实现偷懒了。- 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 变量,新值长度大于旧值,这里没有悬念的在堆上进行了分配。- #include <stdlib.h>
- [ISO C/POSIX.1] char *getenv(const char *name);
- [POSIX.1] int setenv(const char *name, const char *value, int overwrite);
- [POSIX.1] int unsetenv(const char *name);
- [XSI] int putenv(char *string);
- [linux] int clearenv(void);
复制代码 删除 PATH 变量,这一步主要验证再次新增环境变量时,会不会重复利用已删除的空位,到目前为止 environ 指针地址 (0x7fff15e16468) 没有发生变化,仍位于栈顶之上。- #include "../apue.h"
- int data1 = 2;
- int data2 = 3;
- int data3;
- int data4;
- int main (int argc, char *argv[])
- {
- char buf1[1024] = { 0 };
- char buf2[1024] = { 0 };
- char *buf3 = malloc(1024);
- char *buf4 = malloc(1024);
- printf ("onstack %p, %p\n",
- buf1,
- buf2);
- extern char ** environ;
- printf ("env %p\n", environ);
- printf ("arg %p\n", argv);
- printf ("onheap %p, %p\n",
- buf3,
- buf4);
- free (buf3);
- free (buf4);
- printf ("on bss %p, %p\n",
- &data3,
- &data4);
- printf ("on init %p, %p\n",
- &data1,
- &data2);
- printf ("on code %p\n", main);
- return 0;
- }
复制代码 增加 DISAPPEAR 变量,没有悬念的在堆上分配了空间,最大的变化在于 environ 指针变了!从栈顶之上移动了到了堆中 (0x16fc0d0),看起来之前删除 PATH 变量腾空的位置没有利用上。- $ ./layout
- onstack 0x7ffe31b752a0, 0x7ffe31b74ea0
- env 0x7ffe31b757b8
- arg 0x7ffe31b757a8
- onheap 0x1984010, 0x1984420
- on bss 0x6066b8, 0x6066bc
- on init 0x606224, 0x606228
- on code 0x40179d
复制代码 增加 ADDISION 变量,仍然在堆上分配空间,而且 environ 指针地址 (0x16fc0d0) 没有发生变化,看起来仍有足够的空间让 realloc 分配。
指令跳转 (setjmp & longjmp)
说到指令跳转,第一印象就是 goto。由于程序的执行本质是一条条机器代码的执行,有些指令本身自带跳转属性,像函数调用 (call)、函数返回 (return) 、switch-case 都是某种形式的指令跳转,goto 则将这种能力公布给了开发者,然而下面的两个限制导致它在实际应用上的推广受阻:
- 只能在函数内部跳转,无法跨越函数栈
- 滥用 goto 导致代码逻辑不清晰、后期维护困难
setjmp & longjmp 完美的解决了上述 goto 的缺点,支持跨函数栈的跳转、且使用上更不易被滥用,也被称为非局部 goto。
它的跳转逻辑和现代 C++ 的异常机制已经非常相似了,区别是后者加入了对栈上对象析构函数的自动调用等更多的内容。
先来看函数原型:- $ size ./layout ./layout_s /bin/sh
- text data bss dec hex filename
- 20073 2152 80 22305 5721 ./layout
- 802535 7292 11120 820947 c86d3 ./layout_s
- 905942 36000 22920 964862 eb8fe /bin/sh
复制代码 书上给的例子就不错,这里找到另外一个更简单的例子:- #include <stdlib.h>
- void *malloc(size_t size);
- void *calloc(size_t nmemb, size_t size);
- void *realloc(void *ptr, size_t size);
- void free(void *ptr);
复制代码 编译运行这个 demo,输出如下:- #include <unistd.h>
- 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 时的状态,还是保留最后的状态呢?对上面的例子稍加修改来进行一番考察:- DESCRIPTION
- brk() and sbrk() change the location of the program break, which defines the end of the process's data segment (i.e., the pro‐
- gram break is the first location after the end of the uninitialized data segment). Increasing the program break has the effect
- of allocating memory to the process; decreasing the break deallocates memory.
复制代码 在原来的基础上添加了几种类型的变量:
- globaval:全局变量
- autoval:main 栈上自动变量
- regival:main 栈上寄存器变量
- valaval:main 栈上易失变量
- statval:main 栈上静态变量
并分别在 call_jmp 内部和 longjmp 后 (第二次从 setjmp 返回) 时打印它们的值:- $ ./jumpvarin call_jmp ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99#include <unistd.h>
- void *sbrk(intptr_t increment);in main ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99
复制代码 在没开优化的情况下,各个变量都最新的状态,没有发生值回退现象,添加 -O 编译选项: - $ ./envpos
- base 0x7fff15e16468
- [0x7fff15e17488] XDG_SESSION_ID=318004
- [0x7fff15e1749e] HOSTNAME=yunhai.bcc-bdbl.baidu.com
- [0x7fff15e174c1] SHELL=/bin/bash
- [0x7fff15e174d1] TERM=xterm-256color
- [0x7fff15e174e5] HISTSIZE=1000
- [0x7fff15e174f3] SSH_CLIENT=172.31.23.41 52661 22
- [0x7fff15e17514] ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
- [0x7fff15e17546] QTDIR=/usr/lib64/qt-3.3
- [0x7fff15e1755e] QTINC=/usr/lib64/qt-3.3/include
- [0x7fff15e1757e] SSH_TTY=/dev/pts/6
- [0x7fff15e17591] USER=yunhai01
- [0x7fff15e17c57] TMOUT=0
- [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
- [0x7fff15e17de4] MAIL=/var/spool/mail/yunhai01
- [0x7fff15e17e02] PWD=/home/users/yunhai01/code/apue/07.chapter
- [0x7fff15e17e30] LANG=en_US.UTF-8
- [0x7fff15e17e41] HISTCONTROL=ignoredups
- [0x7fff15e17e58] HOME=/home/users/yunhai01
- [0x7fff15e17e72] SHLVL=2
- [0x7fff15e17e7a] GTAGSFORCECPP=1
- [0x7fff15e17e8a] LOGNAME=yunhai01
- [0x7fff15e17e9b] QTLIB=/usr/lib64/qt-3.3/lib
- [0x7fff15e17eb7] SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
- [0x7fff15e17eea] LESSOPEN=||/usr/bin/lesspipe.sh %s
- [0x7fff15e17f0d] ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
- [0x7fff15e17f4b] XDG_RUNTIME_DIR=/run/user/383278
- [0x7fff15e17f6c] LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
- [0x7fff15e17f9c] HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
- [0x7fff15e17fbe] OLDPWD=/home/users/yunhai01/code/apue
- [0x7fff15e17fe4] _=./envpos
复制代码 再次运行:- $ ./jumpvarin call_jmp ():globval = 95,autoval = 96,regival = 97,volaval = 98,statval = 99#include <unistd.h>
- 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 查询或更改它们:- after set LOGNAME:
- base 0x7fff15e16468
- [0x7fff15e17488] XDG_SESSION_ID=318004
- [0x7fff15e1749e] HOSTNAME=yunhai.bcc-bdbl.baidu.com
- [0x7fff15e174c1] SHELL=/bin/bash
- [0x7fff15e174d1] TERM=xterm-256color
- [0x7fff15e174e5] HISTSIZE=1000
- [0x7fff15e174f3] SSH_CLIENT=172.31.23.41 52661 22
- [0x7fff15e17514] ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
- [0x7fff15e17546] QTDIR=/usr/lib64/qt-3.3
- [0x7fff15e1755e] QTINC=/usr/lib64/qt-3.3/include
- [0x7fff15e1757e] SSH_TTY=/dev/pts/6
- [0x7fff15e17591] USER=yunhai01
- [0x7fff15e17c57] TMOUT=0
- [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
- [0x7fff15e17de4] MAIL=/var/spool/mail/yunhai01
- [0x7fff15e17e02] PWD=/home/users/yunhai01/code/apue/07.chapter
- [0x7fff15e17e30] LANG=en_US.UTF-8
- [0x7fff15e17e41] HISTCONTROL=ignoredups
- [0x16fc010] HOME=ME
- [0x7fff15e17e72] SHLVL=2
- [0x7fff15e17e7a] GTAGSFORCECPP=1
- [0x16fc060] LOGNAME=this is a very very long user name
- [0x7fff15e17e9b] QTLIB=/usr/lib64/qt-3.3/lib
- [0x7fff15e17eb7] SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
- [0x7fff15e17eea] LESSOPEN=||/usr/bin/lesspipe.sh %s
- [0x7fff15e17f0d] ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
- [0x7fff15e17f4b] XDG_RUNTIME_DIR=/run/user/383278
- [0x7fff15e17f6c] LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
- [0x7fff15e17f9c] HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
- [0x7fff15e17fbe] OLDPWD=/home/users/yunhai01/code/apue
- [0x7fff15e17fe4] _=./envpos
复制代码 resource 指定了限制的类型,rlim 则包含了资源限制的信息,主要包含两个成员:
- rlim_cur:软限制值,当前生效的限制值
- rlim_max:硬限制值,大于等于软限制值,软限制值的提升上限
- 任何用户可以降低硬限制值,只有超级用户可以提升硬限制值
- 每次降低的硬限制值必需大于等于软限制值
RLIM_INFINITY 表示无限量:- after unset PATH:
- base 0x7fff15e16468
- [0x7fff15e17488] XDG_SESSION_ID=318004
- [0x7fff15e1749e] HOSTNAME=yunhai.bcc-bdbl.baidu.com
- [0x7fff15e174c1] SHELL=/bin/bash
- [0x7fff15e174d1] TERM=xterm-256color
- [0x7fff15e174e5] HISTSIZE=1000
- [0x7fff15e174f3] SSH_CLIENT=172.31.23.41 52661 22
- [0x7fff15e17514] ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
- [0x7fff15e17546] QTDIR=/usr/lib64/qt-3.3
- [0x7fff15e1755e] QTINC=/usr/lib64/qt-3.3/include
- [0x7fff15e1757e] SSH_TTY=/dev/pts/6
- [0x7fff15e17591] USER=yunhai01
- [0x7fff15e17c57] TMOUT=0
- [0x7fff15e17de4] MAIL=/var/spool/mail/yunhai01
- [0x7fff15e17e02] PWD=/home/users/yunhai01/code/apue/07.chapter
- [0x7fff15e17e30] LANG=en_US.UTF-8
- [0x7fff15e17e41] HISTCONTROL=ignoredups
- [0x16fc010] HOME=ME
- [0x7fff15e17e72] SHLVL=2
- [0x7fff15e17e7a] GTAGSFORCECPP=1
- [0x16fc060] LOGNAME=this is a very very long user name
- [0x7fff15e17e9b] QTLIB=/usr/lib64/qt-3.3/lib
- [0x7fff15e17eb7] SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
- [0x7fff15e17eea] LESSOPEN=||/usr/bin/lesspipe.sh %s
- [0x7fff15e17f0d] ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
- [0x7fff15e17f4b] XDG_RUNTIME_DIR=/run/user/383278
- [0x7fff15e17f6c] LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
- [0x7fff15e17f9c] HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
- [0x7fff15e17fbe] OLDPWD=/home/users/yunhai01/code/apue
- [0x7fff15e17fe4] _=./envpos
复制代码 可以指定的资源限制类型及在本地环境上的软硬限制值列表如下:
resource | 含义 | 软限制 | 硬限制 | RLIMIT_AS | 进程可用存储区的最大字节长度,会影响 sbrk & mmap 函数,非 Linux 平台也命名为 RLIMIT_VMEM | infinite | infinite | RLIMIT_CORE | 崩溃转储文件的最大字节数,0 表示阻止创建,生成的 core 文件大于限制值时会被截断 | 0 | infinite | RLIMIT_CPU | CPU 的最大量值,单位秒,超过软限制时,向进程发送 SIGXCPU 信号;超过硬限制时,向进程发送 SIGKILL 信号 | infinite | infinite | RLIMIT_DATA | 数据段的最大字节长度,是 init + bss + heap 的总长度,即除栈、环境变量、命令行参数外的内存总长度 | infinite | infinite | RLIMIT_FSIZE | 可以创建的文件的最大字节长度,当超过软限制时,向进程发送 SIGXFSZ 信号,若信号被捕获,则 write 返回 EBIG 错误 | infinite | infinite | RLIMIT_LOCKS | 一个进程可持有的文件锁的最大数量 (仅 Linux 支持) | infinite | infinite | RLMIT_MEMLOCK | 一个进程使用 mlock 能够锁定在存储器中的最大字节长度,当超过软限制时,mlock 返回 ENOMEM 错误 | 65536 | 65536 | RLIMIT_NOFILE | 每个进程能打开的最大文件数,当超过软限制时,open 返回 EMFILE 错误,更改软限制会影响 sysconf (_SC_OPEN_MAX) 返回的值 | 1024 | 4096 | RLIMIT_NPROC | 每个实际用户 ID 可拥有的最大进程数,当超过软限制时,fork 返回 EAGAGIN 错误,更改软限制会影响 sysconf (_SC_CHILD_MAX) 返回的值 | 4096 | 63459 | RLIMIT_RSS | 最大驻内存集的字节长度 (resident set size in bytes),如果物理内存不足,内核将从进程处取回超过 RSS 的部分 | infinite | infinite | RLMIT_SBSIZE | 用户任意给定时刻可以占用的套接字缓冲区的最大字节长度 (仅 FreeBSD 支持) | n/a | n/a | RLMIT_STACK | 栈的最大字节长度 | 8388608 | infinite | 限制值获取的 demo 就直接用书上提供的,感兴趣的读者可以查看原书,这里就不再列出了。
进程的资源限制通常是在系统初始化时由进程 0 建立的,然后由每个后续进程继承,对于其中非 RLIM_INFINITY 限制值的,进程终其一生无法提升限制值 (超级用户进程除外)。
shell 也提供相应的内置命令 (一般为 ulimit) 来修改默认的限制值,在启动命令前设置各种限制值才能在新进程中生效,在 CentOS 上使用 -a 选项可以查看所有的限制值:- after set DISAPPEAR:
- base 0x16fc0d0
- [0x7fff15e17488] XDG_SESSION_ID=318004
- [0x7fff15e1749e] HOSTNAME=yunhai.bcc-bdbl.baidu.com
- [0x7fff15e174c1] SHELL=/bin/bash
- [0x7fff15e174d1] TERM=xterm-256color
- [0x7fff15e174e5] HISTSIZE=1000
- [0x7fff15e174f3] SSH_CLIENT=172.31.23.41 52661 22
- [0x7fff15e17514] ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
- [0x7fff15e17546] QTDIR=/usr/lib64/qt-3.3
- [0x7fff15e1755e] QTINC=/usr/lib64/qt-3.3/include
- [0x7fff15e1757e] SSH_TTY=/dev/pts/6
- [0x7fff15e17591] USER=yunhai01
- [0x7fff15e17c57] TMOUT=0
- [0x7fff15e17de4] MAIL=/var/spool/mail/yunhai01
- [0x7fff15e17e02] PWD=/home/users/yunhai01/code/apue/07.chapter
- [0x7fff15e17e30] LANG=en_US.UTF-8
- [0x7fff15e17e41] HISTCONTROL=ignoredups
- [0x16fc010] HOME=ME
- [0x7fff15e17e72] SHLVL=2
- [0x7fff15e17e7a] GTAGSFORCECPP=1
- [0x16fc060] LOGNAME=this is a very very long user name
- [0x7fff15e17e9b] QTLIB=/usr/lib64/qt-3.3/lib
- [0x7fff15e17eb7] SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
- [0x7fff15e17eea] LESSOPEN=||/usr/bin/lesspipe.sh %s
- [0x7fff15e17f0d] ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
- [0x7fff15e17f4b] XDG_RUNTIME_DIR=/run/user/383278
- [0x7fff15e17f6c] LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
- [0x7fff15e17f9c] HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
- [0x7fff15e17fbe] OLDPWD=/home/users/yunhai01/code/apue
- [0x7fff15e17fe4] _=./envpos
- [0x16fc1e0] DISAPPEAR=not exist before
复制代码 大部分限制值与调用接口的 demo 打印的一致,但是单位可能和接口不同,使用时需要注意。
下面大体按上表的顺序对各个限制类型分别施加资源限制,观察程序的行为是否和预期一致。
RLIMIT_AS (RLIMIT_VMEM)
- after set ADDISION:
- base 0x16fc0d0
- [0x7fff15e17488] XDG_SESSION_ID=318004
- [0x7fff15e1749e] HOSTNAME=yunhai.bcc-bdbl.baidu.com
- [0x7fff15e174c1] SHELL=/bin/bash
- [0x7fff15e174d1] TERM=xterm-256color
- [0x7fff15e174e5] HISTSIZE=1000
- [0x7fff15e174f3] SSH_CLIENT=172.31.23.41 52661 22
- [0x7fff15e17514] ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
- [0x7fff15e17546] QTDIR=/usr/lib64/qt-3.3
- [0x7fff15e1755e] QTINC=/usr/lib64/qt-3.3/include
- [0x7fff15e1757e] SSH_TTY=/dev/pts/6
- [0x7fff15e17591] USER=yunhai01
- [0x7fff15e17c57] TMOUT=0
- [0x7fff15e17de4] MAIL=/var/spool/mail/yunhai01
- [0x7fff15e17e02] PWD=/home/users/yunhai01/code/apue/07.chapter
- [0x7fff15e17e30] LANG=en_US.UTF-8
- [0x7fff15e17e41] HISTCONTROL=ignoredups
- [0x16fc010] HOME=ME
- [0x7fff15e17e72] SHLVL=2
- [0x7fff15e17e7a] GTAGSFORCECPP=1
- [0x16fc060] LOGNAME=this is a very very long user name
- [0x7fff15e17e9b] QTLIB=/usr/lib64/qt-3.3/lib
- [0x7fff15e17eb7] SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
- [0x7fff15e17eea] LESSOPEN=||/usr/bin/lesspipe.sh %s
- [0x7fff15e17f0d] ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
- [0x7fff15e17f4b] XDG_RUNTIME_DIR=/run/user/383278
- [0x7fff15e17f6c] LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
- [0x7fff15e17f9c] HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
- [0x7fff15e17fbe] OLDPWD=/home/users/yunhai01/code/apue
- [0x7fff15e17fe4] _=./envpos
- [0x16fc1e0] DISAPPEAR=not exist before
- [0x16fc240] ADDISION=addision adding
复制代码 设置进程内存软限制 1M ,然后分配 1M 的堆内存:- #include <setjmp.h>
- int setjmp(jmp_buf env);
- void longjmp(jmp_buf env, int val);
复制代码 果然内存超限失败了。
RLIMIT_DATA
例子同上,只需将 RLIMIT_AS 修改为 RLIMIT_DATA 即可,输出也一致。
毕竟 RLIMIT_DATA 所包含的三个段 (init / bss / heap) 中有堆内存,通过分配堆内存肯定是会挤占这部分限制的。
RLIMIT_CORE
- #include <setjmp.h>
- #include <stdio.h>
-
- static jmp_buf g_jmpbuf;
-
- void exception_jmp()
- {
- printf ("throw_exception_jmp start.\n");
- longjmp(g_jmpbuf, 1);
- printf ("throw_exception_jmp end.\n");
- }
-
- void call_jmp()
- {
- exception_jmp();
- }
-
- int main(int argc, char *argv[])
- {
- /* using setjmp and longjmp */
- if (setjmp(g_jmpbuf) == 0)
- {
- call_jmp();
- }
- else
- {
- printf ("catch exception via setimp-longjmp.\n");
- }
-
- return 0;
- }
复制代码 设置崩溃转储文件软限制为 1K,在遭遇空指针崩溃后,能正常生成 core 文件:- throw_exception_jmp start.
- catch exception via setimp-longjmp.
复制代码 文件大小未超过 1K。当然前提是需要通过 ulimit -c 指定一个大于 1K 的数值 (非 root 用户),否则在 setrlimit 时会报错:- #include <setjmp.h>
- #include <stdio.h>
-
- static jmp_buf g_jmpbuf;
- static int globval;
-
- void exception_jmp()
- {
- printf ("throw_exception_jmp start.\n");
- longjmp(g_jmpbuf, 1);
- printf ("throw_exception_jmp end.\n");
- }
-
- void call_jmp(int i, int j, int k, int l)
- {
- printf ("in call_jmp (): \n"
- "globval = %d,\n"
- "autoval = %d,\n"
- "regival = %d,\n"
- "volaval = %d,\n"
- "statval = %d\n\n",
- globval,
- i,
- j,
- k,
- l);
- exception_jmp();
- }
-
- int main(int argc, char *argv[])
- {
- int autoval;
- register int regival;
- volatile int volaval;
- static int statval;
- globval = 1;
- autoval = 2;
- regival = 3;
- volaval = 4;
- statval = 5;
- /* using setjmp and longjmp */
- if (setjmp(g_jmpbuf) == 0)
- {
- /*
- * Change variables after setjmp, but before longjmp
- */
- globval = 95;
- autoval = 96;
- regival = 97;
- volaval = 98;
- statval = 99;
- call_jmp(autoval, regival, volaval, statval);
- }
- else
- {
- printf ("catch exception via setimp-longjmp.\n");
- printf ("in main (): \n"
- "globval = %d,\n"
- "autoval = %d,\n"
- "regival = %d,\n"
- "volaval = %d,\n"
- "statval = %d\n\n",
- globval,
- autoval,
- regival,
- volaval,
- statval);
- }
-
- return 0;
- }
复制代码 另外生成的 core 文件应该是被截断了,通过 gdb 加载过程日志可以判断:- $ ./jumpvar
- in call_jmp ():
- globval = 95,
- autoval = 96,
- regival = 97,
- volaval = 98,
- statval = 99
- throw_exception_jmp start.
- catch exception via setimp-longjmp.
- in main ():
- globval = 95,
- autoval = 96,
- regival = 97,
- volaval = 98,
- statval = 99
复制代码 因此也是不能用的。最后补充一点,设置 core 文件的最小尺寸必需大于 1,否则不会生成任何 core 文件。
RLIMIT_CPU
- ...
- jumpvar: jumpvar.o apue.o
- gcc -Wall -g $^ -o $@
- jumpvar.o: jumpvar.c ../apue.h
- gcc -Wall -g -c $< -o $@ -std=c99
-
- jumpvar_opt: jumpvar_opt.o apue.o
- gcc -Wall -g $^ -o $@
- jumpvar_opt.o: jumpvar.c ../apue.h
- gcc -Wall -g -c $< -o $@ -std=c99 -O
- ...
复制代码 设置了 CPU 软限制为 1 秒,硬限制为 5 秒,且捕获 SIGXCPU 信号,之后进入一个计算死循环,不停消耗 CPU 时间:- $ ./jumpvar
- in call_jmp ():
- globval = 95,
- autoval = 96,
- regival = 97,
- volaval = 98,
- statval = 99
- throw_exception_jmp start.
- catch exception via setimp-longjmp.
- in main ():
- globval = 95,
- autoval = 2,
- regival = 3,
- volaval = 98,
- statval = 99
复制代码 日志几乎是一秒输出一行,第 5 秒时达到 CPU 硬限制,进程被强制杀死。
RLIMIT_FSIZE
- #include <sys/resource.h>
- // struct rlimit {
- // rlim_t rlim_cur; /* Soft limit */
- // rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
- // };
- int getrlimit(int resource, struct rlimit *rlim);
- int setrlimit(int resource, const struct rlimit *rlim);
复制代码 设置最大写入文件字节数软限制 1K,捕获 SIGXFZE 信号后打开 core.tmp 文件不停写入,每次写入 32 字节直到失败:- # 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
|