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 用於存放 '\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調用)
外部程序的輸入流會被改變。到這里我們解決了兩個問題,評測程序執行用戶程序,且修改用戶程序的輸入輸出流。
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 '\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時間,我延時用的是讓進程的主線程休眠的命令,所以沒有引發異常。
運行錯誤是因為那個程序死遞歸跑死了
4.虛擬化技術
我們的評測機要創建一個沙盒,在沙盒里面跑我們的評測系統。主要是為了屏蔽一些非法代碼操作。同樣通過系統調用模擬docker實現了。詳情下回分解。凌晨了。。。
