merge main

This commit is contained in:
2024-11-13 20:08:46 +08:00
297 changed files with 33644 additions and 1250 deletions

View File

@ -0,0 +1,13 @@
====================================
容器化
====================================
这里是DragonOS中与容器化相关的说明文档。
主要包括namespaceoverlayfs和cgroup
.. toctree::
:maxdepth: 2
namespaces/index
filesystem/unionfs/index

View File

@ -0,0 +1,14 @@
====================================
名称空间
====================================
DragonOS的namespaces目前支持pid_namespace和mnt_namespace 预计之后会继续完善
namespace是容器化实现过程中的重要组成部分
由于目前os是单用户user_namespace为全局静态
.. toctree::
:maxdepth: 1
pid_namespace
mnt_namespace

View File

@ -0,0 +1,19 @@
# 挂载名称空间
## 底层架构
pcb -> nsproxy -> mnt_namespace
每一个挂载文件系统都有自立独立的挂载点,表现在数据结构上是一个挂载的红黑树,每一个名称空间中挂载是独立的,所以文件系统的挂载和卸载不会影响别的
## 系统调用接口
- clone
- CLONE_NEWNS用于创建一个新的 MNT 命名空间。提供独立的文件系统挂载点
- unshare
- 使用 CLONE_NEWPID 标志调用 unshare() 后,后续创建的所有子进程都将在新的命名空间中运行。
- setns
- 将进程加入到指定的名称空间
- chroot
- 将当前进程的根目录更改为指定的路径,提供文件系统隔离。

View File

@ -0,0 +1,21 @@
# 进程名称空间
:::{note} 本文作者:操丰毅 1553389239@qq.com
2024年10月30日 :::
pid_namespace 是内核中的一种名称空间用于实现进程隔离允许在不同的名称空间中运行的进程有独立的pid试图
## 底层架构
pcb -> nsproxy -> pid_namespace
- pid_namespace 内有独立的一套进程分配器以及孤儿进程回收器独立管理内部的pid
- 不同进程的详细信息都存放在proc文件系统中里面的找到对应的pid号里面的信息都在pid中记录的是pid_namespace中的信息
- pid_namespace等限制由ucount来控制管理
## 系统调用接口
- clone
- CLONE_NEWPID用于创建一个新的 PID 命名空间。使用这个标志时,子进程将在新的 PID 命名空间内运行,进程 ID 从 1 开始。
- unshare
- 使用 CLONE_NEWPID 标志调用 unshare() 后,后续创建的所有子进程都将在新的命名空间中运行。
- getpid
- 在命名空间中调用 getpid() 会返回进程在当前 PID 命名空间中的进程 ID

View File

@ -13,4 +13,5 @@ todo: 由于文件系统模块重构文档暂时不可用预计在2023年4
vfs/index
sysfs
kernfs
unionfs/index

View File

@ -0,0 +1,10 @@
====================================
联合文件系统
====================================
Union Filesystem
OverlayFS 将多个文件系统(称为“层”)合并为一个逻辑文件系统,使用户看到一个统一的目录结构。
.. toctree::
:maxdepth: 1
overlayfs

View File

@ -0,0 +1,26 @@
# overlayfs
OverlayFs是目前使用最多的联合文件系统原理简单方便使用主要用于容器中
在 Docker 中OverlayFS 是默认的存储驱动之一。Docker 为每个容器创建一个独立的上层目录,而所有容器共享同一个下层镜像文件。这样的设计使得容器之间的资源共享更加高效,同时减少了存储需求。
## 架构设计
overlayfs主要有两个层以及一个虚拟的合并层
- Lower Layer下层通常是 只读 文件系统。可以包含多层。
- Upper Layer上层为 可写层,所有的写操作都会在这一层上进行。
- Merged Layer合并层上层和下层的逻辑视图合并后向用户呈现的最终文件系统。
## 工作原理
- 读取操作:
- OverlayFS 会优先从 Upper Layer 读取文件。如果文件不存在于上层,则读取 Lower Layer 中的内容。
- 写入操作:
- 如果一个文件位于 Lower Layer 中,并尝试写入该文件,系统会将其 copy-up 到 Upper Layer 并在上层写入。如果文件已经存在于 Upper Layer则直接在该层写入。
- 删除操作:
- 当删除文件时OverlayFS 会在上层创建一个标记为 whiteout 的条目,这会隐藏下层的文件。
## Copy-up
- 写时拷贝
当一个文件从 下层 被修改时,它会被复制到 上层(称为 copy-up。之后的所有修改都会发生在上层的文件副本上。
## 实现逻辑
通过构建ovlInode来实现indexnode这个trait来代表上层或者下层的inode具体的有关文件文件夹的操作都在

324
docs/kernel/trace/eBPF.md Normal file
View File

@ -0,0 +1,324 @@
# eBPF
> 作者: 陈林峰
>
> Email: chenlinfeng25@outlook.com
## 概述
eBPF 是一项革命性的技术,起源于 Linux 内核,它可以在特权上下文中(如操作系统内核)运行沙盒程序。它用于安全有效地扩展内核的功能,而无需通过更改内核源代码或加载内核模块的方式来实现。
从历史上看,由于内核具有监督和控制整个系统的特权,操作系统一直是实现可观测性、安全性和网络功能的理想场所。同时,由于操作系统内核的核心地位和对稳定性和安全性的高要求,操作系统内核很难快速迭代发展。因此在传统意义上,与在操作系统本身之外实现的功能相比,操作系统级别的创新速度要慢一些。
eBPF 从根本上改变了这个方式。通过允许在操作系统中运行沙盒程序的方式,应用程序开发人员可以运行 eBPF 程序,以便在运行时向操作系统添加额外的功能。然后在 JIT 编译器和验证引擎的帮助下,操作系统确保它像本地编译的程序一样具备安全性和执行效率。这引发了一股基于 eBPF 的项目热潮,它们涵盖了广泛的用例,包括下一代网络实现、可观测性和安全功能等领域。
## eBPF In DragonOS
在一个新的OS上添加eBPF的支持需要了解eBPF的运行过程通常eBPF需要用户态工具和内核相关基础设施配合才能发挥其功能。而新的OS通常会兼容Linux上的应用程序这可以进一步简化对用户态工具的移植工作只要内核实现相关的系统调用和功能就可以配合已有的工具完成eBPF的支持。
## eBPF的运行流程
![image-20240909165945192](./ebpf_flow.png)
如图所示eBPF程序的运行过程分为三个主要步骤
1. 源代码->二进制
1. 用户可以使用python/C/Rust编写eBPF程序并使用相关的工具链编译源代码到二进制程序
2. 这个步骤中用户需要合理使用helper函数丰富eBPF程序功能
2. 加载eBPF程序
1. 用户态的工具库会封装内核提供的系统调用接口以简化用户的工作。用户态工具对eBPF程序经过预处理后发出系统调用请求内核加载eBPF程序。
1. 内核首先会对eBPF程序进行验证检查程序的正确性和合法性同时也会对程序做进一步的处理
1. 内核会根据用户请求将eBPF程序附加到内核的挂载点上(kprobe/uprobe/trace_point)
1. 在内核运行期间,当这些挂载点被特定的事件触发, eBPF程序就会被执行
3. 数据交互
1. eBPF程序可以收集内核的信息用户工具可以选择性的获取这些信息
2. eBPF程序可以直接将信息输出到文件中用户工具通过读取和解析文件中的内容拿到信息
3. eBPF程序通过Map在内核和用户态之间共享和交换数据
## 用户态支持
用户态的eBPF工具库有很多比如C的libbpfpython的bcc, Rust的Aya总体来说这些工具的处理流程都大致相同。DragonOS当前支持[Aya](https://github.com/aya-rs/aya)框架编写的eBPF程序以Aya为例用户态的工具的处理过程如下:
1. 提供eBPF使用的helper函数和Map抽象方便实现eBPF程序
2. 处理编译出来的eBPF程序调用系统调用创建Map获得对应的文件描述符
3. 根据需要更新Map的值(.data)
4. 根据重定位信息对eBPF程序的相关指令做修改
5. 根据内核版本对eBPF程序中的bpf to bpf call进行处理
6. 加载eBPF程序到内核中
7. 对系统调用封装提供大量的函数帮助访问eBPF的信息并与内核交互
DragonOS对Aya 库的支持并不完整。通过对Aya库的删减我们实现了一个较小的[tiny-aya](https://github.com/DragonOS-Community/tiny-aya)。为了确保后期对Aya的兼容tiny-aya只对Aya中的核心工具aya做了修改**其中一些函数被禁用因为这些函数的所需的系统调用或者文件在DragonOS中还未实现**。
### Tokio
Aya需要使用异步运行时通过增加一些系统调用和修复一些错误DragonOS现在已经支持基本的tokio运行时。
### 使用Aya创建eBPF程序
与Aya官方提供的[文档](https://aya-rs.dev/book/start/development/)所述只需要根据其流程安装对应的Rust工具链就可以按照模板创建eBPF项目。以当前实现的`syscall_ebf`为例这个程序的功能是统计系统调用的次数并将其存储在一个HashMap中。
```
├── Cargo.toml
├── README.md
├── syscall_ebpf
├── syscall_ebpf-common
├── syscall_ebpf-ebpf
└── xtask
```
在user/app目录中项目结构如上所示
- `syscall_ebpf-ebpf`是 eBPF代码的实现目录其会被编译到字节码
- `syscall_ebpf-common` 是公共库,方便内核和用户态进行信息交互
- `syscall_ebpf` 是用户态程序其负责加载eBPF程序并获取eBPF程序产生的数据
- `xtask` 是一个命令行工具,方便用户编译和运行用户态程序
为了在DragonOS中运行用户态程序暂时还不能直接使用模板创建的项目
1. 这个项目不符合DragonOS对用户程序的项目结构要求当然这可以通过稍加修改完成
2. 因为DragonOS对tokio运行时的支持还不是完整体需要稍微修改一下使用方式
```
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
```
3. 因为对Aya支持不是完整体因此项目依赖的aya和aya-log需要换成tiny-aya中的实现。
```
[dependencies]
aya = { git = "https://github.com/DragonOS-Community/tiny-aya.git" }
aya-log = { git = "https://github.com/DragonOS-Community/tiny-aya.git" }
```
只需要稍加修改就可以利用Aya现有的工具完成eBPF程序的实现。
## 内核态支持
内核态支持主要为三个部分:
1. kprobe实现位于目录`kernel/crates/kprobe`
2. rbpf运行时位于目录`kernel/crates/rbpf`
3. 系统调用支持
4. helper函数支持
### rbpf
由于rbpf之前只是用于运行一些简单的eBPF程序其需要通过一些修改才能运行更复杂的程序。
1. 增加bpf to bpf call 的支持:通过增加新的栈抽象和保存和恢复必要的寄存器数据
2. 关闭内部不必要的内存检查,这通常由内核的验证器完成
3. 增加带所有权的数据结构避免生命周期的限制
### 系统调用
eBPF相关的系统调用都集中在`bpf()`通过参数cmd来进一步区分功能目前对其支持如下:
```rust
pub fn bpf(cmd: bpf_cmd, attr: &bpf_attr) -> Result<usize> {
let res = match cmd {
// Map related commands
bpf_cmd::BPF_MAP_CREATE => map::bpf_map_create(attr),
bpf_cmd::BPF_MAP_UPDATE_ELEM => map::bpf_map_update_elem(attr),
bpf_cmd::BPF_MAP_LOOKUP_ELEM => map::bpf_lookup_elem(attr),
bpf_cmd::BPF_MAP_GET_NEXT_KEY => map::bpf_map_get_next_key(attr),
bpf_cmd::BPF_MAP_DELETE_ELEM => map::bpf_map_delete_elem(attr),
bpf_cmd::BPF_MAP_LOOKUP_AND_DELETE_ELEM => map::bpf_map_lookup_and_delete_elem(attr),
bpf_cmd::BPF_MAP_LOOKUP_BATCH => map::bpf_map_lookup_batch(attr),
bpf_cmd::BPF_MAP_FREEZE => map::bpf_map_freeze(attr),
// Program related commands
bpf_cmd::BPF_PROG_LOAD => prog::bpf_prog_load(attr),
// Object creation commands
bpf_cmd::BPF_BTF_LOAD => {
error!("bpf cmd {:?} not implemented", cmd);
return Err(SystemError::ENOSYS);
}
ty => {
unimplemented!("bpf cmd {:?} not implemented", ty)
}
};
res
}
```
其中对创建Map命令会再次细分以确定具体的Map类型目前我们对通用的Map基本添加了支持:
```rust
bpf_map_type::BPF_MAP_TYPE_ARRAY
bpf_map_type::BPF_MAP_TYPE_PERCPU_ARRAY
bpf_map_type::BPF_MAP_TYPE_PERF_EVENT_ARRAY
bpf_map_type::BPF_MAP_TYPE_HASH
bpf_map_type::BPF_MAP_TYPE_PERCPU_HASH
bpf_map_type::BPF_MAP_TYPE_QUEUE
bpf_map_type::BPF_MAP_TYPE_STACK
bpf_map_type::BPF_MAP_TYPE_LRU_HASH
bpf_map_type::BPF_MAP_TYPE_LRU_PERCPU_HASH
bpf_map_type::BPF_MAP_TYPE_CPUMAP
| bpf_map_type::BPF_MAP_TYPE_DEVMAP
| bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH => {
error!("bpf map type {:?} not implemented", map_meta.map_type);
Err(SystemError::EINVAL)?
}
```
所有的Map都会实现定义好的接口这个接口参考Linux的实现定义:
```rust
pub trait BpfMapCommonOps: Send + Sync + Debug + CastFromSync {
/// Lookup an element in the map.
///
/// See https://ebpf-docs.dylanreimerink.nl/linux/helper-function/bpf_map_lookup_elem/
fn lookup_elem(&mut self, _key: &[u8]) -> Result<Option<&[u8]>> {
Err(SystemError::ENOSYS)
}
/// Update an element in the map.
///
/// See https://ebpf-docs.dylanreimerink.nl/linux/helper-function/bpf_map_update_elem/
fn update_elem(&mut self, _key: &[u8], _value: &[u8], _flags: u64) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Delete an element from the map.
///
/// See https://ebpf-docs.dylanreimerink.nl/linux/helper-function/bpf_map_delete_elem/
fn delete_elem(&mut self, _key: &[u8]) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// For each element in map, call callback_fn function with map,
/// callback_ctx and other map-specific parameters.
///
/// See https://ebpf-docs.dylanreimerink.nl/linux/helper-function/bpf_for_each_map_elem/
fn for_each_elem(&mut self, _cb: BpfCallBackFn, _ctx: *const u8, _flags: u64) -> Result<u32> {
Err(SystemError::ENOSYS)
}
/// Look up an element with the given key in the map referred to by the file descriptor fd,
/// and if found, delete the element.
fn lookup_and_delete_elem(&mut self, _key: &[u8], _value: &mut [u8]) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// perform a lookup in percpu map for an entry associated to key on cpu.
fn lookup_percpu_elem(&mut self, _key: &[u8], cpu: u32) -> Result<Option<&[u8]>> {
Err(SystemError::ENOSYS)
}
/// Get the next key in the map. If key is None, get the first key.
///
/// Called from syscall
fn get_next_key(&self, _key: Option<&[u8]>, _next_key: &mut [u8]) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Push an element value in map.
fn push_elem(&mut self, _value: &[u8], _flags: u64) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Pop an element value from map.
fn pop_elem(&mut self, _value: &mut [u8]) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Peek an element value from map.
fn peek_elem(&self, _value: &mut [u8]) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Freeze the map.
///
/// It's useful for .rodata maps.
fn freeze(&self) -> Result<()> {
Err(SystemError::ENOSYS)
}
/// Get the first value pointer.
fn first_value_ptr(&self) -> *const u8 {
panic!("value_ptr not implemented")
}
}
```
联通eBPF和kprobe的系统调用是[`perf_event_open`](https://man7.org/linux/man-pages/man2/perf_event_open.2.html)这个系统调用在Linux中非常复杂因此Dragon中并没有按照Linux进行实现目前只支持其中两个功能:
```rust
match args.type_ {
// Kprobe
// See /sys/bus/event_source/devices/kprobe/type
perf_type_id::PERF_TYPE_MAX => {
let kprobe_event = kprobe::perf_event_open_kprobe(args);
Box::new(kprobe_event)
}
perf_type_id::PERF_TYPE_SOFTWARE => {
// For bpf prog output
assert_eq!(args.config, perf_sw_ids::PERF_COUNT_SW_BPF_OUTPUT);
assert_eq!(
args.sample_type,
Some(perf_event_sample_format::PERF_SAMPLE_RAW)
);
let bpf_event = bpf::perf_event_open_bpf(args);
Box::new(bpf_event)
}
}
```
- 其中一个`PERF_TYPE_SOFTWARE`是用来创建软件定义的事件,`PERF_COUNT_SW_BPF_OUTPUT` 确保这个事件用来采集bpf的输出。
- `PERF_TYPE_MAX` 通常指示创建kprobe/uprobe事件也就是用户程序使用kprobe的途径之一用户程序可以将eBPF程序绑定在这个事件上
同样的perf不同的事件也实现定义的接口:
```rust
pub trait PerfEventOps: Send + Sync + Debug + CastFromSync + CastFrom {
fn mmap(&self, _start: usize, _len: usize, _offset: usize) -> Result<()> {
panic!("mmap not implemented for PerfEvent");
}
fn set_bpf_prog(&self, _bpf_prog: Arc<File>) -> Result<()> {
panic!("set_bpf_prog not implemented for PerfEvent");
}
fn enable(&self) -> Result<()> {
panic!("enable not implemented");
}
fn disable(&self) -> Result<()> {
panic!("disable not implemented");
}
fn readable(&self) -> bool {
panic!("readable not implemented");
}
}
```
这个接口目前并不稳定。
### helper函数支持
用户态工具通过系统调用和内核进行通信完成eBPF数据的设置、交换。在内核中eBPF程序的运行也需要内核的帮助单独的eBPF程序并没有什么太大的用处因此其会调用内核提供的`helper` 函数完成对内核资源的访问。
目前已经支持的大多数`helper` 函数是与Map操作相关:
```rust
/// Initialize the helper functions.
pub fn init_helper_functions() {
let mut map = BTreeMap::new();
unsafe {
// Map helpers::Generic map helpers
map.insert(1, define_func!(raw_map_lookup_elem));
map.insert(2, define_func!(raw_map_update_elem));
map.insert(3, define_func!(raw_map_delete_elem));
map.insert(164, define_func!(raw_map_for_each_elem));
map.insert(195, define_func!(raw_map_lookup_percpu_elem));
// map.insert(93,define_func!(raw_bpf_spin_lock);
// map.insert(94,define_func!(raw_bpf_spin_unlock);
// Map helpers::Perf event array helpers
map.insert(25, define_func!(raw_perf_event_output));
// Probe and trace helpers::Memory helpers
map.insert(4, define_func!(raw_bpf_probe_read));
// Print helpers
map.insert(6, define_func!(trace_printf));
// Map helpers::Queue and stack helpers
map.insert(87, define_func!(raw_map_push_elem));
map.insert(88, define_func!(raw_map_pop_elem));
map.insert(89, define_func!(raw_map_peek_elem));
}
BPF_HELPER_FUN_SET.init(map);
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,11 @@
内核跟踪机制
====================================
内核跟踪机制由很多功能构成, 比如kprobe/uprobe/tracepoint/ftrace等 以及用于扩展内核可观测性的eBPF内核当前支持kprobe和eBPF 本章将介绍这两种机制。
.. toctree::
:maxdepth: 1
:caption: 目录
eBPF
kprobe

View File

@ -0,0 +1,57 @@
# kprobe
> 作者: 陈林峰
>
> Email: chenlinfeng25@outlook.com
## 概述
Linux kprobes调试技术是内核开发者们专门为了便于跟踪内核函数执行状态所设计的一种轻量级内核调试技术。利用kprobes技术内核开发人员可以在内核的绝大多数指定函数中动态的插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。
kprobes技术依赖硬件架构相关的支持主要包括CPU的异常处理和单步调试机制前者用于让程序的执行流程陷入到用户注册的回调函数中去而后者则用于单步执行被探测点指令。需要注意的是在一些架构上硬件并不支持单步调试机制这可以通过一些软件模拟的方法解决(比如riscv)。
## kprobe工作流程
<img src="./kprobe_flow.png" style="zoom: 67%;" alt="xxx"/>
1. 注册kprobe后注册的每一个kprobe对应一个kprobe结构体该结构中记录着探测点的位置以及该探测点本来对应的指令。
2. 探测点的位置被替换成了一条异常的指令这样当CPU执行到探测点位置时会陷入到异常态在x86_64上指令是int3如果kprobe经过优化后指令是jmp
3. 当执行到异常指令时系统换检查是否是kprobe 安装的异常如果是就执行kprobe的pre_handler,然后利用CPU提供的单步调试single-step功能设置好相应的寄存器将下一条指令设置为插入点处本来的指令从异常态返回
4. 再次陷入异常态。上一步骤中设置了single-step相关的寄存器所以原指令刚一执行便会再次陷入异常态此时将single-step清除并且执行post_handler然后从异常态安全返回.
5. 当卸载kprobe时探测点原来的指令会被恢复回去。
内核目前对x86和riscv64都进行了支持由于 riscv64 没有单步执行模式,因此我们使用 break 异常来进行模拟,在保存探测点指令时,我们会额外填充一条 break 指令这样就可以使得在riscv64架构上在执行完原指令后会再次触发break陷入异常。
## kprobe的接口
```rust
pub fn register_kprobe(kprobe_info: KprobeInfo) -> Result<LockKprobe, SystemError>;
pub fn unregister_kprobe(kprobe: LockKprobe) -> Result<(), SystemError>;
impl KprobeBasic {
pub fn call_pre_handler(&self, trap_frame: &dyn ProbeArgs)
pub fn call_post_handler(&self, trap_frame: &dyn ProbeArgs)
pub fn call_fault_handler(&self, trap_frame: &dyn ProbeArgs)
pub fn call_event_callback(&self, trap_frame: &dyn ProbeArgs)
pub fn update_event_callback(&mut self, callback: Box<dyn CallBackFunc>)
pub fn disable(&mut self)
pub fn enable(&mut self)
pub fn is_enabled(&self) -> bool
pub fn symbol(&self) -> Option<&str>
}
```
- `call_pre_handler` 在探测点指令被执行前调用用户定义的回调函数
- `call_post_handler` 在单步执行完探测点指令后调用用户定义的回调函数
- `call_fault_handler` 在调用前两种回调函数发生失败时调用
- `call_event_callback` 用于调用eBPF相关的回调函数通常与`call_post_handler` 一样在单步执行探测点指令会调用
- `update_event_callback`用于运行过程中更新回调函数
- `disable``enable` 用于动态关闭kprobe`disable`调用后kprobe被触发时不执行回调函数
- `symbol` 返回探测点的函数名称

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB