【项目】基于负载均衡式-在线OJ系统

负载均衡式在线OJ

1.项目技术和开发环境

项目技术

  • C++ STL 标准库
  • Boost 库
  • cpp-httplib 第三方开源网络库
  • ctemplate 第三方开源前端网页渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • 多进程、多线程
  • MySQL C Connect
  • html /css/js/jquery/ajax

开发环境

  • centos 7 服务器
  • vim / gcc(g++) / makefile

2. 结构设计和实现思路

image-20221125193145524

1)如果用户请求的是:题目列表/特定的题目, 此时并不需要对代码进行编译,只需要通过oj_server访问后端的数据库/文件去拉取对应的题目列表/题目即可

2)如果用户请求的是:提交代码,此时就需要oj_server负载均衡的选择一台后端主机进行编译服务

编译服务器和OJ服务器,两个模块之间采用网络套接字的方式互相通信,就可以把编译模块部署在服务器的多台机器上,OJ服务只需要一台,能够以集群处理的方式对外输出在线OJ服务


项目宏观结构

我们的项目核心是如下三个模块:

目录 介绍
comm 公共模块,存放公用的代码如一些工具类:文件操作,字符串处理,日志
compile_server 编译运行模块,让用户的代码在自己的服务器上形成临时文件,并且编译,运行,得到运行结果
oj_server 采用MAC的设计模式,使用算法负载均衡式的调用编译模块以及访问文件或数据库,把题目列表和编辑界面展示给用户

关于题目:实现两个版本:文件版本和数据库版本

用户直接访问的是OJServer模块,OJServer收到请求后会进行功能路由,根据不同的请求给用户返回不同的结果,如果用户是提交代码,那么OJServer模块还会更具后端的CompilerServer服务器的负载负载均衡的选择主机提供的编译服务,然后拿到编译的结果在给用户返回


项目目录结构大致如下:

Online_judge/
├── comm/   		//存放工具模块
│   ├── httplib.h -> ../third_part/cpp-httplib/httplib.h
│   ├── log.hpp		//日志类
│   └── util.hpp	//工具类:路径,时间,文件,字符串相关工具
├── compile_server/	 		//编译运行模块
│   ├── compile_server.cc	//以网络的形式,对外提供服务
│   ├── compile_run.hpp		//对外提供接口  -编译并且运行
│   ├── compiler.hpp		//编译
│   ├── runner.hpp			//运行
│   ├── Makefile
│   └── temp/
├── oj_server/			//负责负载均衡的调用
│   ├── oj_server.cc	//我们要形成的网络服务 -> 负载均衡式的去调用后端的编译服务
│   ├── oj_model.hpp	//主要用来和数据进行交互,对外提供访问数据的接口
│   ├── oj_control.hpp	
│   ├── oj_view.hpp		//形成网页给用户显示
│   └── Makefile
└── Makefile

3.日志模块-log.hpp

日志功能整个项目都要使用,所以自然应该属于Comm公共模块,在Comm目录下创建一个专属的log.hpp文件

我们将日志设计为五个等级:

  • INFO:表示正常的打印信息
  • DEBUG:表示用来dubug的信息
  • WARNING:警告,但是不影响继续使用,但存在风险
  • ERROR:错误,用户无法正常使用了,但该事件不影响服务器继续运行
  • FATAL:致命错误,该事件将导致服务器停止运行

上述的等级,可以通过枚举定义


日志说明:

  • 日志级别: 分为5个等级,从低到高依次是INFO、DEBUG,WARNING、ERROR、FATAL
  • 时间戳: 事件产生的时间
  • 日志信息: 事件产生的日志信息
  • 错误文件名称: 事件在哪一个文件产生
  • 行数: 事件在对应文件的哪一行产生

关于时间戳:

方法1:调用time函数时传入nullptr即可获取当前的时间戳

方法2:调用时间工具类的函数


关于文件名称和行数的问题

1)通过C语言中的预定义符号__FILE__和_LINE_,分别可以获取当前文件的名称和当前的行数,但最好在调用Log函数时不用调用者显示的传入__FILE__和__LINE__,因为每次调用Log函数时传入的这两个参数都是固定的,

2)需要注意的是,不能将__FILE____LINE__设置为参数的缺省值,因为这样每次获取到的都是Log函数所在的文件名称和所在的行数,而宏可以在预处理期间将代码插入到目标地点,因此我们可以定义如下宏:

#define LOG(level) Log(#level, __FILE____LINE__) 

Log函数返回的是输出流对象,调用该日志的方法是:LOG(level)<<“日志信息”<<endl,我们这里的是开放式的日志

#pragma once
#include <iostream>
#include <string>
#include "util.hpp"
namespace ns_log
{
   
    using namespace ns_util;//使用时间工具类
    // 日志等级
    enum    
    {
   
        INFO, 
        DEBUG,
        WARNING,
        ERROR,  
        FATAL
    }; 
    //参数:日志等级  在哪个文件打的日志  哪一行,方便追溯出错原因
    //将来是这样调用的: LOG(INFO) << "message输出信息" << "\n";
    //返回值:标准输出std::cout
    inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)
    {
   
        // 添加日志等级
        std::string message = "[";
        message += level;
        message += "]";

        // 添加报错文件名称
        message += "[";
        message += file_name;
        message += "]";

        // 添加报错行
        message += "[";
        message += std::to_string(line);
        message += "]";

        // 日志时间戳   是什么时候打印的报错信息
        message += "[";
        message += TimeUtil::GetTimeStamp();
        message += "]";

        // cout 本质内部是包含缓冲区的 ,将刚才的message写入到缓冲区
        std::cout << message; //不要endl进行刷新,此时message就会暂存到cout的缓冲区当中

        return std::cout;
    }

    // 将来是这样调用的: LOG(等级) << "message" << "\n";
    // 开放式日志 
    #define LOG(level) Log(#level, __FILE____LINE__) //给宏参带#,将宏名称以字符串形式进行展示
} 

4.公共模块 -comm存放工具类

我们定义一个名字为comm的文件夹,里面存放我们需要用的公共内容: 日志模块 + 工具类 + httplib.h

comm/httplib.h
comm/log.hpp
comm/util.hpp

关于路径的工具类-PathUtil

后序我们进行编译及运行,都只是只传入文件的名字,所以我们要根据文件的名字,拼接出带路径的文件名字+后缀


需要形成的文件的路径:源文件,可执行程序,编译报错文件,标准输入,标准输出,标准错误文件

后缀分别为: .cpp .exe .compile_error .stdin .stdout .stderr

做法:定义一个函数:AddSuffix,用于给指定名字的文件添加后缀和路径,后序只需要传递对应的文件名+后缀即可

const std::string temp_path = "./temp/"; //生成的文件保存的位置,也就是compiler_server/temp下

class PathUtil //提供对路径的操作方法-路径工具
{
   
    public:
    //添加后缀
    static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
    {
   
        //例如:file_name=1234  
        std::string path_name = temp_path    ;//    ./temp/
        path_name += file_name;//               ./temp/1234
        path_name += suffix;//                  ./temp/1234.cpp
        return path_name;
    }

    // 编译时需要有的临时文件
    // 构建源文件路径+后缀的完整文件名  file_name:1234 -> 返回: ./temp/1234.cpp
    static std::string Src(const std::string &file_name)
    {
   
        return AddSuffix(file_name, ".cpp");
    }

    // 构建可执行程序的完整路径+后缀名  file_name:1234 -> 返回: ./temp/1234.exe
    static std::string Exe(const std::string &file_name)
    {
   
        return AddSuffix(file_name, ".exe");
    }

    static std::string CompilerError(const std::string &file_name)
    {
   
        return AddSuffix(file_name, ".compile_error"); //编译报错文件
    }
    //-----------------------------------------------------------------------------------
    // 运行时需要的临时文件
    static std::string Stdin(const std::string &file_name) //形成一个标准输入文件
    {
   
        return AddSuffix(file_name, ".stdin");
    }

    static std::string Stdout(const std::string &file_name)//形成一个标准输出文件
    {
   
        return AddSuffix(file_name, ".stdout");
    }

    // 构建该程序对应的标准错误完整的路径+后缀名  file_name:1234 -> 返回: ./temp/1234.stderr
    //运行时报错的文件
    static std::string Stderr(const std::string &file_name) //形成一个标准错误文件
    {
   
        return AddSuffix(file_name, ".stderr");
    }
};

关于时间的工具类-TimeUtil

需要提供两个函数:

1.因为打日志我们需要知道是什么时候打的日志,所以我们需要一个可以获取系统的时间戳 (秒级别的) 函数

2.因为每时每刻都有用户在提交代码,生成对应的文件,形成的文件名必须具有唯一性,我们通过毫秒级时间戳+原子性递增唯一值来保证这个事情, 所以我们需要提供一个可以获得毫秒时间戳的函数 (如果用秒,跨度太长了,所以使用毫秒级别的)


关于gettimeofday函数

方法:使用系统调用接口gettimeofday

image-20221125201516467

参数:

  • 第一个参数:要获得的时间是什么,是输出型参数,返回的是当前的秒级别时间戳和微秒级别的时间戳

  • 第二个参数:时区,但是我们不关心,置为空

函数返回值:

  • 成功是0,失败-1,但是我们这里并不关心
class TimeUtil  //关于时间的工具类
{
   
public:
    static std::string GetTimeStamp() //获取系统的时间戳 (秒级别的) 
    {
   
        struct timeval _time;
        gettimeofday(&_time, nullptr);
        return std::to_string(_time.tv_sec);//返回秒级别时间戳
    }

    //获得毫秒时间戳 ->如果用秒,跨度太长了,所以使用毫秒级别的
    static std::string GetTimeMs()
    {
   
        struct timeval _time;
        gettimeofday(&_time, nullptr);
        //毫秒级时间戳 = 秒级别时间戳*1000 + 微秒级别时间戳/1000
        return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
    }
};

关于文件的工具类-FileUtil

需要提供的函数:

1.判断文件是否存在

关于stat函数

方法1:把文件以读取方式打开,如果打开失败了,说明文件不存在 -》太简单粗暴了

方法2: 使用系统调用接口stat,可以检测特定路径下的文件,获取它的属性

image-20221125201300823

返回值:获取文件属性成功了返回0 -> 说明文件存在 如果函数返回-1,则说明文件不存在

// path_name是完整的路径 例如: ./temp/1234.stderr
static bool IsFileExists(const std::string &path_name) //判断文件是否存在
{
   
    struct stat st;
    //返回值:获取文件属性成功了返回0 -> 说明文件存在  否则返回-1
    if (stat(path_name.c_str()&st) == 0)
    {
   
        //获取属性成功,文件已经存在
        return true;
    }
    return false;
}

2.形成唯一的文件名

方法:根据毫秒级时间戳+原子性递增唯一值: 来保证唯一性,

  • 关于毫秒级时间戳: 直接复用上面时间工具类当中的函数

  • 关于原子性递增唯一值:这里需要使用C++11当中的atomic,其就是原子性递增的计数器,需要引入头文件#include ,因为值是整数,所以我们使用atomic_uint

static std::string UniqFileName() //形成唯一的文件名
{
   
    //要定义为static,否则每一次都是对不同的id++
    static std::atomic_uint id(0);
    id++;//原子性++
    std::string ms 	= TimeUtil::GetTimeMs(); //获得毫秒级别的时间戳
    std::string uniq_id = std::to_string(id);//原子递增的唯一值
    return ms + "_" + uniq_id;
}

3.写入文件

方法:打开要被写入的文件,判断是否打开成功, 如果打开失败直接返回, 打开成功:调用系统调用write函数进行写入,最后记得关闭文件流

image-20221125203046657

//把content的内容写入到target文件
static bool WriteFile(const std::string &target, const std::string &content) 
{
   
    std::ofstream out(target);//打开文件,ofstream默认就是写入
    if (!out.is_open()) 
    {
   
        return false;//打开失败
    }
    //第一个参数:要写谁 第二个参数:写多少个字节
    out.write(content.c_str(), content.size()); //写入文件
    out.close();//关闭文件流
    return true;
}

4.读取文件的内容

把文件的内容读取出来, 使用getline,一次读取一行的内容,但是需要注意:getline读取一行内容的时候不保存\n,这里我们在读取的时候,有时需要保存文件当中的\n,而有时不需要保存,所以我们可以增加一个参数:是否保存\n

参数1:文件路径 参数2:输出型参数,读取的内容放到这个位置 参数3:是否需要保留\n 默认是false,表示不需要

static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
{
   
    (*content).clear();//最好先把传入的空间先清空!
    std::ifstream in(target);
    if (!in.is_open())
    {
   
        return false;
    }

    std::string line;//每次读取一行内容
    // getline:不保存行分割符(\n)->不读取\n,但是有些时候需要保留\n,
    // getline内部重载了强制类型转化,所以读取成功与否,有true和false保证
    while (std::getline(in, line)) //第一个参数:从那里读 第二个参数:读到哪里
    {
   
        (*content) += line;
        (*content) += (keep ? "\n" : ""); //如果keep为真:保留\n
    }
    in.close();//关闭文件流!
    return true;
}

关于字符串的工具类-StringUtil

1.字符串切分函数

关于boost::split函数

这里我们需要使用boost库当中的split函数,需要引入头文件:#include <boost/algorithm/string.hpp>

函数原型

boost::split(type, select_list, boost::is_any_of(","), boost::token_compress_off)

第一个参数:用于存放切割好的字符串

第二个参数:要切割的字符串,可以为空

第三个参数 :切割符(分隔符)

第四个参数:是一个选项,token_compress_on或者token_compress_off,默认是token_compress_off

image-20221112102357867

如果是token_compress_on:含义就是将连续多个分隔符当一个, 一般建议打开!

例子: 针对第四个参数: aaa\3\3\3\3bb 如果是token_compress_on:意思就是把字符串压缩成:aaa\3bb,然后切分

class StringUtil //进行字符串切分
{
   
public: 
    //第一个参数:切分谁    第二个参数: 切分好的内容放到哪里     第三个参数:以什么为分隔符切分
    static void SplitString(const std::string &target, std::vector<std::string> *out, const std::string &sep)
    {
   
        //is_any_of表示:凡是在这个里面的任何一个字符都作为原始字符串当中的分隔符
        //第一个参数:保存在哪里 第二个参数:数据源 第三个:分隔符 
        //第四个参数:分隔符和分隔符之间的内容是否进行压缩
        boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
    }
};

测试这个split函数的代码:

#include<iostream>
#include<string>
#include<vector>
#include<boost/algorithm/string.hpp>
int main()
{
   
    std::vector<std::string> result;
    std::string  target = "aaa\3\3\3bbb\3ccc";
    boost::split(result,target,boost::is_any_of("\3"),boost::token_compress_on); 
    for(auto& s:result)
    {
   
      std::cout << s<<std::endl;
    }
    return 0;
}

image-20221112144223718

token_compress_on:把压缩打开,可以理解为将相连的分隔符压缩成一个


class StringUtil 
{
   
 public:
    /*************************************
         * str: 输入型,目标要切分的字符串
         * target: 输出型参数,保存切分完毕的结果 
         * sep: 指定的分割符
         * **********************************/
    static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string &sep)
    {
   
        boost::split((*target),str,boost::is_any_of(sep), boost::algorithm::token_compress_on);
    }
};

5.编译运行模块-compile_server

编译运行模块的功能是:编译并运行客户端通过网络提交的代码,并得到运行的结果

把整个模块分为4个部分:

  • compiler模块**:只负责代码的编译,拿到待编译代码的文件名,进行编译,并形成对应的可执行或者错误保存编译时错误信息的临时文件,**
  • run模块:只负责运行代码,通过文件名执行指定的可执行程序,并且形成用于保存运行的结果的临时文件方便获得对应的内容,
  • compiler_run模块:整合编译模块和运行模块,解析用户发来的json串,得到json串形式的代码内容,形成一个可以用来编译的源文件,调用编译和运行两个模块完成功能,构建结果返回给编译服务模块,
  • compiler_server模块:负责搭建http服务,接收客户端发来的请求,提取出json串形式的代码,然后调用compiler_run模块编译运行,得到结构后返回给客户端
image-20221126204957588

image-20221127194023172


在compiler_server目录下定义一个temp目录,用来存储生成的临时文件,但是实际上并不需要存储,我们编译运行完成之后,就可以清除这些临时文件


编译模块-compiler.hpp

主要功能:提供编译服务 -只负责进行代码的编译

image-20221125194405353

需要实现的函数:static bool Compile(const std::string &file_name)

  • 参数:要进行编译的文件的名字,不需要带后缀,在函数内部自动调用路径工具进行拼接

1.我们需要根据文件名字,构建出带路径和后缀的:源文件,可执行文件,标准错误文件

2.我们需要创建子进程来帮我们完成编译工作,而不是让主进程干这个活,

子进程:

  • 编译之前,首先需要打开该文件名同名的,后缀为compile_error的临时文件(编译时报错的文件),就是为了编译失败的时候写入它

  • 因为该文件可能不存在,所以如果不存在就创建它,因为需要设置文件的权限,权限还受权限掩码的影响,所以我们先把权限掩码清0

  • 我们调用dup函数,把标准错误重定向到我们打开的文件当中,后序如果编译报错,该文件就会有错误信息

    • #include <unistd.h>
      int dup2(int oldfd,int newfd);
      
    • 让newfd指向的内容变成oldfd指向内容的一份拷贝,也就是说让newfd指向之前oldfd指向的文件

    • 例如:想要把2号文件描述符重定向到_stderr文件, :dup(_stderr,2)

  • 子进程需要做的:调用g++编译器,完成对代码的编译工作 ->需要使用程序替换

    • #include <unistd.h>
      int execlp(const char* file,const char* arg, ...);
      
    • 第一个参数是要进行替换的程序的名字,因为我们是要编译代码,所以这个程序名就是g++,后面的参数是一个可变参数,我们在命令行怎么执行改程序,就怎么把参数传给这个替换函数,这就需要用到上面封装的路径工具类了,每个参数都是字符串类型,最后还有一个nullptr

    • 明确一下:程序替换,并不影响进程的文件描述符表

    • g++ -o 目标文件 源文件 -std=c++11 ,注意程序替换的参数不要忘记以nullptr作为结尾

注意:

1.这里的编译选项还有一点需要注意,在编译模块只需要可以编译代码就可以了,但是我们整个代码的设计是分为两部分的,一部分预设代码,即在用户请求编译时已经存在的一部分代码,然后提交给后端

2.后端通过用户提交的代码和题目对应的测试用例拼接起来,才形成一个完整的代码,而为了在测试用例一般会加入条件编译,具体原因我会在题库设计时讲解,所有编译时还要多加一个选项才能让代码正常编译

父进程:

1.等待子进程结束,我们不关心退出结果,因为如果有编译失败,错误信息已经保存在了和该文件同名的,后缀为compile_error的临时文件当中

2.判断是否编译成功 (本质是看有没有形成对应的可执行程序 -> 该文件是否存在)


所以是可以通过:g++ -o 路径/123.exe 路径/123.cpp -std=c++11 来编译cpp文件,在该路径下生成名字为123.exe的可执行程序

image-20221125212531966


namespace ns_compiler 
{
   
    using namespace ns_util;//为了引入路径工具类
    using namespace ns_log; //引入日志
    class Compiler 
    {
   
        public:
        Compiler()
        {
   }
        ~Compiler()
        {
   }

        //返回值:编译成功:true,否则:false
        //输入参数:编译的文件名,例如:file_name: 1234
        //构建源文件 可执行文件 标准错误的文件名
        //1234 -> ./temp/1234.cpp      ./temp/1234.exe ./temp/1234.stderr
        static bool Compile(const std::string &file_name) //参数:文件名
        {
   
            pid_t pid = fork();
            if(pid < 0)  //创建子进程失败
            {
   
                LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
                return false;
            }
            else if (pid == 0) //子进程
            {
   
                //编译前,先打开后缀为compile_error的临时文件(编译时报错的文件),为了失败的时候写入
                umask(0);//将默认的umask清0,
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);//不存在就创建这个文件,文件权限为rw-r--r--
                if(_stderr < 0) //判断是否打开文件成功
                {
   
                    LOG(WARNING) << "没有成功形成stderr文件" << "\n";
                    exit(1);//打开文件失败->说明一旦编译报错,也不能给用户提供报错信息
                }
                dup2(_stderr, 2);  //将标准错误重定向到文件_stderr
                //g++ -o  ./temp/1234.exe ./temp/1234.cpp -std=c++11
				execlp(
                    "g++"//第一个参数:要执行的命令
                    //后续的就像在命名行上的参数一样
                    "g++""-o"PathUtil::Exe(file_name).c_str()PathUtil::Src(file_name).c_str()"-std=c++11"nullptr //最后不要忘了nullptr
                );
                LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";
                exit(2);//如果走到这里,说明程序替换失败,退出码设为2
            }
            else	//父进程
            {
    
                waitpid(pid, nullptr0); //等待id为pid子进程结束,不关心退出结果设为nullptr 
                //判断编译是否成功-> 就看有没有形成对应的可执行程序
                if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
                {
   
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
                    return true;
                } 
            }
            
            LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
            return false;
        }
    };
}

测试编译模块

首先我们先在temp目录下添加一个名字为code的.cpp文件,并且编写测试代码

image-20221126194210902

在temp目录下生成对应的文件:源文件,可执行文件,编译时出错文件


运行模块-runner.hpp

我们通常在OJ刷题平台上看的,大部分题目都是限制时间复杂度和空间复杂度的,运行的时候, 有可能用户提供的是恶意的代码,例如:死循环消耗CPU资源,,所以运行的时候,我们要进行时间和空间的约束,以及能根据返回的信号,映射到对应的错误原因,供上层去使用

所以这里我们也需要写一个函数,限制程序的资源的占用大小,如CPU时间,占用空间大小等

setrlimit函数

这是一个系统调用函数,作用是设置资源使用限制

image-20221126102303384

软限制设置的值不能超过硬限制,一般把硬限制设置成RLIM_INFINITY表示无限制,软限制设置成限制的值


测试代码:

image-20221126103531232


如果超时: OS发送的是24号信号SIGXCPU 如果是资源不足:OS发送的是6号信号SIGABRT

Run函数设计

程序运行结果有3种可能:

1.代码跑完,结果正确 2.代码跑完,结果不正确 3.代码没跑完,异常退出了

在这里,我们不需要关心代码运行的结果是否正确,结果正确与否:这个要交给上层的OJ模块根据测试用例去判断!在这里我们只考虑:程序是否正确运行完毕

其次我们要控制程序的输入输出,

  • 标准输入:不作处理 ->即:不考虑用户自己写测试用例的情况,必须由我们来做
  • 标准输出:一般是程序运行的结果
  • 标准错误:运行时错误信息 (注意:有两种错误:编译时错误和运行时错误,运行时错误才是往stderr打印)

我们执行这个程序,想要的有两个东西:

1.程序执行完的临时数据,尤其是标准输出和标准错误, 写到文件当中,方便查看,

2.程序运行结果是什么已经在标准输出文件中保存了,我们不关心, 我们只关心文件有没有异常,如果异常,结果没必要看了.

如何得知进程有没有异常? 程序运行异常,一定是因为因为收到了信号!


static int Run(const std::string &file_name, int cpu_limit, int mem_limit)

参数:

第一个参数:file_name 指明文件名即可,不需要带路径,不需要带后缀

第二个参数:cpu_limit: 该程序运行的时候,可以使用的最大cpu资源上限

第三个参数:mem_limit: 该程序运行的时候,可以使用的最大的内存大小(以KB为单位) 如果按字节为单位,数字太大


返回值:

关于返回值的设定: 我们这里的返回值是关心运行是否成功

  • 返回值>0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
  • 返回值==0:正常运行完毕的,运行接轨保存到了对应的临时文件中(后缀为.stdout)
  • 返回值<0:发生了内部错误

1)先根据文件名,使用路径工具类的函数,获取对应的带路径的临时同名文件,包括:可执行程序文件,标准输入文件(因为要往这个文件读取,但是实际我们不处理),标准输出文件(程序运行的结果往这个文件写),标准错误文件(运行的时候出错了就把错误信息写入到该文件)

2)打开上述的三个标准文件,如果不存在就先创建 (因为需要指定权限,所以先把权限掩码清为0)

3)创建子进程执行运行任务,子进程也会形成自己的file_strcut结构体,子进程内核的数据结构task_strcut会以父进程为模板初始化自身,因此父进程和子进程的文件描述符表就是一样的,也就是说子进程会继承父进程的文件描述符表

image-20221126104431709
  • 如果子进程创建失败:此时就要关闭主进程的文件描述符的内容,然后让主进程退出

子进程:

1.子进程的标准输入,标准输出,标准错误, 默认是键盘显示器显示器,所以我们需要把它们重定向到我们打开的文件,将来程序运行后,运行结果/错误信息都会写入到该文件,注意:并不会影响父进程 此处需要使用dup函数

2.设置子进程的资源限制,添加资源约束是为了让子进程受到资源可控的运行上下文当中

3.执行进程替换,运行程序,需要注意:我们是可以带路径运行程序的,例如: /bin/ls来运行ls指令

父进程:

1.因为父进程不需要使用上述打开的三个标准文件,所以我们要关闭其对应的文件描述符,不会影响子进程,因为进程具有独立性

2.等待子进程退出,这里我们只关心退出时结果是否是异常的,如果程序运行异常,一定是因为因为收到了信号!我们可以在oj模块,根据Run函数的返回值,来判断是因为什么原因出错的


code:
class Runner
{
   
public:
    Runner() {
   }
    ~Runner() {
   }

public:
    //提供设置进程占用资源大小的接口
    static void SetProcLimit(int _cpu_limit, int _mem_limit)
    {
   
        // 限制(设置)CPU时长
        struct rlimit cpu_rlimit;
        cpu_rlimit.rlim_max = RLIM_INFINITY;
        cpu_rlimit.rlim_cur = _cpu_limit;
        setrlimit(RLIMIT_CPU, &cpu_rlimit);

        // 限制(设置)内存大小
        struct rlimit mem_rlimit;
        mem_rlimit.rlim_max = RLIM_INFINITY;//对硬件资源没有限制
        mem_rlimit.rlim_cur = _mem_limit * 1024; //默认限制的单位是字节,转化成为KB 
        setrlimit(RLIMIT_AS, &mem_rlimit);
    }

    /*******************************************
        * 返回值:关心运行是否成功
        * 返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
        * 返回值 == 0: 正常运行完毕的,结果保存到了对应的临时文件中
        * 返回值 < 0: 内部错误
        * 
        参数:
        * file_name 指明文件名即可,不需要带路径,不需要带后缀 
        * cpu_limit: 该程序运行的时候,可以使用的最大cpu资源上限
        * mem_limit: 该程序运行的时候,可以使用的最大的内存大小(KB)  按字节来看,数字太大了,不好看
        * ****************************************
    */
    static int Run(const std::string &file_name, int cpu_limit, int mem_limit)  
    {
   
        /*
            ********************************************
            * 程序运行结果:
            * 1. 代码跑完,结果正确
            * 2. 代码跑完,结果不正确
            * 3. 代码没跑完,异常了

            * Run需要考虑代码跑完,结果正确与否吗? 不考虑!
            * 结果正确与否:是由我们的测试用例决定的!在这里我们只考虑:程序是否正确运行完毕
            
            * 一个程序在默认启动的时候
            * 标准输入: 不处理 -> 即:不考虑用户自己写测试用例的情况,必须由我们来做
            * 标准输出: 程序运行完成,输出结果是什么
            * 标准错误: 运行时的错误信息
            * ******************************************
        */
        //根据文件名称 获取 对应的带路径的临时同名文件 ./temp/file_name.exe
        std::string _execute = PathUtil::Exe(file_name); //带路径的可执行程序文件  
        std::string _stdin   = PathUtil::Stdin(file_name);//标准输入的东西往这个文件读取
        std::string _stdout  
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芒果再努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值