研究一下在线评测系统编写的一些细节,加深对操做系统的理解,实现一个基本能用的评测机,经过shell脚本控制评测机监控用户程序。web接口和日志功能没写。linux
另外PE和CE功能还没写c++
T^T 学长牛逼!!! Orzweb
在评测系统中,咱们提交一个由标准输入输出的程序,咱们判断正确性的方法一部分是修改输入输出流,将输入导入程序,输出导出,和标准答案进行比对。算法
例如添加一下代码,在程序的开始docker
freopen("file_name_input", "r", stdin); freopen("file_name_output", "w", stdout);
用户从web页面提交代码,服务器拿到代码,造成一个服务器本地的文件。那么咱们若是经过评测程序去调用,监控这个用户代码便可。可是这就意味着咱们须要在文件头部加上上面两句话。虽然修改用户代码是可行的,可是却比较麻烦。这个问题先放一边,咱们先解决另外一个问题shell
ps:若是修改用户代码,一种解决方案是把main函数修改,就改为适宜CppUnit库调用的形式。CppUnit是一种c++单元测试的库,虽然没用过,可是类似的Junit有提供对应的内存,时间检测。编程
如何让评测程序调用另外一个程序vim
在windows下咱们只须要system(cmd_commond), 在函数中填写对应cmd命令便可,linux下的system函数做用还未证明windows
在Linux环境下咱们须要调用的是exec函数家族
当进程调用exec函数时,该进程的程序彻底替换新程序,而新程序从main函数开始,建立的新程序的进程ID并未改变。exec只是从磁盘上替换了当前进程的正文段,数据段,堆段和栈段
UNIX提供了几种exe函数execl,execv,execle,execve,execlp,execvp,fexecve.这几个函数出错返回-1.若成功不返回
#include <unistd.h> //int execv(const char* pathname, char *const argv[]) void start_bash(std::string bash) { // 将 C++ std::string 安全的转换为 C 风格的字符串 char * // 从 C++14 开始, C++编译器将禁止这种写法 `char *str = "test";` // std::string bash = "/bin/bash"; char *c_bash = new char[bash.length() + 1]; // +1 用于存放 '\0' strcpy(c_bash, bash.c_str()); char* const child_args[] = { c_bash, NULL }; execv(child_args[0], child_args); // 在子进程中执行 /bin/bash delete []c_bash; }
咱们能够经过封装一个函数来执行咱们路径下的程序,调用的是execv。因为上面咱们说的替换程序部分。是为了解释以前看到的一个现象。
ps: 程序范例来着实验楼会员课。c++虚拟化技术实现简易docker容器
主程序: freopen调用 执行外部程序(exec调用)
外部程序的输入流会被改变。到这里咱们解决了两个问题,评测程序执行用户程序,且修改用户程序的输入输出流。
参考《UNIX环境高级编程》第八章
每一个进程都有一些其余的标识符,下列函数返回这些标识符,注意这些函数没有出错返回,更详细的说明见原著,后面不在赘述
#include <unistd.h> pid_t getpid(void); //返回调用进程的ID pid_t getppid(void); //返回调用进程的父进程ID
下面咱们介绍一个函数fork()
#include <unistd.h> pid_t fork(void); //出错返回-1,子进程返回0,父进程返回子进程ID
fork建立的进程成为子进程,一个程序调用id = fork(); 那么程序运行的进程会返回两次,也就是会有两个进程,同时执行,一个是父进程,一个子进程,具体那个先执行是不肯定的,取决于操做系统的调度算法。同时进程是操做系统分配资源的基本单位。子进程是父进程的副本,例如子进程得到父进程的数据空间,堆,栈的副本。而不共享这一部分。
咱们看一个fork的例子
#include <bits/stdc++.h> #include <unistd.h> #include <sys/types.h> // 提供类型 pid_t 的定义 #include <sys/wait.h> #include <sys/resource.h> void start_bash(std::string bash) { char *c_bash = new char[bash.length() + 1]; strcpy(c_bash, bash.c_str()); char* const child_args[] = { c_bash, NULL }; execv(child_args[0], child_args); delete []c_bash; } int main() { pid_t pid = fork(); if(pid < 0) { std::cout << "create error" << std::endl; exit(0); } else if(pid == 0) { //当前进程ID std::cout << "this is child program " << getpid() << std::endl; //父进程ID std::cout << "this is child's father " << getppid() << std::endl; } else if(pid > 0) { std::cout << "this is father program " << getpid() << std::endl; } return 0; } /** this is father program 20061 this is child program 20062 this is child's father 20061 */
fork后程序执行两个进程,注意前后顺序默认是不可控的。咱们能够经过wait等控制这是后话。咱们可让子进程先去执行用户程序。在执行前设置文件输入输出流,已经进程限制等。父进程等待子进程执行结束。检测结果。
以前咱们说两个进程的执行顺序是取决于操做系统调度的。咱们想让父亲进程等待调用则调用wait, waitp, wait3, wait4
wait3() 和 wait4() 函数除了能够得到子进程状态信息外,还能够得到子进程的资源使用信息,这些信息是经过参数 rusage 获得的。而 wait3() 与 wait4() 之间的区别是,wait3() 等待全部进程,而 wait4() 能够根据 pid 的值选择要等待的子进程,参数 pid 的意义与 waitpid() 函数的同样
因而咱们就能够在父进程中调用,等待编号p_id的进程结束,并收集状态
#include <sys/wait.h> #include <sys/types.h> //定义pid_t #inlcude <reasource.h> //定义rusage int status = 0; struct rusage use; wait4(p_id, &status, 0, &use);
关于status的状态的宏
宏 | 说明 |
---|---|
WIFEXITED(status) | 子进程正常终止为真。能够执行WEXITSTATUS(status),获取exit的参数 |
WIFSIGNALED(status) | 进程异常终止为真,能够调用WTERMSIG(status)获取使子进程禁止的编号 |
WIFSTOPPED(status) | 进程暂停子进程的暂停返回为真,调用WSTOPSIG(STATUS)能够得到暂停信号的编号 |
WIFCONTINUED(status) | 做业控制暂停后已经继续的子进程返回了状态,则为真 |
《UNIX高级编程》191页
若是子进程正常返回咱们就能够认为用户程序在时间空间限制下完成了要求。表格第一行。若是超时,内存不足则会出现异常退出。
《UNIX高级编程》251页定义了一些异常的常量
宏 | 说明 | OJ断定 |
---|---|---|
SIGXCPU | 超过CPU限制(setrlimit) | |
SIGXFSZ | 超过文件长度限制(setrlimit) | |
SIGXRES | 超过资源控制 | |
SIGKILL | 终止 |
到此,咱们解决了父进程监控子进程的目的。那么下面则须要咱们解决限制资源的问题
咱们一样须要从系统调用的角度限制内存
#include <sys/resource.h> int getrlimit( int resource, struct rlimit *rlptr ); int setrlimit( int resource, const struct rlimit *rlptr ); 两个函数返回值:若成功则返回0,若出错则返回非0值
struct rlimit { rlim_t rlim_cur; /* soft limit: current limit */ rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */ };
在更改资源限制时,须遵循下列三条规则:
(1)任何一个进程均可将一个软限制值更改成小于或等于其硬限制值。
(2)任何一个进程均可下降其硬限制值,但它必须大于或等于其软限制值。这种下降对普通用户而言是不可逆的。
(3)只有超级用户进程能够提升硬限制值
两个参数的resource是一个宏,咱们去库里面看看
enum __rlimit_resource { /* Per-process CPU limit, in seconds. */ RLIMIT_CPU = 0, #define RLIMIT_CPU RLIMIT_CPU /* Largest file that can be created, in bytes. */ RLIMIT_FSIZE = 1, #define RLIMIT_FSIZE RLIMIT_FSIZE /* Maximum size of data segment, in bytes. */ RLIMIT_DATA = 2, #define RLIMIT_DATA RLIMIT_DATA /* Maximum size of stack segment, in bytes. */ RLIMIT_STACK = 3, #define RLIMIT_STACK RLIMIT_STACK /* Largest core file that can be created, in bytes. */ RLIMIT_CORE = 4, #define RLIMIT_CORE RLIMIT_CORE /* Largest resident set size, in bytes. This affects swapping; processes that are exceeding their resident set size will be more likely to have physical memory taken from them. */ __RLIMIT_RSS = 5, #define RLIMIT_RSS __RLIMIT_RSS /* Number of open files. */ RLIMIT_NOFILE = 7, __RLIMIT_OFILE = RLIMIT_NOFILE, /* BSD name for same. */ #define RLIMIT_NOFILE RLIMIT_NOFILE #define RLIMIT_OFILE __RLIMIT_OFILE /* Address space limit. */ RLIMIT_AS = 9, #define RLIMIT_AS RLIMIT_AS /* Number of processes. */ __RLIMIT_NPROC = 6, #define RLIMIT_NPROC __RLIMIT_NPROC /* Locked-in-memory address space. */ __RLIMIT_MEMLOCK = 8, #define RLIMIT_MEMLOCK __RLIMIT_MEMLOCK /* Maximum number of file locks. */ __RLIMIT_LOCKS = 10, #define RLIMIT_LOCKS __RLIMIT_LOCKS /* Maximum number of pending signals. */ __RLIMIT_SIGPENDING = 11, #define RLIMIT_SIGPENDING __RLIMIT_SIGPENDING /* Maximum bytes in POSIX message queues. */ __RLIMIT_MSGQUEUE = 12, #define RLIMIT_MSGQUEUE __RLIMIT_MSGQUEUE /* Maximum nice priority allowed to raise to. Nice levels 19 .. -20 correspond to 0 .. 39 values of this resource limit. */ __RLIMIT_NICE = 13, #define RLIMIT_NICE __RLIMIT_NICE /* Maximum realtime priority allowed for non-priviledged processes. */ __RLIMIT_RTPRIO = 14, #define RLIMIT_RTPRIO __RLIMIT_RTPRIO /* Maximum CPU time in µs that a process scheduled under a real-time scheduling policy may consume without making a blocking system call before being forcibly descheduled. */ __RLIMIT_RTTIME = 15, #define RLIMIT_RTTIME __RLIMIT_RTTIME __RLIMIT_NLIMITS = 16, __RLIM_NLIMITS = __RLIMIT_NLIMITS #define RLIMIT_NLIMITS __RLIMIT_NLIMITS #define RLIM_NLIMITS __RLIM_NLIMITS };
咱们能够在父亲进程中监听发生的型号
/* We define here all the signal names listed in POSIX (1003.1-2008); as of 1003.1-2013, no additional signals have been added by POSIX. We also define here signal names that historically exist in every real-world POSIX variant (e.g. SIGWINCH). Signals in the 1-15 range are defined with their historical numbers. For other signals, we use the BSD numbers. There are two unallocated signal numbers in the 1-31 range: 7 and 29. Signal number 0 is reserved for use as kill(pid, 0), to test whether a process exists without sending it a signal. */ /* ISO C99 signals. */ #define SIGINT 2 /* Interactive attention signal. */ #define SIGILL 4 /* Illegal instruction. */ #define SIGABRT 6 /* Abnormal termination. */ #define SIGFPE 8 /* Erroneous arithmetic operation. */ #define SIGSEGV 11 /* Invalid access to storage. */ #define SIGTERM 15 /* Termination request. */ /* Historical signals specified by POSIX. */ #define SIGHUP 1 /* Hangup. */ #define SIGQUIT 3 /* Quit. */ #define SIGTRAP 5 /* Trace/breakpoint trap. */ #define SIGKILL 9 /* Killed. */ #define SIGBUS 10 /* Bus error. */ #define SIGSYS 12 /* Bad system call. */ #define SIGPIPE 13 /* Broken pipe. */ #define SIGALRM 14 /* Alarm clock. */ /* New(er) POSIX signals (1003.1-2008, 1003.1-2013). */ #define SIGURG 16 /* Urgent data is available at a socket. */ #define SIGSTOP 17 /* Stop, unblockable. */ #define SIGTSTP 18 /* Keyboard stop. */ #define SIGCONT 19 /* Continue. */ #define SIGCHLD 20 /* Child terminated or stopped. */ #define SIGTTIN 21 /* Background read from control terminal. */ #define SIGTTOU 22 /* Background write to control terminal. */ #define SIGPOLL 23 /* Pollable event occurred (System V). */ #define SIGXCPU 24 /* CPU time limit exceeded. */ #define SIGXFSZ 25 /* File size limit exceeded. */ #define SIGVTALRM 26 /* Virtual timer expired. */ #define SIGPROF 27 /* Profiling timer expired. */ #define SIGUSR1 30 /* User-defined signal 1. */ #define SIGUSR2 31 /* User-defined signal 2. */ /* Nonstandard signals found in all modern POSIX systems (including both BSD and Linux). */ #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ /* Archaic names for compatibility. */ #define SIGIO SIGPOLL /* I/O now possible (4.2 BSD). */ #define SIGIOT SIGABRT /* IOT instruction, abort() on a PDP-11. */ #define SIGCLD SIGCHLD /* Old System V name */
参考《UNIX高级编程》185页
咱们测试以下程序。输出和预期有一些不符合
虽然限制了CPU时间,可是父进程监听的却不是SIGXCPU,经过信号咱们能够查到是被KILL了。可是大体实现了父进程监听子进程设置超时信息。程序最终跑了两秒。
#include <bits/stdc++.h> #include <unistd.h> #include <sys/types.h> // 提供类型 pid_t 的定义 #include <sys/wait.h> #include <sys/resource.h> void start_bash(std::string bash) { char *c_bash = new char[bash.length() + 1]; strcpy(c_bash, bash.c_str()); char* const child_args[] = { c_bash, NULL }; execv(child_args[0], child_args); delete []c_bash; } int main() { pid_t pid = fork(); if(pid < 0) { std::cout << "create error" << std::endl; exit(0); } else if(pid == 0) { std::cout << "this is child program " << getpid() << std::endl; rlimit limit; limit.rlim_cur = 2; limit.rlim_max = 2; setrlimit(RLIMIT_CPU , &limit); unsigned int i = 0; while(1) { i++; } } else if(pid > 0) { std::cout << "this is father program " << getpid() << std::endl; int status = 0; struct rusage use; wait4(pid, &status, 0, &use); if(WIFSIGNALED(status)) { int res = WTERMSIG(status); std::cout << "res = " << res << std::endl; std::cout << "SIGXCPU = " << SIGXCPU << std::endl; if(res == SIGXCPU) { std::cout << "超过期间限制" << std::endl; } else { std::cout << "没有超时" << std::endl; } } } return 0; } this is father program 24042 this is child program 24043 res = 9 SIGXCPU = 24 没有超时
另外一个问题是,用上述方法监控内存没有做用,和子进程的内存不符。咱们经过动态查询linux目录 /proc/进程ID/status 文件,最后status是文件,Linux会为每个正在运行的进程在proc目录下建立文件夹,在进程结束后删除文件,其目录下status就存储这咱们要的内存信息。那么咱们直接去读那个文件的内容便可。
到此我经过大概300行的c++代码加上一些系统调用实现了一个简易的。能检测用户进程内存时间的评测机
//main.cpp #include <bits/stdc++.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/resource.h> const int INF = 0x7FFFFFFF; const int DEFAULT_MEMERY = 1024 * 1024 * 128; // 128 MB std::chrono::system_clock::time_point begin_time; std::chrono::system_clock::time_point end_time; namespace util { auto isnum = [](char ch) -> bool { return ch >= '0' && ch <= '9'; }; auto split_string = [](std::string str) -> std::vector<std::string> { std::vector<std::string> vec; char* ttr = new char[str.size() + 1]; int top = 0; for(int i = 0; i < str.size(); i++ ) { ttr[i] = str[i]; if(ttr[i] == 9 || ttr[i] == 32) { // ' ' or '\t' ttr[i] = 0; } } ttr[str.size()] = 0; for(int i = 0; i < str.size(); i++ ) { if(i == 0 && ttr[i] != 0 || ttr[i - 1] == 0 && ttr[i] != 0) { vec.push_back(ttr + i); } } delete []ttr; return vec; }; auto int_to_string = [](int pid) -> std::string { char str[20] = {0}; int top = 0; if(pid == 0) { return std::string("0"); } else { while(pid) { str[top++] = pid % 10 + '0'; pid /= 10; } str[top] = 0; std::string number(str); std::reverse(number.begin(), number.end()); return number; } }; auto string_to_int = [](std::string number, int default_val = 0) -> int { int num = 0; for(int i = 0; i < number.size(); i++ ) { if(util::isnum(number[i])) { num = num * 10 + number[i] - '0'; } else { return default_val; } } return num; }; } void start_bash(std::string bash = "/bin/bash") { char *c_bash = new char[bash.length() + 1]; strcpy(c_bash, bash.c_str()); char* const child_args[] = { c_bash, NULL }; execv(child_args[0], child_args); delete []c_bash; } enum class JudgeResult : unsigned int { AC, RE, MLE, OLE, SE, CE, PE, WA, TLE }; struct Result { int tot_time; //ms int tot_memery; //kb JudgeResult result; }; class Problem { public: int memery_limit; //kb int time_limit; //s std::string pathname; std::string input_file; std::string output_file; std::string answer_file; Problem() = default; Problem(std::string &input_time, std::string &path, std::string &input_file, std::string &output_file, std::string &answer_file) { time_limit = util::string_to_int(input_time); memery_limit = DEFAULT_MEMERY; pathname = path; this->input_file = input_file; this->output_file = output_file; this->answer_file = answer_file; } static bool check_answer(const char* answer_file1, const char* answer_file2) { std::ifstream input1(answer_file1); std::ifstream input2(answer_file2); if(!input1.is_open() || !input2.is_open()) { return false; } while(1) { if(input1.eof() && input2.eof()) { return true; } if(input1.eof() || input2.eof()) { return false; } if(input1.get() != input2.get()) { return false; } } return true; } }; class OnlineJudge { public: static void father_program(const int this_pid, const int child_pid, Problem problem) { listen_child_program(child_pid, problem); } static void child_program(const int this_pid, Problem problem) { set_user_limit(problem); // set problem limit set_freopen(problem.input_file, problem.output_file); // set file freopen start_bash(problem.pathname.c_str()); //run user problem } private: static void set_freopen(std::string input, std::string output) { freopen(input.c_str(), "r", stdin); freopen(output.c_str(), "w", stdout); } static void set_user_limit(Problem problem) { struct rlimit *r = new rlimit(); r->rlim_cur = problem.time_limit; r->rlim_max = problem.time_limit; setrlimit(RLIMIT_CPU, r); setrlimit(RLIMIT_CORE, NULL); //禁止建立core文件 } static void listen_child_program(const int child_pid, Problem &problem) { int status = 0; struct rusage use; Result result; result.tot_memery = get_progress_memery(child_pid); int wait_pid = wait4(child_pid, &status, 0, &use); end_time = std::chrono::system_clock::now(); result.tot_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - begin_time).count(); std::cout << "memery = " << result.tot_memery << "kb" << std::endl; std::cout << "time = " << result.tot_time << "ms" << std::endl; // exit success spj if(WIFEXITED(status)) { //std::cout << "WIFEXITED = " << WEXITSTATUS(status) << std::endl; if(Problem::check_answer(problem.output_file.c_str(), problem.answer_file.c_str())) { result.result = JudgeResult::AC; } else { result.result = JudgeResult::WA; } } // exit fail if(WIFSIGNALED(status)) { switch WTERMSIG(status) { case SIGXCPU: // TLE //std::cout << "SIGXCPU" << std::endl; result.result = JudgeResult::TLE; break; case SIGKILL: // TLE //std::cout << "SIGKILL" << std::endl; result.result = JudgeResult::TLE; break; case SIGXFSZ: // OLE //std::cout << "SIGXFSZ" << std::endl; result.result = JudgeResult::OLE; break; default: // RE //std::cout << "default" << std::endl; result.result = JudgeResult::RE; break; } } if(result.result == JudgeResult::AC) { std::cout << "Accept" << std::endl; } if(result.result == JudgeResult::WA) { std::cout << "Wrong answer" << std::endl; } if(result.result == JudgeResult::TLE) { std::cout << "Time limit except" << std::endl; } if(result.result == JudgeResult::RE) { std::cout << "Running time error" << std::endl; } if(result.result == JudgeResult::OLE) { std::cout << "Output limit except" << std::endl; } } static int get_progress_memery(const int pid) { //VmPeak: 290748 kB auto show = [](std::vector<std::string>vec) { puts(""); for(auto &str: vec) { std::cout << "[" << str << "]"; } }; std::string path = "/proc/"; path += util::int_to_string(pid); path += "/status"; std::ifstream fp(path); std::string line; std::string goal = "VmPeak:"; while(getline(fp, line)) { std::vector<std::string>vec = util::split_string(line); if(vec.size() == 3 && vec[0] == goal) { return util::string_to_int(vec[1], INF); } } return INF; } }; /** argv: time memery path */ int main(int argc, char *argv[]) { std::cout << "========================Judging begin=========================" << std::endl; int pid = fork(); begin_time = std::chrono::system_clock::now(); std::string time = argv[1]; std::string path = argv[2]; std::string input_file = argv[3]; std::string output_file = argv[4]; std::string answer_file = argv[5]; Problem problem(time, path, input_file, output_file, answer_file); if(pid < 0) { exit(0); } if(pid == 0) { OnlineJudge::child_program(getpid(), problem); } else { OnlineJudge::father_program(getpid(), pid, problem); } return 0; }
目录结构:
.
├── back.cpp
├── main
├── main.cpp
├── main.o
├── run.sh
├── test
├── test.cpp
├── test.o
└── user_pro
........├── 1.in
........├── 1.out
........├── user_ac
........├── user.out
........├── user_re
........├── user_tle
........├── user_tle2
........└── user_wa
有用的就main.cpp和run.sh
#run.sh g++ main.cpp -std=c++11 mv a.out main #time_limit user_problem std_in user_in std:out ./main 2 ./user_pro/user_ac ./user_pro/1.in ./user_pro/user.out ./user_pro/1.out ./main 2 ./user_pro/user_wa ./user_pro/1.in ./user_pro/user.out ./user_pro/1.out ./main 2 ./user_pro/user_tle ./user_pro/1.in ./user_pro/user.out ./user_pro/1.out ./main 2 ./user_pro/user_re ./user_pro/1.in ./user_pro/user.out ./user_pro/1.out ./main 2 ./user_pro/user_tle_2 ./user_pro/1.in ./user_pro/user.out ./user_pro/1.out
运行结果
========================Judging begin=========================
memery = 13712kb
time = 1ms
Accept
========================Judging begin=========================
memery = 13712kb
time = 1ms
Wrong answer
========================Judging begin=========================
memery = 13712kb
time = 1998ms
Time limit except
========================Judging begin=========================
memery = 13712kb
time = 21ms
Running time error
========================Judging begin=========================
memery = 13712kb
time = 2501ms
Wrong answer
上文是各类程序的测试结果,最后一个执行2.5s,我设置的时间是2s都是未超时,多是由于监控的是cpu时间,我延时用的是让进程的主线程休眠的命令,因此没有引起异常。
运行错误是由于那个程序死递归跑死了
咱们的评测机要建立一个沙盒,在沙盒里面跑咱们的评测系统。主要是为了屏蔽一些非法代码操做。一样经过系统调用模拟docker实现了。详情下回分解。凌晨了。。。