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, &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) ){/*...*/}
    //太多了省略掉啦
 }   

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

原文地址:https://www.cnblogs.com/CodeMIRACLE/p/5079910.html