評測機
思路
通過父進程啟動一個子進程,子進程運行用戶提交的代碼。通過父進程監控子進程使用的內存,時間等計算機資源,最后對子進程生成的答案與正確答案進行比對。
第一步:生成一個進程
用戶提交的代碼可能會編譯不通過,這時需要將編譯報錯信息返回給用戶。我在編譯的makefile命令后加這一句make >err.txt 2>&1
將編譯報錯的信息輸出到一個文件內
函數fork
一個現有的進程可以調用fork函數創建一個新進程。
#include<unistd.h>
pid_t fork(void);
子進程和父進程繼續執行fork調用之后的指令。子進程是父進程的副本。子進程獲得父進程數據空間、堆、棧的副本,父子進程並不共享。父子進程共享正文段。
例子:
#include <iostream>
#include <unistd.h>
#include <sys/types.h> // 提供類型 pid_t 的定義
#include <sys/wait.h>
#include <sys/resource.h>
int main()
{
pid_t pid = fork();
if(pid < 0) {
std::cout << "error" << std::endl;
exit(0);
} else if(pid == 0) {
std::cout << "in child: now id = " << getpid() << std::endl; //子進程id
std::cout << "in child: father id = " << getppid() << std::endl; //父進程id
} else if(pid > 0) {
std::cout << "in father: child id = " << pid << std::endl; //子進程id
std::cout << "in father: now id = " << getpid() << std::endl; //父進程id
}
return 0;
}
第二歩:執行用戶的代碼
當我們調用fork函數后,生成的子進程執行的代碼與父進程一樣這不是我們想要的。
這時我們可以調用exec函數,該進程執行的程序完全替換為新程序,而新程序從其main函數開始執行。替換了當前進程的正文段、數據段、堆段和棧段。
exec |
---|
int execl(const char *pathname,const char *agr0,.../*(char*)0*/); |
int execv(const char *pathname,char *cosnt agrv[] |
int execle(const char *pathname,const char *agr0,.../*(char*)0,char *const envp[]*/); |
int execve(const char *pathname,char *const agrv[],char *const envp[]) |
int execlp(const char *filename,const char *arg0,.../*(char*)0*/); |
int execvp(const char *filename,char* const agrv[] |
int fexecve(int fd,char *const agrv[],char *const envp[] |
詳見《UNIX環境高級編程》第八章第十節
#include <bits/stdc++.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>
//bash為相對路徑
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); //調用exec
delete []c_bash;
}
int main()
{
pid_t pid = fork();
if(pid < 0) {
std::cout << "error" << std::endl;
exit(0);
} else if(pid == 0) {
std::cout << "in child" << getppid() << std::endl;
start_bash(std::string("test2"));
} else if(pid > 0) {
std::cout << "in father" << getpid() << std::endl;
}
return 0;
}
第三步:限制子進程的資源使用
獲得子進程運行時間
獲取當前系統時間:std::chrono::system_clock::now();
返回的類型std::chrono::system_clock::time_point
得到運行時間的代碼
using namespace std::chrono;
system_clock::time_point begin_time,end_time;
begin_time = system_clock::now(); //獲取運行前時間
std::cout << "start"<<std::endl;
for (int i=0; i<100000000; ++i)
int num =i;
std::cout << "end"<<std::endl;
end_time = system_clock::now(); //獲取運行后時間
duration<double> time_span = duration_cast<duration<double>>(end_time - begin_time);
std::cout << "run time = " << time_span.count() << " seconds."<< std::endl;
用戶所提交的代碼有可能會需要非常長的時間甚至根本就是死循環。當用戶的程序超出所規定時間的時候應該主動關閉進程。
下面介紹兩個函數getrlimit/setrlimit
#include<sys/resource.h>
int getrlimit(int resource,struct rlimit *rlptr)
int setrlimit(int resource,const struct rlimit *rlptr)
struct rlimit{
rlim_t rlim_cur; //軟限值
rlim_t rlim_max; //硬限值
}
在更改資源限制時,須遵循下列 3 條規則。
(1)任何一個進程都可將一個軟限制值更改為小於或等於其硬限制值。
(2)任何一個進程都可降低其硬限制值,但必須不小於其軟限制值。這種降低對普通用戶而言是不可逆的。
(3)只有超級用戶進程可以提高硬限制值。
詳見《UNIX環境高級編程》第七章第十一節
在子進程中加入以下測試的代碼:
rlimit limit;
limit.rlim_cur = limit.rlim_max = 1; //設置資源限制參數
setrlimit(RLIMIT_CPU , &limit); //設置資源限制
int i = 0;
while(1) {
i++;
}
可以觀察到程序在運行大約1s后被終止,但是父進程如何知道?
當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號。父進程可以選擇忽略它,或者調用信號處理函數處理。
pid_t wait3(int *status,int options,struct rusage *rusage);
pid_t wait4(pid_t pid,int *status,int options,struct rusage *rusage);
wait3() 和 wait4() 函數除了可以獲得子進程狀態信息外,還可以獲得子進程的資源使用信息,這些信息是通過參數 rusage 得到的。而 wait3() 與 wait4() 之間的區別是,wait3() 等待所有進程,而 wait4() 可以根據 pid 的值選擇要等待的子進程,參數 pid 的意義與 waitpid() 函數的一樣。
struct rusage的定義
struct rusage {
struct timeval ru_utime; // user time used
struct timeval ru_stime; // system time used
long ru_maxrss; // maximum resident set size
long ru_ixrss; // integral shared memory size
long ru_idrss; // integral unshared data size
long ru_isrss; // integral unshared stack size
long ru_minflt; // page reclaims
long ru_majflt; // page faults
long ru_nswap;// swaps
long ru_inblock; // block input operations
long ru_oublock; // block output operations
long ru_msgsnd; // messages sent
long ru_msgrcv; //messages received
long ru_nsignals; // signals received
long ru_nvcsw; // voluntary context switches
long ru_nivcsw; // involuntary context switches
};
通過wait4()得到結構體rusage可以獲得子進程運行的信息
獲得子進程運行時間
auto use_time = use.ru_utime.tv_sec*1000+use.ru_utime.tv_usec/1000 + use.ru_stime.tv_sec*1000+use.ru_stime.tv_usec/1000;
獲得進程最大使用內存
liunx關於文件描述的文件存在 /proc/進程號/status
的文件內,通過不停讀取文件內VmPeak: 內存使用量
的信息可以得到該進程的實時內存使用量,此外依舊通過setrlimit()來限制內存使用量。
static long get_proc_mem(unsigned int pid){
char file_name[64]={0};
FILE *fd;
char line_buff[512]={0};
sprintf(file_name,"/proc/%d/status",pid);
//std::cout<<file_name<<std::endl;
fd =fopen(file_name,"r");
if(nullptr == fd){
return 0;
}
char name[64];
long long vmrss = 0;
for (int i=0; i<17-1;i++){
fgets(line_buff,sizeof(line_buff),fd);
}
fgets(line_buff,sizeof(line_buff),fd);
sscanf(line_buff,"%s %d",name,&vmrss);
fclose(fd);
return vmrss;
}
第四步:獲取子進程結束的狀態
用戶的進程可能不會正常的結束。
當我們調用的wait4(pid_t pid,int *status,int options,struct rusage *rusage)
函數,其中第二個參數status是代表進程結束的狀態。當子進程結束后會向父進程發送信號
序號 | 信號 | 作用 |
---|---|---|
1 | SIGHUP | 本信號在用戶終端連接(正常或非正常)結束時發出, 通常是在終端的控制進程結束時, 通知同一session內的各個作業, 這時它們與控制終端不再關聯。 |
2 | SIGINT | 程序終止(interrupt)信號,在用戶鍵入INTR字符(通常是Ctrl-C)時發出,用於通知前台進程組終止進程。 |
3 | SIGQUIT | 和SIGINT類似, 但由QUIT字符(通常是Ctrl-/)來控制.進程在因收到SIGQUIT退出時會產生core文件, 在這個意義上類似於一個程序錯誤信號。 |
4 | SIGILL | 執行了非法指令. 通常是因為可執行文件本身出現錯誤, 或者試圖執行數據段.堆棧溢出時也有可能產生這個信號。 |
5 | SIGTRAP | 由斷點指令或其它trap指令產生. 由debugger使用。 |
6 | SIGABRT | 調用abort函數生成的信號。 |
7 | SIGBUS | 非法地址, 包括內存地址對齊(alignment)出錯。比如訪問一個四個字長的整數, 但其地址不是4的倍數。它與SIGSEGV的區別在於后者是由於對合法存儲地址的非法訪問觸發的(如訪問不屬於自己存儲空間或只讀存儲空間)。 |
8 | SIGFPE | 在發生致命的算術運算錯誤時發出. 不僅包括浮點運算錯誤,還包括溢出及除數為0等其它所有的算術的錯誤。 |
9 | SIGKILL | 用來立即結束程序的運行.本信號不能被阻塞、處理和忽略。如果管理員發現某個進程終止不了,可嘗試發送這個信號。 |
10 | SIGUSR1 | 留給用戶使用 |
11 | SIGSEGV | 試圖訪問未分配給自己的內存, 或試圖往沒有寫權限的內存地址寫數據. |
12 | SIGUSR2 | 留給用戶使用 |
13 | SIGPIPE | 管道破裂。這個信號通常在進程間通信產生,比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。此外用Socket通信的兩個進程,寫進程在寫Socket的時候,讀進程已經終止。 |
14 | SIGALRM | 時鍾定時信號, 計算的是實際的時間或時鍾時間. alarm函數使用該信號. |
15 | SIGTERM | 程序結束(terminate)信號,與SIGKILL不同的是該信號可以被阻塞和處理。通常用來要求程序自己正常退出,shell命令kill缺省產生這個信號。如果進程終止不了,我們才會嘗試SIGKILL。 |
16 | SIGCHLD | 子進程結束時, 父進程會收到這個信號。如果父進程沒有處理這個信號,也沒有等待(wait)子進程,子進程雖然終止,但是還會在內核進程表中占有表項,這時的子進程稱為僵屍進程。這種情況我們應該避免(父進程或者忽略S |
18 | SIGCONT | 讓一個停止(stopped)的進程繼續執行. 本信號不能被阻塞.可以用一個handler來讓程序在由stopped狀態變為繼續執行時完成特定的工作. 例如, 重新顯示提示符 |
19 | SIGSTOP | 停止(stopped)進程的執行. 注意它和terminate以及interrupt的區別:該進程還未結束, 只是暫停執行. 本信號不能被阻塞, 處理或忽略. |
20 | SIGTSTP | 停止進程的運行, 但該信號可以被處理和忽略.用戶鍵入SUSP字符時(通常是Ctrl-Z)發出這個信號 |
21 | SIGTTIN | 當后台作業要從用戶終端讀數據時, 該作業中的所有進程會收到SIGTTIN信號.缺省時這些進程會停止執行. |
22 | SIGTTOU | 類似於SIGTTIN, 但在寫終端(或修改終端模式)時收到. |
23 | SIGURG | 有"緊急"數據或out-of-band數據到達socket時產生. |
24 | SIGXCPU | 超過CPU時間資源限制. 這個限制可以由getrlimit/setrlimit來讀取/改變。 |
25 | SIGXFSZ | 當進程企圖擴大文件以至於超過文件大小資源限制。 |
26 | SIGVTALRM | 虛擬時鍾信號. 類似於SIGALRM, 但是計算的是該進程占用的CPU時間. |
27 | SIGPROF | 類似於SIGALRM/SIGVTALRM, 但包括該進程用的CPU時間以及系統調用的時間. |
28 | SIGWINCH | 窗口大小改變時發出. |
29 | SIGIO | 文件描述符准備就緒, 可以開始進行輸入/輸出操作. |
30 | SIGPWR | Power failure |
31 | SIGSYS | 非法的系統調用。 |
下面是我用來判斷各種狀態的代碼
if(WIFSIGNALED(status)){
printf("child killed by %d\nstatus = %d\n", WTERMSIG(status),status);
switch (WTERMSIG(status)){
case SIGXCPU:
case SIGKILL: //超出時間限制
std::cout<<"TLE"<<std::endl;
result.result = JudgeResult ::TLE;
break;
case SIGXFSZ: //輸出超出限制
std::cout<<"OLE"<<std::endl;
result.result = JudgeResult ::OLE;
break;
case SIGSEGV: //全局數組超過限制
case SIGABRT: //申請堆內存失敗
result.result = JudgeResult ::MLE;
std::cout<<"MLE"<<std::endl;
break;
default:{ //運行錯誤
result.result = JudgeResult ::RE;
std::cout<<"RE"<<std::endl;
}
}
}