决赛开发日志
初赛按照入围排名我们排到了28/58,初赛截止提交的时候应该是25/121,评审阶段某天第一次集体提交的时候主办方不小心露出(?)榜单我们应该是第三,综合文档分结果来到28有点子意外的,但是事已至此只能说T1 fighting(?
7.11~7.16
主要是参考着moss把之前全部塞在uspace的大份进行了拆分和重构,拆分完tree一下顺眼多了
oskernel2026-orays/
├── src/ 这次重构主要修改的地方
│ ├── abi/
│ ├── fs/
│ ├── ipc/
│ ├── memory/
│ ├── net/
│ ├── process/
│ ├── sched/
│ ├── signal/
│ ├── system/
│ ├── time/
│ └── diagnostics/
├── kernel/ ArceOS内核组件层
│ ├── arch/axhal/
│ ├── memory/
│ ├── task/axtask/
│ ├── fs/axfs/
│ ├── net/axnet/
│ ├── drivers/
│ ├── namespace/axns/
│ ├── runtime/axruntime/
│ ├── sync/axsync/
│ ├── smp/axipi/
│ ├── config/axconfig/
│ └── diagnostics/axlog/
├── api/ 内核能力向上暴露的 API 与 feature 汇总
│ ├── arceos_api/
│ ├── arceos_posix_api/
│ └── axfeat/
├── ulib/ 用户态/应用侧兼容库
│ ├── axlibc/
│ └── axstd/
├── configs/ 配置
├── runtime_compat/ musl/oscomp 运行时兼容补丁
├── scripts/ 构建、网络、评测、G002-G013 检查脚本
├── tools/ 板卡、依赖和带宽测试辅助工具
├── vendor/ 本地固定的第三方依赖
├── docs/ 项目文档、图、演示稿、PDF 和视频
不过感觉过两天回来看万一又觉得是大份了再重构一次也有可能(x
应队友要求还添加了EEVDF调度策略
EEVDF调度策略是一个基于虚拟时间的公平调度算法,用以保证每个任务都能获得公平的CPU时间,通过维护每个任务的虚拟时间来决定下一个要运行的任务,从而实现公平性和高效性
参考linux官方文档里的描述:EEVDF 对睡眠任务采用延迟退列和虚拟滞后衰减机制,使任务的负 lag 随运行队列虚拟时间推进而逐渐归零,而不是因短暂休眠被立即清除,从而避免睡眠作弊。同时,合格任务可凭借更早的虚拟截止时间触发抢占;延迟敏感任务还可以请求较短的服务时间片,以获得更早的虚拟截止时间和更快的调度响应
根据目前框的大概思路就是
nice / sched_setscheduler / sched_setattr
↓
src/sched Linux语义层
↓
axtask调度器适配接口
↓
AxRunQueue 传入真实 current_ticks
↓
EEVDF 公平类 / RT 类 / EDF+CBS 类
↓
抢占标记与精确定时器
首先是EEVDF调度实体EEVDFTask,维护了:
EEVDFTask
├── inner 实际任务
├── vruntime 任务经过权重修正后的虚拟运行时间,权重大、优先级高的任务,vruntime 增长较慢,而调度器会认为 vruntime 较小的任务获得的 CPU 服务较少
├── virtual_deadline 虚拟截止时间,EEVDF在满足资格条件的任务中,选择虚拟截止时间最早的任务,权重越大,同样的实际时间片对应的虚拟时间越小,虚拟截止时间也更早
├── priority / weight 优先级和对应权重,默认使用Linux nice 0 对应的权重
├── remaining_slice 当前请求剩余时间片,初始设置为了1,避免deadline与vruntime完全相同,以及任务一创建就被认为时间片耗尽,还有就是出现零长度request
├── last_virtual_time 上次观察到的运行队列虚拟时间
├── last_accounted_at 上次统计实际运行时间的时刻
├── enqueue_sequence 入队顺序
├── initialized 是否首次进入调度器
└── deadline_state 截止时间相关扩展状态
fn begin_request(&self, slice: u64)
这个函数负责建立一个新的EEVDF request,先设置实际剩余时间片,然后计算虚拟截止时间和虚拟运行时间
fn prepare_wakeup(&self, virtual_time: u64, slice: u64, now: u64)
这部分负责唤醒处理,参数包括当前运行队列的虚拟时间,唤醒后分配的request时间片,以及当前真实tick
在任务首次进入调度器时 v = V,让任务以零lag进入系统防止新任务因虚拟运行时间过小而长期独占CPU
非首次进入的任务则是计算旧lag = V - v,这里也是EEVDF的核心要求,lag > 0表示任务vruntime落后于系统虚拟时间,欠服务,lag < 0表示任务vruntime领先于系统虚拟时间,超服务,EEVDF要求欠服务的任务优先调度,超服务的任务延迟调度
但是这样的lag计算其实和linux不太相同,唤醒时正负统一衰减lag,容易惩罚本来就欠服务的任务
还有就是一开始计算vruntime的时候,linux通过权重修正vruntime的增长速度,但是我一开始直接使用了实际时间片,但是反复思考(?)后发现linux的设计真是妙哉妙哉,于是把这个也修了
虚拟时间计算点击一键跳转linux源码:linux源码
总的来说现在核心算法和思想应该大概也许已经很贴近linux的实现了
7.17~7.18
期间我还去看了blueos/moss/asterinas,寻找一些值得参考的东西,然后发现哎asterinas写的是真完善啊
然后回过头看我们的内核还有很多需要修改和完善的地方,初赛的时候为了刷ltp分加入了很多伪实现语义不完整,现在该刻舟求剑精卫填海(?
考虑了很多,我决定先从用户地址访问和错误处理这部分先进行修修补补
首先就是重构拆分后的user_access,pwner后遗症(? 在这部分有俩write_user_value<T:Copy>和read_user_value<T:Copy>函数,作用分别类似于linux中的copy_to_user()和copy_from_user() 问题在于泛型控制太宽,不能保证任意copy类型都能安全地从原始字节构造,如果在传入结构体的时候带有padding,在copy-out的时候可能会把未初始化的padding中的栈残留复制给用户
解决是在当前 crate 内定义一个不unsafe trait,只有同时实现 Copy,并且保证任意输入字节都能构成合法值的类型,才可以实现这个trait
当我写到这里的时候,T1已经被KC抬走了,lec最大的孝子,踢万我的头好疼。。
7.19
继续修,接着是发现了arceos本来就写的很有问题的地方
在原版arceos/modules/axmm/aspace里 缺页处理只是简单返回bool或者笼统的AxError,把各种可能出现的错误比如权限问题、映射问题、页表异常等等全都混在一起,上层不能区分错误类型的话后面会越写越笼统来了
解决就是新增错误类型
Unmapped //地址未映射
Protection //权限不允许
Outfemory //物理页或页表页分配失败
BackingIo //后端IO读取失败
Beyongof //文件地址映射越过EOF
PageTable //页表异常
设计是axmm内层报告错误,处理放到syscall、exec、和异常处理边界
7.20~7.21
修了一部分之后决赛赛题发布了,然后决定先去看看决赛测例什么情况,结果惨不忍睹喵
先是试了一下buildstorm,原因是初赛修这部分相关修的头疼,想先看看这部分,结果是从rustc就开始卡住捏
rustc 会大量映射动态库、.rlib、元数据和目标文件,氮素我们的内核在一开始sys_mmap 对所有文件映射都设置 populate=true:
let populate = file_backed || shared || locked;
这会在 mmap 时读取并分配整个映射,而不是只加载真正访问的4K页,于是应该大概也许Rust workspace 会因此产生额外的一些磁盘读、页帧分配和长锁持有时间。另一方面,旧实现只在少数 shared/write 条件下保留文件 backing,私有只读映射无法通过统一的文件缺页路径恢复内容
然后大型elf读入的时候librustc driver约有300mb,造成数分钟无输出
修完mmap后rustc依旧未返回,调试,观察pc,发现pc一直卡在了0x100624d800附近,猜测是地址翻译不正确才导致pc固定
查看了一下这个地址的VMA,VMA是存在的,权限也是正确的
继续查页表的问题,发现叶子页表项缺失,猜测是上一个修改只记录了VMA但是没有建立页表结构,导致叶子页表项缺失
修复页表后串口进度继续但是依旧没有返回
继续调试,发现rustc已经进入exit_group但是迟迟没有返回,发现是活跃线程数没有归零
查看线程列表发现tid = 22的线程处于futex wait,然后再观察入队时的key和唤醒时的key可能会因为COW、映射变化使两者不一致
修复后使用最小程序复现rustc-cc-colect2-ld,最终是返回shell了
然后是日志开始报错,一点一点从syscall参数查到fd返回值,补齐了一些linux标准
再接下来就是kernelsmp=8,把任务队列闹崩了呀。。
修到这里日志勉强能到达BUILDSTORM_TOOLCHAIN ok了 后面的明天再说
7.22
今天主要修cagent部分 祈祷BIGBANG日巡我能有好座位ing
这部分测例主要是测试内核能不能支撑一个完整的 Agent 工作流,十个测例包含验证时间查询、TCP连接、查询CPU核心数、查询内核版本、创建文件写入文件读取文件、查找文件、查询文件系统大小
第一个问题是测例8核并发启动十个客户端,放大了阻塞和唤醒竞争,之前加的eevdf调度写的有点问题导致panic
分析了一下时序大概是:
CPU 0 CPU 1
----- -----
当前任务 T 调用 blocked_resched()
T.state = Blocked
释放等待队列锁
获得等待队列锁
wake(T)
T.state:
Blocked -> Ready
activate(T)
enqueue(T) 此时T已加入 EEVDF 队列
CPU 0 继续进入 resched()
重新读取 T.state,得到 Ready
然后错误处理会以为T已经是 Ready,应该是走普通 ready 路径, 因此跳过 EEVDF deactivate(T) 结果T.eevdf.active 仍然为 true
后续某条路径再次执行activate/enqueue的时候,因为任务的EEVDF active标志仍是 true,就会导致同一个任务重复 activate,同一个任务重复 enqueue,run queue 中出现重复节点,然后就panic了
修完后这个问题后cagent运行一段时间后,所有的CPU都停在idle,一键查询pc发现卡在axtask::api::run_idle,然后watchdog也没被唤醒,大概率是任务进入Blocked状态氮素没有被重新唤醒
修完这个之后cagent shell计算duration输出divide by zero且testcase输出不完整,发现是BusyBox date 返回了非十进制字符串,字符串进入 shell 算术展开,其中的 % 被解释为算术运算符导致duration计算失败于是testcase无法输出
然后是date测例
执行 date -d ‘N days ago’ +%A 的时候BusyBox date 不接受自然语言日期导致无有效 weekday 输出,validation 无法匹配 Monday~Sunday,于是date测例失败
解决是做受限转换解决格式匹配
7.23~7.28
修了waitpid回收子进程时的语义不完整问题
修了管道等待路径持锁睡眠问题
补充文件系统语义
修复大块字符串访问不成功的问题
la rustc 越过工具链检查后,评测偶尔会出现SIGILL/SIGSEGV/长时间异常
fp: [u64; 32]
上下文宏:
fst.d / fld.d
这些指令每个寄存器只保存64位,但rust 1.98的la编译器用的是128位LSX向量寄存器,于是线程切换后,低64位被恢复,高64位保留了另一个任务的值 修复:
#[repr(C, align(16))]
fp: [[u64; 2]; 32]
vst vr0..vr31
vld vr0..vr31
按16字节保存/恢复128位LSX状态,同时重新设计了FCC/FCSR偏移
7.29~8.4
参照着moss,给内核添加了virtio-rng实现
moss的设计十分巧妙也更符合linux的实现方式,把VirtIO RNG当作随机子系统的一个底层熵源,syscall 只和随机子系统交互,而我们的内核当前的axdriver没有 RNG和EntropySource设备抽象,所以我只是axdriver 单独保存一个加锁的VirtIO RNG,并提供统一的 fill_random_bytes() 接口,暂时没们moss的熵池和 ChaCha20,但也能避免syscall直接操作设备,保留了moss的模块分层思路
然后是参照asterinas接入AF_VSOCK和virtio-vsock, asterinas的设计是将功能分成了linux socket层用来处理阻塞语义地址和文件描述符状态,以及VirtIO transport层用来负责RX/TX/event三队列、request、response、shutdown、credit_update等
我们的内核使用virtio_drivers 0.13,提供了VsockConnectionManager,目前实现了credit-based transport、监听端握手、数据缓冲、三队列,所以我复用这个manager,只在上层补充open-file-description语义
然后是参照asterinas接入了基础可用的virtiofs
asterinas对virtio-fs的分层同样依旧是最底层的VirtIO驱动只负责设备发现、特性协商、配置空间读取、virtqueue 建立和原始请求传输,中间的文件系统层负责 FUSE 协议编解码、nodeid 和 lookup 引用生命周期,以及把 LOOKUP、READ、WRITE、CREATE、RENAME、READDIR 等请求转换为 VFS 操作,最上层的 Linux syscall 则负责解析 mount -t virtiofs
我们的内核也保留了这种思路,axdriver 处理 VirtIO filesystem 设备和 hiprio、request 两条队列,不处理文件路径和 inode 语义,然后axfs::fs::virtiofs 负责发送和解析 FUSE 请求,并向 axfs 提供普通文件系统节点,Linux syscall 层只负责根据 mount tag 创建并挂载对应的文件系统,而不会直接绕过 axfs 调用设备
我我们的内核使用virtio-drivers 0.13还没有在 DeviceType 中定义 filesystem 设备,解决是在本地增加专用探测逻辑,rv侧通过MMIO原始设备ID识别virtio-fs,la侧则继续复用已有的现代 PCI transport,只对设备类型识别过程做最小适配,PCI capability、BAR、通知区和配置空间访问仍然使用通用实现
我我们的内核会把原有的普通文件兼容层会把文件处理为前64KiB真实后端数据,加上内核中的稀疏覆盖数据,这样子的设计用于内存文件系统时能补充部分文件语义,但放在virtio-fs 会导致guest内看到的文件大小和内容正确,而宿主机上的真实文件只有前64KiB,所以得让axfs识别节点是不是来自virtio-fs
然后是参照asterinas接入VirtIO console,并完善VirtIO/PCI设备枚举
asterinas对VirtIO console的分层依旧是:底层VirtIO驱动负责RX/TX virtqueue和原始字节收发,TTY/HVC层负责把设备注册为Linux可见的字符设备,日志系统则在设备完成注册后将内核输出写入console
我我们的内核原有的 axdriver 只为block、net和display维护统一设备容器,之前接入的RNG、vsock和virtio-fs都采用模块内部保存专用设备实例的方式,为了避免为单个 console 设备扩展整套字符设备枚举,我沿用了这种最小适配方式:由本地 axdriver::console 模块持有第一个探测到的 VirtIOConsole,对上层只提供设备是否存在、可读字节数以及按字节读写等稳定接口,rv通过VirtIO MMIO的DeviceType::Console完成探测,la则同时识别transitional和modern VirtIO PCI console设备ID,两种transport都复用了virtio-drivers 0.13的 VirtIOConsole实现
之前内核总线扫描没有系统记录设备身份、transport版本和未被驱动认领的设备,部分 PCI BAR配置失败还会通过unwrap或expect直接触发panic,修复是让MMIO枚举先验证magic、version、device ID和vendor ID,跳过空窗口,并在结束时统计扫描窗口、真实设备、成功初始化和未认领设备数量;PCI枚举则统计function、endpoint、bridge、VirtIO设备和初始化结果,同时识别 transitional、modern以及当前依赖枚举尚未覆盖的virtio-fs类型
然后是参照asterinas,对原有的DMA实现进行了重构,并为后续可能(?接入IOMMU建立统一的DMA address-space domain抽象
原有 axdma 只提供 coherent 内存分配、释放和简单的 phys_to_bus() 转换,而VirtIO HAL基本绕过了这套接口:virtqueue ring直接从全局页分配器申请,descriptor buffer的 share() 只执行 virt_to_phys(),unshare() 则没有任何操作,DMA方向也被完全忽略
参考asterinas的设计,我把coherent DMA和streaming DMA分成了两种生命周期不同的对象,Coherent DMA用于virtqueue ring等由CPU和设备长期共享的内存,分配时同时建立CPU映射和device mapping,在整个对象生命周期内保持可见;,treaming DMA则用于一次具体的设备收发操作,需要记录ToDevice、FromDevice或Bidirectional方向,然后在设备访问前后保留sync_for_device() 和 sync_for_cpu()边界
新增了统一的DmaDomain接口,由domain负责map_pages、unmap和同步操作,DMA 映射会传入物理页数组、首页内偏移、精确长度、访问方向和 coherent/streaming 类型,最终返回连续的device address
T1nb终于看到一次鼓舞士气的胜利了我爱踢万一辈子。。。
然后是借鉴blueos,进一步完善了我们内核的VirtIO console输入事件链和Linux可见语义
blueos中的VirtIO console位于VMM一侧,承担的是虚拟设备端职责:物理UART收到输入中断后,VMM从guest RXvirtqueue中取出available descriptor,将字符写入 guest内存,推进used ring,并向guest注入虚拟中断,当guest kick TX queue时,VMM再取出descriptor中的数据并写入物理UART
而我们的内核所处的位置正好相反,它是VirtIO console的guest驱动端,因此只能借鉴的是blueos的事件处理边界:设备完成输入和用户进程读取输入不应绑定在同一次调用中,设备侧产生的数据应先进入内核缓冲区,再由 read、poll/epoll 和 ioctl 共同消费同一个可读状态。
原有VirtIO console输入路径比较简单,每次用户读取时都直接探测底层VirtIO队列,暂时无数据时通过持续yield_now()等待,可读状态无法记录实际积压字节数,也缺少一个可供未来IRQ handler复用的统一事件入口
我在virtio-drivers 0.13``VirtIOConsole的基础上增加了一层本地输入缓存,底层库继续负责 ack_interrupt()、recv()和send_bytes(),ConsoleState则统一保存设备实例和一个固定容量的16 KiB软件环形缓冲区
还增加了独立的virtio_console_poll_input(),将其作为稳定的设备事件入口
然后此时出现了一下OOM panic,原因是用户页没有内核保留区,中型对象错误走了连续页分配,用户任务创建链存在不可恢复分配