C语言的OJ判题机设计与实现

1. 接收判题入参

判题需要作答代码、测试输入和期望输出、编译器名称、时空限制。对于支持special judge的还需要传入是否为sj和sj代码。推荐使用消息队列,应对高并发的比赛情况会比较好。
但是消息队列是异步的,我为了快点实现能提交后在当前页面获得判题结果,就单纯的用了rpc+nginx负载均衡,不过我觉得如果要实现当场获得判题结果,也可以mq+websocket

2. 编写判题镜像

我的设计是一个镜像对应一个编译器,好处是方便对于每个语言的编译运行做独立的修改,坏处是因为镜像基于ubuntu容器,至少也有1.7G的大小
下面为我的judger:base包的dockerfile,因为我需要python进行special judge,c进行判题,所以安装了gcc和python

# 使用基础镜像, Ubuntu
FROM ubuntu:latest

ENV DEBIAN_FRONTEND=noninteractive

# 安装所需的编译器和其他依赖项
RUN apt-get update && apt-get install -y \
    build-essential \
    libssl-dev \
    zlib1g-dev \
    libbz2-dev \
    libreadline-dev \
    libsqlite3-dev \
    llvm \
    libncurses5-dev \
    libncursesw5-dev \
    xz-utils \
    tk-dev \
    libffi-dev \
    liblzma-dev \
    python3-openssl \
    python3-pip \
    wget

# 将本地的 Python 压缩包复制到容器中
COPY Python-3.8.12.tar.xz .

# 解压 Python 压缩包并进行安装
# RUN wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tar.xz &&
RUN tar -xf Python-3.8.12.tar.xz && \
	cd Python-3.8.12 && \
    ./configure --enable-optimizations && \
    make -j$(nproc) && \
    make altinstall

# 删除临时文件
RUN rm -f Python-3.8.12.tar.xz
# 方便直接执行python
RUN ln Python-3.8.12/python /usr/bin/python
# 设置容器启动时的默认命令
CMD ["bash"]

2.1 编写判题脚本

我的是先在判题服务上选择启动对应的判题容器,然后将测试输入和期望输出以及代码保存到本地,然后将测试数量和时空限制传入判题机,所以c语言只需要接收这几个,向容器中传入的方面便是环境变量。go操作docker的操作如下

import (
	"context"
	"fmt"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/client"
)
func getClient() *client.Client{
	cli, err := client.NewClientWithOpts(client.WithHost("tcp://localhost:2375"), client.WithVersion("1.44"))
	if err != nil {
		panic(err)
	}
	return cli
}
func Run(params *JudgeParams,compiler string,dataDir string){
	cli := getClient()
	ctx :=context.Background()
	env := []string{
		fmt.Sprintf("special=%d", params.Special),
		fmt.Sprintf("timelimit=%d", params.TimeLimit),
		fmt.Sprintf("memorylimit=%d", params.MemoryLimit),
		fmt.Sprintf("casenum=%d", params.CaseNum),
	}
	// 准备配置,单位是毫秒->秒,再两倍
	timeout := int(params.TimeLimit)/500
	config := &container.Config{
		Image: fmt.Sprintf("judger:%s",compiler), 
		Env:   env,
		StopTimeout: &timeout,
	}
	// 准备 HostConfig,设置挂载点
	hostConfig := &container.HostConfig{
		Mounts: []mount.Mount{
			{
				Type:   mount.TypeBind,
				Source: dataDir,
				Target: "/app/data",
			},
		},
	}

	// 创建容器
	cont, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, "")
	if err != nil {
		panic(err)
	}

	// 启动容器
	if err := cli.ContainerStart(ctx, cont.ID, container.StartOptions{}); err != nil {
		panic(err)
	}

	fmt.Printf("Container %s started.\n", cont.ID)
    // 等待结束
	statusCh, errCh := cli.ContainerWait(ctx, cont.ID, container.WaitConditionNotRunning)
	select {
	case err := <-errCh:
	    if err != nil {
	        fmt.Println(err)
	    }
	case status := <-statusCh:
	    fmt.Println("Container exited with status:", status.StatusCode)
	}
	//删除容器
	cli.ContainerRemove(ctx,cont.ID,container.RemoveOptions{
		// Force: true,
	})
}

所以在传入这些参数之后,镜像内的c语言进行接收,注意需要从字符串转换

int main(int argc,char **argv) {
    int isSpecial = atoi(getenv("special"));
    int testCaseNum = atoi(getenv("casenum"));
    int timeLimit = atoi(getenv("timelimit"));
    int memoryLimit = atoi(getenv("memorylimit"));
    ...

接着便是正式判题
在这里插入图片描述

可以看到即使通过Docker开辟了独立的容器空间,但内部还是要通过fork来限制程序运行的时空。

2.2 fork

fork开辟子进程

pid_t pid = fork();
if(pid<0){
    printf("error in fork!\n");
    result->status = WRONG_ANSWER;
    result->log = "无法创建新进程";
    return;
}
// 父进程监听
if(pid>0){
    monitor(pid, timeLimit, memoryLimit, result);
}else{
//子进程运行
    setProcessLimit(timeLimit,memoryLimit);
    _runExe(exeFile,timeLimit,memoryLimit,inputFile,outputFile);
}

限制时空是下面代码,具体为什么有两个限制内存的,我也不知道,unix的api我一点不会,java选手嗯造c语言

// ms kb
void setProcessLimit(const int timelimit, const int memory_limit) {
    struct rlimit rl;
    /* set the time_limit (second)*/
    rl.rlim_cur = timelimit / 1000;
    rl.rlim_max = rl.rlim_cur + 1;
    setrlimit(RLIMIT_CPU, &rl);
    /* set the memory_limit (b)*/
    rl.rlim_cur = memory_limit * 1024;
    rl.rlim_max = rl.rlim_cur;
    setrlimit(RLIMIT_DATA, &rl);
    rl.rlim_cur = memory_limit * 1024;
    rl.rlim_max = rl.rlim_cur;
    setrlimit(RLIMIT_AS, &rl);
}

运行可执行程序。通过重定向将输入文件内容作为程序输入,将程序输出传入实际输出文件中。

void _runExe(char *exeFile,long timeLimit, long memoryLimit, char *in, char *out) {
    int newstdin = open(in,O_RDWR|O_CREAT,0644);
    int newstdout = open(out,O_RDWR|O_CREAT|O_TRUNC,0644);
    if (newstdout != -1 && newstdin != -1){
        dup2(newstdout,fileno(stdout));
        dup2(newstdin,fileno(stdin));
        char cmd[20];
        char *args[] = {"./program", NULL};
        if (execvp(args[0], args) == -1){
            printf("====== Failed to start the process! =====\n");
        }
    } else {
        printf("====== Failed to open file! =====\n");
    }
    close(newstdin);
    close(newstdout);
}

注意是在运行程序,具体的api细节我不清楚。但是args[0]作为execvp的第一个参数,只是起一个程序名的作用,没啥用,主要还是args作为按空格分隔的多个运行参数,放在execvp的第二个位置。然后第三个参数放NULL就行了。如python的就是char *args[] = {"python","main.py", NULL};

char *args[] = {"./program", NULL};
if (execvp(args[0], args) == -1){
       printf("====== Failed to start the process! =====\n");
}

还是execvp这个api,如果是python这种解释性脚本语言,他语法错误时不会什么返回-1,直接打印语法错误然后就返回0了,为什么专门提这个呢?看父进程是怎么监听的。

2.3 父进程

我这里使用了rusage和wait4的api来获取子进程的返回结果和运行时空。

int status;
struct rusage ru;
// 等待进程结束
if (wait4(pid, &status, 0, &ru) == -1)printf("wait4 failure");

因为我们限制了子进程的时空,所以当子进程触碰到阈值后,就会异常终止,下方代码就是判断进入异常终止和正常结束的情况。可以自行理解TERM和EXIT。

// 异常
 if(WIFSIGNALED(status)){
        int sig = WTERMSIG(status);
 }else{
 //正常结束
		int sig = WEXITSTATUS(status);
}       

然后接下来的异常信号量就是我在网上看别人的了,不过也确实能用。

void monitor(pid_t pid, int timeLimit, int memoryLimit, Result *rest) {
    int status;
    struct rusage ru;
    // 等待进程结束
    if (wait4(pid, &status, 0, &ru) == -1)printf("wait4 failure");
    rest->timeUsed = ru.ru_utime.tv_sec * 1000
            + ru.ru_utime.tv_usec / 1000
            + ru.ru_stime.tv_sec * 1000
            + ru.ru_stime.tv_usec / 1000;
    // 另一个可能可行的方案:缺页错误就是使用内存的次数,乘页面大小就是内存占用,java可能用:`ru.ru_minflt * (sysconf(_SC_PAGESIZE) / 1024))` ;
    rest->memoryUsed = ru.ru_maxrss;
    // 程序异常中断
    if(WIFSIGNALED(status)){
        int sig = WTERMSIG(status);
        switch (WTERMSIG(status)) {
            case SIGSEGV:
                if (rest->memoryUsed > memoryLimit)
                    rest->status = MEMORY_LIMIT_EXCEED;
                else
                    rest->status = RUNTIME_ERROR;
                break;
            case SIGALRM:
            case SIGXCPU:
                rest->status = TIME_LIMIT_EXCEED;
                break;
            default:
                rest->status = RUNTIME_ERROR;
                break;
        }
    } else {
        // 注意语法错误和运行错误都会进这里
        int sig = WEXITSTATUS(status);
        if (sig==0){
            rest->status = ACCECPT;
        }else{
            rest->status = RUNTIME_ERROR;
        }
    }
}

注意看代码的正常结束判断的代码段,其实这个判断是我的python判题机里的,因为他因为语法运行错误不会做什么运行错误的返回,而是进入正常返回,所以在这里还需要判断,0是正常结束,1是不正常。而gcc和g++就不用在这里判断(应该是的)。
正好也给一个python运行前检查语法错误的法子,万一哪个老师脑子一抽想加个和编译错误同等的语法错误的判断

import sys

def check_syntax(file_path):
    try:
        with open(file_path, 'r') as file:
            script = file.read()
        # 尝试编译脚本
        compile(script, file_path, 'exec')
        print(f"The script '{file_path}' has no syntax errors.")
        return True
    except SyntaxError as e:
        # 捕获语法错误
        print(f"Syntax error in '{file_path}': {e}")
        return False

if __name__ == "__main__":
    file_path = sys.argv[1] if len(sys.argv) > 1 else "data/code.py"
    # 检查文件语法
    check_syntax(file_path)

2.4 比较输出结果

实际输出和期望输出的比较,就见仁见智了,毕竟有些题目要求完全一致,不然格式错误什么的,顺便一提我这里没给出输出超限格式错误的判断方法。更别说还有的什么可以忽略最后的换行符或者每行最后一个空格,那个要自己写了(指不用linux自带的diff命令)

然后关于特判,我的python代码模版如下。这里面限制了运行时间以及读取实际输出文件,并将返回的True或False的字符串写入文件中,还是通过文件通信。而出题人编写的代码,就放在这下面的第一行的上面,模版再见更下面。

import signal  
import sys  
from contextlib import contextmanager  
  
@contextmanager  
def time_limit(seconds):  
    def signal_handler(signum, frame):  
        raise Exception()  
    signal.signal(signal.SIGALRM, signal_handler)  
    signal.alarm(seconds)  
    try:  
        yield  
    finally:  
        signal.alarm(0)
        

try:  
    with open(sys.argv[1], 'r') as file:  
        lines = file.readlines()  
    with time_limit(int(sys.argv[2])):
        res = judge(lines)  
except Exception as e:  
        res = False
with open(sys.argv[3], 'w') as f:  
    f.write(str(res))

这是出题人的模板,他要负责编写这个函数,入参是实际输出的每行的字符串(所以还需要手动split和类型转换),返回值必须是True或False

def judge(lines)->bool:  
    for line in lines:
        pass
    return True

2.5 返回判题结果

至于为什么保存为json进行volume通信,这个见仁见智,我是用的cJSON库,还挺有意思,给你们瞟一眼,其实就是创建链表节点,然后挂载到父结点上,毕竟json也可以看作一个多叉树

void res2json(Result *compileResult,Result *runResults,int testCaseNum,char *lastOuput){
    // 创建 JSON 对象
    cJSON *root = cJSON_CreateObject();
    if (root == NULL) {
        fprintf(stderr, "Failed to create JSON object.\n");
        return;
    }
    //编译结果
    cJSON *compileNode = cJSON_CreateObject();
    cJSON_AddNumberToObject(compileNode, "status", compileResult->status);
    cJSON_AddStringToObject(compileNode, "log", compileResult->log);
    cJSON_AddItemToObject(root, "compile", compileNode);
    // 运行结果
    cJSON * runNodes = cJSON_CreateArray();
    for(int i=0; i<testCaseNum;i++){
        cJSON *runNode = cJSON_CreateObject();
        cJSON_AddNumberToObject(runNode, "status", runResults[i].status);
        cJSON_AddStringToObject(runNode, "log", runResults[i].log);
        cJSON_AddNumberToObject(runNode, "time", runResults[i].timeUsed);
        cJSON_AddNumberToObject(runNode, "memory", runResults[i].memoryUsed);
        cJSON_AddItemToArray(runNodes, runNode);
    }
    cJSON_AddItemToObject(root, "run", runNodes);
    //最后一次输出
    cJSON *lastOutputNode = cJSON_CreateString(lastOuput);
    cJSON_AddItemToObject(root, "lastOutput", lastOutputNode);
    // // 将 JSON 对象转换为 JSON 字符串
    char *jsonStr = cJSON_Print(root);
    if (jsonStr == NULL) {
        fprintf(stderr, "Failed to convert JSON object to string.\n");
        cJSON_Delete(root);
        return;
    }
    cJSON_Delete(root);
    // 打开文件,如果不存在则创建,准备写入  
    FILE *file = fopen(RES_FILE, "w");  
    if (file == NULL) {  
        perror("Error opening file");  
        return;  
    }  
    // 写入字符串到文件
    fputs(jsonStr, file);
    fclose(file);
    printf("%s\n",jsonStr);
    free(jsonStr);
}

2.6 编译型和解释型语言

我这个每种语言各自一个镜像就是为了这种情况。像gcc、g++、java(有编译为字节码和虚拟机运行字节码两步)这种编译型就把编译步骤加上,然后运行也是运行输出的可执行文件。
像python nodejs这些就可以注释掉compile操作,然后改写运行的那句命令(execvp那里)

2.7 请求头

我写的很困难,因为很多api都不知道,是chatgpt+stackoverflow告诉我的。姑且分享一下。cjson这里没放,读者自己学着去仓库里下.c和.h然后include h文件(只需要下载两个文件,很容易的,不要什么cmake)

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h> 
#include <string.h>
#include <pthread.h>
#include <sys/resource.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/types.h> 
#include <sys/stat.h>

不足之处欢迎指正。
不欢迎讨论(因为我很菜,真的答不出什么),也不欢迎要全部代码的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/553487.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch:(一)ES简介

搜索引擎是什么&#xff1f;在不少开发者眼中&#xff0c;ES似乎就是搜索引擎的代名词&#xff0c;然而这实际上是一种误解。搜索引擎是一种专门用于从互联网中检索信息的技术工具&#xff0c;它主要可以划分为元搜索引擎、全文搜索引擎和垂直搜索引擎几大类。其中&#xff0c;…

AIGC算法1:Layer normalization

1. Layer Normalization μ E ( X ) ← 1 H ∑ i 1 n x i σ ← Var ⁡ ( x ) 1 H ∑ i 1 H ( x i − μ ) 2 ϵ y x − E ( x ) Var ⁡ ( X ) ϵ ⋅ γ β \begin{gathered}\muE(X) \leftarrow \frac{1}{H} \sum_{i1}^n x_i \\ \sigma \leftarrow \operatorname{Var}(…

【中级软件设计师】上午题08-UML(下):序列图、通信图、状态图、活动图、构件图、部署图

上午题08-UML 1 序列图2 通信图3 状态图3.1 状态和活动3.2 转换和事件 4 活动图5 构件图&#xff08;组件图&#xff09;6 部署图 【中级软件设计师】上午题08-UML(上)&#xff1a;类图、对象图、用例图 UML图总和 静态建模&#xff1a;类图、对象图、用例图 动态建模&#xff…

【简单介绍下PostCSS】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

仿真测试的应用领域

仿真测试在各种领域中都有广泛的应用&#xff0c;以下是一些应用最广泛的场景&#xff1a; 工业制造&#xff1a;通过模拟制造过程&#xff0c;可以预测产品的质量和性能&#xff0c;优化生产流程&#xff0c;降低成本。航空航天&#xff1a;飞机、导弹、航天器等的设计和研发…

AWS Key disabler:AWS IAM用户访问密钥安全保护工具

关于AWS Key disabler AWS Key disabler是一款功能强大的AWS IAM用户访问密钥安全保护工具&#xff0c;该工具可以通过设置一个时间定量来禁用AWS IAM用户访问密钥&#xff0c;以此来降低旧访问密钥所带来的安全风险。 工具运行流程 AWS Key disabler本质上是一个Lambda函数&…

如何访问内网?

在互联网万维网上&#xff0c;我们可以轻松访问各种网站和资源。但是&#xff0c;有时我们需要访问局域网内的资源&#xff0c;例如公司内部的文件共享、打印机等。本文将介绍几种方法&#xff0c;帮助您实现访问内网的需求。 内网穿透技术 内网穿透技术是一种通过互联网将局域…

SQL表连接详解:JOIN与逗号(,)的使用及其性能影响

省流版 在这个详细的解释中&#xff0c;我们将深入探讨SQL中表连接的概念&#xff0c;特别是JOIN和逗号&#xff08;,&#xff09;在连接表时的不同用法及其对查询性能的影响。通过实际示例和背后的逻辑分析&#xff0c;我们将揭示在不同场景下选择哪种连接方式更为合适。 1.…

Mysql查询表的结构信息 把列名 数据类型 等变成列数据(适用于生成数据库表结构文档) (二)

书接上文 Mysql查询表的结构信息 把列名 数据类型 等变成列数据(适用于生成数据库表结构文档) (一) 好&#xff0c;怎么生成文档呢&#xff1f;很简单 用navicat 或者sqlyog navicat操作如下 举个例子 如下查询结果 全选查询结果&#xff0c;右键&#xff0c;复制为指标…

什么是神经网络和机器学习?【云驻共创】

什么是神经网络和机器学习&#xff1f; 一.背景 在当今数字化浪潮中&#xff0c;神经网络和机器学习已成为科技领域的中流砥柱。它们作为人工智能的支柱&#xff0c;推动了自动化、智能化和数据驱动决策的进步。然而&#xff0c;对于初学者和专业人士来说&#xff0c;理解神经…

使用CCS软件查看PID曲线

在刚开始学习PID的时候&#xff0c;都需要借助PID的曲线来理解比例&#xff0c;积分&#xff0c;微分这三个参数的具体作用。但是这些曲线生成一般都需要借助上位机软件或者在网页上才能实现。如果是在单片机上调试程序的话&#xff0c;想要看曲线&#xff0c;一般就是通过串口…

[Algorithm][滑动窗口][长度最小的子数组] + 滑动窗口原理

目录 0.滑动窗口原理讲解1.长度最小的子数组1.题目链接2.算法原理讲解3.代码实现 0.滑动窗口原理讲解 滑动窗口&#xff1a;“同向双指针”滑动窗口可处理「⼀段连续的区间」问题如何使用&#xff1f; left 0, right 0进窗口判断 是否出窗口 更新结果 -> 视情况而定 可能…

使用Canal同步MySQL 8到ES中小白配置教程

&#x1f680; 使用Canal同步MySQL 8到ES中小白配置教程 &#x1f680; 文章目录 &#x1f680; 使用Canal同步MySQL 8到ES中小白配置教程 &#x1f680;**摘要****引言****正文**&#x1f4d8; 第1章&#xff1a;初识Canal1.1 Canal概述1.2 工作原理解析 &#x1f4d8; 第2章&…

数据赋能(60)——要求:数据服务部门能力

“要求&#xff1a;数据服务部门实施数据赋能影响因素”是作为标准的参考内容编写的。 在实施数据赋能中&#xff0c;数据服务部门的能力体现在多个方面&#xff0c;关键能力如下图所示。 在实施数据赋能的过程中&#xff0c;数据服务部门应具备的关键能力如下。 业务理解和沟…

C++:文件内容完全读入

在上一篇文章中我留下了一点小坑&#xff1a;使用>> 运算符&#xff0c;这个运算符默认将空格作为分隔符&#xff0c;所以在文件内容读取的时候发现在读到空格时就会停止读取&#xff0c;导致读取内容不完整&#xff0c;这显然不符合日常的使用用能&#xff0c;那么今天就…

Djanog的中间件

1 中间件的五个方法 process_request(self,request)process_response(self, request, response)process_view(self, request, view_func, view_args, view_kwargs)process_exception(self, request, exception)process_template_response(self,request,response) 中间件处理函…

详解运算符重载,赋值运算符重载,++运算符重载

目录 前言 运算符重载 概念 目的 写法 调用 注意事项 详解注意事项 运算符重载成全局性的弊端 类中隐含的this指针 赋值运算符重载 赋值运算符重载格式 注意点 明晰赋值运算符重载函数的调用 连续赋值 传引用与传值返回 默认赋值运算符重载 前置和后置重载 前…

Scikit-Learn

机器学习中的重要角色 Scikit-Leran&#xff08;官网&#xff1a;https://scikit-learn.org/stable/&#xff09;&#xff0c;它是一个基于 Python 语言的机器学习算法库。Scikit-Learn 主要用 Python 语言开发&#xff0c;建立在 NumPy、Scipy 与 Matplotlib 之上&#xff0c;…

嵌入式中strstr函数详解

一、strstr函数是什么? strstr函数是 C 语言中的一个标准库函数(使用时要引入头文件string.h),用于在一个字符串中查找另一个字符串首次出现的位置。如果找到子串,则返回子串在主串中首次出现的位置的指针;如果未找到,则返回 NULL。 二、使用场景 1.用来找出字符串1在字…

学习了解大模型的四大缺陷

由中国人工智能学会主办的第十三届吴文俊人工智能科学技术奖颁奖典礼暨2023中国人工智能产业年会于2024年4月14日闭幕。 会上&#xff0c;中国工程院院士、同济大学校长郑庆华认为&#xff0c;大模型已经成为当前人工智能的巅峰&#xff0c;大模型之所以强&#xff0c;是依托了…
最新文章