【HUSTOJ 判题机源码解读系列01】判题机架构、判题流程浅析

HUSTOJ 判题机源码解读系列01

1. HUSTOJ

HUSTOJ 是一个开源的在线判题系统,很早之前就已经开发了,在源码里我甚至看到过 2008 年的 Git 提交记录(也有可能没这么早,记不太清了),时至本篇博客编写之际,2025 年了作者还在维护更新。

感谢 zhblue(原作者)贡献的代码。

本系列文章会介绍 HUSTOJ 的核心部分——判题机。

主要内容包括以下两个部分:

  1. 判题机的整体设计
  2. 使用到的核心技术

会介绍源码中的关键点和难点,但不会逐行的对源码进行注释。

希望本系列文章能够帮助到想要学习 OJ 的判题机怎么开发的同学(并非零基础,操作系统最基础的知识,例如进程、文件 IO 等概念需要知道)。

2. 源码文件分析

HUSTOJ 判题机的源码默认安装在 /home/judge/src/core 目录下,其结构目录如下:

./core
├── judge_client
│   ├── getindocker.sh
│   ├── judge_client
│   ├── judge_client.cc
│   ├── judge_client.http
│   ├── judge_client.o
│   ├── log.txt
│   ├── loggedcalls.sh
│   ├── makefile
│   ├── ncalls.h
│   ├── nohup.out
│   ├── okcalls.h
│   ├── okcalls32.h
│   ├── okcalls64.h
│   ├── okcalls_aarch64.h
│   ├── okcalls_arm.h
│   └── okcalls_mips.h
├── judged
│   ├── judged
│   ├── judged.cc
│   ├── judged.http
│   ├── judged.o
│   ├── judgehub
│   ├── judgehub.cc
│   ├── judgehub2.cc
│   └── makefile
├── make.sh
└── sim├── sim.sh└── sim_3_01

在这么多文件中,我们只需要关心 judge_client.cc 文件和 judge.cc 这两个核心文件。

除此之外,其它的文件与判题机的核心原理关系不大,这里只需要简单了解一下。

  • okcall 系列文件可以当作配置文件,其中是各种编程语言在各种平台上允许的系统调用编号
  • sim系列文件是用来做代码查重的功能的
  • judgehub 系列文件是用于 Saas 服务的

回到核心文件 judge_client.ccjudge.cc,这两个 C 语言文件会被分为编译为两个可执行文件,并一起放到 /usr/bin/目录下。

./core/make.sh 是编译脚本,执行这个脚本就可以编译了。

编译结果:

  • judge_client.cc -> /usr/bin/judge_client
  • judge.cc-> /usr/bin/judged

编译成功后,执行命令(开启判题服务):

sudo judged

/usr/bin/judged 可执行文件就会从磁盘加载到内存中,成为一个守护进程。现在我们称这个守护进程为 judged

执行命令(关闭判题目服务):

sudo pkill -9 judged

就可以杀掉 judged 进程。

当判题服务启动的时候,judgedjudge_client 就可以相互配合进行判题了,具体情况是:

  • judged 负责接取判题任务,然后将任务转交给 judge_client
  • judged_client 拿到任务后开始进行判题

可以将 judged 当做一个经理,只管接活,手底下可能会有几个 judged_client 去干活(具体多少个可以通过配置文件配置)。

3. 判题流程(简略版)

因为 judged 是通过轮询数据库接取判题任务的(也支持 HTTP 判题,由于默认是数据库判题,这里就省略 HTTP 判题了),所以这里需要了解一下系统里和判题有关的几张表的相关信息。

  • problem:题目信息表,保存题目的标题、描述、输入输出示例、难度、限制等信息
  • solution:提交信息表,记录用户针对某道题目的提交记录、判题结果、使用语言、提交时间等
  • source_code:源代码表,保存对应提交的代码内容
  • compileinfo:编译信息表,主要存储编译错误信息
  • runtimeinfo:运行时错误信息表,主要存储程序运行出错时的错误信息

这几个表之间通过 solution_idproblem_id 等关键字段进行关联。

表的结构过长,放在文中影响文章结构,文章跨度过大,所以表结构和字段解释都放在文章末尾,这里知道每张表大致是什么就可以了,并不用精确到字段。

接下来是用户提交代码到判题机评测代码到最终用户查询到判题的结果的大致过程(后续系列会通过源码精讲)。

第一步:用户提交代码

用户点击题库中的题目,选了一道题目,使用一种编程语言,写了代码,然后点击提交,这里提交时候后端会得到的数据有:

  • problem_id:问题 ID
  • user_id: 用户 ID
  • language:编程语言编号,例如(0 = C, 1 = C++, 2 = Java)
  • source:用户编写的代码

第二步:后端写数据库

solution 表中有一个 result 字段,表示用户提交状态,是一个枚举值,具体值如下:

#define OJ_WT0 0     // 提交排队
#define OJ_WT1 1     // 重判排队
#define OJ_CI 2      // 编译中(任务已派发)
#define OJ_RI 3      // 运行中
#define OJ_AC 4      // 答案正确
#define OJ_PE 5      // 格式错误
#define OJ_WA 6      // 答案错误
#define OJ_TL 7      // 时间超限
#define OJ_ML 8      // 内存超限
#define OJ_OL 9      // 输出超限
#define OJ_RE 10     // 运行错误
#define OJ_CE 11     // 编译错误
#define OJ_CO 12     // 编译完成
#define OJ_TR 13     // 测试运行结束
#define OJ_MC 14     // 等待裁判手工确认

当后端接收到用户在前端提交的数据后,将执行以下步骤:

  1. 初始化提交记录
    首先,将 problem_iduser_idlanguage 作为一条新记录插入 solution 表中(此时源码尚未存入)。同时,将该记录的 result 字段设置为 14,以标识该提交的源码尚未插入 source_code 表,避免判题机立即进行评测。插入完成后,数据库会自动生成该记录的主键 solution_id
  2. 存储源码并更新状态
    随后,后端会使用生成的 solution_id 和源码 source,将其作为一条新记录插入 source_code 表。插入成功后,solution 表中的 result 字段会被更新为 0,表示源码已成功存储,判题机可以开始评测该提交。

在做完这些工作后,后端需要 solution_id 返回给前端,前端此时需要使用solution_id 轮询后端提供的根据 solution_id 查询判题信息的接口。

第三步:judged 获取到提交

由于 judged 会轮询数据库,也就是每隔几秒执行一下这条 SQL:

SELECT * FROM solution WHERE result = 0;

judged 查询到 result = 0的记录后,就会将这条记录的 problem_idsolution_id告诉 judge_client,然后 judge_client就会准备判题了。

第四步:judge_client 开始判题(重点部分,先简略描述,后续系列会通过源码的方式十分详细的介绍)

在第三步中,judged 会用户的提交的 solution_idproblem_id 告诉 judge_client 后,judge_client 将执行以下步骤:

  1. 获取判题所需信息
    judge_client 根据 solution_idproblem_id 从数据库中查询详细信息,包括题目时间限制、空间限制、用户代码、提交语言等。
  2. 编译用户代码(解释型语言跳过这一步
    将用户的代码编译为对应的可执行文件,如果编译错误,则将错误信息与 solution_id 记录至 compileinfo 表,并将 solution 表中该提交的状态标记为“编译错误(CE)”,随后判题流程终止并返回结果。
  3. 创建沙箱环境
    根据获取的语言类型,创建对应的隔离沙箱环境,并在其中运行编译后的可执行文件,或直接执行无需编译的解释型语言代码。
  4. 运行用户代码
    用户代码在沙箱中运行,同时需要处理以下各种情况。
    1. 运行错误(RE):用户代码运行过程中异常终止(如数组越界、除零等错误)。此时需将错误信息存入 runtimeinfo 表,终止判题,并将 solution 表中的提交状态设为 “运行错误(Run Error)”。
    2. 输出超限(OLE):用户代码运行过程中生成的标准输出文件超出限制。此时需要终止判题,并将 solution 表中的提交状态设为 “输出超限(Output Limit Error)”。
    3. 运行超时(TLE,未运行完毕):代码尚未完成所有测试用例,但运行时间已超过最大时间限制。此时需终止判题,并将 solution 表中的提交状态设为 “时间超限(Time Limit Error)”。
    4. 内存超限(MLE,未运行完毕):代码尚未完成所有测试用例,但内存使用已超出最大内存限制。此时需终止判题,并将 solution 表中的提交状态设为 “内存超限(Memory Limit Error)”。
    5. 错误答案(WA):代码运行完所有测试用例后,与标准答案比对发现结果不匹配。需生成用户输出与标准答案的差异比对文件,并存入 runtimeinfo 表,并将 solution 表中的提交状态设为 “错误答案(Wrong Answer)”。
    6. 运行超时(TLE,运行完毕):代码已运行完所有测试用例,但运行时间超过题目设定的时间限制。此时需将 solution 表中的提交状态设为 “时间超限(Time Limit Error)”。
    7. 内存超限(MLE,运行完毕):代码已运行完所有测试用例,但内存使用超出题目设定的内存限制。此时需将 solution 表中的提交状态设为 “内存超限(Memory Limit Error)”。
    8. 格式错误(PE):代码输出与标准答案仅在空格、回车或其他空白字符上存在差异。此时需将 solution 表中的提交状态设为 “格式错误(Presentation Error)”。
    9. 答案正确(AC)。若以上情况都没出现,则将 solution 表中的提交状态设为 “答案正确(Accepted)”
  5. 销毁沙箱环境
    销毁沙箱,清除判题过程中生成的临时文件,进程退出。

注:判题机中存在两种时间限制——最大时间限制和题目时间限制。

  • 最大时间限制:用于防止判题机被恶意代码或用户写的死循环无限占用 CPU 资源。所以一旦用户代码运行时间超过该限制,系统将立即终止用户的代码运行。该限制通常由判题机统一设定,例如 10 秒或其他固定阈值。
  • 题目时间限制:针对特定题目设定,主要用于评估算法效率。即使用户提交的代码能产生正确结果,但若运行时间超出题目设定的时间限制,仍将被判定为超时(TLE)。该限制存储在 problem 表中,每道题可能有所不同。

同理,内存限制也分为全部最大限制和题目特定限制,具体规则与时间限制类似。

第五步:前端获取判题结果

当判题机将判题结果写入到数据库后,前端通过 solution_id 查询到判题结果后,将判题结果显示到浏览器界面上,整个判题流程到此完毕。

4. 数据库详细结构

4.1 problem 表详细结构

FieldTypeNullKeyDefaultExtra
problem_idintNOPRINULLauto_increment
titlevarchar(200)NO
descriptiontextYESNULL
inputtextYESNULL
outputtextYESNULL
sample_inputtextYESNULL
sample_outputtextYESNULL
spjchar(1)NO0
hinttextYESNULL
sourcevarchar(100)YESNULL
in_datedatetimeYESNULL
time_limitdecimal(10,3)NO0.000
memory_limitintNO0
defunctchar(1)NON
acceptedintYES0
submitintYES0
solvedintYES0
remote_ojvarchar(16)YESNULL
remote_idvarchar(32)YESNULL

字段解释:

  • problem_id: 主键,自增,用于唯一标识题目。
  • title: 题目标题。
  • description / input / output: 题目详细描述、输入和输出规范的文本。
  • sample_input / sample_output: 示例输入输出,用于帮助用户理解题目。
  • spj: 是否采用 Special Judge(特判),0 表示否,1 表示是特殊裁判,2 表示是文本裁判。
  • hint: 提示信息,可选,用于给出解题提示。
  • source: 题目来源或出处信息,如比赛名称或网站链接。
  • in_date: 题目录入系统的时间。
  • time_limit / memory_limit: 运行时间限制(单位秒)和内存限制
  • defunct: 是否下架该题目,N 表示正常显示,Y 表示不再显示在前台。
  • accepted / submit / solved: accepted:该题目被所有用户解题成功的次数,submit:该题目的提交总次数,solved:解出该题目的用户数量
  • remote_oj / remote_id: 用于支持远程判题(如同 LeetCode、Codeforces 等远程接口)的字段。记录远程 OJ 的名称和题目编号。

4.2 solution 表

FieldTypeNullKeyDefaultExtra
solution_idint unsignedNOPRINULLauto_increment
problem_idintNOMUL0
user_idchar(48)NOMULNULL
nickchar(20)NO
timeintNO0
memoryintNO0
in_datedatetimeNO2016-05-13 19:24:00
resultsmallintNOMUL0
languageint unsignedNO0
ipchar(46)NONULL
contest_idintYESMUL0
validtinyintNO1
numtinyintNO-1
code_lengthintNO0
judgetimetimestampYESCURRENT_TIMESTAMPDEFAULT_GENERATED
pass_ratedecimal(4,3) unsignedNO0.000
lint_errorint unsignedNO0
judgerchar(16)NOLOCAL
remote_ojchar(16)NO
remote_idchar(32)NO

字段解释:

  • solution_id: 提交记录主键,自增。
  • problem_id:problem 表的 problem_id 关联。表示该提交针对哪道题目。
  • user_id: 提交者的用户 ID,可与用户表进行关联(此处未展示用户表)。
  • nick: 提交者的昵称,用于在榜单或判题记录中显示。
  • time / memory: 程序运行时所耗费的 CPU 时间(单位毫秒或秒,视系统而定)和内存使用量。
  • in_date: 提交时间。
  • result: 判题结果,枚举值,见上文。
  • language: 提交所使用的编程语言代码(如 0 = C, 1 = C++, 2 = Java 等)。
  • ip: 提交者的 IP 地址。
  • contest_id: 如果是比赛期间的提交,可存储对应比赛的 ID;非比赛情况下可为默认值 0。
  • valid: 用于标识该提交记录是否有效,例如在判题故障时可能会设置为无效。
  • num: 在比赛场景中,一道题在某场比赛中的编号可能与系统默认的 problem_id 不同,可通过此字段记录。
  • code_length: 提交的源代码长度(单位字节)。
  • judgetime: 判题时间戳,记录判题所用时间,也可在某些业务场景用于分析统计。
  • pass_rate: 测试点通过率,若题目有多个测试点则可以记录通过的比例。
  • lint_error: 代码静态检测或风格检测的错误数(如 Clang-Tidy、ESLint 等)。
  • judger: 判题机的名称或标识,用于分布式判题时区分不同判题服务器。
  • remote_oj / remote_id: 远程判题平台及对应的提交 ID。

4.3 source_code 表

FieldTypeNullKeyDefaultExtra
solution_idintNOPRINULL
sourcetextNONULL
  • solution_id:solution 表的主键相对应,表示该源代码属于哪个提交。
  • source: 保存提交的完整源代码文本。由于可能非常长,使用 text 类型存储。

4.4 compileinfo 表

FieldTypeNullKeyDefaultExtra
solution_idintNOPRI0
errortextYESNULL
  • solution_id:solution 表的主键相对应,一对一关联。
  • error: 如果编译失败,此字段记录编译器返回的错误或警告信息,便于用户查看具体原因,例如 C 语言的中文分号等。

4.5 runtimeinfo 表

FieldTypeNullKeyDefaultExtra
solution_idintNOPRI0
errortextYESNULL
  • solution_id:solution 表的主键相对应,一对一关联。
  • error: 如果程序运行出错(如数组越界、除零等错误),或者在用户答案错误后的标准答案和用户答案的差异比对文件等。

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

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

相关文章

【Prometheus】prometheus结合cAdvisor监控docker容器运行状态,并且实现实时告警通知

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

HTML应用指南:利用GET请求获取全国海底捞门店位置信息

随着新零售业态的快速发展,门店位置信息的获取变得越来越重要。作为餐饮服务行业的先锋,海底捞不仅在服务质量上持续领先,还积极构建广泛的门店网络,以支持其不断增长的用户群体。为了更好地理解和利用这些数据,本篇文…

Arduino 第十六章:pir红外人体传感器练习

Arduino 第十六章:PIR 传感器练习 一、引言 在 Arduino 的众多有趣项目中,传感器的应用是非常重要的一部分。今天我们要学习的主角是 PIR(被动红外)传感器。PIR 传感器能够检测人体发出的红外线,常用于安防系统、自动…

Spring Data JPA 基本用法笔记整理

写在前面&#xff1a; 之前一直写MyBatis&#xff0c;去年开始做的这个新项目用的是JPA&#xff0c;整理了一些基本使用方法 1、 集成方法&#xff1a; 1.1 引入依赖 <!--spring data 依赖--> <dependency><groupId>org.springframework.data</groupI…

leetcode-495.提莫攻击

leetcode-495.提莫攻击 文章目录 leetcode-495.提莫攻击一.题目描述二.代码提交三.解释 一.题目描述 二.代码提交 #include <vector> using namespace std;int findPoisonedDuration(vector<int>& timeSeries, int duration) {int total 0;for (int i 0; i …

团队没有测试人员,要快速完成测试?

有同学提问:产品没有专职的测试人员,要怎么才能快速完成测试? 先说结论,这个问题并没有标准答案 没有测试岗位只是没有专门负责这个职能的人员,但测试这个事并没有消失 快速完成测试,其实应该换种说法,就是快速建立质量信心。 因为测试无穷尽,所以没有绝对的完成测试…

光学相位---描述周期性波动现象

相位是描述周期性波动现象的一个重要物理量&#xff0c;用来表示波在某一时刻或位置上的振荡状态。它是时间、空间和频率的函数&#xff0c;通常用角度&#xff08;单位为度或弧度&#xff09;来表示。对于光波而言&#xff0c;其相位可以通过波的传播距离、波长以及波数来定义…

一、通义灵码插件保姆级教学-IDEA(安装篇)

JetBrains IDEA 中安装教学 第一步&#xff1a;事先准备 支持系统&#xff1a;Windows 7 ~ Windows 11、Linux、macOS&#xff1b;下载并安装兼容的 JetBrains IDEA 2020.3 及以上版本&#xff1b; 第二步&#xff1a;在 IntelliJ IDEA 中安装通义灵码 方法 1&#xff1a;通…

系统之间数据对接怎么做

系统之间数据对接怎么做&#xff1a;数据同步功能详解 在当今数字化时代&#xff0c;企业往往依赖于多个系统来管理不同的业务流程。这些系统可能包括客户关系管理&#xff08;CRM&#xff09;、企业资源规划&#xff08;ERP&#xff09;、供应链管理&#xff08;SCM&#xff…

Java NIO ByteBuffer 详解

什么是 ByteBuffer ByteBuffer 是 Buffer 的一个具体实现&#xff0c;专门用于存储和操作字节数据。它提供了高效的、基于内存的 I/O 数据处理方式。 Buffer 类是构建 Java NIO 的基础&#xff0c;其中 ByteBuffer 类是 Buffer 子类中最受欢迎的。这是因为字节类型是最通用的…

OnlyOffice编辑器下载失败排查与解决方案

OnlyOffice编辑器下载失败排查与解决方案 问题描述原因分析&#xff1a;1. 检查后端服务地址是否正确2. 使用内部IP地址访问后端服务 其他常见问题 在使用OnlyOffice编辑器时&#xff0c;常见的问题之一是无法正确显示图片或打开文档。 具体表现为提示图片URL地址不正确或打开…

栈的实现-

栈 栈的概念及结构 栈是一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶&#xff0c;另一端称为栈底。栈中元素遵循**后进先出 LIFO&#xff08;Last In First Out&#xff09;**的原则。 压栈&#xff1a;栈的…

在vivado中对数据进行延时,时序对齐问题上的理清

在verilog的ISP处理流程中&#xff0c;在完成第一个模块的过程中&#xff0c;我经常感到困惑&#xff0c;到底是延时了多少个时钟&#xff1f;今日对这几个进行分类理解。 目录 1.输入信号激励源描述 1.1将数据延时[9]个clk 1.2将vtdc与hzdc延时[9]个clk(等价于单bit的数据…

singleTaskAndroid的Activity启动模式知识点总结

一. 前提知识 1.1. 任务栈知识 二. Activity启动模式的学习 2.1 standard 2.2 singleTop 2.3.singleTask 2.4.singleInstance 引言&#xff1a; Activity作为四大组件之一&#xff0c;也可以说Activity是其中最重要的一个组件&#xff0c;其负责调节APP的视图&#xff…

Tetragon:一款基于eBPF的运行时环境安全监控工具

关于Tetragon Tetragon是一款基于eBPF的运行时环境安全监控工具&#xff0c;该工具可以帮助广大研究人员检测并应对安全重大事件&#xff0c;例如流程执行事件、系统调用活动、I/O活动&#xff08;包括网络和文件访问等&#xff09;。 在 Kubernetes 环境中使用时&#xff0c;…

提升编程效率,体验智能编程助手—豆包MarsCode一键Apply功能测评

提升编程效率&#xff0c;体验智能编程助手—豆包MarsCode一键Apply功能测评 &#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 目录 引言豆包…

卷积定理理解:如何将系数多项式乘法降到n*log n的复杂度?

目标 两个向量&#xff08;每个向量各自对应一个多项式&#xff09;的简单相乘&#xff08;时间复杂度 O ( n 2 ) O(n^2) O(n2)&#xff09;可以通过两个向量各自对应的离散傅里叶变换的相乘&#xff08;时间复杂度 O ( n ⋅ lg n ) O(n\cdot \text{lg }n) O(n⋅lg n)&#xf…

【devops】 Git仓库如何fork一个私有仓库到自己的私有仓库 | git fork 私有仓库

一、场景说明 场景&#xff1a; 比如我们Codeup的私有仓库下载代码 放入我们的Github私有仓库 且保持2个仓库是可以实现fork的状态&#xff0c;即&#xff1a;Github会可以更新到Codeup的最新代码 二、解决方案 1、先从Codeup下载私有仓库代码 下载代码使用 git clone 命令…

解析 JavaScript 面试题:`index | 0` 确保数组索引为整数

文章目录 一、JavaScript 中的数字类型二、按位或运算符 | 的作用&#xff08;一&#xff09;对于整数&#xff08;二&#xff09;对于小数&#xff08;三&#xff09;对于非数字值 三、用于数组索引的意义 在 JavaScript 面试中&#xff0c;常常会涉及到一些看似简单却蕴含着深…

考研操作系统----操作系统的概念定义功能和目标(仅仅作为王道哔站课程讲义作用)

目录 操作系统的概念定义功能和目标 操作系统的四个特征 操作系统的分类 ​编辑 操作系统的运行机制 系统调用 操作系统体系结构 操作系统引导 虚拟机 操作系统的概念定义功能和目标 什么是操作系统&#xff1a; 操作系统是指控制和管理整个计算机系统的软硬件资源&…