设计实现OJ平台的遇到的一些问题和解决方法

需求

毕业设计,实现一个能够自动编译、运行、监测程序运行使用资源、恶意系统调用的监控的一个OJ平台。 在设计实现的过程中的想法、碰到的问题、求解的过程以及方法,在这里记录下来。

基础结构

OJ主要由前端系统(WEB)和后端的判题程序构成,想法是后端的裁判程序做通用点,减少和前端系统的耦合,所以把后端给分离出来成一个独立的程序,大概的结构图是这样的。

解释下: 1. 前端其实可以由任何流行的web语言来实现。 2. 这里的代理可有可无,代理在这里可以实现很多功能,比如负载均衡、数据库的业务逻辑等都可以在这里实现。 3. 裁判模块主要实现程序的编译、运行、监控、给出判定结果,是无状态的,所以整个系统的扩展性高。

后端使用到的技术

操作系统选的是linux,原因是工具多,稳定,系统API丰富。 裁判模块用的语言是C++,主要原因是性能、系统编程方便。 裁判模块的网络i/o用的是cheetah,一个事件驱动的网络库。 模块间的通信采用的是protobuf。

裁判模块的设计

这里借鉴了nginx的设计,单线程多进程的方式,由master进程和minion进程(数量可配置)组成。

碰到的问题问题的思考的过程和解决方法1.cpu时间、内存使用量的监控,限制程序的cpu时间、内存使用量。

如何做到cpu时间和内存使用量的测量呢?linux操作系统上有很多方法可以获得一个进程的这些信息,这里列举几个方法以及各自的优缺点。

在获得程序的资源使用量之后,可以通过一下结合这些方法方式来实现时间、内存的限制。

当top,ps返回的结果中发现内存、时间的使用量超出了限制之后,发送SIGKILL给子进程。这样做还是需要频率的问题,间隔时间大了,子进程可能分配了大量的内存,对整个系统安全造成威胁。使用setrlimit系统调用来实现cpu、内存使用量的限制,当使用量超出限制就发送SIGXCPU(超时),malloc调用失败(内存超了,errno设置为ENOMEM)。这种做法比较清爽,因为进程的资源使用量操作系统是最清楚不过了。缺点是无法得到到底超出了多少内存,如果一次malloc调用申请大量的内存(如内存限制的一半),超了,但是进程的内存使用量却不会被操作系统维护(因为malloc失败了)。

在经过一段时间研究,资料搜寻,找不到一种完美、简单的方式完成这个功能。最后经过权衡之后还是选择了waitpid获取cpu使用资源,使用setrlimit限制CPU和内存使用量,这里通过一种比较丑陋的hack来判断是否是内存超了,通过setrlimit系统调用将进程的内存限制设置为限制的125%,然后在子程序结束时,调用waitpid之前,分析/proc文件系统,读取/proc/pid/status文件,解析出VmSize这行,然后在通过VmSize(虚拟内存空间)的大小判断是否超出了内存限制,这样做可以缓解问题,但是不能完美地解决。

…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);}…/* 这个时候子进程还没退出,在调用wait前先去/proc文件系统里读VmSize */struct meminfo m = retrieve_mem_usage_from_proc(pid);struct rusage usage;/* 收割子进程,顺便获取cpu使用量 */wait4(pid, &s, 0, &usage);cpu_usage = usage.ru_utime.tv_sec * 1000 + usage.ru_utime.tv_usec / 1000 + usage.ru_stime.tv_sec * 1000 +usage.ru_stime.tv_usec / 1000;mem_usage = m.VmSize;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; /* 超出内存限制 */elsesf = 1;} else if (WTERMSIG(s) == SIGXCPU) { //SIGXCPU indicates the process used up the CPU time assigned to ittf = 1; /* 超出时间限制 */} else if (WTERMSIG(s) == SIGKILL ||WTERMSIG(s) == SIGABRT) {if (mem_usage> mem_limit)mf = 1; /* 超出内存限制 */elseof = 1; /* 运行时错误 */} else {of = 1; /* 运行时错误 */}}2.恶意系统调用的监控

用户提交上来的程序可能会包含恶意的系统调用,如操作文件系统(unlink),进程复制(fork),网络(socket, sendto, recvfrom)等系统调用。如何限制这些调用呢?

使用strace工具来查看程序的系统调用,一旦发现非法的系统调用,就发送SIGKILL,这样做的一个很大的缺点是实时性,当一个子进程系统调用调用完毕之后,我们的程序才可能反应过来。我们想要的是实时的监控子进程的系统调用过程,在尝试调用禁止的系统调用的时候我们就需要把程序杀死,好在linux提供了ptrace系统调用,原理是在程序trap进系统调用之前会先检查当前进程是否被traced了,如果是的话,通过SIGTRAP暂停当前进程,通知tracer,给tracer一个机会来做一些事情,如恢复子进程执行、杀死进程等。strace就是基于ptrace来实现的。

综上所述,我采用了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));elsefprintf(stderr, “done.\n”);} else if (WIFSTOPPED(s) && (WSTOPSIG(s) & 0x80)) {/* 这里的WORD_SIZE和ORIG_EAX根据机器字长不同需要做特殊处理 *//* 在进入或退出系统调用前子进程被暂停下来, 暂停信号的第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));elsefprintf(stderr, “done.\n”);v.set_status(verdict_result::RUNTIME_ERROR);return std::move(v);}}}3.execv系统调用的一些pitfalls。这是我在实现的时候碰到的一个问题,提交的程序定义了一个超过内存限制的全局数组,当调用setrlimit给程序设置了内存使用限制之后,调用execv,本以为execv会返回ENOMEM表示内存不够,但是却出现了进程直接被杀死(SIGKILL)的情况。而父进程在却在等待子进程执行execv后发送的第一个信号,,waitpid返回了ENOCHLD的错误,表示不存在子进程。在网络上搜索很长的时间,并没有发现这类的问题,只好看下linux kernel的实现了,版本是2.6.32.65的,后面的版本这块的实现变化不大。 首先是找到elf加载器的实现,在fs/binfmt_elf.c里,定位到load_elf_binary函数。注意到714行和719行的关键注释。 …读取可执行文件,一致性检查,在这里可以通过返回值来返回错误…/* 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;}人生,一场人喧鼓响的戏,我只是一个平凡的过客,

设计实现OJ平台的遇到的一些问题和解决方法

相关文章:

你感兴趣的文章:

标签云: