zoukankan      html  css  js  c++  java
  • 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) ){/*...*/}
        //太多了省略掉啦
     }   

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

  • 相关阅读:
    JS 弹窗到右下角
    datatable绑定comboBox显示数据[C#]
    ASP.NET MVC html help
    在Filter 无法跳转地址
    js清除浏览器缓存的几种方法
    htm.dropdownlist
    C# 公历转农历
    验证码 禁止输入中文
    反射实例
    使用 Daynamic 动态添加属性
  • 原文地址:https://www.cnblogs.com/CodeMIRACLE/p/5079910.html
Copyright © 2011-2022 走看看