当前位置: 首页 > article >正文

【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.kler.cn/a/542563.html

相关文章:

  • 模块的加载机制
  • 防火墙安全综合实验
  • 嵌入式软件C语言面试常见问题及答案解析(四)
  • 计算机毕业设计——Springboot餐厅点餐系统
  • 《DeepSeek大模型的微调和部署案例实操课程》
  • 代码随想录day11
  • 基于进化式大语言模型的下一代漏洞挖掘范式:智能对抗与自适应攻防体系
  • 在JVM的栈(虚拟机栈)中,除了栈帧(Stack Frame)还有什么?
  • 【学习笔记】企业数字化转型顶层设计与企业架构【TOGAF9.2】-第1章 概念基础
  • 多项式插值(数值计算方法)Matlab实现
  • 深度学习中的门控机制详解
  • centos 7.x无法安装kong gateway 3.9X的解决方案
  • 【无人机遥感技术】无人机遥感在农林信息提取中的实现方法与GIS融合应用(如何将无人机真彩色、多光谱和高光谱数据,与农林业应用结合)
  • 【华为OD-E卷 - 119 评论转换输出 100分(python、java、c++、js、c)】
  • Neurlps2024论文解读|Credit Attribution and Stable Compression-water-merged
  • Unity3D MMORPG 任务系统的架构与设计详解
  • 中间件-redis-(ubantu)
  • 聚焦 AUTO TECH China 2025,共探汽车内外饰新未来Automotive Interiors
  • 2025年智慧城市解决方案下载:AI-超脑中台,体系架构整体设计
  • 【ubuntu下验证下菜品识别,训练,部署,验证 CNN + TensorFlow / PyTorch】
  • 14.1 Auto-GPT 项目定位与价值解读:揭开自主智能体的神秘面纱
  • 【CubeMX+STM32】SD卡 U盘文件系统 USB+FATFS
  • 物联网有哪些就业方向?
  • 问卷数据分析|SPSS实操之独立样本T检验
  • 【大数据安全分析】数据传输方式
  • vue知识点2