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

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

时间: 2024-12-11 13:10:30

本渣的OJ开发的笔记(1)的相关文章

Xamrin开发安卓笔记(三)

安装片 Xamrin开发安卓笔记(一) Xamrin开发安卓笔记(二) 这次记录的是滚动条跟sqlite创建.存储和读取. 先说滚动条相关,这个是比较简单的知识点. 当有一屏的东西需要填写的时候例如下图 我们都知道如果点击第一个文本框则会出现输入法.但是如果没有滚动条的话,只能依靠输入法中的回车一个一个的向下移动(虽然现在输入法都带自我关闭功能),很不友好,那么就需要滚动条,看了一下布局属性有滚动条,但是使用起来不好使.隐约想起来,安卓有滚动条控件,就在左边找了一下,果真找到这个玩意了.如下图

Android深度探索(卷1)HAL与驱动开发学习笔记(2)

Android深度探索(卷1)HAL与驱动开发学习笔记(2) 第二章搭建Android开发环境 书中介绍了两种JDK的安装方法, 方法一: 从官网下载JDK并进行配置,解压后在终端打开profile文件来设置PATH环境变量(# soure /etc/profile),打开profile文件后输入下面的内容 export PATH=.:developer/jdk6/bin:$PATH 保存profile文件以后,有两种方法可以重新加载profile文件. 1.# sourse  /etc/pro

web开发学习笔记(3):HTML表格制作——table标签以及th、td、tr标签的使用例子

//纯属新手学习笔记,仅供参考. 代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http

IBatis .NET 开发学习笔记&mdash;&mdash;.NET 开发环境搭建

大家好,今天给大家带来的是web应用程序配置,至于windows应用程序或者其他类型解决方案可以相同的配置,web应用程序配置文件为web.config,windows应用程序是app.config. 通过以下步骤可以建立属于你自己的环境: 1.首先,肯定是打开Visual Studio(文章后面简称VS),如果你有其他工具开发,我也不介意,反正我用VS,VS目前最新版是2013,不过我喜欢复古,所以,我目前用安装VS2010来当作教程,不管目前是多少版本了,都可以同理得到. 2.然后,新建一个

安卓开发复习笔记——Fragment+FragmentTabHost组件(实现新浪微博底部菜单)

记得之前写过2篇关于底部菜单的实现,由于使用的是过时的TabHost类,虽然一样可以实现我们想要的效果,但作为学习,还是需要来了解下这个新引入类FragmentTabHost 之前2篇文章的链接: 安卓开发复习笔记——TabHost组件(一)(实现底部菜单导航) 安卓开发复习笔记——TabHost组件(二)(实现底部菜单导航) 关于Fragment类在之前的安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)也介绍过,这里就不再重复阐述了. 国际惯例,先来张效果图: 下面

IOS开发学习笔记-(2)键盘控制,键盘类型设置,alert 对话框

一.关闭键盘,放弃第一响应者,处理思路有两种 ① 使用文本框的 Did End on Exit 绑定事件 ② UIControl on Touch 事件 都去操作 sender 的  resignFirstResponder #import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UITextField *txtUserName; @pro

【web开发学习笔记】Structs2 Action学习笔记(一)

1.org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter准备和执行 2. <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> url-pattern约定熟成只写/*,没必要写*.action 3. <

cocos2dx游戏开发学习笔记3-lua面向对象分析

在lua中,可以通过元表来实现类.对象.继承等.与元表相关的方法有setmetatable().__index.getmetatable().__newindex. 具体什么是元表在这里就不细说了,网上很多介绍,这里主要讲与cocos2dx相关联的部分. 在lua-binding库中extern.lua里,有如下方法: --Create an class. function class(classname, super) local superType = type(super) local c

cocos2dx游戏开发学习笔记1-基本概念

这里主要讲构建整个游戏需要的基本元素,很大部分都摘自cocos2dx官网. 1.Director 导演 导演,顾名思义,就是对整个游戏进行整体控制的. "Director"是一个共享的(单元素集)对象,负责不同场景之间的控制.导演知道当前哪个场景处于活动状态,允许你改变场景,或替换当前的场景,或推出一个新场景.当你往场景堆中推出一个新场景时,"Director"会暂停当前场景,但会记住这个场景.之后场景堆中最顶层的场景跳离时,该场景又会继续活跃.此外"Di