判题需要几个步骤:
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