研究一下在線評測系統編寫的一些細節,加深對操做系統的理解,實現一個基本能用的評測機,經過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實現了。詳情下回分解。凌晨了。。。