比原生更快:在 Linux 内核中运行 WebAssembly
过去的几个月间,我们尝试了各种方法来给 Wasmer WebAssembly 运行环境提速。这些方法包括缓存编译后的代码、实现不同等级的编译后端(Singlepass/Cranelift/LLVM)等,也都取得了不错的效果。
这些优化性能的尝试使我们开始考虑一个更加“基础”的问题:基于 VM(虚拟机)的程序与原生程序相比,有哪些优势?我们是否可以让 WASM 运行得比原生代码更快?
这篇文章将介绍我们在 Linux 内核中实现的 WebAssembly 安全运行环境。我们在 Linux 内核中成功运行了一个 TCP Echo 服务端程序,并取得了相对原生代码 10% 的性能提升。
背景
- “第二个操作系统“
许多语言和运行环境,包括 WebAssembly(支持 WASI 的实现)和 JavaScript (Node.js 和浏览器)等,都在尝试于真实的操作系统之上构建第二个沙箱化的“操作系统”。然而,这多出来的一层会带来不小的性能损耗。
如上图所示,在这样的传统架构中,来自 VM 上应用的系统服务请求(系统调用),在到达内核前,需要经过两层边界。
这两层边界的性能损耗很大。一个普通的函数调用所需时间一般小于 5 ns,然而一次来源于 VM 内部的系统调用可能会消耗上百纳秒。
- Cervus 的后继者
我大约一年之前写过另一个内核中的 WebAssembly “用户模式”子系统 - Cervus 。那时候 WASI 和“生产级别”的 WebAssembly 运行时都还不存在,但 Cervus 项目已经证明这个想法是可行且有巨大潜力的。
现在 WebAssembly 生态正在快速成长,是时候做一个完整的、面向真实应用的内核模式 WebAssembly 运行环境了。
为什么要在内核中运行 WebAssembly ?
主要原因是性能和灵活性。
WASM 是由虚拟机保护的虚拟指令集。我们不需要依靠外部的软件/硬件保护来确保安全性。
在内核中运行 WASM 避免了这些外部保护引入的性能损耗,如系统调用(上下文切换)、用户态/内核态数据复制等。
同时,有了对底层的控制,我们可以实现很多在用户模式中低效或难以实现的特性,例如直接访问硬件、处理密集的内核事件(如网络包过滤)等。
安全性
在内核模式运行用户代码是件危险的事情。虽然我们用了很多技巧来保护系统免受恶意代码的攻击,我们仍然建议短期之内,在我们没有完整 Review 运行环境代码前,只通过这个模块执行可信的代码。
这里是一些已知的安全风险和我们的应对措施:
- 栈溢出:在代码生成环节插入边界检查代码
- 内存访问越界:为每个 WASM 任务分配 6GB 的虚拟地址空间,使越界访问无法表达。
- 信号无法终止处于内核态的进程:接收到终止信号后,将 WASM 代码页面设置为禁止执行(NX)以强制终止执行。
- 内核态进程浮点状态丢失:用 kernel_fpu_{begin,end} 与 preempt_notifier 手动保存和恢复浮点状态。
- 内核不支持 Red Zone :在代码生成器中避免使用之。
例子和性能测试
我们提供了两个例子:echo-server 和 http-server
。它们位于 Wasmer 主仓库的 examples 目录下。
当使用 singlepass
后端编译(无优化直接生成 x86-64 代码),并在本地使用 tcpkali
/wrk
测试时,echo-server 比它的用户模式等价实现快约 10% (25210 Mbps / 22820 Mbps) ,http-server 快约 6% (53293 Rps / 50083 Rps) 。
这两个例子使用了 WASI (文件抽象、控制台输出)和我们的异步网络扩展(通过 kernel-net 库)。
可以阅读这两个例子的代码,学习怎样编写在 kernel-wasm 中运行的高性能网络程序。
编译、运行
加载内核模块前,请确保:
- 你的内核版本大于等于 4.15
- 你的内核启用了抢占执行(preemption)。尝试在未启用抢占的内核上执行 WASM 用户代码会锁死你的系统。
- 内核头文件和构建环境已安装
首先,clone 仓库:https://github.com/wasmerio/kernel-wasm
然后在仓库的根目录和 networking
、 wasi 目录
下执行 make :
make
cd networking && make
cd ../wasi && make
cd ..
加载模块:
sudo insmod kernel-wasm.ko
sudo insmod wasi/kwasm-wasi.ko
sudo insmod networking/kwasm-networking.ko
运行 Wasmer 时选择 singlepass 后端和 kernel 加载器:
sudo wasmer run --backend singlepass --loader kernel the_file.wasm
(在 kernel-wasm 上运行的 cowsay )
- C#用链式方法表达循环嵌套1
- C#用链式方法表达循环嵌套2
- 【开源】1句代码搞定图片批量上传,无需什么代码功底【无语言界限】
- C#异步调用的方法
- 【David Silver 深度强化学习教程代码实战07】 DQN的实现
- AutoMapper随笔记
- 简析.NET Core 以及与 .NET Framework的关系
- 1627: [Usaco2007 Dec]穿越泥地
- 1651: [Usaco2006 Feb]Stall Reservations 专用牛棚
- 1615: [Usaco2008 Mar]The Loathesome Hay Baler麻烦的干草打包机
- Polyspace不认识Interrupt,肿么办?
- 1639: [Usaco2007 Mar]Monthly Expense 月度开支
- 1638: [Usaco2007 Mar]Cow Traffic 奶牛交通
- 1642: [Usaco2007 Nov]Milking Time 挤奶时间
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Linux下使用python脚本执行BCP导入导出操作
- 通用高效字符串匹配--Sunday算法
- Golang fmt Printf 格式化参数手册/详解/说明
- 正则表达式(RegEx)官方手册/权威指南【Python】
- 玩透二叉树(Binary-Tree)及前序(先序)、中序、后序【递归和非递归】遍历
- 如何写出优雅的 Golang 代码
- 检测代码潜在bug和质量之SonarQube
- 基于cephfs搭建高可用分布式存储并mount到本地
- rsync性能终极优化【Optimize rsync performance】
- 你不知道的Golang盲点汇总【持续更新】
- TCP SYN flood洪水攻击原理和防御破解
- 你不知道的Golang map
- go 1.11 模块和版本管理
- Golang Testing单元测试指南
- 你不知道的Go unsafe.Pointer uintptr原理和玩法