第四阶段总结报告
about-me: heke1228@gitee, heke1228@atom, Lfan-ke@github, heke1228@codeberg
本阶段从理解与熟悉Rust异步编程开始,探究内核态的
GPU/GPGPU
资源管理与异步操作系统的结合方案。
Rust异步编程
异步协程/纤程/微线程/绿色线程/虚拟线程/Future/Fiber/Promise/Coroutine/Goroutine/GreenTask/GreenThread/Microthread……名字各异(下文统一称:协程),但是表述的都是轻量级的用户态线程,挂起和恢复不涉及系统调用,开销小且灵活。使用方式在不同语言环境中大同小异,但是在实现上多多少少有不同。
Python的协程使用:
2006年通过PEP 342引入,利用生成器yield
实现协程,Py3.4正式引入asyncio
库,Py3.5正式协程标准化。到目前[2025.06.20 Py3.13]为止,Py协程仍然在不断发展,比如Py3.7引入的asyncio.run/create_task
、Py3.11引入的async with asyncio.TaskGroup() as tg
方法等等。
Py的协程源于yield
生成器,目前也是可以将async def
视为返回类生成器的coroutine
对象。await
相当于yield from
。同一个线程同一时间只会运行一个协程任务队列,可以使用new_event_loop
创建队列手动塞入不同的任务再使用set_event_loop
管理当前活跃的任务队列,倘若开启多个协程任务队列则会直接报错。事件循环由asyncio
库管理,用户直接使用高层API
即可:
1 | import asyncio |
C++的协程使用:
C++和Java的协程支持较晚,C++20才正式引入协程支持。通过co_await
、co_yield
和co_return
关键字实现,Java可以使用SE19的虚拟线程,也可以使用子语言比如Kotlin的协程支持。C++的协程和Rust相似,依赖编译器生成状态机代码,属于无栈协程。
1 | #include <cppcoro/task.hpp> |
JavaScript的协程使用:
之前的Js异步大多使用定时器/下帧调用实现,早期Promise形式的Promises/A规范率先在CommonJS社区流行,后续ECMA在ES6增加了Promises/A+规范的完善支持。在ES8之后正式引入了async/await
语法。Js的协程是单线程事件驱动模型,通过微任务队列调度。
1 | // 早期 Promise 链 |
Rust的协程使用:
Rust的协程基于Future Trait。Rust的Future得手动轮询poll函数实现才会执行。所以需要用户开发的运行时才会驱动执行。与C++类似,为无栈协程,会被编译为状态机模型,涉及唤醒模型的时候需要Awake Trait注册唤醒器,在任务均阻塞的时候避免CPU空转,而是被挂起等待被唤醒。常用的驱动库有:tokio、async-std等等。tokio 正在成为事实上的 Rust 异步运行时标准。
1 | use tokio::time::{sleep, Duration}; |
Rust异步运行时简易实现
由于Rust提供了Future接口,其余的调度策略等等等均由用户自定义,这样子可操作性就非常高。上述不同语言的协程实现思路均可以作为灵感来源。抛去官方的无栈协程概念不谈,也可以自己利用进程跳板的类似机制封装一个有栈协程调度器。这里实现一个简易的无栈协程调度器(暂时[2025.06]不涉及唤醒机制,优先级也是结构体多封装一个数字,使用优先队列存任务,所以只讲解最简单原型)。
目前方案及其简陋,是一个单线程的异步运行时模型,但是在合适的地方会提示多线程调度器或者其他优化的实现方案。
首先讲解Future Trait:
1 | pub trait Future { |
所有实现Future
特质的对象必须有poll
惰性轮询方法。就像你管理5个小朋友,你需要将他们的作业收起来交给老师。那么你为了尽快收齐作业,你会如何去做?当然是一遍一遍一个挨着一个问:”小朋友,你的周末作业写完了吗?“。Rust的异步类似,轮询的时候只有两个状态:写完了-Poll::Ready(Output)
和没写完再等等-Poll::Pending
。
1 | // 实际上在此基础上你可以封装更为复杂的轮询类型,比如:接收数据直到没有数据为止: |
你为了方便管理这5个小朋友,你在QQ拉了一个小群,同时有一些小朋友家里有节假日活动,得过几天才能继续完成作业,你为了避免打扰他们,给他们建了另外一个小群:
1 | let ready_queue = vec![student; 2]; |
你说:”完成作业的小朋友就可以退群!当然,有活动的小朋友在活动完成之后可以加入收集作业群一起讨论作业!“
这样子一来,便成为了:你常日里可以轮询作业群的小朋友:”写完了吗?“,写完就收集作业踢出群聊。在轮询结束就在每日晚问请假群的小朋友:”接下来可以加入作业群了吗?“
1 | // 上述的情况适用于单线程的轮询,为了节省CPU资源,检查sleep_queue的时候可以gap几百毫秒 |
但是很快,你发现你一直在push小朋友,你自己烦,小朋友也烦,所以有没有办法让他们准备好的时候告诉你,你再去将他们移动群聊?比如,告诉小朋友的家长:”你家孩子还没写作业,办完活动告我一声,给孩子拉到作业群“。或者直接给对方父母入群二维码,当他们一家游玩结束后自己加群,这样子就不会自己一直轮询一直问了。
1 | // 那么你现在就相当于spawn了一个额外的线程,设置了一个waker |
其中学校要统计学生节假日的行程,以确保孩子们安全,这个时候你就可以新建一个收集表,每天让孩子一家填写相关的事宜,当你发现有危险地区时就能及时阻止,或者孩子一家块回来了,就能让父母按照对应的方式提醒你:
1 | static VTABLE: RawWakerVTable = RawWakerVTable::new( |
比如上面的你需要父母按照某个方式提醒你,或者他们自己到时候自己加群:
1 | // 在 RawWakerVTable::new 的第二个函数参数位置写入: |
那么接下来,请假群节假日和父母出去玩的孩子加入作业群的策略就变成了:
1 | // 刚开始老师让你管理五个学生: |
接下来你只需要Poll作业群里的孩子们,让他们交作业即可了!当请加群和作业群都没人之后就是作业收齐了,就可以完成任务走人了!
后续如果想自己封装一个有特殊功能的Rust异步协程运行时可以参考:简易实现。其中如果需要异步的IO,可以基于tokio的子项目:mio进行组装,当然也可以自己基于硬件特性、操作系统特性封装唤醒机制,比如epoll、kqueue、iocp等等。
其中封装前对具体是路不是非常明确可以先行参考:利用std:net封装一个异步http客户端。此博客的思路,受益匪浅。
Rust异步爬虫的简单使用
就像py的aiohttp,rust也有自己的异步网络请求库:
1 | use reqwest; // reqwest = "0.12.15" |
之后利用tokio运行时运行异步任务即可。
Rust嵌入式异步框架介绍
Embassy是一款异步嵌入式开发框架。比RTOS更加轻量级,采用Rust的异步协程模型进行开发。其中包含一个异步执行器、一些硬件抽象层供不同板子的开发和一些异步硬件组件库:
- embassy-executor
- embassy-stm32、emmbassy-nrf、embassy-rp、esp-rs
- embassy-net、nrf-softdevice、embassy-lora、embassy-usb、embassy-boot
其中,直接使用PAC层编程比较繁杂,使用HAL层抽象编程便比较轻便简单:
1 |
|
其中Embassy的大卖点是异步框架:
1 |
|
补充内容:
WebGPU
WebGPU,WWW在2021年发布WebGPU的新API,以解决上述跨平台问题,真正的跨平台框架。WebGPU是WebGL的继任者,语法类似 Rust,支持更复杂的着色器功能。比VLK更容易使用,使用WGSL作为着色器语言。可以跨平台多端使用,不仅局限于Web场景。提供更高效、灵活、安全的图形编程接口。
其中Rust依据WebGPU规范有封装框架WGPU,可以利用便捷的接口来使用GPU的计算和渲染能力。
WebGPU规范概览
WebGPU是一个提供GPU能力调用的规范接口。其中GPU嘛,目前火热的就是进行渲染-Render
(比如:某3A大作震撼的特效渲染)和通用计算-GPGPU
(比如:人工智能模型要在某卡上训练/推理)。所以GPU的能力大致就归类为:
- Render Pass
- Compute Pass
其中无论是渲染还是计算,都需要外界代码指导GPU如何进行计算,这些外界代码被称为:着色器代码。但是一定是用户可读的代码吗?不一定,比如:SPIR-V着色器代码中间表示,大部分都是面向用户的Cuda C/OCL C代码(C扩展代码),WSGL代码等等。在送向GPU的时候会被编译为GPU可执行的字节码,供GPU取址译码执行(详见第三小节-GPU架构
)。
代码示例:
- wsgl
1 | struct Uniforms { |
- opencl c
1 | kernel void wildpointer(global uint * buffer) { |
可以看到,都需要buffer
(例子1的uniforms、pos等等,例子2的buffer),而buffer一般是由CPU将数据传输到GPU的,最后的结果也可以利用数据传输指令传回。这里提到了一个非常主要的资源:buffer-缓冲区
。
除了代码和缓存区外,渲染管线还可能需要以下的资源:
- 纹理 - texture - 比如你CF枪上的皮肤/建模次世代阴影等等
- 采样器 - sample - 决定纹理如何映射到面
- 图形管道/计算管道 - pipeline - 渲染和计算
- 组和布局 - bindgroup & layout - 决定数据在GPU是什么样子的,什么数据什么时候可读写
当你定义好对应的资源,以及缓冲区和代码后,就可以提交命令到管道,然后等待GPU执行渲染/计算了。
WGPU相关的介绍
WGPU是基于WebGPU规范封装的跨平台异步GPU能力调用的库。由于Rust可以非常方便的与C-BindGen/Web-WasmPack互通,WGPU可以被非常方便地跨各平台使用,安卓、手表、浏览器、小程序、桌面端、其他嵌入式设备等等等。
WGPU的项目库关系为:
1 | 用户代码(JS/TS) 用户代码(Rust) |
倘若WGPU直接编译在Web平台,则不会依赖wgpu-core,而是直接利用wasm调用WebGPU/WebGL接口。
其中用户层的vk, gles, mtl, dx12
直接利用了现有的crate
:ash, glow, metal, windows(winapi::um::d3d12)
,而这些库大多是靠bindgen-c/o-c
来绑定API的。之后被wgpu-hal
统一抽象为WebGPU
编程模型接口,不同的着色器语言被naga
编译为中间表示后按照目前所选后端转化为对应的表示。
这里以vulkan
为例,wgpu-hal
使用vk::Fence+Semaphores
来封装Future
的fn poll
来提供上层的async
能力。而wgpu-core
提供不安全的资源管理与交互。wgpu
顶层则将wgpu-core
安全化。
Rust的ash
通过c-abi
调用c-vk
,而vk
又是如何调用内核态的驱动以及如何驱动GPU设备进行计算的呢?
Vulkan Driver
会自带一个加载器,通过读取特定目录的json
来加载对应硬件的ICD
驱动。之后通过调用符合Vulkan
规范的ICD
驱动提供的函数接口来驱动GPU
进行渲染/计算。OpenCL
也类似。
GPU架构
src:VirtioGPUv1.2规范,Vortex,MIAOW,POCL
GPU,一个熟悉又陌生的芯片。阶段四为了完成目标分析文档,实现统一内核态异步GPU计算能力资源管理驱动,理解GPU的架构是非常必要的。它和CPU类似,都有取指译码执行访存错处,也有流水线冒险分支预测等等优化手段,但是与CPU相比,究竟是什么样子的结构呢?
VirtioGPU简易介绍
暂时略,有缘补,可以先参考rCore-ch9
的简易虚拟GPU设备
Vortex GPGPU介绍
一款基于RV架构的GPGPU。实现了OpenCL ICD及其测例,可以作为非常好的软硬一体的学习材料。
暂略,之后补,可以先看源码解析文档。
GPU驱动
远古时期的”GPU/UI”
远古时期,仅仅是一块简单的LED/LCD/OLED
小屏幕,像素较小,使用颜色矩阵就可以精确的控制每一个像素的颜色,只需要板子接电使用应用层协议比如IIC
传输指令即可。随着发展,每次都从计算某个像素的某点亮灭/颜色,过于麻烦,所以有了简易驱动,内部包含着绘制点线几何以及基本字体的代码和文件,此时UI编程便变成了发送指令:(x, y, w, h[, data])
来控制显示。随着用户的画面需求逐渐升级,3D渲染的需求激增,英伟达推出了一个硬件支持3D渲染的显卡,后续微软、苹果等等公司也推出了相应的3D图形API,如上文提到的D3D,MTL(此时还是OGL)。
随着算力激增,绘制图形的任务被高度抽象为了渲染。此时推出的渲染引擎都接口高度化,用户不能精细控制每一个细节,比如OpenGL
,而上述计算顶点与颜色的过程被抽象为:计算顶点,片段着色,光栅化,输出帧缓冲,显示。随着人工智能需求的算力激增,利用纹理存储数据、顶点变换模拟数学运算、使用帧缓冲作为输出结果:以逃课的方式使用GPU进行并行计算的大有人在,人工智能研究人员迫切需要一个“流计算”模型来并行计算大规模数据。2003年斯坦福提出BrookGPU,为GPGPU编程提供了抽象层,2006年英伟达闭源推出Cuda,此后利用GPU的并行计算能力的通用计算框架发展至今。
由于渲染绘制画面不是问题了,人们更多的开始关心如何显示的更加流畅美观,从此3D模型三角面越来越多,前端从画点画面逐渐变为了浏览器堆DIV组件、桌面端堆CMP等等。硬件变为了向人工智能通用计算助力的XPU,如:NPU等等,或者追求画质的光追RTC/RA等等。
通用计算与渲染引擎
暂略,之后补。
用户态驱动OCL介绍
暂略,之后补,可以先看适配RISC-V的POCL源码。