相当于写给烧写板子刚入门的萌新,以及附带一些常用的基本工具...
0%
Lfan-ke: 镜像 烧录 设备 挂载 共享 映射 挂载
基于 uring 的异步 IPC
异步通信框架: evering
evering 是受 io_uring 启发的的异步通信框架,作为本文实现异步 IPC 的基石,它
- 🦀 基于 Rust 异步模型
- 🐧 基于共享内存
- 📡 提供双向 SPSC 消息队列
- 🎬 提供灵活的资源管理机制
evering 使用两个单生产者单消费者(SPSC)并发队列进行通信,
1 | Client Server |
受限于 SPSC 通信,目前 evering 只能遵循 thread-per-core 模型,不同于 tokio、async-std 等使用 work-stealing 的运行时.在基于 thread-per-core 的异步模型中,每个线程有一个局部的运行时,不同线程之间保持最小的通信,甚至完全独立.而基于 work-stealing 的异步模型中,任何异步任务都可能由于运行时的调度而被“偷”到其他线程上运行.二者相比有以下异同:
- 在 thread-per-core 模型中,异步任务不必担心同步问题,即在 Rust 中不需要使用诸如
impl Send + Future的类型,这可以大大简化异步编码体验.而在 work-stealing 模型中,几乎所有的异步任务都需要注意线程之间的同步,这样难免会带来一些额外的开销,也使得异步编码更为繁琐^1. - 正如 work-stealing 这名称所示,在此类模型中,运行时会根据程序实时状态对不同线程上的异步任务进行负载均衡,使得每个线程都不会陷入长时间的忙碌或空间.而对于 thread-per-core 模型,由于异步任务不能在多线程之间共享,当处理某个 CPU 密集型任务时,很可能因为长时间阻塞而导致该线程上的其他任务迟迟得不到处理,最终出现较高的响应延迟.因此 thread-per-core 不适合 I/O 密集混合 CPU 密集的场景.
实验性异步 IPC 模型: evering-ipc
evering-ipc 在 evering 的基础上,同时利用 Linux 的共享内存机制实现了异步 IPC.具体而言,要通信的两个进程使用以下所示的共享内存区进行同步:
1 | .-----------------------------------------------------------------------------. |
- [1] 是整个共享内存区的起始,通过 mmap(2) 映射到地址空间中,因此是页对齐的.
- [1] 中包含 evering 的两个通信队列的偏移量,基于这个偏移量配合 evering 提供的构造队列的接口,可以确保两个进程看到的状态是相同的.
- [2] 中包含一个单线程的内存分配器,这个分配器通常由客户端使用.
- [3] 是通信队列的实际所在位置.
- [4] 是其余全部的空闲内存,这些内存由 [2] 中的分配器管理.
在 evering-ipc 中,uring 仅用于传递静态类型且通常仅有十几到几十个字节的消息.而对于更大的以及编译期间无法确定大小的数据,它利用共享的空闲内存来传递.具体而言,
- 客户端获取内存分配器,从中分配合适的内存块,并写入请求体作为待传递数据.
- 客户端将该内存块指针以及其他信息构造成请求头作为消息写入请求队列.
- 服务端接收并解析请求,得到请求头和请求体.
- 服务端处理请求,将处理结果构造响应头作为消息写入响应队列.
- 客户端收到响应,检查结果并执行后续流程.
当服务端同样也需要传响应体作为数据时,有多种思路可以实现:
- 将分配器加锁使它能同时在服务端和客户端使用.对于请求频率特别高的场景,锁可能会降低整体性能.
- 增加一个分配器供服务端使用.对于空闲内存分配比例的设定可能不容易权衡.
- 结合前两个思路,使用两个分配器,但空闲内存加锁,根据程序运行状况动态调整分配给两方的空闲内存.
- 空闲内存完全由客户端管理,在请求时,客户端分配合适大小的内存块供服务端写入响应体.实际中,客户端可能需要预请求一次来确定响应体的大小.
evering-ipc 目前采用第 4 种思路.此外,不难发现,在 evering-ipc 中,数据都是通过指针进行传递的.而在程序运行时,共享内存通常被映射到不同的地址,因此实际传递的是数据块在共享内存中的偏移量.同时,共享内存区的起始地址都是页对齐的,这保证了所有基于相对偏移量得到的地址,即使在两个进程间不同,对齐也是一致的,从而满足 Rust 对不同类型内存布局的要求.
多种 IPC 方案的性能测试: ipc-benchmark
ipc-benchmark 针对多种 IPC 方案进行了关于通信延迟的性能测试,这些方案包括:
- 基于 evering + memfd 的 IPC
- 基于 shmipc + memfd 的 IPC
- 基于 tokio + epoll + UDS 的 IPC
- 基于 tokio + io_uring + UDS 的 IPC
- 基于 monoio + io_uring + UDS 的 IPC
其中,
- memfd 是 create_memfd(2) 创建的匿名内存.
- UDS 是指 Unix Domain Socket.
- shmipc 是基于共享内存的高性能 IPC 框架.
- tokio 是基于 epoll(7) 和 work-stealing 模型的异步 I/O 运行时.
- tokio-uring 为 tokio 提供了基于 io_uring 的异步 I/O 实现.
- monoio 是基于 io_uring 和 thread-per-core 模型的异步 I/O 运行时.
对于每个框架我们采用如下测试方案:
- 启动两个线程作为客户端和服务端.
- 客户端写入
i32的请求头以及大小为buf_size的请求体. - 服务端接收并校验请求头和请求体,随后同样写入
i32的响应头和buf_size大小的响应体. - 客户端接收并校验响应头和响应体.
其中,buf_size 以字节为单位,由 4B 逐渐增长到 4M.
结果对比
详细的性能测试的结果见 https://github.com/loichyan/openoscamp-2025s/tree/ipc-benchmark,下面我们将对该结果进行简略的分析.
此图对比了完整的测试结果,其中 $x$ 轴是上述的 buf_size,而 $y$ 轴则是完成一次测试所需的平均时间.可以看到,随着数据大小的增长 evering 和 shmipc 明显优于其他三者,并且相对于表现最差的 tokio_epoll,二者分别有接近 50% 和 30% 的性能提升.
此图对比了前五个测试的结果,此时数据并不算大,都在通常范围之内.这里能发现相对于另外三者,evering 和 shmipc 都有超过 80% 的性能提升.
此图对比了中间五轮测试的结果,此时数据大小开始逐渐出现大幅度的增长.可以看到,除了 evering 和 shmipc 外的三者针对大块数据的传输并无明显差异.
此图对比了最后五轮测试的结果,此时数据大小已接近极端情况.这里能观察到与第一个对比图同样的结果.
测试结论
单从性能的角度来看,对于上述五种 IPC 方案,evering > shmipc >> monoio > tokio_uring > tokio_epoll.
对比前两者,shmipc 支持 MPSC(多生产者多消费者)的通信方式,而本测试中仅使用 SPSC 的模型进行测试,因此无法发挥其完整的优势.另外,对共享内存处理的方式不同也可能导致了一些性能差异.而对于另外三者,由于使用 UDS 需要将数据在用户空间和内核空间来回拷贝,在面对大块数据时,这将大大降低整体性能.而对于极小的数据块,又由于系统调用等带来的开销,最终需要接近 10 倍的额外时间来完成测试.这一点可以在火焰图^2中体现:
| evering (buf_size=4B) | tokio-epoll (buf_size=4B) |
|---|---|
![]() |
![]() |
此图中展示了在 4B 数据下,性能测试主体函数中各子过程的占比.其中,蓝色高亮部分是校验数据过程,用作参照.不难发现,evering 中主要时间都消耗在传递消息所需的多线程同步上了.而在 tokio-epoll 中则是多个与内核交互的函数调用占用主要时间.在后几轮测试中,当数据变得非常大时,这些消耗则变得无关紧要,此时的性能热点是数据传递引起的内存拷贝.下面的火焰图可以佐证:
| evering (buf_size=4M) | tokio-epoll (buf_size=4M) |
|---|---|
![]() |
![]() |
此图与上面两个图相同,不过这里的数据大小是 4M.很明显,当数据非常大时,evering 中绝大部分时间用来初始化需要传递的数据,但传递的过程几乎不占用太多时间.而 tokio-epoll 中的情况更加复杂,除了拷贝数据以外,还花费了相当一大部分时间执行内存分配,这些内存用于放置从内核空间传递来的数据.
至于后三者的性能差别,我们猜测主要是由于:
- 设计架构不同,monoio 是单线程的 thread-per-core 模型,因此与我们的测试相性更好.
- 基于 io_uring 实现的异步 I/O 相对于基于 epoll 的,理论上会花费更少的时间在与内核交互上.并且测试中我们利用 io_uring_register(2) 做了进一步的优化,减少了传递小数据时内核处理的开销.
未完成的任务
让 evering 支持 MPSC
正如开头所述,evering 目前只实现了 SPSC 队列,受限于此,不能很好的处理复杂的应用场景.而实现 MPSC 队列之后,客户端就能更好的与 tokio 等运行时协同工作.要实现这个目标,主要是对 evering 的队列结构 RawUring 和操作驱动结构 Driver 进行多线程改造,并且尽可能要实现无锁的数据结构.对于 RawUring 的改造可以参考现有的并发队列实现.而 Driver 底层依赖于 slab::Slab 结构体,因此改造难度稍高.不过,目前 evering 已经支持使用固定大小的 Slab,基于这一点可以大幅降低多线程化的难度.
基于 evering 实现系统调用
evering-ipc 只适用于用户进程之间的通信,而本项目最初的目标是实现用户进程和内核的通信.对于这个目标,除了处理共享内存以外,可能还会遇到页表隔离的问题,以及用户进程意外退出后,对应内核异步任务的清理问题.更长远来看,当实现异步系统调用之后,那么可以将用户进程转彻底换成类似 Rust 中 Future 的无栈协程,从而将开销进一步降低.此外,迁移到异步系统调用时,不可避免的会破坏与现有同步代码的兼容性.因此,实现基于有栈绿色线程的异步系统调用也是一个值得尝试的目标.
第四阶段总结-邵卓炜
Posted on
Edited on
基于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]
2025春夏开源操作系统训练营四阶段总结报告-Wanderlust
Posted on
Edited on
序言
非常高兴能参加开源操作系统训练营第四阶段的学习,并与大家共同进步。
在此,我首先要感谢陈渝老师,您在每周学习中给予的指导和鼓励,成为了我前进的坚实支柱。同时,我也非常感谢其他同学,在第四阶段的学习中,我从大家那里学到了很多,无论是便捷地获取学习资料和代码,还是在遇到疑惑时能找到理解并深入探讨技术的伙伴,都让我受益匪浅。
经历完这四个阶段,我取得了显著的收获:不仅深刻理解了操作系统内部的运行机制,更掌握了通过组件化管理实现现代化操作系统的方法。具体而言,我成功完成了arceos-org/oscamp的aarch64架构支持,并为starry-next适配了iperf的TCP部分。
任务1 完成arceos-org/oscamp对aarch64架构的支持
使用
QEMU的monitor info mtree和monitor info block找出pflash区域,并在aarch64-qemu-virt.toml中增加正确的映射区域,在tour代码中写入正确的PFLASH_START增加了
aarch64部分的uspace代码,进行适配对makefile内的规则进行修改,修改
payload、mk_pflash对其他架构进行适配增加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对其他架构也能适配增加aarch64的CI测试,并测试通过
任务2 为starry-next适配iperf
提交https://github.com/oscomp/starry-next/pull/56/files 。修复了
oscomp/starry-next中的c中的sockaddr和arceos/axnet中的SocketAddr的类型转换问题,改为使用trait直接将sockaddr转换为SocketAddr,而不需要加一个中间层SockAddr,并测试通过。已合并。提交https://github.com/oscomp/testsuits-for-oskernel/pull/52 。在为
starry-next兼容iperf的过程中,我发现一个段错误问题。具体来说,如果在cJSON_New_Item函数中未对全局变量global_hooks进行初始化,会导致空指针访问。然而,当我单独编译cJSON的相关代码时,并未复现此异常。我推测这可能是由于编译为 ELF 文件时,编译器进行了某种优化所致。将global_hooks的初始化操作增加到cJSON_New_Item函数的起始位置后,该段错误便得以消除。musl无openssl库,使用build_riscv.sh, 进行openssl库的交叉编译创建iperf_wrap, 进行本地编译载入测试
在
arceos/modules/axfs/src/mount.rs中增加/dev/urandom的挂载,并增加了一个简单的urandom的实现修改iperf中
autoreconf的configure.ac,增加--disable-xxxx选项的支持实现可增加
--disable参数去除部分 Linux 特有的选项如SO_MAX_PACING_RATE、SO_BINDTODEVICE、IP_MTU_DISCOVER等,为交叉编译提供支持,参考 build.sh,宏定义生成结果可通过src/iperf_config.h查看,也为调试提供方便。允许用户在配置
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
fiAC_ARG_ENABLE([have-dont-fragment], ...)定义了--disable-have-dont-fragment选项。
如果用户指定了--disable-have-dont-fragment,则disable_have_dont_fragment变量被设置为true。
如果disable_have_dont_fragment为true,则会发出警告,并且不会定义HAVE_DONT_FRAGMENT宏。
否则(用户未禁用),并且如果Autoconf之前的检查 (iperf3_cv_header_dontfragment) 确认系统支持IP_MTU_DISCOVER等选项,则会定义HAVE_DONT_FRAGMENT宏。针对 Linux 特有的套接字选项(如
SO_MAX_PACING_RATE、SO_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.ac是Autoconf的输入文件,它需要被处理才能生成实际的configure脚本。这个处理过程就是通过运行autoreconf命令来完成的。autoreconf命令会执行一系列工具(如aclocal,autoconf,autoheader,automake等),它们会:- 处理
configure.ac: 将configure.ac中的Autoconf宏转换为可执行的shell脚本代码,生成configure脚本。 - 生成
config.h.in: 如果你的configure.ac中使用了AC_CONFIG_HEADERS,autoheader会根据AC_DEFINE等宏生成config.h.in文件,这是一个模板文件,最终会被configure脚本处理成config.h。 - 处理
Makefile.am: 如果项目使用了Automake,automake会处理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
........
在
api/src/imp中进行syscall的适配对于跨平台elf调试,使用
1
2int i = 1;
assert(i == 0);进行手动打断点结合
printf一步步调试,最终找到https://github.com/oscomp/testsuits-for-oskernel/pull/52 的段错误的具体问题。iperf3测量原理- 基本工作流程:
- 服务器端启动: 一台机器作为服务器端,启动
iperf3并监听特定端口,等待客户端连接。 - 客户端启动: 另一台机器作为客户端,启动
iperf3并指定服务器的IP地址和端口,发起连接请求。 - 数据传输: 连接建立后,客户端或服务器(取决于测试模式)开始发送数据包。
- 性能测量: 双方在数据传输过程中记录时间、传输数据量、丢包等信息。
- 结果报告: 传输结束后,客户端和/或服务器会计算并报告测量的网络性能指标。
- 在本机
apt install iperf3后,自动安装并自启动了/usr/lib/systemd/system/iperf3.service
text 1
2
3
4
5
6
7
8
9
10iperf3.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虚拟网卡 ->qemuNAT 转换 -> 主机物理网卡 -> 主机iperf3服务器。这种测试测量的是虚拟机到主机之间的网络性能,包括
qemuNAT 层的开销。无论哪种场景,
iperf3的基本客户端-服务器通信原理不变。qemu的 NAT 模式只是在网络路径中增加了一个虚拟化的层,iperf3测量的是经过这个虚拟化层后的实际吞吐量。关键设计点:
处理程序中断信号(如
Ctrl+C)的机制。它使用了signal和setjmp/longjmp组合来实现非局部跳转,以便在接收到中断信号时能够优雅地退出并报告结果。iperf_catch_sigend函数1
2
3
4
5
6
7
8
9
10
11
12
13void
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
5iperf_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
6static 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);: 这是核心操作。当SIGINT、SIGTERM或SIGHUP信号被捕获时,这个函数会被调用,然后它会执行longjmp。 longjmp会将程序的执行流从当前位置(信号处理函数内部)直接跳转到setjmp(sigend_jmp_buf)所在的位置。
- 这是实际的信号处理函数。
iperf_got_sigend函数- 捕获到中断信号后,实际执行清理、报告和退出的函数
这段代码实现了一个健壮的信号处理机制,确保
iperf3在接收到中断信号(如Ctrl+C)时,能够:- 立即停止当前的数据传输。
- 收集并报告截至中断时的所有统计数据。
- 通过控制连接通知另一端的 iperf3 进程,以便对方也能感知到测试的结束并进行相应的处理。
- 最终优雅地退出程序。
适配成功:


2025春夏季开源操作系统训练营第四阶段学习总结-elebirds
Posted on
Edited on
oscamp_stage4_summary_wyd
Posted on
Edited on
序言
非常高兴能参加今年的操作系统训练营,经过这四个阶段的学习,对操作系统的理解逐渐加深。在此过程中,对陈渝老师和郑友捷老师的耐心指导表示衷心感谢。
在第四阶段的学习中,我主要做了以下工作:
starry-next支持新特性:大页分配
后端映射实现
- 修改后端数据结构,为
Linear和Alloc这两种映射方式增加对齐参数align,该参数的类型为PageSize。根据对齐方式,分配相应的内存块。 - 修改物理页帧的分配方法
alloc_frame,以4KB为单位,根据对齐参数大小,计算页面数量num_pages,调用全局分配器,分配num_pages个连续的物理页面,将这段连续内存的地址返回并映射到页表中,作为一整个页面。 - 修改释放方法
dealloc_frame,基于以上alloc_frame,物理页帧的释放同理,根据对齐参数,以4KB为单位,计算需要释放的物理页帧数量,并调用全局分配器,释放这一段连续的物理内存。 - 修改空闲内存查找方法
find_free_area,为适配不同的对齐要求,该函数首先对建议的起始地址hint进行对齐,然后执行两轮扫描,第一轮扫描处理hint之前的区域,以确定起始搜索位置,第二轮扫描检查各内存区域之间的空隙,跳过重叠和已占用的区域,检查满足对齐要求和大小的区域,最后检查末尾区域,验证并返回找到的地址。 - 修改
unmap方法,考虑取消映射的内存区域可能存在不同对齐的情况,对unmap方法进行改进。首先验证起始地址和大小是否满足4K对齐,然后查找定位取消映射的内存块,根据每个内存块的对齐要求,验证对齐,最后执行取消映射。 - 增加一个
page_iter_wraper.rs文件,包装PageIter4K,PageIter2M,PageIter1G为一个PageIterWrapper,方便遍历。 - 对线性映射,文件读写,DMA和懒分配的支持,在相关函数如
new_linear,process_area_data,handle_page_fault中增加对齐参数支持不同页面的对齐要求。
- 修改后端数据结构,为
内存扩展
- 为测试大页尤其是1G大页的分配,读写和回收等情况,需要扩展平台内存以运行测例。
- 修改
aarch64架构配置文件,修改其物理内存大小为4G - 修改
riscv64架构配置文件,修改其物理内存大小为4G - 修改
loongarch64架构的配置文件,扩展其物理内存,并修改其物理基址和内核基址以避免内存重叠 - 修改
x86_64架构的配置文件,扩展其物理内存,在mmio-regions新增高地址支持,修改modules/axhal/src/mem.rs文件,新增free_memory_region,扩展x86_pc中的multiboot.S支持高位地址空间
- 修改
- 扩展内存之后在所有架构下测试
- 将以上所做的修改工作落实成文档提交
- 为测试大页尤其是1G大页的分配,读写和回收等情况,需要扩展平台内存以运行测例。
分析和改进axalloc
- 分析axalloc代码逻辑,撰写文档
- 改进axalloc
- 新增buddy_page.rs,基于buddy算法,实现了一个页分配器,并添加相关的测试模块。实现思路如下:
- 初始化过程:将起始地址和结束地址对齐到页边界,内存基址对齐到1G边界(与arceos的
BitmapPageAllocator保持一致),将整个内存区域分解为最大可能的2的幂次个块。 - 分配算法:检查需要分配的页面数量和页面大小,验证其是否与
PAGE_SIZE对齐,根据所需页面数量和对齐,计算所需阶数,查找可用块,将大块分割到所需大小,将多余部分加入对应的空闲链表,标记页面为已分配,更新使用统计。 - 释放与合并算法:首先标记当前块为空闲,然后递归检查伙伴块是否空闲,如果是则合并,将最终合并的块加入到对应阶数的空闲链表。
- 初始化过程:将起始地址和结束地址对齐到页边界,内存基址对齐到1G边界(与arceos的
- 新增tests.rs,提供一个基准测试模块,用于比较
BitmapPageAllocator和BuddyPageAllocator两种页分配器的性能表现。该测试模块包含两个主要测试函数,用于评估不同分配器在碎片化和内存合并方面的表现 ,测试结果如下:
- 合并效率测试结果:两个分配器都显示了 100% 的合并效率,这意味着它们都能完美地将释放的单页重新合并成大的连续内存块
- 碎片化测试结果分析:
- 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 表现更好
- Random Small Pattern(随机小块分配):
- 新增buddy_page.rs,基于buddy算法,实现了一个页分配器,并添加相关的测试模块。实现思路如下:
开源操作系统训练营第四阶段总结-改进Starry文档
Posted on
Edited on
开源操作系统训练营第四阶段总结-改进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 的设计理念和目标。在完善文档的过程中,郑友捷同学也给予了我很多建议和指导,在此表示感谢。
我的工作具体如下:
PR#23 修复了指导书不能切换主题的bug, 可以切换到深色模式,便于完善文档时的调试(一边黑一边亮容易被吸引注意力)。
PR#24 在阅读完郑同学的论文之后,修改了文档的欢迎页,说明了 StarryOS 和 ArceOS 之间的关系和差别。因为从第三阶段到第四阶段,作为初学者的我一开始是一头雾水,不知道 Starry 要实现一个什么样的目标,所以我也在欢迎页添加了设计目标的说明(仍需完善)。在我与郑同学的交流中感觉到 Starry 的目标可能是:
在 crate 层中希望能够开发一些独立的组件,一部分就像 ArceOS 一样,能够被任意类型内核复用, 一部分则是能够被其他的宏内核使用。
使 Starry 能够兼容 Linux 程序,即提供 POSIX API 接口。
完成作为一个宏内核该有的功能,包括进程管理、信号处理等。
作为一个第一次接触 Starry 的开发者,我觉得可能还需要一个更宏大的目标,或者更明确的商用的可能性来吸引更多的人加入我们的开源社区,并做出贡献。
pr#25 在进入到第四阶段的时候,配置完环境后,只是按照 README.md 中给的指令运行了一遍,但是依然没有理解具体 Starry 究竟做了些什么,他的目标是什么。而且我在创建镜像的过程中遇到了一些问题——loop设备满了导致无法加载,我也是通过读了一遍Makefile的流程才定位到这个错误。因而我写了一个一个案例快速上手 Starry,介绍了这些指令内部的一些细节,帮助初学者快速理解。PS: 今天我再读的时候发现了其中的一些错误(镜像文件应该是给QEMU加载的,不是ArceOS的文件系统),而且没有介绍案例 nimbos 的主要作用,可能对于学习过操作系统的同学来说,不用说也知道是一个测试操作系统功能的测试集,从一个门外汉的角度来看,他就是测试了几个testcase而已。我会在后续的工作中对文档进行修正。
pr#26 可能对于大佬来说,各种 git 指令都已经理解透彻,烂熟于心,但对于初学者来说,只会用一个
1
2
3git add .
git commit "update XXX"
git push在这之前我都没有创建过分支来提交PR,导致了一些混乱,因此在附录中添加了一个创建分支提交PR的标准流程。此外对于理解StarryOS究竟在做什么,理解他的工作流很有必要,因此我也在附录中添加了对于工作流相关的说明。
总结和将来的工作
由于时间不多,我的水平有限,相比其他同学对社区的贡献,我的工作可能微不足道。如果社区的大佬们不嫌弃,入职工作之后我也愿意继续帮助完成文档(主要因为这个项目里面用到的Rust语法很全面,我想要学习rust, 还包含了很多汇编和c,将来也能对用rust驱动硬件做为一个参考)。
可能有一部分同学和我一样,在阶段一到阶段三主要注重于做题(阅读测例->知道预期的结果是什么->查看相关的接口和需要用到的函数->实现功能),完成任务即可。没有特别理解整体的设计和原理,因而到了阶段四之后没有了题目之后感到迷茫。
因而为了帮助第三阶段的同学能够丝滑的进入到第四阶段,我觉得当务之急是需要完善ArceOS的文档(我第三阶段完全就是参考PPT完成的,对于很多细节没有掌握)我接下来的工作就是理解ArceOS的细节,说明Starry如何使用了ArceOS的接口和模块。然后首先实现文档整体框架的从无到有,最后在掌握了细节和整体设计思路之后再完善指导书并修正其中的错误。也希望有大佬能够一起加入到完善文档队伍中,一起交流。
2025年春夏开源操作系统训练营前三阶段总结-moot
序言
过去一个月,由于我对 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 的支持这一项工作需要完成以下的功能实现:
- 改进 context.rs,保存相关的寄存器,并完善 context_switch 和 enter_uspace。
- 保存/恢复的寄存器集对齐 SysV x86‑64 调用约定:
RBX RBP R12‑R15+CR3+RFLAGS;FPU/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。
- 保存/恢复的寄存器集对齐 SysV x86‑64 调用约定:
- 改进 trap.S
- IDT 256 项:
0x20时钟、0x80软中断、0x0E#PF……全部指向统一的trap_entry;硬中断通过 APIC 自动切到 IST[0] emergency stack 防止内核栈溢出。 trap.rs根据向量号派发到handle_page_fault / handle_irq / handle_syscall。
- IDT 256 项:
- 改进 syscall.rs 和 syscall.S
- 选
SYSCALL/SYSRET而非INT 0x80;入口先swapgs用 GS 保存/恢复用户栈。 - 按 SysV ABI
RAX=nr,RDI RSI RDX R10 R8 R9传六参 —— 在汇编里把寄存器序列化到栈,统一传给x86_syscall_handler()。 - 退出路径:恢复通用寄存器 →
swapgs→sysretq
- 选
第四周
对内核组件 x86_rtc 说明文档和测试的完善。
repo
未来展望
- Transparent HugePages:复用前期完成的巨页 API,引入
khugepaged合并线程。 - vDSO:把高频
clock_gettime胶水放到用户态,加速 Sys‑API。
Lfan-ke: GPU GPGPU GPGPGPU!
解析`WebGPU`、`Virtio1.2`规范,以及`WGPU`、`Vortex`源码,探索异步操作系统与内核态GPU资源管理的结合方案。
2025 oscamp blog of hailong
Posted on
Edited on
第一阶段 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 | fn sys_mmap( |
参数addr为0时要特别注意,需要寻找一空间,还有地址和size的对齐。返回值,当开始时认为0是成功,为负时,返回失败原因,后来再三确认失败返回0,成功返回地址,isize作为地址返回,有点不符合直觉?
Next
期待第四阶段



