毕业设计,实现一个能够自动编译、运行、监测程序运行使用资源、恶意系统调用的监控的一个OJ平台。
在设计实现的过程中的想法、碰到的问题、求解的过程以及方法,在这里记录下来。
OJ主要由前端系统(WEB)和后端的判题程序构成,想法是后端的裁判程序做通用点,减少和前端系统的耦合,所以把后端给分离出来成一个独立的程序,大概的结构图是这样的。
解释下:
1. 前端其实可以由任何流行的web语言来实现。
2. 这里的代理可有可无,代理在这里可以实现很多功能,比如负载均衡、数据库的业务逻辑等都可以在这里实现。
3. 裁判模块主要实现程序的编译、运行、监控、给出判定结果,是无状态的,所以整个系统的扩展性高。
操作系统选的是linux,原因是工具多,稳定,系统API丰富。
裁判模块用的语言是C++,主要原因是性能、系统编程方便。
裁判模块的网络i/o用的是cheetah,一个事件驱动的网络库。
模块间的通信采用的是protobuf。
这里借鉴了nginx的设计,单线程多进程的方式,由master进程和minion进程(数量可配置)组成。
如何做到cpu时间和内存使用量的测量呢?linux操作系统上有很多方法可以获得一个进程的这些信息,这里列举几个方法以及各自的优缺点。
在获得程序的资源使用量之后,可以通过一下结合这些方法方式来实现时间、内存的限制。
在经过一段时间研究,资料搜寻,找不到一种完美、简单的方式完成这个功能。最后经过权衡之后还是选择了3和2,在wait4返回的子进程退出状态来判断各种情况,这里只能通过一种比较丑陋的hack来判断是否是内存超了,通过setrlimit系统调用将进程的内存限制设置为限制的125%,如本来的内存限制为1M,现将内存限制设置为1.25M,在进程退出之后,判断内存使用量是否超过1M。
...
if (fork() == 0) {
/* 子进程 */
rlimit r;
getrlimit(RLIMIT_CPU, &r);
r.rlim_cur = time_limit() / 1000;
setrlimit(RLIMIT_CPU, &r);
getrlimit(RLIMIT_AS, &r);
r.rlim_cur = mem_limit() * 1024 * 1.25;
setrlimit(RLIMIT_AS, &r);
}
...
struct rusage usage;
wait4(pid, &s, 0, &usage); // reap the process and retrieve resources usage
mem_usage = usage.ru_maxrss;
if (WIFEXITED(s)) {
fprintf(stderr, "WEXITSTATUS %d\n", WEXITSTATUS(s)); /* 正常退出 */
} else if (WIFSIGNALED(s)) { /* 程序被信号结束了 */
fprintf(stderr, "WTERMSIG %d\n", WTERMSIG(s));
if (WTERMSIG(s) == SIGSEGV) {
if (mem_usage> mem_limit)
mf = 1; /* 超出内存限制 */
else
sf = 1;
} else if (WTERMSIG(s) == SIGXCPU) { //SIGXCPU indicates the process used up the CPU time assigned to it
tf = 1; /* 超出时间限制 */
} else if (WTERMSIG(s) == SIGKILL ||
WTERMSIG(s) == SIGABRT) {
if (mem_usage> mem_limit)
mf = 1; /* 超出内存限制 */
else
of = 1; /* 运行时错误 */
} else {
of = 1; /* 运行时错误 */
}
}
用户提交上来的程序可能会包含恶意的系统调用,如操作文件系统(unlink),进程复制(fork),网络(socket, sendto, recvfrom)等系统调用。如何限制这些调用呢?
综上所述,我采用了ptrace的方式来实现监控系统调用,关于ptrace系统调用可以参考manpage。
...
if (fork() == 0) {
...
/* 这里设置当前进程要被trace */
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
fprintf(stderr, "failed to ptrace : %s\n", strerror(errno));
exit(1);
}
...
}
...
/* 等到execv调用后的第一个信号 */
if (waitpid(pid, &s, 0) == -1) {
fprintf(stderr, "waitpid(%d, 0, 0) error %s\n", pid, strerror(errno));
v.set_status(verdict_result::UNKNOWN_ERROR);;
return std::move(v);
}
/* 设置trace选项,在子进程退出的时候发送EXIT事件,监视syscall,在父进程发生错误退出的时候杀死所有的子进程 */
if (ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEEXIT | PTRACE_O_EXITKILL ) == -1) {
fprintf(stderr, "ptrace(PTRACE_SETOPTIONS, %d, 0, PTRACE_O_TRACEEXIT | PTRACE_O_EXITKILL) error %s\n", pid, strerror(errno));
v.set_status(verdict_result::UNKNOWN_ERROR);;
return std::move(v);
}
while (true) {
/* 恢复运行但是监视syscall */
if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) {
fprintf(stderr, "ptrace(PTRACE_SYSCALL, %d, 0, 0) error %s\n", pid, strerror(errno));
v.set_status(verdict_result::UNKNOWN_ERROR);;
return std::move(v);
}
/* 获取子进程的状态 */
if (waitpid(pid, &s, 0) == -1) {
fprintf(stderr, "waitpid(%d, &s, 0); error %s\n",pid, strerror(errno));
v.set_status(verdict_result::UNKNOWN_ERROR);;
return std::move(v);
}
/* 这里判断进程是否退出了 */
if (WIFEXITED(s) || (WSTOPSIG(s) == SIGTRAP && (s & (PTRACE_EVENT_EXIT << 8))))break;
fprintf(stderr, "WSTOPSIG %d\n", WSTOPSIG(s));
if (WSTOPSIG(s) == SIGSEGV) {
fprintf(stderr, "child process %d received SIGSEGV, killing... ", pid);
if (kill(pid, SIGKILL) == -1)
fprintf(stderr, "failed. %s\n", strerror(errno));
else
fprintf(stderr, "done.\n");
} else if (WIFSTOPPED(s) && (WSTOPSIG(s) & 0x80)) {
/* 这里的WORD_SIZE和ORIG_EAX根据机器字长不同需要做特殊处理 */
#ifdef __x86_64__
#define ORIG_EAX ORIG_RAX
#define WORD_SIZE 8
#else
#define WORD_SIZE 4
#endif
/* 在进入或退出系统调用前子进程被暂停下来, 暂停信号的第7位被置1, 也就是0x80中断号*/
long call_num = ptrace(PTRACE_PEEKUSER, pid, WORD_SIZE * ORIG_EAX);/* 拿到系统调用号*/
assert(call_num < NR_syscalls);
fprintf(stderr, "child process calling syscall, number: %ld\n", call_num);
if (syscall_mask[call_num]) {
/* 调用了禁用的系统调用, 发送SIGKILL */
fprintf(stderr, "child process %d is trying to invoke the banned system call, killing it... ", pid);
if (kill(pid, SIGKILL) == -1)
fprintf(stderr, "failed. %s\n", strerror(errno));
else
fprintf(stderr, "done.\n");
v.set_status(verdict_result::RUNTIME_ERROR);
return std::move(v);
}
}
}
...读取可执行文件,一致性检查,在这里可以通过返回值来返回错误...
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
if (retval)
goto out_free_dentry;
/* OK, This is the point of no return */
current->flags &= ~PF_FORKNOEXEC;
current->mm->def_flags = def_flags;
...加载可执行文件里的elf格式信息,计算程序的bss(未初始化数据段,全局变量)大小...
/*
* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections. We must do this before
* mapping in the interpreter, to make sure it doesn‘t wind
* up getting placed where the bss needs to go.
*/
retval = set_brk(elf_bss, elf_brk);
if (retval) {
send_sig(SIGKILL, current, 0);
goto out_free_dentry;
}
也就是说在执行完flush_old_exec之后,如果没有返回错误,那么就不能使用返回值了,因为老的executable的信息已经被完全的清理了,但是默认信号处理程序没有被清空。所以接下来的操作出现了错误之后只能通过发送信号来处理了。注意到874行调用set_brk来扩大堆空间,如果这个大小超过了rlimit,那么就会失败,然后这个程序就被杀掉了。
所以父进程在等待子进程的第一个信号时,要判断程序是否被kill掉了。当然也有可能是其他情况导致程序被KILL掉,无法用SIGKILL来断定程序内存超了,所以这种情况实在是没有什么好的方法来解决,希望有这方面经验的大神能指点一二。
//wait for the first signal caused by execv
if (waitpid(pid, &s, 0) == -1) {
fprintf(stderr, "waitpid(%d, 0, 0) error %s\n", pid, strerror(errno));
v.set_status(verdict_result::UNKNOWN_ERROR);;
return std::move(v);
}
/*
* there might be a case where the child process failed to execv
* due to virtual memory limit caused by huge global variables,
* in that case the child process is killed by kernel by sending a SIGKILL.
*/
if (WIFEXITED(s)) {
fprintf(stderr, "child process exited, WEXITSTATUS %d\n", WEXITSTATUS(s));
v.set_status(verdict_result::UNKNOWN_ERROR);
return std::move(v);
} else if (WIFSIGNALED(s)) {
fprintf(stderr, "child process terminated at execv, WTERMSIG %d(", WTERMSIG(s));
if (WTERMSIG(s) == SIGSEGV) {
fprintf(stderr, "SIGSEGV)\n");
v.set_status(verdict_result::SEGMENTATION_FAULT);
} else if (WTERMSIG(s) == SIGKILL) {
fprintf(stderr, "SIGKILL) memory limit exceeded.\n");
v.set_status(verdict_result::MEMORY_LIMIT_EXCEEDED);
v.set_mem_usage(sub.mem_limit());
} else {
v.set_status(verdict_result::UNKNOWN_ERROR);;
fprintf(stderr, ")\n");
}
return std::move(v);
}
原文地址:http://blog.csdn.net/zxjcarrot/article/details/45476693