OJ开发笔记(1)
最近想挖坑搞个跑在linux上的OJ,查了很多资料,也读了很多大神的代码,自己有些感悟。于是把评测内核的部分功能写了出来,这里做个笔记备忘啦。
学校用的OJ是几年前的hustoj,只支持C/C++,Java和Pascal跑起来老是出现莫名奇妙的错误,感觉某学长没有搭好(╬ ̄皿 ̄)。自己想搞的OJ以后应该能支持C/C++/Java/Pascal/C#/Scala/Python这些语言吧(期待ing),之前在网上看见一些比较牛的在线编译器,如 http://codepad.org/ 和 http://ideone.com/,和OJ评测内核的实现基本上一致。特别是那个ideone,与著名的SPOJ有关,都是基于他们的Sphere Engine,支持40多种编程语言啊,Orz......
看了一些OJ的源码感觉思路都差不多,本渣在这里记一下思路啦。
我把评测内核分成了网络通信、编译、运行、评测四个模块。目前网络通信方面一团糟,主要是并发服务器的实现渣渣不懂还需要学习。编译的功能实现就是fork后exec调用一下啦。重要的就是这个运行程序的功能,毕竟是把别人的程序拿到你机子上跑,不管他就太危险啦。
这里需要对其进行监控,用到的核心功能就是linux系统的ptrace函数啦(gdb好像就是用这个系统调用实现的调试功能?)。
废话不多写了,先把我写的函数原型贴上。
头文件
1 #include<cstdio> 2 #include<cstdlib> 3 #include<fcntl.h> 4 #include<cstring> 5 #include "constant.h" 6 extern "C" 7 { 8 #include<sys/wait.h> 9 #include<sys/resource.h> 10 #include<sys/ptrace.h> 11 #include<sys/user.h> 12 #include<unistd.h> 13 }
函数
1 int Execute(const int lang,const std::string inputFile, const std::string outputFile,const std::string execFile, 2 const int memoryLimit,const int timeLimit,const int outputLimit,int &execMemory,double &execTime);
通过execMemory,execTime这两个引用的变量来返回程序运行的内存和时间。函数的返回值就表示程序的运行状态啦。
linux下限制进程运行的时间和内存需要sys/resource.h这个头文件
1 #include <sys/resource.h>
还有rlimit结构体和setrlimit,rlimit结构体是这样声明的
1 struct rlimit { 2 rlim_t rlim_cur; //软限制 3 rlim_t rlim_max; //硬限制 4 };
然后是setrlimit的函数
1 int setrlimit(int resource, const struct rlimit *rlim);
对setrlimit的函数的详细介绍,可以看看这个
http://www.cnblogs.com/niocai/archive/2012/04/01/2428128.html
resource主要用了这些
RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,达到硬限制,发送 SIGKILL信号终止其执行。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
下面就是代码啦
int Execute(const int lang,const std::string inputFile, const std::string outputFile,const std::string execFile, const int memoryLimit,const int timeLimit,const int outputLimit,int &execMemory,double &execTime) { execTime = 0; rlimit limit; pid_t exe = fork(); if (exe < 0) { printf("[Exec-Parent]: fork error.\n"); exit(1); } if(exe == 0) { printf("[Exec-child]: ready.\n"); //时间的限制 limit.rlim_cur = timeLimit; limit.rlim_max = timeLimit + 1; if (setrlimit(RLIMIT_CPU, &limit)) { printf("[Exec-child]: set time limit error.\n"); exit(1); } //输出文件大小的限制 limit.rlim_cur = outputLimit - 1; limit.rlim_max = outputLimit; if (setrlimit(RLIMIT_FSIZE, &limit)) { printf("[Exec-child]: set output limit error.\n"); exit(1); } //转存文件大小的限制 limit.rlim_cur = 0; limit.rlim_max = 0; if (setrlimit(RLIMIT_CORE, &limit)) { printf("[Exec-child]: set core limit error.\n"); exit(1); }
这里我没有用RLIMIT_AS限制内存大小,我用的是每次系统调用时计算内存并比较的方法。
设置好限制后就是输入输出重定向啦。为什么不用freopen?我觉得系统级的调用dup2更好点吧(其实是我用freopen重定向的时候老是出问题,囧)。
int out = open(outputFile.c_str(),O_CREAT | O_TRUNC | O_RDWR, 0666); int in = open(inputFile.c_str(),O_RDWR, 0666); dup2(out,STDOUT_FILENO); dup2(in,STDIN_FILENO); close(in); close(out); if(lang == langPython||lang == langJava) { int err = open("run.log", O_CREAT | O_TRUNC | O_RDWR, 0666); dup2(err,STDERR_FILENO); close(err); }
java和python运行在解释器中,也有必要把运行时解释器的错误流重定向了(C# mono需不需要?这个我不知道欸 (~ ̄▽ ̄)→))* ̄▽ ̄*))。
接下来是关键啦,
ptrace(PTRACE_TRACEME, 0, 0, 0); switch(lang) { case langJava: execlp("java", "java","-Djava.security.manager", "-Djava.security.policy=java.policy","Main", NULL); break; case langPython: execlp("python","python",execFile.c_str(),NULL); break; case langCs: execlp("mono","mono",execFile.c_str(),NULL); break; default: execl(execFile.c_str(), execFile.c_str(),NULL); } fprintf(stderr,"[Exec-child]: execute failed.\n"); exit(1); }
这条语句让父进程跟踪子进程,TRACEME,来追我。(你追我,如果你,追到我,我就让你 (´∀`*) )
1 ptrace(PTRACE_TRACEME, 0, 0, 0);
然后就是调用exec族函数执行程序了。
接下来是父进程的代码
printf("[Exec-parent]: child process id:%d.\n", exe); bool syscallEnter = true; int status, exitCode, sig, syscallId, syscallCount = 0,maxMem = 0,nowMem = 0; rusage ru; user_regs_struct regs; Initsyscall(lang); ptrace(PTRACE_SYSCALL, exe, 0, 0);
这里我用的rusage结构体来计算使用的内存啦,用user_regs_struct是获取程序的系统调用号。
下面记一下怎么拦截非法调用的:
Initsyscall函数根据相应的语言初始化系统调用白名单,也就是一个数组syscallTable里面存放了程序可以进行某个系统调用的次数
0表示禁止调用;<0表示可以无限次数调用;>0表示可以调用,但是每调用一次,次数减一。
然后通过一个判断系统调用是否合法的函数来实行拦截。
这里给出它的代码
1 bool IsLeagalSyscall(int id) 2 { 3 if(syscallTable[id] <0) 4 return true; 5 else if(syscallTable[id] >0) 6 { 7 syscallTable[id]--; 8 return true; 9 } 10 else 11 return false; 12 }
初始化完系统调用表后
让子进程暂停或者运行,用的就是
1 ptrace(PTRACE_SYSCALL, exe, 0, 0);
接下来就是等待子进程改变状态并获取它的系统调用啦
while (wait4(exe,&status,0,&ru)>0) //wait4函数等待子进程状态改变 { if (WIFEXITED(status)) //子进程退出 { printf("[Exec-parent]: %d exited; status = %d \n", exe, WEXITSTATUS(status)); if(WEXITSTATUS(status) == EXIT_SUCCESS) exitCode = executeSucceed; else exitCode = executeRuntimeError; break; } if(WIFSIGNALED(status) || (WIFSTOPPED(status)&&WSTOPSIG(status) != SIGTRAP)) //接受到了特定信号 { if (WIFSIGNALED(status)) sig = WTERMSIG(status); else sig = WSTOPSIG(status); switch (sig) { case SIGXCPU: exitCode = executeTimeLimitExceeded; break; case SIGXFSZ: exitCode = executeOutputLimitExceeded; break; case SIGSEGV: case SIGFPE: case SIGBUS: case SIGABRT: default: exitCode = executeRuntimeError; break; } ptrace(PTRACE_KILL, exe, 0, 0); break; }
获取系统调用
ptrace(PTRACE_GETREGS, exe, 0, ®s); syscallId = regs.orig_rax; if (syscallEnter) { syscallCount++; printf("\n[Exec-parent]: <trace>----entering syscall----\n"); printf("[Exec-parent]: <trace> : syscalls[%d]: %s\n", syscallId, syscallNames[syscallId].c_str()); if (syscallId == 12) { printf("[Exec-parent]: <trace> : brk (ebx = 0x%08llx) %llu\n", regs.rbx, regs.rbx); } if (!IsLeagalSyscall(syscallId)) { printf("[Exec-parent]: <trace> : syscalls[%d]: %s : Restrict function!\n", syscallId, syscallNames[syscallId].c_str()); printf("[Exec-parent]: <trace> : killing process %d.\n", exe); ptrace(PTRACE_KILL, exe, 0, 0); exitCode = executeRestrictFunction; break; } }
我是在64位ubuntu上面写的所以syscallId应该等于regs.orig_rax而不是regs.orig_eax啦。子进程进入系统调用前暂停一下,离开系统调用前也暂停一下。
syscallId对应的系统调用我是在linux的unistd64.h头文件里面找到的,调用太多了我就不帖出来了。
判断是不是合法调用,不是就kill掉啦。
没有kill掉就继续计算其内存,用的是ru结构体的ru_minflt
else { int m = getpagesize() * ru.ru_minflt; if (m != nowMem) { printf("[Exec-parent]: proc %d memory usage: %dk\n", exe, m); nowMem = m; maxMem = nowMem > maxMem ? nowMem : maxMem; if (nowMem > memoryLimit) { printf("[Exec-parent]: Memory Limit Exceed\n"); printf("[Exec-parent]: killing process %d.\n", exe); ptrace(PTRACE_KILL, exe, 0, 0); exitCode = executeMemoryLimitExceeded; continue; } } printf("[Exec-parent]: <trace>----leaving syscall----\n\n"); } syscallEnter = !syscallEnter; ptrace(PTRACE_SYSCALL, exe, 0, 0); }
内存超了也kill掉。
离开while循环后进行一些收尾工作。计算时间等..
printf("[Exec-parent]: maximum memory used by %s: %dk\n", execFile.c_str(), maxMem); printf("[Exec-parent]: utime sec %d, usec %06d\n", (int) ru.ru_utime.tv_sec, (int) ru.ru_utime.tv_usec); printf("[Exec-parent]: stime sec %d, usec %06d\n", (int) ru.ru_stime.tv_sec, (int) ru.ru_stime.tv_usec); printf("[Exec-parent]: mem usage %d \n", (int) ru.ru_maxrss); execTime = ru.ru_utime.tv_sec + ru.ru_utime.tv_usec * 1e-6 + ru.ru_stime.tv_sec + ru.ru_stime.tv_usec * 1e-6; execMemory = nowMem; printf("[Exec-parent]: cputime used %.4lf\n", execTime); printf("[Exec-parent]: exiting total syscall is %d\n", syscallCount); return exitCode; }
其实最麻烦的是Initsyscall函数,要根据不同语言的情况初始化syscallTable,需要结合系统调用号和相应的系统调用函数的用途来设置。不同语言不一样,我是一种语言慢慢试的,有些系统调用都不知道是干啥的,好在网上有这些文章,可以参考 http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html
慢慢试啦。
void Initsyscall(int lang) { memset(syscallTable,0,sizeof(syscallTable)); if(lang==langC||lang==langCpp||lang==langCpp11) { syscallTable[0] = -1; syscallTable[1] = -1; syscallTable[5] = -1; syscallTable[9] = -1; syscallTable[12] = -1; syscallTable[21] = -1; syscallTable[59] = 1; syscallTable[63] = 1; syscallTable[89] = 1; syscallTable[158] = 1; } if(lang == langJava){/*...*/} if(lang ==langCs) ){/*...*/} if(lang ==langPython) ){/*...*/} if(lang ==langPas) ){/*...*/} //太多了省略掉啦 }
笔记先记这么多啦,各位大神有什么建议或者发现什么问题也可以给本渣留个言,本渣还需要学好多东西。