Online judge system
概述:
研究一下在线评测系统编写的一些细节,加深对操作系统的理解,实现一个基本能用的评测机,通过shell脚本控制评测机监控用户程序。web接口和日志功能没写。
另外PE和CE功能还没写
- 编写语言c/c++, bash
- 编写环境deppin linux
- 编写工具vim gcc 7.3.0
T^T 学长牛逼!!! Orz
关键技术
1.如何在Linux中调用另一个程序
在评测系统中,我们提交一个由标准输入输出的程序,我们判断正确性的方法一部分是修改输入输出流,将输入导入程序,输出导出,和标准答案进行比对。
例如添加一下代码,在程序的开始
freopen("file_name_input", "r", stdin);
freopen("file_name_output", "w", stdout);
用户从web页面提交代码,服务器拿到代码,形成一个服务器本地的文件。那么我们如果通过评测程序去调用,监控这个用户代码即可。但是这就意味着我们需要在文件头部加上上面两句话。虽然修改用户代码是可行的,但是却比较麻烦。这个问题先放一边,我们先解决另一个问题
ps:如果修改用户代码,一种解决方案是把main函数修改,就改成适宜CppUnit库调用的形式。CppUnit是一种c++单元测试的库,虽然没用过,但是相似的Junit有提供对应的内存,时间检测。
如何让评测程序调用另一个程序
在windows下我们只需要system(cmd_commond), 在函数中填写对应cmd命令即可,linux下的system函数作用还未证实
在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 用于存放 ' '
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调用)
外部程序的输入流会被改变。到这里我们解决了两个问题,评测程序执行用户程序,且修改用户程序的输入输出流。
2.如何监控进程执行时间
参考《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 | 终止 |
到此,我们解决了父进程监控子进程的目的。那么下面则需要我们解决限制资源的问题
3.如何限制子进程的资源
我们同样需要从系统调用的角度限制内存
#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 ' '
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时间,我延时用的是让进程的主线程休眠的命令,所以没有引发异常。
运行错误是因为那个程序死递归跑死了
4.虚拟化技术
我们的评测机要创建一个沙盒,在沙盒里面跑我们的评测系统。主要是为了屏蔽一些非法代码操作。同样通过系统调用模拟docker实现了。详情下回分解。凌晨了。。。