OJ开发笔记(1)

摘要:
最近,我想挖个洞让OJ在Linux上运行。我查了很多资料,读了很多上帝的代码。我有一些感觉。你想要制作的OJ将来应该支持C/C++/Java/Pascal/C#/Scala/Python。我在网上看到了一些优秀的在线编译器,比如http://codepad.org/和http://ideone.com/,与OJ评估内核的实现基本一致。RLIMIT_CORE//内核转储文件的最大长度。当进程达到软限制时,内核将向其发送SIGX CPU信号。当达到硬限制时,内核将发送SIGKILL信号以终止其执行。如果进程试图超过此限制,内核将向其发送SIGXFSZ信号,并且默认情况下将终止进程执行。

  最近想挖坑搞个跑在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, &regs);
       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) ){/*...*/}
    //太多了省略掉啦
 }   

  笔记先记这么多啦,各位大神有什么建议或者发现什么问题也可以给本渣留个言,本渣还需要学好多东西。

免责声明:文章转载自《OJ开发笔记(1)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇R语言代写之文本分析:主题建模LDA阿里云Centos7安装mysql教程下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Vue:计算属性(使用、set和get方法、缓存)

1、计算属性的使用 (1)计算属性的基本运用 <body> <div id="app"> {{message1}}{{message2}}<br> {{message1 + ' '+message2}}<br> {{getAll()}}<br> {{all}} </div&g...

Android智能指针sp wp详解

研究Android的时候,经常会遇到sp、wp的东西,网上一搜,原来是android封装了c++中对象回收机制。说明:1. 如果一个类想使用智能指针,那么必须满足下面两个条件:a. 该类是虚基类RefBase的子类或间接子类b. 该类必须定义虚构造函数。如virtual ~MyClass(); 2. 本文以类BBinder来进行说明,其余类使用sp或wp...

Chromium base 基础库概览

Chromium 基础库概览 Chromium 基础库包括的内容十分繁杂,我把其中的主要部分大致分为以下几类: 1. 容器类型Chromium 的代码主要使用 STL 容器类型,比如 std::vector,std::list,另外 GCC 和 MSVC 提供的 STL 扩展容器类型 hash_map 和 hash_set 也在 Chromium 中使用...

delphi函数调用约定

指令 参数存放位置 参数传递顺序 参数内存管理 使用地方 Register CPU寄存器 从左到右 被调用者 默认,published属性存取方法必须使用 Pascal 栈 从左到右 被调用者 向后兼容 Cdecl 栈 从右到左 调用者 调用c/c++共享库 Stdcall 栈 从右到左 被调用者 API调用 Safecall 栈...

【DLL相关】实现函数的DLL封装,并在另一个项目中调用

直接给出步骤: ===========函数的DLL封装=========== 1.创建第一个项目:win32控制台程序,应用程序类型:DLL,附加选项:导出符号(命名:double_dll) 2.double_dll.h中加入函数定义   extern DOUBLE_DLL_API int doublefun(int);//DOUBLE_DLL_API 根...

php的session

来源:http://blog.163.com/lgh_2002/blog/static/4401752620105246517509/ http协议是WEB服务器与客户 端(浏览器)相互通信的协议,它是一种无状态协议。所谓无状态,指的是不会维护http请求数据,http请求是独立的,非持久的。而越来越复杂的WEB 应用,需要保存一些用户状态信息。这时候,S...