初赛按照入围排名我们排到了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 和 umount 语义

我们的内核也保留了这种思路,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.13VirtIOConsole实现

之前内核总线扫描没有系统记录设备身份、transport版本和未被驱动认领的设备,部分 PCI BAR配置失败还会通过unwrapexpect直接触发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则用于一次具体的设备收发操作,需要记录ToDeviceFromDeviceBidirectional方向,然后在设备访问前后保留sync_for_device()sync_for_cpu()边界

新增了统一的DmaDomain接口,由domain负责map_pagesunmap和同步操作,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的事件处理边界:设备完成输入和用户进程读取输入不应绑定在同一次调用中,设备侧产生的数据应先进入内核缓冲区,再由 readpoll/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,原因是用户页没有内核保留区,中型对象错误走了连续页分配,用户任务创建链存在不可恢复分配