0%

基于Iouring的异步运行时

我首先选择完成的任务是基于iouring的用户态异步运行时,支持常见的文件和网络(Tcp)io。在我的实践经历来看,构建一个高性能的用户态异步运行时,就像是在应用程序内部再造了一个微型的操作系统,它接管了传统上由内核负责的部分调度和I/O管理职责,目的是为了消除内核/用户态切换的开销,并最大化I/O吞吐量。

为了实现的简单,我选择了thread-per-core 的任务调度模型。简单来说,thread-per-core可以理解为每个CPU核心分配一个独立的执行线程,每个线程(每个核心)都拥有自己的任务队列。当一个异步任务被提交时,它会被放入相应核心的任务队列中。这种设计有几个优势,一是减少竞争,由于每个线程操作自己的队列,线程间避免了共享锁的开销;二是缓存友好:任务和数据在特定核心上处理,能更好地利用CPU缓存,减少缓存失效,提高数据访问速度;三是不需要对每个任务有Send的限制。

IO接口的异步封装,负责与将os暴露的io接口改造成rust async/await异步语法的形式。传统的I/O模型(如 select, poll, epoll)虽然是非阻塞的,但它们本质上是“事件通知”模型——通知你有事件发生了,你再去读取数据。这依然涉及用户态和内核态之间的多次上下文切换。io_uring 则是提供了更为本质的异步io接口,一种全新的提交-完成模型————队列 (SQ):用户态应用程序将各种I/O操作(如文件读写、网络套接字的发送接收等)封装成请求,批量地放入一个共享的内核提交队列中;完成队列 (CQ):内核处理完这些I/O请求后,会将结果(成功与否、处理了多少字节等)批量地放入另一个共享的内核完成队列中。应用程序只需定期检查这个完成队列,就能得知哪些I/O操作已经完成,以及它们的结果。

无锁ringbuffer BBQ

为了进一步优化我们比赛的OS内核的任务队列,我选择参考BBQ paper实现的无锁ringbuffer,虽然最终性能并不理想,但实现该结构是一个很有趣的历程。大多数lock-free ringbuffer基于version+idx组成的 Atmoicusize 作为头尾指针,并通过loop + CAS方式更新头尾指针,version主要用于解决ABA问题;而BBQ通过将数组分块,头尾指针变为头尾块指针,并且在每个块的内部额外维护2个指针(allocated/reserved)以及2个计数(committed/consumed),一个显然的好处是头节点可以直接通过FAA指令获取分配位置。我对我实现的bbq进行了性能测试,目前实现的BBQ的性能表现非常糟糕,对比crossbeam-arrayqueue,尤其在SPMC、MPMC场景下吞吐差距在10倍以上甚至更多。并且我在实践中认为算法本身还有些边缘情形处理的问题,感兴趣的同学可移步讨论区。无锁的设计总是“危险”而精妙的,哪怕论文给出算法伪代码,实现的过程依然是相当曲折的,内存序的问题,aba问题,以及如何调整测试复现特定的bug,这个过程只有踩过坑才能知道痛。

os内核赛中组件化和异步化尝试

关于我们的比赛内核,我和我的队友在原先宏内核的基础上做了大量的改动,内容聚焦在组件化拆分以及异步化改造,前者主要集中在工作量上的庞大,如果确定好组件的依赖,如何设计出合适接口,这都需要仔细考量;异步化的改造客观来说工作量也很大,这是async传染性带来的必然,(如果重头构建一个异步内核可能相对好点),所以说目前我们为了必然大范围的传染性,会使用block_on语义的函数做一个暂时解决方案。异步os一个大的优势是不需要对每个task分配内核栈,这确实会节约相当大的内存开销,但任务异步化引入的问题之一就是内核抢占,2024届内核赛获奖内核Phoenix给出的解决方案是通过设置抢占标志允许至多一次的内核抢占,这是一个不错的方案,但通用性能否做的更好一点呢?或许早先组会上听到有无栈的结合是最佳的解决方案,但由于内核比赛测试临近,最近的工作在不停的修syscall,暂时没时间研究,希望能在决赛时拿出我们认为优秀的解决方案。

相关参考资料
monoio设计介绍[https://rustmagazine.github.io/rust_magazine_2021/chapter_12/monoio.html]
iouring介绍[https://arthurchiao.art/blog/intro-to-io-uring-zh/]
BBQ论文[https://www.usenix.org/conference/atc22/presentation/wang-jiawei]
AsyncOs[https://asyncos.github.io]

序言

非常高兴能参加开源操作系统训练营第四阶段的学习,并与大家共同进步。

在此,我首先要感谢陈渝老师,您在每周学习中给予的指导和鼓励,成为了我前进的坚实支柱。同时,我也非常感谢其他同学,在第四阶段的学习中,我从大家那里学到了很多,无论是便捷地获取学习资料和代码,还是在遇到疑惑时能找到理解并深入探讨技术的伙伴,都让我受益匪浅。

经历完这四个阶段,我取得了显著的收获:不仅深刻理解了操作系统内部的运行机制,更掌握了通过组件化管理实现现代化操作系统的方法。具体而言,我成功完成了arceos-org/oscampaarch64架构支持,并为starry-next适配了iperfTCP部分。

任务1 完成arceos-org/oscampaarch64架构的支持

  1. 使用QEMUmonitor info mtreemonitor info block 找出pflash区域,并在aarch64-qemu-virt.toml中增加正确的映射区域,在tour代码中写入正确的PFLASH_START

  2. 增加了aarch64部分的uspace代码,进行适配

  3. makefile内的规则进行修改,修改payloadmk_pflash对其他架构进行适配

  4. 增加payload(https://github.com/879650736/oscamp/blob/main/arceos/payload/hello_c/Makefile )(https://github.com/879650736/oscamp/blob/main/arceos/payload/origin/Makefile )内其他架构的编译规则,使payload对其他架构也能适配

  5. 增加aarch64的CI测试,并测试通过

  6. pr:https://github.com/arceos-org/oscamp/pull/9

  7. fork仓库:https://github.com/879650736/oscamp

任务2 为starry-next适配iperf

  1. 提交https://github.com/oscomp/starry-next/pull/56/files 。修复了oscomp/starry-next中的c中的sockaddrarceos/axnet中的SocketAddr的类型转换问题,改为使用trait直接将sockaddr 转换为SocketAddr,而不需要加一个中间层SockAddr,并测试通过。已合并。

  2. 提交https://github.com/oscomp/testsuits-for-oskernel/pull/52 。在为 starry-next 兼容 iperf的过程中,我发现一个段错误问题。具体来说,如果在cJSON_New_Item 函数中未对全局变量 global_hooks进行初始化,会导致空指针访问。然而,当我单独编译cJSON的相关代码时,并未复现此异常。我推测这可能是由于编译为 ELF 文件时,编译器进行了某种优化所致。将 global_hooks的初始化操作增加到cJSON_New_Item函数的起始位置后,该段错误便得以消除。

  3. muslopenssl库,使用build_riscv.sh, 进行openssl库的交叉编译

  4. 创建iperf_wrap, 进行本地编译载入测试

  5. arceos/modules/axfs/src/mount.rs中增加/dev/urandom的挂载,并增加了一个简单的urandom的实现

  6. 修改iperf中 autoreconfconfigure.ac,增加--disable-xxxx选项的支持

  7. 实现可增加--disable参数去除部分 Linux 特有的选项如SO_MAX_PACING_RATESO_BINDTODEVICEIP_MTU_DISCOVER等,为交叉编译提供支持,参考 build.sh,宏定义生成结果可通过src/iperf_config.h查看,也为调试提供方便。

  8. 允许用户在配置 iperf3 时,通过命令行参数禁用特定的功能或特性,特别是那些可能与特定操作系统(如 Linux)紧密相关的特性,以便于在其他平台或进行交叉编译时避免兼容性问题。

    • configure.ac 文件中使用 AC_ARG_ENABLE 宏来定义新的配置选项。

    • --disable-have-dont-fragment 为例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      # Check if Don't Fragment support should be disabled
      AC_ARG_ENABLE([have-dont-fragment],
      [AS_HELP_STRING([--disable-have-dont-fragment], [Disable Don't Fragment (DF) packet support])],
      [
      case "$enableval" in
      yes|"")
      disable_have_dont_fragment=false
      ;;
      no)
      disable_have_dont_fragment=true
      ;;
      *)
      AC_MSG_ERROR([Invalid --enable-have-dont-fragment value])
      ;;
      esac
      ],
      [disable_have_dont_fragment=false]
      )

      if test "x$disable_have_dont_fragment" = "xtrue"; then
      AC_MSG_WARN([Don't Fragment (DF) packet support disabled by user])
      else
      if test "x$iperf3_cv_header_dontfragment" = "xyes"; then
      AC_DEFINE([HAVE_DONT_FRAGMENT], [1], [Have IP_MTU_DISCOVER/IP_DONTFRAG/IP_DONTFRAGMENT sockopt.])
      fi
      fi

      AC_ARG_ENABLE([have-dont-fragment], ...) 定义了 --disable-have-dont-fragment 选项。
      如果用户指定了 --disable-have-dont-fragment,则 disable_have_dont_fragment 变量被设置为 true
      如果 disable_have_dont_fragmenttrue,则会发出警告,并且不会定义 HAVE_DONT_FRAGMENT 宏。
      否则(用户未禁用),并且如果 Autoconf 之前的检查 (iperf3_cv_header_dontfragment) 确认系统支持 IP_MTU_DISCOVER 等选项,则会定义 HAVE_DONT_FRAGMENT 宏。

    • 针对 Linux 特有的套接字选项(如 SO_MAX_PACING_RATESO_BINDTODEVICE、IP_MTU_DISCOVER),提供 --disable 选项,以便在非 Linux 环境下(如交叉编译到嵌入式系统或其他操作系统)能够顺利编译,避免因缺少这些特性而导致的在其他环境下的运行错误。

    • 其通用模式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      # 定义一个名为 'have-feature-name' 的选项
      AC_ARG_ENABLE([have-feature-name],
      [AS_HELP_STRING([--disable-have-feature-name], [Disable support for Feature Name])],
      [
      case "$enableval" in
      yes|"")
      disable_feature_name=false
      ;;
      no)
      disable_feature_name=true
      ;;
      *)
      AC_MSG_ERROR([Invalid --enable-have-feature-name value])
      ;;
      esac
      ],
      [disable_feature_name=false] # 默认启用
      )

      # 根据用户选择和系统检测结果,决定是否定义宏
      if test "x$disable_feature_name" = "xtrue"; then
      AC_MSG_WARN([Feature Name support disabled by user])
      else
      # 这里可以添加额外的系统特性检测,例如检查头文件、函数或套接字选项
      # if test "x$ac_cv_header_some_header" = "xyes"; then
      AC_DEFINE([HAVE_FEATURE_NAME], [1], [Description of the feature macro.])
      # fi
      fi
    • 当修改了 configure.ac 文件后,仅仅保存文件是不够的。configure.acAutoconf 的输入文件,它需要被处理才能生成实际的 configure 脚本。这个处理过程就是通过运行 autoreconf 命令来完成的。

    • autoreconf 命令会执行一系列工具(如 aclocal, autoconf, autoheader, automake 等),它们会:

      1. 处理 configure.ac: 将 configure.ac 中的 Autoconf 宏转换为可执行的 shell 脚本代码,生成 configure 脚本。
      2. 生成 config.h.in: 如果你的 configure.ac 中使用了 AC_CONFIG_HEADERSautoheader 会根据 AC_DEFINE 等宏生成 config.h.in 文件,这是一个模板文件,最终会被 configure 脚本处理成 config.h
      3. 处理 Makefile.am: 如果项目使用了 Automakeautomake 会处理 Makefile.am 文件,生成 Makefile.in
        因此,每次修改 configure.ac 后,你都必须在项目根目录运行 autoreconf -fi 命令,以确保这些修改能够体现在新生成的 configure 脚本中。 否则,你新添加的 --disable-xxxx 选项将不会被识别。
    • build.sh 脚本中,可以根据编译目标或环境变量来决定是否添加这些 --disable 参数。

      1
      2
      ./configure --disable-have-dont-fragment --disable-openssl --disable-cpu-affinity  
      ........
  9. api/src/imp中进行syscall的适配

  10. 对于跨平台elf调试,使用

    1
    2
    int i = 1;
    assert(i == 0);

    进行手动打断点结合printf一步步调试,最终找到https://github.com/oscomp/testsuits-for-oskernel/pull/52 的段错误的具体问题。

  11. iperf3测量原理

    • 基本工作流程:
    1. 服务器端启动: 一台机器作为服务器端,启动 iperf3 并监听特定端口,等待客户端连接。
    2. 客户端启动: 另一台机器作为客户端,启动 iperf3 并指定服务器的IP地址和端口,发起连接请求。
    3. 数据传输: 连接建立后,客户端或服务器(取决于测试模式)开始发送数据包。
    4. 性能测量: 双方在数据传输过程中记录时间、传输数据量、丢包等信息。
    5. 结果报告: 传输结束后,客户端和/或服务器会计算并报告测量的网络性能指标。
    • 在本机apt install iperf3后,自动安装并自启动了/usr/lib/systemd/system/iperf3.service
    text
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    iperf3.service - iperf3 server
    Loaded: loaded (/usr/lib/systemd/system/iperf3.service; enabled; preset: enabled)
    Active: active (running) since Fri 2025-06-20 05:23:30 UTC; 7h ago
    Docs: man:iperf3(1)
    Main PID: 1326 (iperf3)
    Tasks: 1 (limit: 9434)
    Memory: 472.0K (peak: 5.4M swap: 440.0K swap peak: 440.0K)
    CPU: 30.580s
    CGroup: /system.slice/iperf3.service
    └─1326 /usr/bin/iperf3 --server --interval 0

    每次开机后,systemd 会根据 iperf3.service 的定义,自动启动 /usr/bin/iperf3 --server --interval 0 命令,使其作为后台服务持续运行,等待客户端连接。

    • 当你在本机运行 iperf3 -c 127.0.0.1 时,这个命令会启动一个 iperf3 客户端进程。这个客户端进程会尝试连接到 127.0.0.1(即本机)上正在监听的 iperf3 服务器。iperf3 -c 127.0.0.1 会向服务器发送数据包,服务器接收这些包并进行统计。客户端也会统计发送的数据量和时间,最终报告发送端的吞吐量。

    • 客户端和服务器之间建立 TCP 连接(默认)。客户端以尽可能快的速度向服务器发送数据,服务器接收并记录数据量。双方都记录开始和结束时间。通过传输的数据量除以传输时间,即可计算出吞吐量。

    • qemu内运行的starry-next同理,因为qemu与主机是通过NAT。在 qemu 虚拟机内部运行的 starry-next(假设它也包含 iperf3 客户端或服务器)与主机之间的网络通信,会经过 qemu 的网络虚拟化层。

    • qemu 使用 NAT(网络地址转换)模式时,虚拟机拥有一个私有 IP 地址,它通过主机的 IP 地址访问外部网络。对于虚拟机来说,主机看起来像一个路由器。

    • 场景 : qemu 内的 iperf3 客户端连接到主机上的 iperf3 服务器。

    • qemu 虚拟机内的 iperf3 -c <主机IP地址>

    • 数据流:qemu 客户端 -> qemu 虚拟网卡 -> qemu NAT 转换 -> 主机物理网卡 -> 主机 iperf3 服务器。

    • 这种测试测量的是虚拟机到主机之间的网络性能,包括 qemu NAT 层的开销。

    • 无论哪种场景,iperf3 的基本客户端-服务器通信原理不变。qemu 的 NAT 模式只是在网络路径中增加了一个虚拟化的层,iperf3 测量的是经过这个虚拟化层后的实际吞吐量。

    • 关键设计点:

    • 处理程序中断信号(如 Ctrl+C)的机制。它使用了 signalsetjmp/longjmp 组合来实现非局部跳转,以便在接收到中断信号时能够优雅地退出并报告结果。

    • iperf_catch_sigend 函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      void
      iperf_catch_sigend(void (*handler)(int))
      {
      #ifdef SIGINT
      signal(SIGINT, handler);
      #endif
      #ifdef SIGTERM
      signal(SIGTERM, handler);
      #endif
      #ifdef SIGHUP
      signal(SIGHUP, handler);
      #endif
      }

      这段代码将 sigend_handler 函数注册为 SIGINT, SIGTERM, SIGHUP 这三个信号的处理函数。这意味着当程序接收到这些信号中的任何一个时,sigend_handler 函数就会被调用。

    • 信号处理的设置和跳转点

      1
      2
      3
      4
      5
      iperf_catch_sigend(sigend_handler); // 注册信号处理函数
      if (setjmp(sigend_jmp_buf)){ // 设置跳转点
      printf("caught SIGEND\n");
      iperf_got_sigend(test);
      }
      • if (setjmp(sigend_jmp_buf)): 这是 setjmp/longjmp 机制的关键。
      • setjmp(sigend_jmp_buf)
        第一次调用时(正常执行流程): 它会保存当前程序的执行上下文到sigend_jmp_buf 中,并返回 0。因此,if (setjmp(...))条件为假,程序会继续执行 if 语句块后面的代码。
      • longjmp被调用时(从信号处理函数中):longjmp 会使用 sigend_jmp_buf中保存的上下文,使程序“跳回”到 setjmp 被调用的位置。此时,setjmp 会返回longjmp 传递的非零值(这里是 1)。因此,if (setjmp(...))条件为真,if 语句块内的代码会被执行。
    • sigend_handler 函数

      1
      2
      3
      4
      5
      6
      static jmp_buf sigend_jmp_buf; // 用于存储跳转上下文的缓冲区
      static void __attribute__ ((noreturn))
      sigend_handler(int sig)
      {
      longjmp(sigend_jmp_buf, 1);
      }
      • 这是实际的信号处理函数。
        __attribute__ ((noreturn)): 这是一个 GCC 扩展属性,告诉编译器这个函数不会返回(即它会通过 longjmp 跳转出去,而不是正常返回)。这有助于编译器进行优化,并避免一些警告。
        longjmp(sigend_jmp_buf, 1);: 这是核心操作。当SIGINTSIGTERMSIGHUP 信号被捕获时,这个函数会被调用,然后它会执行longjmp
      • longjmp 会将程序的执行流从当前位置(信号处理函数内部)直接跳转到 setjmp(sigend_jmp_buf)所在的位置。
    • iperf_got_sigend 函数

      • 捕获到中断信号后,实际执行清理、报告和退出的函数
    • 这段代码实现了一个健壮的信号处理机制,确保 iperf3 在接收到中断信号(如 Ctrl+C)时,能够:

      1. 立即停止当前的数据传输。
      2. 收集并报告截至中断时的所有统计数据。
      3. 通过控制连接通知另一端的 iperf3 进程,以便对方也能感知到测试的结束并进行相应的处理。
      4. 最终优雅地退出程序。

适配成功:

iperf-V
iperf-c

序言

非常高兴能参加今年的操作系统训练营,经过这四个阶段的学习,对操作系统的理解逐渐加深。在此过程中,对陈渝老师和郑友捷老师的耐心指导表示衷心感谢。

在第四阶段的学习中,我主要做了以下工作:

  1. starry-next支持新特性:大页分配

    1. 后端映射实现

      1. 修改后端数据结构,为LinearAlloc这两种映射方式增加对齐参数align,该参数的类型为PageSize。根据对齐方式,分配相应的内存块。
      2. 修改物理页帧的分配方法alloc_frame,以4KB为单位,根据对齐参数大小,计算页面数量num_pages,调用全局分配器,分配num_pages个连续的物理页面,将这段连续内存的地址返回并映射到页表中,作为一整个页面。
      3. 修改释放方法dealloc_frame,基于以上alloc_frame,物理页帧的释放同理,根据对齐参数,以4KB为单位,计算需要释放的物理页帧数量,并调用全局分配器,释放这一段连续的物理内存。
      4. 修改空闲内存查找方法find_free_area,为适配不同的对齐要求,该函数首先对建议的起始地址hint进行对齐,然后执行两轮扫描,第一轮扫描处理hint之前的区域,以确定起始搜索位置,第二轮扫描检查各内存区域之间的空隙,跳过重叠和已占用的区域,检查满足对齐要求和大小的区域,最后检查末尾区域,验证并返回找到的地址。
      5. 修改unmap方法,考虑取消映射的内存区域可能存在不同对齐的情况,对unmap方法进行改进。首先验证起始地址和大小是否满足4K对齐,然后查找定位取消映射的内存块,根据每个内存块的对齐要求,验证对齐,最后执行取消映射。
      6. 增加一个page_iter_wraper.rs文件,包装PageIter4KPageIter2MPageIter1G为一个PageIterWrapper,方便遍历。
      7. 对线性映射,文件读写,DMA和懒分配的支持,在相关函数如new_linearprocess_area_datahandle_page_fault中增加对齐参数支持不同页面的对齐要求。
    2. 内存扩展

      1. 为测试大页尤其是1G大页的分配,读写和回收等情况,需要扩展平台内存以运行测例。
        1. 修改aarch64架构配置文件,修改其物理内存大小为4G
        2. 修改riscv64架构配置文件,修改其物理内存大小为4G
        3. 修改loongarch64架构的配置文件,扩展其物理内存,并修改其物理基址和内核基址以避免内存重叠
        4. 修改x86_64架构的配置文件,扩展其物理内存,在mmio-regions新增高地址支持,修改modules/axhal/src/mem.rs文件,新增free_memory_region,扩展x86_pc中的multiboot.S支持高位地址空间
      2. 扩展内存之后在所有架构下测试
      3. 将以上所做的修改工作落实成文档提交
  2. 分析和改进axalloc

    1. 分析axalloc代码逻辑,撰写文档
    2. 改进axalloc
      1. 新增buddy_page.rs,基于buddy算法,实现了一个页分配器,并添加相关的测试模块。实现思路如下:
        1. 初始化过程:将起始地址和结束地址对齐到页边界,内存基址对齐到1G边界(与arceos的BitmapPageAllocator保持一致),将整个内存区域分解为最大可能的2的幂次个块。
        2. 分配算法:检查需要分配的页面数量和页面大小,验证其是否与PAGE_SIZE对齐,根据所需页面数量和对齐,计算所需阶数,查找可用块,将大块分割到所需大小,将多余部分加入对应的空闲链表,标记页面为已分配,更新使用统计。
        3. 释放与合并算法:首先标记当前块为空闲,然后递归检查伙伴块是否空闲,如果是则合并,将最终合并的块加入到对应阶数的空闲链表。
      2. 新增tests.rs,提供一个基准测试模块,用于比较BitmapPageAllocatorBuddyPageAllocator两种页分配器的性能表现。该测试模块包含两个主要测试函数,用于评估不同分配器在碎片化和内存合并方面的表现 ,测试结果如下:image-20250621162956014
        1. 合并效率测试结果:两个分配器都显示了 100% 的合并效率,这意味着它们都能完美地将释放的单页重新合并成大的连续内存块
        2. 碎片化测试结果分析:
          • Random Small Pattern(随机小块分配):
            • Bitmap: 50.0% 碎片化率
            • Buddy: 83.3% 碎片化率
            • BitmapPageAllocator 表现更好
          • Mixed Size Pattern(混合大小分配):
            • Bitmap: 41.7% 碎片化率
            • Buddy: 33.3% 碎片化率
            • BuddyPageAllocator 表现更好
          • Power-of-2 Pattern(2的幂次分配):
            • Bitmap: 16.7% 碎片化率
            • Buddy: 33.3% 碎片化率
            • BitmapPageAllocator 表现更好

开源操作系统训练营第四阶段总结-改进Starry文档

学习心得

赶着入职前进入到了第四阶段,留给我的时间不多,都不到一周,但是陈老师没有放弃我,仍然耐心的给与了我如何开始学习Starry的指导,我非常感动。四阶段项目一群中的群友也给与了我很大的帮助,让我感受到了开源社区的魅力。

在四阶段学习中,我收获良多,由于不是计算机专业的学生,有许许多多的第一次:

  • 第一次学习操作系统:还是 RISCV 指令集的,在MacOS上跑代码没少折腾。很多内容让我回想起了本科学习计算机组成原理的时光(还是很不相同)。
  • 第一次使用 docker:本来倔强的我想在 MacOS 上坚持到最后,没想到还是在第三阶段涉及到交叉编译的部分败下阵来,配置 docker 环境的时候可能由于网络问题也没少折腾。
  • 第一次提交 PR:之前使用GitHub也只是上传论文的代码,单打独斗。在刚进入第四阶段的时候,陈老师很耐心的用一个修改 README.md 的机会指导我怎么提交 PR。第一次了解了团队合作乐趣。
  • 第一次了解 GitHub 工作流:前三阶段大概知道有这么一个东西在给我们提交的文档打分,在第三阶段中 musl 被官方屏蔽了微软的访问之后一直访问超时,我才开始去理解其中的含义。

闲聊了这么多,还是因为我入门尚浅,翻来覆去看代码也只知道还要学习的有很多,难以下手,留给我的时间又只有一周。还好陈老师给我指了一条明路——完善文档,接下来我就介绍一下这一周我的工作以及将来的计划。

本周工作

我的工作主要是分析starry-next,并阅读 starry-next tutorial book,从各个层面改进starry-next tutorial book,帮助自己和其他初学者更好地学习操作系统开发。

在开始完善文档之前,我阅读了郑友捷同学的综合论文训练——《组件化操作系统 ArceOS 的异构实践》,宏观的了解了 StarryOS 的设计理念和目标。在完善文档的过程中,郑友捷同学也给予了我很多建议和指导,在此表示感谢。

我的工作具体如下:

  1. PR#23 修复了指导书不能切换主题的bug, 可以切换到深色模式,便于完善文档时的调试(一边黑一边亮容易被吸引注意力)。

  2. PR#24 在阅读完郑同学的论文之后,修改了文档的欢迎页,说明了 StarryOS 和 ArceOS 之间的关系和差别。因为从第三阶段到第四阶段,作为初学者的我一开始是一头雾水,不知道 Starry 要实现一个什么样的目标,所以我也在欢迎页添加了设计目标的说明(仍需完善)。在我与郑同学的交流中感觉到 Starry 的目标可能是:

    • 在 crate 层中希望能够开发一些独立的组件,一部分就像 ArceOS 一样,能够被任意类型内核复用, 一部分则是能够被其他的宏内核使用。

    • 使 Starry 能够兼容 Linux 程序,即提供 POSIX API 接口。

    • 完成作为一个宏内核该有的功能,包括进程管理、信号处理等。

      作为一个第一次接触 Starry 的开发者,我觉得可能还需要一个更宏大的目标,或者更明确的商用的可能性来吸引更多的人加入我们的开源社区,并做出贡献。

  3. pr#25 在进入到第四阶段的时候,配置完环境后,只是按照 README.md 中给的指令运行了一遍,但是依然没有理解具体 Starry 究竟做了些什么,他的目标是什么。而且我在创建镜像的过程中遇到了一些问题——loop设备满了导致无法加载,我也是通过读了一遍Makefile的流程才定位到这个错误。因而我写了一个一个案例快速上手 Starry,介绍了这些指令内部的一些细节,帮助初学者快速理解。PS: 今天我再读的时候发现了其中的一些错误(镜像文件应该是给QEMU加载的,不是ArceOS的文件系统),而且没有介绍案例 nimbos 的主要作用,可能对于学习过操作系统的同学来说,不用说也知道是一个测试操作系统功能的测试集,从一个门外汉的角度来看,他就是测试了几个testcase而已。我会在后续的工作中对文档进行修正。

  4. pr#26 可能对于大佬来说,各种 git 指令都已经理解透彻,烂熟于心,但对于初学者来说,只会用一个

    1
    2
    3
    git add .
    git commit "update XXX"
    git push

    在这之前我都没有创建过分支来提交PR,导致了一些混乱,因此在附录中添加了一个创建分支提交PR的标准流程。此外对于理解StarryOS究竟在做什么,理解他的工作流很有必要,因此我也在附录中添加了对于工作流相关的说明。

总结和将来的工作

由于时间不多,我的水平有限,相比其他同学对社区的贡献,我的工作可能微不足道。如果社区的大佬们不嫌弃,入职工作之后我也愿意继续帮助完成文档(主要因为这个项目里面用到的Rust语法很全面,我想要学习rust, 还包含了很多汇编和c,将来也能对用rust驱动硬件做为一个参考)。

可能有一部分同学和我一样,在阶段一到阶段三主要注重于做题(阅读测例->知道预期的结果是什么->查看相关的接口和需要用到的函数->实现功能),完成任务即可。没有特别理解整体的设计和原理,因而到了阶段四之后没有了题目之后感到迷茫。

因而为了帮助第三阶段的同学能够丝滑的进入到第四阶段,我觉得当务之急是需要完善ArceOS的文档(我第三阶段完全就是参考PPT完成的,对于很多细节没有掌握)我接下来的工作就是理解ArceOS的细节,说明Starry如何使用了ArceOS的接口和模块。然后首先实现文档整体框架的从无到有,最后在掌握了细节和整体设计思路之后再完善指导书并修正其中的错误。也希望有大佬能够一起加入到完善文档队伍中,一起交流。

序言

过去一个月,由于我对 ArceOS 的架构理解较少,为了快速掌握 ArceOS 的架构,Unikernel宏内核,我的主要目标改进 oscamp,为其完善对 x86_64 的支持。同时还做了对内核组件 x86_rtc 说明文档和测试的完善。
最直接的收获有两点:

  • Unikernel 思想 —— “用户态就是内核的一部分” 的最小可信边界,让我重新审视传统多进程操作系统里“内核/用户”硬隔离的成本。
  • 宏内核工程学 —— 模块划分、内核线程、系统调用网关、设备驱动归一化,这些都在 Starry/ArceOS 的设计里有了“先行者版本”。

工作记录

第一周

为了快速掌握 ArceOS 的架构,Unikernel宏内核,我选择了改进 oscamp,为其完善对 x86_64 的支持这一项工作。并开始了对 x86_64 的学习。

第二、三周

主要是做代码工作,以下是一些总结
RISC‑V 是通过 scause + stval;x86‑64 要区分 Exception Class(#PF/#UD)与 IRQ Vector,且栈布局不一样。
x86_64 的支持这一项工作需要完成以下的功能实现:

  1. 改进 context.rs,保存相关的寄存器,并完善 context_switchenter_uspace
    • 保存/恢复的寄存器集对齐 SysV x86‑64 调用约定:RBX RBP R12‑R15 + CR3 + RFLAGSFPU/AVX 延后到 lazy fp 任务。
    • context_switch(old, new) = 保存旧任务栈顶 → 恢复新任务栈顶 → iretq;为支持 SMP,加了 core::arch::asm!("swapgs") 保证每 CPU 的 GS 基址切换。
    • 进入用户态 (enter_uspace):手动构造 iretq 帧:SS|RSP|RFLAGS|CS|RIP,再写 CR3 = user_pml4; 关中断→加载帧→开中断→ iretq
  2. 改进 trap.S
    • IDT 256 项0x20 时钟、0x80 软中断、0x0E #PF……全部指向统一的 trap_entry;硬中断通过 APIC 自动切到 IST[0] emergency stack 防止内核栈溢出。
    • trap.rs 根据向量号派发到 handle_page_fault / handle_irq / handle_syscall
  3. 改进 syscall.rssyscall.S
    • SYSCALL/SYSRET 而非 INT 0x80;入口先 swapgs 用 GS 保存/恢复用户栈。
    • 按 SysV ABI RAX=nr,RDI RSI RDX R10 R8 R9 传六参 —— 在汇编里把寄存器序列化到栈,统一传给 x86_syscall_handler()
    • 退出路径:恢复通用寄存器 → swapgssysretq

第四周

对内核组件 x86_rtc 说明文档和测试的完善。
repo

未来展望

  • Transparent HugePages:复用前期完成的巨页 API,引入 khugepaged 合并线程。
  • vDSO:把高频 clock_gettime 胶水放到用户态,加速 Sys‑API。

第一阶段 rust基础

rustlings比较顺利,已关注rust好几年了,也一直跟进rust的发展和变化,用了大概几天的时间突击完成

第二阶段 rcore

这是我第一次接触操作系统,印象还都停留在原来的书本上,概念上,都是理论,没有真正接触操作系统是什么样的,所以,这个机会能让我了解探究操作系统内部真正的原理和运行逻辑,解开我心中的多年的疑惑,很是开心到起飞。

总体印象最深的就是操作系统内核是以什么形态存在的,上下文如何切换,进程空间如何形成,页表是如何实现的,跳板页又是怎么回事

第三阶段 ArceOS

这个和我预期不一样,没有沿着rcore继续走,这是一个全新的设计,一时间,有点慌乱,不能和rcore相关的思路很自然的顺承下来,显然这个更具有前瞻性,为此我也花了好多时间来梳理这个逻辑和rcore都关联起来。这个模块化设计的理念很突出,相互之间的关联及细节,需要仔细的研读和体会。

第三阶段的具体case

color

对println!() 把颜色表示直接加入后,可以显示颜色,
如在当前文件写了一个宏print_with_color!,让后让println!去去调用此宏,就得不到正确结果,提示找不到这个宏,查了资料,和 $crate有关,宏的暴露方式有关,引用路径,由于时间关系,后面调研

HashMap

开始的时候,从rust官方移植,这个太痛苦了,依赖太多了,最后放弃。
自己写个简单的,只是利用了官方的
use core::hash::Hasher;
use core::hash::Hash;
这两个hash算法,主体采用了最简单的线性插入算法

bumpallocator

需要仔细理解题意和上下文,思路选用:
申请一直从可用空间起始向前申请,
释放如果全部的空间都释放了,就把下一个可用空间调整到开始

rename

采用递归的方式,一层一层的先找到所在当前目录
增加当前目录的,更改命名的方法,查找相应的文件,删除后,再插入一个,因为当前存储使用的是BtreeMap,不能更改index

hv

这个耗费了我好多时间,主要是因为运行例程会卡住,后来发现可能是qemu的版本问题,升级到9.2后,还是一样卡住,当时环境为windows11,wsl2,卡在这里,不同的情况卡的还不一样:

1
Write file 'payload/skernel2/skernel2' into disk.img

最后没有办法,找一台空闲机器,实在不行,我就安装裸机linux系统,所以先尝试装了另一种虚拟机,virtualbox7.18,也花了些时间,配置好环境后,默认的qemu为6.x时,还是会出错,不过不是卡主的问题,是会触发异常访问,升级到9.2.x后,按照预期执行了,难道说,wsl2在某些情况就是不行,我真是难过。

之后遇到提交github,出错,musl.cc被block,

下载不下来:
wget https://musl.cc/riscv64-linux-musl-cross.tgz

下载不下来,终于等来了替代方案

wget https://musl.cc.timfish.dev/riscv64-linux-musl-cross.tgz
最后,终于得以解决

sys_mmap

参数和返回值我理解出现偏差,

1
2
3
4
5
6
7
8
fn sys_mmap(
addr: *mut usize,
length: usize,
prot: i32,
flags: i32,
fd: i32,
_offset: isize,
) -> isize

参数addr为0时要特别注意,需要寻找一空间,还有地址和size的对齐。返回值,当开始时认为0是成功,为负时,返回失败原因,后来再三确认失败返回0,成功返回地址,isize作为地址返回,有点不符合直觉?

Next

期待第四阶段

前三阶段总结

主要收获

在第四阶段中,更多的时间留给了自由探索.虽然起初缺少具体的目标有些令人摸不着头脑,不过跟随老师的引导,也一步步确立了整个阶段的目标:基于 uring^1 机制实现异步系统调用.尽管最后只实现了基于 uring 的异步 IPC 机制,一路上走来也有许多收获.

Rust 的异步机制

虽然有一些 Rust 异步编程经验,但尚未从更底层的角度了解过 Rust 的异步模型.在第一周中,通过动手实现一个简易的异步运行时 local-executor,认识到 Rust 的异步原语 Future 是如何与运行时交互的.以及深入到内存安全层面上,了解了 Rust 中如何通过 pin 语义来巧妙的保证自引用结构体的安全性.尽管这并不是一个完美的模型——几乎所有涉及到 pin 语义的数据结构和函数都需要 unsafe 代码,而这些 unsafe 代码所需的诸多安全性保证又着实有些令人头大.因为,Rust 提供了静态安全性,而编译器会基于这些安全保证进行比较“激进”的优化.所以,Rust 中的 unsafe 要比其他生来便“不安全”的语言更加不安全,对开发者的要求也更高.

在第三周的探索中,又了解到一个之前从未考虑过的问题——Future 的终止安全性^2.而这对于实现基于共享内存的异步通信机制来说尤其关键,稍有不慎就会引发难以察觉的漏洞.在后来着手实现异步通信机制的时候,又对这个问题进行了更深入的思考,并在现有方案的基础上提出了另外几个可行的思路

原子类型和内存排序

尽管曾了解过原子类型和内存排序相关的知识,但从未真正彻底搞懂过,直到在第二周的探索中发现了一本优秀的电子书 Rust Atomics and Locks^3.这本书从抽象的并发模型深入到具体的硬件细节,比较全面的介绍了几种原子操作和内存排序的设计初衷以及对应的汇编层面实现.结合这本书和自己的思考,又经过悉心整理最终形成了一篇比较详实的学习笔记.尽管在实践时还不能完全掌握各种内存排序的选择,通过翻看笔记以及参考相似场景下现有项目的做法,也都能找到一个安全正确的选项.

基于 uring 的异步通信

经过两周的调查和学习,最终在第三周完成了基于 uring 的异步通信框架 evering,同时利用 GitHub Pages 部署了它详细的设计文档

evering 最重要的两个数据结构是用来管理消息队列的 Uring 和用来管理操作生命周期的 DriverUring 的实现借鉴了 io_uring 的做法^4,但结合 Rust 的特性做了一些简化.比如,io_uring 支持 IOSQE_IO_LINK 来要求响应侧顺序处理请求.而在 Rust 中,每个异步请求都被封装为 Future,故可以利用 .await 来在请求侧实现顺序请求.Driver 的实现则借鉴了 tokio-uringringbahn.但相比后两者,evering 提供了更灵活、通用的异步操作管理机制.

不过,目前 evering 相对简陋,仅支持 SPSC,因此请求侧或响应侧只能在单线程上使用.也许未来可以实现 MPSC 的队列,以便于更好的与现有的异步生态(比如 tokio)兼容.

基于 evering 的异步 IPC

经过三周的铺垫,第四周正式开始实践跨进程的异步通信.在第三周中,基于 evering 实现了简易的跨线程异步通信 evering-threaded,而对跨进程来说,主要的难点就是内存的共享.好在 Linux 提供了易于使用的共享内存接口,基于 shm_open(3)memfd_create(2)mmap(2) 可以轻松在不同进程之间建立共享内存区.而 ftruncate(3p) 配合缺页延迟加载机制,使得程序启动后仅需一次初始化就能配置好可用的共享内存区间.不过,目前 evering 只能做到基础的“对拍式”的通信方式.而近期字节跳动开源的 shmipc 则是一个相对成熟、全面的异步通信框架,这对未来 evering 的改进提供了方向.

基于 evering 的异步系统调用

由于时间相对仓促,加之备研要占用大量的时间,遗憾的是,在第四阶段并没有完成最初的目标——实现基于 uring 的异步系统调用.与 用户线程 <-> 用户线程 的通信相比,用户线程 <-> 内核线程 的通信要额外处理内核任务的调度和用户进程的生命周期管理.即如何处理多个不同用户进程的请求,以及用户进程意外退出后对应内核任务的清理.而就共享内存而言,由于用户对内核单向透明,这看起来似乎比 IPC 的共享内存更容易解决.

用户态线程与协程的调度

去年的夏令营中,embassy-preempt 实现了内核中线程和协程的混合调度.那么用户态的协程能否被内核混合调度呢?在实现异步系统调用的前提下,当用户态线程由于内核尚未完成调用处理而让权(通过 sched_yield(2) 等系统调用)时,实际上,内核可以获知该线程应何时被唤醒.这就与 Rust 协程中的 Waker 机制非常相似,而用户态的让权又与 .await 很类似.基于这些,那么可以将一个实现异步系统调用的用户线程转换为一个用户协程.此后,内核就充当了这个协程的运行时和调度器的角色.

而相比用户态的线程,使用协程的一个显著优点是,对用户任务的唤醒实际上相当于执行一次 Future::poll.这意味着,当用户主动让权时,它不需要保存任何上下文——用户任务的唤醒本质上变成了函数调用,而主动让权表示该函数的返回.如此便能够进一步减少用户和内核切换的开销,以及系统中所需执行栈的数量.当然,当用户协程被抢占时,它便回退成了类似线程的有独立执行栈和上下文的存在.

总结

经过近两个月的学习,对操作系统和异步编程的许多方面都有了一些相对清晰的认知.非常感谢夏令营中各位老师的付出和历届同学的努力,学习的过程中让我切身的感受到操作系统发展到现在那段波澜壮阔的历史,以及在不断推陈出新的技术潮流中一点微不足道的参与感.尽管最后没能完成目标有些遗憾,不过,这也为将来再次参加夏令营留下了充足的理由 :P