运行模块

运行模块作用是:执行编译模块编译好的可执行程序,我们希望,输出、输出、错误等信息能写入到文件中。Run模块只关心运行,不关注结果的正确与否,结果正确与否靠测试用例判断

运行逻辑

这里需要重定向工作

  • 标准输入
  • 标准输出
  • 标准错误

运行服务大致过程类似编译:

  • 创建子进程
  • 子进程通过execl执行编译好的可执行程序

那么这里会有两个问题

  1. 编译的代码可能是死循环或者恶意代码
  2. 运行出错如何知道是哪种错误

问题1可以通过资源限制来解决,通过限制运行时间,和申请内存上限等方式可以解决

问题2解决方式:
首先设计运行类,类中提供静态方法Run,该函数通过文件名可以自动拼接路径,然后再执行对应逻辑,返回值可以代表程序运行时的各种错误,比如我们规定:

  • 返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
  • 返回值 == 0: 正常运行完毕的,结果保存到了对应的临时文件中
  • 返回值 < 0: 内部错误

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "../common/util.hpp"
#include "../common/log.hpp"

namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
class Runner
{
public:
Runner(){}
~Runner(){}
public:
static int Run(const std::string& file_name)
{
std::string _execute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin.c_str(), O_CREAT|O_RDONLY, 0644);
int _stdout_fd = open(_stdout.c_str(), O_CREAT|O_WRONLY, 0644);
int _stderr_fd = open(_stderr.c_str(), O_CREAT|O_WRONLY, 0644);

if(_stdin_fd <0 || _stdout_fd<0 || _stderr_fd<0)
{
LOG(ERROR)<<"运行时打开文件失败"<<"\n";
return -1; //-1 代表打开文件失败
}

pid_t pid = fork();

if(pid<0)
{
LOG(ERROR)<<"运行时创建子进程失败"<<"\n";
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);

return -2; //-2 代表创建子进程失败
}
else if(pid==0)
{
// 子进程,先进行重定向,再程序替换
dup2(_stdin_fd, 0);
dup2(_stdout_fd,1);
dup2(_stderr_fd, 2);

execl(_execute.c_str(), _execute.c_str(), nullptr);
exit(1);
}
else
{
// 父进程
// 先关闭文件描述符

close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);

int status=0;
waitpid(pid, &status, 0);
LOG(INFO)<<"运行完毕 Info: "<<(status & 0x7F)<<"\n";
return status & 0x7F;
}
}
};
}

资源限制

具体资源限制,我们使用linux系统的系统调用setrlimit函数

setrlimit

setrlimit是一个用于设置进程资源限制的函数,它的原型如下:

1
2
3
#include <sys/resource.h>

int setrlimit(int resource, const struct rlimit *rlim);

其中,第一个参数resource指定了要设置的资源类型,它可以取以下值之一:

  • RLIMIT_CORE:设置core文件的最大大小(以字节为单位);
  • RLIMIT_CPU:设置CPU时间的最大值(以秒为单位);
  • RLIMIT_DATA:设置数据段的最大大小(以字节为单位);
  • RLIMIT_FSIZE:设置可创建文件的最大大小(以字节为单位);
  • RLIMIT_NOFILE:设置进程可以打开的最大文件数;
  • RLIMIT_STACK:设置进程堆栈的最大大小(以字节为单位);
  • RLIMIT_AS:设置进程地址空间的最大大小(以字节为单位);
  • RLIMIT_NPROC:设置用户可以拥有的最大进程数;
  • RLIMIT_RSS:设置进程驻留集大小的最大值(以字节为单位);
  • RLIMIT_MEMLOCK:设置进程能够锁定在内存中的最大字节数;
  • RLIMIT_NICE:设置进程的nice值的最大值。

第二个参数rlim是一个指向rlimit结构体的指针,该结构体定义如下:

1
2
3
4
struct rlimit {
rlim_t rlim_cur; // 资源限制的当前值
rlim_t rlim_max; // 资源限制的最大值
};

rlim_t类型的rlim_max成员可以被赋值为以下宏:

  • RLIM_INFINITY:表示无限制,将rlim_max设置为此宏将取消对资源的任何限制;
  • RLIM_SAVED_MAX:表示使用之前保存的最大值,最初由系统管理员设置;
  • RLIM_SAVED_CUR:表示使用之前保存的当前值,最初由系统管理员设置;
  • 其他正整数:表示将rlim_max设置为指定的正整数值,表示资源的最大限制。需要注意的是,此值必须小于或等于系统的最大限制值,并且通常应该小于或等于当前进程的软限制值。

需要注意的是,不同的资源类型可能会有不同的最大限制值。例如,对于RLIMIT_NOFILE(最大打开文件数),通常的最大限制值是系统的最大文件描述符数。对于RLIMIT_CPU(CPU时间限制),通常的最大限制值是LONG_MAX(通常为2147483647)。

使用setrlimit函数可以为进程设置指定资源的限制,比如限制进程可以打开的最大文件数、限制进程的CPU时间等。调用该函数前,需要先定义并初始化一个rlimit结构体,然后将其作为参数传递给setrlimit函数。

例如,以下代码将限制进程可以打开的最大文件数为1000:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/resource.h>
#include <stdio.h>

int main() {
struct rlimit limit;

limit.rlim_cur = 1000;
limit.rlim_max = 1000;

if (setrlimit(RLIMIT_NOFILE, &limit) == -1) {
perror("setrlimit");
return 1;
}

printf("限制进程可以打开的最大文件数为:%lld\n", (long long) limit.rlim_cur);

return 0;
}

setrlimit函数只能将资源限制设置为小于或等于系统默认值或进程当前限制值的值。如果试图将资源限制设置为大于这些值的值,setrlimit函数将失败并返回-1。在这种情况下,可以考虑使用sysctl函数调整系统级别的资源限制。同时,需要注意,setrlimit函数只能影响当前进程以及其子进程的资源限制,无法影响其他进程。

资源限制测试

整体测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <sys/resource.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
std::cout<<"signo: "<<signo<<std::endl;
}
int main()
{
for(int i=1; i<=31; i++)
{
signal(i, handler);
}

//测试死循环时间超时
//struct rlimit r;
//r.rlim_cur = 1; //设置1秒限制
//r.rlim_max = RLIM_INFINITY;

//setrlimit(RLIMIT_CPU, &r);

//while(1){}

//测试申请内存
struct rlimit r;
r.rlim_cur = 1024*1024*40;
r.rlim_max = RLIM_INFINITY;

setrlimit(RLIMIT_AS, &r);
int count=0;
while(true)
{
int *p = new int[1024*1024];
count++;
std::cout<<"count: "<<count<<std::endl;
sleep(1);
}
return 0;
}

1、死循环时间限制测试

image.png

收到24号信号 SIGXCPU

image.png

2、申请虚拟空间大小限制

image.png

运行模块加入资源限制

  • cpu_limit: 该程序运行的时候,可以使用的最大cpu资源上限
  • mem_limit: 改程序运行的时候,可以使用的最大的内存大小(KB)

setProcLimit函数

对资源做时间、空间限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在运行之间需要对资源做一些限制
// 包括 时间、空间
static void setProcLimit(int _cpu_limit, int _mem_limit)
{
// 设置CPU的时长
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_cur = _cpu_limit;
cpu_rlimit.rlim_max = RLIM_INFINITY;

setrlimit(RLIMIT_CPU, &cpu_rlimit);

// 设置限制内存
struct rlimit mem_rlimit;
//转成kb
mem_rlimit.rlim_cur = _mem_limit*1024;
mem_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &mem_rlimit);
}

Run方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static int Run(const std::string& file_name, int cpu_rlimit, int mem_rlimit)
{
std::string _execute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin.c_str(), O_CREAT|O_RDONLY, 0644);
int _stdout_fd = open(_stdout.c_str(), O_CREAT|O_WRONLY, 0644);
int _stderr_fd = open(_stderr.c_str(), O_CREAT|O_WRONLY, 0644);

if(_stdin_fd <0 || _stdout_fd<0 || _stderr_fd<0)
{
LOG(ERROR)<<"运行时打开文件失败"<<"\n";
return -1; //-1 代表打开文件失败
}

pid_t pid = fork();

if(pid<0)
{
LOG(ERROR)<<"运行时创建子进程失败"<<"\n";
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);

return -2; //-2 代表创建子进程失败
}
else if(pid==0)
{
// 子进程,先进行重定向,再程序替换
dup2(_stdin_fd, 0);
dup2(_stdout_fd,1);
dup2(_stderr_fd, 2);

setProcLimit(cpu_rlimit, mem_rlimit);
execl(_execute.c_str(), _execute.c_str(), nullptr);
exit(1);
}
else
{
// 父进程
// 先关闭文件描述符

close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);

int status=0;
waitpid(pid, &status, 0);
LOG(INFO)<<"运行完毕 Info: "<<(status & 0x7F)<<"\n";
return status & 0x7F;
}
}

问题以及记录

遇到问题

提问:centos,我一个c++程序,先创建子进程,然后在子进程用setrlimit函数限制程序运行时间,再用execl执行另一个a.out程序,但是父进程waitpid收到11号信号,如果不加setrlimit限制,就正常运行,这是怎么回事呢?

具体原因

AI回复:从描述来看,你遇到的问题可能与 setrlimit 函数设置的资源限制有关。11号信号是 SIGSEGV,即段错误。这个信号通常由于进程试图访问其虚拟地址空间中无法访问的内存区域而引发,例如非法指针解引用、栈溢出等。

在这种情况下,可能的原因是你使用 setrlimit 设置了过于严格的资源限制。这可能导致子进程在运行时无法分配足够的资源(例如内存),从而触发 SIGSEGV 信号。

要解决这个问题,你可以尝试以下步骤:

  1. 仔细检查 setrlimit 函数设置的资源限制,确保它们不会导致子进程无法正常运行。例如,如果你设置的是 RLIMIT_CPU,确保所设置的时间限制允许子进程有足够的时间运行。

  2. 在子进程中添加错误处理和调试信息,以便在出现问题时更容易识别问题所在。你可以使用 trycatch 语句捕获异常,并在控制台上输出有关错误的详细信息。

  3. 如果仍然无法解决问题,可以考虑在父进程中使用 ptrace 函数来监控子进程的系统调用和信号。这样可以帮助你更详细地了解子进程中发生了什么。