OJ判題核心實現(獲取時間消耗、空間消耗)wait4、vfork、ptrace實現
判題需要幾個步驟:
1.在linux搭建編譯器環境:gcc g++ java python2 python3 pascal
2.根據原始碼的型別建立相應的原始檔(.c .cpp .java 等)
3.編譯對應的原始檔
4.執行程式,使用測試用例測試得出時間消耗和記憶體消耗。
步驟中其實最難的就是第四步,怎麼獲得程式的時間消耗和空間消耗?有個思路:開執行緒執行該程式得到程序pid,然後主執行緒開個死迴圈不斷通過這個pid,查出這個程式的記憶體消耗,比較儲存一個最大的空間消耗值,輪訓過程中判斷執行時間是否超過規定時間,如果超過,那麼kill該程序直接得出結果(TLE)。
這樣求得的時間消耗其實不是很準的,比如說機器的其他計算任務很多,那麼很多時間這個程序是處於就緒等待的狀態,也就是沒用CPU,什麼都不做,這樣的話,原本能跑過的程式我們也判為TLE了。獲取程序的時間linux核心提供的有的,接下來會有演示怎麼用。
判題流程:
核心命令:
- vfork();建立子程序
- ptrace(PTRACE_TRACEME, 0, NULL, NULL);與子程序建立跟蹤
- setrlimit(RLIMIT_CPU, &rl);設定子程序的時間、記憶體限制
- execvp(args[0],args);執行可執行程式
- wait4(pid, &status, WUNTRACED, &ru);暫停子程序,獲取子程序的資訊
- ptrace(PTRACE_CONT, pid, NULL, NULL);喚醒子程序
- ptrace(PTRACE_KILL, pid, NULL, NULL);殺死子程序
第一步:vfork一個程序出來用於執行這個可執行程式
demo:建立一個子程序,獲得pid
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int pid = vfork(); if(pid < 0) printf("error in fork!\n"); else if (pid == 0) { printf("this is child, pid is %d\n", getpid()); int newstdin = open(in,O_RDWR|O_CREAT,0644); int newstdout=open(out,O_RDWR|O_CREAT,0644); dup2(newstdout,fileno(stdout)); dup2(newstdin,fileno(stdin)); exit(0); }else if (pid > 0) { printf("this is parent, pid is %d\n", getpid()); } return 0; }
第二步:重定向標準控制檯IO到測試用例輸入檔案、結果輸出檔案
int newstdin = open("0.in",O_RDWR|O_CREAT,0644);
int newstdout=open("0.out",O_RDWR|O_CREAT,0644);
dup2(newstdout,fileno(stdout));
dup2(newstdin,fileno(stdin));
第三步:向主程序建立聯絡,為了主程序能控制該程序:ptrace(PTRACE_TRACEME, 0, NULL, NULL);
第四步:在子程序中執行使用者提交的程式:execvp函式
子程序程式碼:
if(pid<0)
printf("error in fork!\n");
else if(pid == 0) {
int newstdin = open(in,O_RDWR|O_CREAT,0644);
int newstdout=open(out,O_RDWR|O_CREAT,0644);
if (newstdout != -1 && newstdin != -1){
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1) {
printf("====== ok =====\n");
}
dup2(newstdout,fileno(stdout));
dup2(newstdin,fileno(stdin));
setProcessLimit(timelimit, memory_limit);
if (execvp(args[0],args) == -1) {
printf("execvp is error!\n");
}
close(newstdin);
close(newstdout);
} else {
printf("====== error =====\n");
}
}
上述步驟就是子程序執行程式,說明:linux可以為程序做限制(時間,記憶體限制),也就是setProcessLimit(timelimit, memory_limit)函式的作用了,為的是程式能在限制時間內結束,然後把狀態發回主程序,程式碼:
void setProcessLimit(long timelimit, long memory_limit) {
struct rlimit rl;
/* set the time_limit (second)*/
rl.rlim_cur = timelimit / 1000;
rl.rlim_max = rl.rlim_cur + 1;
setrlimit(RLIMIT_CPU, &rl);
/* set the memory_limit (b)*/
rl.rlim_cur = memory_limit * 1024;
rl.rlim_max = rl.rlim_cur + 1024;
setrlimit(RLIMIT_DATA, &rl);
}
下面就是在主程序中去監控這個子程式了。
第五步:主程序在死迴圈裡不斷獲取子程序的狀態,監聽狀態變化,命令:
pid_t wait4 (pid_t pid, int *status, int options, struct rusage *rusage);
這個函式可以得到子程序的資訊,但是這個資訊是殭屍程序的資訊,也就是程序在退出之後才能得出正確的資訊。引數列表如下:
這個傳出引數就厲害了,可以返回子程序的資源使用情況,包括我們想要的時間消耗,記憶體消耗:
struct rusage {
struct timeval ru_utime;//使用者態時間消耗
struct timeval ru_stime;//核心態時間消耗
long ru_maxrss; //最大記憶體消耗
long ru_ixrss;
long ru_idrss;
long ru_isrss;
long ru_minflt;
long ru_majflt;
long ru_nswap;
long ru_inblock;
long ru_oublock;
long ru_msgsnd;
long ru_msgrcv;
long ru_nsignals;
long ru_nvcsw;
long ru_nivcsw;
};
那麼程式的時間、空間消耗就是:
time_used = ru.ru_utime.tv_sec * 1000
+ ru.ru_utime.tv_usec / 1000
+ ru.ru_stime.tv_sec * 1000
+ ru.ru_stime.tv_usec / 1000;
memory_used = ru.ru_maxrss;
如果WIFEXITED(status)為真,那麼程式就是在規定的時間內完成,暫時AC(因為還沒判斷結果是不是WrongAnswer),就可以獲取時間消耗,記憶體消耗值。
如果沒過,說明還在跑,那麼主程序應該在最後喚醒子程序:ptrace(PTRACE_CONT, pid, NULL, NULL);
如果時間超過了最開始設定的限制時間或者記憶體超過了設定的限制記憶體,那麼主程序會收到一個訊號,主程序kill子程序,根據返回的訊號判斷是記憶體超出還是時間超出。
demo:地址:https://github.com/1510460325/judge-runner
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/ptrace.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
void setProcessLimit(long timelimit, long memory_limit) {
struct rlimit rl;
/* set the time_limit (second)*/
rl.rlim_cur = timelimit / 1000;
rl.rlim_max = rl.rlim_cur + 1;
setrlimit(RLIMIT_CPU, &rl);
/* set the memory_limit (b)*/
rl.rlim_cur = memory_limit * 1024;
rl.rlim_max = rl.rlim_cur + 1024;
setrlimit(RLIMIT_DATA, &rl);
}
int run(char *args[],long timelimit, long memory_limit, char *in, char *out){
pid_t pid = vfork();
if(pid<0)
printf("error in fork!\n");
else if(pid == 0) {
int newstdin = open(in,O_RDWR|O_CREAT,0644);
int newstdout=open(out,O_RDWR|O_CREAT,0644);
if (newstdout != -1 && newstdin != -1){
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1) {
printf("====== ok =====\n");
}
dup2(newstdout,fileno(stdout));
dup2(newstdin,fileno(stdin));
setProcessLimit(timelimit, memory_limit);
if (execvp(args[0],args) == -1) {
printf("execvp is error!\n");
}
close(newstdin);
close(newstdout);
} else {
printf("====== error =====\n");
}
} else {
struct rusage ru;
int status, time_used = 0, memory_used = 0;
printf("the child pid is %d \n", pid);
while (1) {
if (wait4(pid, &status, WUNTRACED, &ru) == -1)
printf("wait4 [WSTOPPED] failure");
if (WIFEXITED(status)) {
time_used = ru.ru_utime.tv_sec * 1000
+ ru.ru_utime.tv_usec / 1000
+ ru.ru_stime.tv_sec * 1000
+ ru.ru_stime.tv_usec / 1000;
memory_used = ru.ru_maxrss;
printf("child process is right!\n");
printf("timeused: %d ms | memoryused : %d b\n",time_used,memory_used);
return 1;
}
else if (WSTOPSIG(status) != SIGTRAP) {
ptrace(PTRACE_KILL, pid, NULL, NULL);
waitpid(pid, NULL, 0);
time_used = ru.ru_utime.tv_sec * 1000
+ ru.ru_utime.tv_usec / 1000
+ ru.ru_stime.tv_sec * 1000
+ ru.ru_stime.tv_usec / 1000;
memory_used = ru.ru_maxrss;
switch (WSTOPSIG(status)) {
case SIGSEGV:
if (memory_used > memory_limit)
printf("ME\n");
else
printf("RE\n");
break;
case SIGALRM:
case SIGXCPU:
printf("TLE\n");
break;
default:
printf("RE\n");
break;
}
printf("child process is wrong!\n");
printf("timeused: %d ms | memoryused : %d b\n",time_used,memory_used);
return 0;
}
ptrace(PTRACE_CONT, pid, NULL, NULL);
}
return -1;
}
}
int main()
{
char *args[] = {"/home/hadoop/tmp/demo",NULL};
run(args,1000,1000,"0.in","0.out");
return 0;
}
測試程式碼:demo程式為a+b程式死迴圈,輸入檔案0.in為兩個整數
執行結果:超時
改為正確程式:
結果:
這個demo流程是參照一個學長的程式碼寫的,一個開源專案Lo-runner,裡面更復雜的實現了程序除錯中獲取暫存器的值,從而判斷出是否越權操作,是一個類似沙盒的實現。
開源專案Lo-runner地址:https://github.com/dojiong/Lo-runner