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:latestENV 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)/500config := &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;elserest->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 sysdef 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 Trueexcept SyntaxError as e:# 捕获语法错误print(f"Syntax error in '{file_path}': {e}")return Falseif __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:passreturn 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.mzph.cn/news/824790.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

量子密钥分发系统的设计与实现(二):光路子系统初步讨论

通过上一篇文章&#xff0c;我们对量子密钥分发系统的基本架构、硬件结构以及密钥分发流程进行了初步的总体介绍&#xff0c;从本文开始&#xff0c;我们就基于系统顶层的架构设计&#xff0c;开始从模块到器件&#xff0c;从硬件到软件开始详细讨论QKD系统的设计与实现。本文主…

双向链表详解

目录 带头双向循环链表带头双向循环链表的实现带头双向循环链表的功能实现创造新节点LTNode* CreateLTNode(LTDataType x)代码 初始化链表LTNode*LTInit(LTNode* phead)代码 打印链表void LTPrint(LTNode* phead)代码 链表尾插void LTPushBack(LTNode* phead, LTDataType x)代码…

C#语法知识之运算符

3、运算符 1、算数运算符 1、赋值符号 //把右侧的值赋给左侧的变量2、算数运算符 _ * / float f 1 / 2f; %3、算数运算符的优先级 //乘除余优先级高于加减 括号可以改变优先级&#xff0c;优先计算括号内的内容4、算数运算符的复合运算 复合运算符是用于自己 自己进行运算…

源码解读——SplitFed: When Federated Learning Meets Split Learning

源码地址 1. 源码概述 源码里一共包含了5个py文件 单机模型&#xff08;Normal_ResNet_HAM10000.py&#xff09;联邦模型&#xff08;FL_ResNet_HAM10000.py&#xff09;本地模拟的SFLV1&#xff08;SFLV1_ResNet_HAM10000.py&#xff09;网络socket下的SFLV2&#xff08;SF…

51单片机入门_江协科技_33~34_OB记录的自学笔记_LED呼吸灯与PWM直流马达调速

33. 直流电机驱动(PWM) 33.1. 直流电机介绍 •直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极&#xff0c;当电极正接时&#xff0c;电机正转&#xff0c;当电极反接时&#xff0c;电机反转 •直流电机主要由永磁体&#xff08;定子&#xff09;、线圈&…

MySQL 死锁案例解析一则

原文链接&#xff1a;https://www.modb.pro/db/448666 一、问题背景某业务模块反馈数据库最近出现过几次死锁告警的情况&#xff0c;本文总结了这次死锁排查的全过程&#xff0c;并分析了导致死锁的原因及解决方案。希望给大家提供一个死锁的排查及解决思路。基础环境&#xff…

一.NODE MCU(ESP8285,ESP8286)开发环境搭建

一.序言: 1.esp8285长什么样? 2.esp8285是什么,能做什么? 通过上面图片,看到上面的芯片,是带有多个阵脚的单片机。实际上,看着该芯片很小,但是却具有完整的wifi无线蓝牙功能,它本身可以运行一个极简的linux小系统,并且该极简的小linux系统具备无线蓝牙功能。。它同…

54岁前港姐与好友因一事反目成仇,20年后方破冰

现年54岁的前「金牌司仪」陈淽菁&#xff08;前名&#xff1a;陈芷菁&#xff09;是1994年落选港姐&#xff0c;之后加入TVB参演电视剧《天地男儿》、《壹号皇庭》入屋&#xff0c;后因口齿伶俐而转战主持界。2017年陈淽菁离巢&#xff0c;外出以个人名义成立「陈芷菁工作室」&…

每日学习笔记:C++ STL算法之容器元素转换、结合、互换

本文API 转换元素 transform(sourceBeg,sourceEnd,destBeg, op) 结合元素 transform(source1Beg,source1End,source2Beg,destBeg, op) 互换元素 swap_ranges(sourceBeg,sourceEnd,destBeg) 转换元素 结合元素 互换元素

深度学习驱动的流体力学计算与应用

在深度学习与流体力学深度融合的背景下&#xff0c;科研边界不断拓展&#xff0c;创新成果层出不穷。从物理模型融合到复杂流动模拟&#xff0c;从数据驱动研究到流场智能分析&#xff0c;深度学习正以前所未有的力量重塑流体力学领域。近期在Nature和Science杂志上发表的深度学…

ARM_day8:温湿度数据采集应用

1、IIC通信过程 主机发送起始信号、主机发送8位(7位从机地址1位传送方向(0W&#xff0c;1R))、从机应答、发数据、应答、数据传输完&#xff0c;主机发送停止信号 2、起始信号和终止信号 SCL时钟线&#xff0c;SDA数据线 SCL高电平&#xff0c;SDA由高到低——起始信号 SC…

汽车零部件制造迎来智能化升级,3D视觉定位系统助力无人化生产线建设

随着新能源汽车市场的蓬勃发展&#xff0c;汽车零部件制造行业正面临着前所未有的机遇与挑战。为了提高产能和产品加工精度&#xff0c;某专业铝合金汽车零部件制造商决定引进智能生产线&#xff0c;其中&#xff0c;对成垛摆放的变速箱壳体进行机床上料成为关键一环。 传统的上…

SpringBootSpringCloud升级可能会出现的问题

1.背景 之前负责过我们中台的SpringBoot和Cloud的升级&#xff0c;特次记录分享一下项目中可能出现的问题&#xff0c;方便后续的人快速定位问题。以及下述选择的解决方案都是基于让升级的服务影响和改动最小以及提供通用的解决方案的提前进行选择的。 1.1版本说明 升级前&a…

陇剑杯 省赛 攻击者1 CTF wireshark 流量分析

陇剑杯 省赛 攻击者1 题目 链接&#xff1a;https://pan.baidu.com/s/1KSSXOVNPC5hu_Mf60uKM2A?pwdhaek 提取码&#xff1a;haek ├───LogAnalize │ ├───linux简单日志分析 │ │ linux-log_2.zip │ │ │ ├───misc日志分析 │ │ acce…

Vue3项目 网易严选_学习笔记

Vue3项目 网易严选_第一天 主要内容 项目搭建vuex基础路由设计首页顶部和底部布局 学习目标 知识点要求项目搭建掌握vuex基础掌握路由设计掌握首页顶部和底部布局掌握 一、项目搭建 1.1 创建项目 vue create vue-wangyi选择vue3.0版本 1.2 目录调整 大致步骤&#xff…

Workerman开启ssl方法如下

参考地址 Workerman开启ssl方法如下-遇见你与你分享 准备工作&#xff1a; 1、Workerman版本不小于3.3.7 2、PHP安装了openssl扩展 3、已经申请了证书&#xff08;pem/crt文件及key文件&#xff09;放在了/etc/nginx/conf.d/ssl下 4、配置文件 location /wss { proxy_set…

unity制作拼接地图

前段时间有个朋友问我想要制作一款地图编辑器&#xff0c;最开始我还想着在一个平面用节点切割制作地图编辑器这倒是也行&#xff0c;但不太好控制每一个点&#xff0c;如果未来项目大了&#xff0c;更加不好维护。 偶然间翻到一篇文章&#xff1a;unity地图边缘检测 或许我们…

基于数字孪生的城市建模和仿真

近年来&#xff0c;数字孪生概念几乎呈爆炸式增长&#xff0c;利用该概念的科学文章数量呈指数级增长就证明了这一点。 这一概念源自制造业&#xff0c;使用 CAD 模型可以创建组件和产品的精确数字复制品。 该术语最早的使用可以追溯到 2003 年&#xff0c;通常归功于 Grieves …

vue3第二十节(新增编译宏defineModel)

为什么会需要使用defineModel() 注意&#xff1a;defineModel() 需要在3.4及以上版本才可使用&#xff1b; 组件之间通讯&#xff0c;通过 props 和 emits 进行通讯,是单向数据流&#xff0c;比如&#xff1a;props是自上而下的&#xff08;父组件数据修改导致子组件更新&…

生成人工智能体:人类行为的交互式模拟论文与源码架构解析(2)——架构分析 - 核心思想环境搭建技术选型

4.架构分析 4.1.核心思想 超越一阶提示&#xff0c;通过增加静态知识库和信息检索方案或简单的总结方案来扩展语言模型。 将这些想法扩展到构建一个代理架构&#xff0c;该架构处理检索&#xff0c;其中过去的经验在每个时步动态更新&#xff0c;并混合与npc当前上下文和计划…