前言
写这篇博客的初衷,仅是记录自己遇到的问题和让自己逻辑自洽的解释。
这是我在实现 【力扣和牛客的OJ子系统】 时遇到的问题,具体是在运行模块,因为我们都知道在OJ提交代码的时候,无非就是结果正确、结果错误、异常、运行超时、内存过大等,所以我在设置资源限制的时候,使用了setrlimit函数,也就只限制了进程在CPU调度的时间和允许使用地址空间的大小。但是就在设置进程地址空间大小时,出现了问题!并且在重定向stderr的时候,也出现了问题。
在往下看的时候,必须要知道setrlimit函数,是在执行期间有效,并只对在调用该函数的以后的字段有效,并不会对调用该函数之前的字段产生限制!
一、RLIMIT_AS与进程替换的问题
【问题一】
「设置的允许使用进程地址空间的大小 是 无法加载库文件的数据时」:
当我设置允许使用进程地址空间大小为1byte,实现进程替换的时候,出现了链接报错
【原因】
setrlimit函数是在执行期间生效,不会对生效之前的资源做限制,只对生效之后的字段做限制。在子进程调用该函数的时候,已经加载过的数据,是不会被影响的。
而在程序替换的时候,是将新程序的 代码和数据,使用到的库 重新覆盖到地址空间上的。所以当我设置很小的空间限制,就会出现动态库加载不完整,链接报错的情况。
【问题二】
「设置的允许使用进程地址空间的大小 可以完全加载库文件数据,但是无法完全加载程序代码和数据」:
当我提高了允许使用进程地址空间的大小,实现进程替换的时候,出现了被SIGSEGV信号(11号信号)杀死的情况
【原因】
此时空间限制提高到,新程序的库可以完全加载,但是代码和数据加载不完整的时候,而CPU在调度这个进程,是通过寄存器,一个用于解析,一个用于保存该条指令的下一条指令的地址。
正好就是下一条指令,是没有被加载的,但是CPU不知道,CPU就会继续去拿这个未被加载内容的地址上的指令,就发生了越界访问,告诉操作系统是越界访问,OS发送SIGSEGV信号给当前进程,直接杀死!
这也就表明了每个进程都要有一个表示该进程结束的指令,告诉CPU,CPU才会将其脱离调度。所以当加载不完整的代码和数据的时候,CPU就拿不到这个终止的指令,就拿到了没有内容的地址,就越界了。
【总结】
所以我们推荐的是,在使用进程替换的时候,使用setrlimit时,一定要有足够的空间来满足替换后的程序的库文件、代码和数据都完整的加载,防止出现不完整,使得运行出现问题。
(切记堆栈上都是程序运行时申请的,我们代码虽然那么写,但是只是将指令的长度加载到地址空间,真当申请堆栈空间时,是CPU执行到这条指令时才申请的,这也就是为什么不是一开始就内存不足了,而是运行过程中出现的不足)
二、 重定向与异常信息的问题
【问题】
在使用dup2重定向stderr时,会发现有的异常信息可以重定向,有的却不可以
【测试程序】
#include <iostream>
#include <vector>
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void SetRlimit(int cpu, int as)
{
// 1. 设置运行时长上限
struct rlimit r_cpu;
r_cpu.rlim_cur = cpu;
r_cpu.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &r_cpu);
// 2. 设置可占用内存上限
rlimit r_as;
r_as.rlim_cur = as * 1024 * 1024;
r_as.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &r_as);
}
int main()
{
umask(0);
int fd = open("debug.stderr", O_CREAT | O_WRONLY, 0644);
dup2(fd, stderr->_fileno);
SetRlimit(1, 20);
while(1)
{
int* arr = new int[1024 * 1024 * 1024];
}
return 0;
}
【结果截图】
我们发现有的异常信息可以重定向到stderr文件中,有的异常信息则不可以
【解释】
" Aborted (core dumped) "
是由操作系统或调试器生成的信息,表示进程因为某种异常情况(比如内存访问错误)而被操作系统终止,并且生成了一个核心转储文件(core dump)。
" terminate called after throwing an instance of 'std::bad_alloc "
是由程序中的异常处理部分生成的消息,表明程序在尝试分配内存时抛出了 std::bad_alloc 异常。
【必须明确】
- "Aborted (core dumped)"这条信息是操作系统生成的信息,
- "terminate called after throwing an instance of 'std::bad_alloc'"是程序内部异常处理部分生成的信息
- 在dup2重定向之前,操作系统和进程指向的是同一个struct file流,但重定向之后,进程的stderr就被重定向到file.stderr这个struct file中了,操作系统不变。
"Aborted (core dumped)" 这条信息通常是由操作系统生成的,而不是程序本身生成的输出。操作系统的fd没有被重定向,因此,无法将其重定向到文件中。
"terminate called after throwing an instance of 'std::bad_alloc'" 这条信息则是由程序生成的标准错误输出,可以被重定向到文件中。
而为什么达到CPU的资源限制时,进程没有抛异常,是因为你能使用的CPU资源都没了,根本就不允许你继续抛异常,所以就让操作系统来直接发送信号杀死该进程,这也就是为什么,每个信号都有不同名字的原因,其实也是告诉用户,你被哪个信号杀死了,当有些异常不允许进程自己抛出异常信息的时候,名字也是异常原因。
(如:使用子进程进行程序替换的时候,我们发生了SIGXCPU异常,我们无法看到操作系统的异常输出,所以只能通过父进程waitpid的时候获得子进程退出信息,从而得知子进程被哪个信号杀死了,名字就是原因!)