0x822a5b87的博客

到码头整点薯条吃

环境配置

效果图

CUDA 开发环境的配置流程虽不算复杂,但主流的实现方案均存在明显的体验短板:

  • macOS 远程对接 Linux 开发:这种在 macOS 系统下通过远程连接 Linux 环境开发的方式,核心痛点包括:
    • 多数 IDE 对跨平台编译、调试(DEBUG)的支持度不足,功能完整性和稳定性欠佳;
    • 开发过程中易遇到不影响核心功能但影响体验的小问题,整体使用感受较差;
    • 核心问题在于算力资源(算例)多按小时租赁且成本较高,为控制开销频繁启停实例时,需反复配置开发环境,严重降低开发效率。
  • Windows 原生开发:该方案能满足基础开发流程需求,但核心问题集中在适配性和工具层面:
    • 行业内多数 CUDA 相关依赖框架、技术文档均基于 Linux 环境开发和撰写,迁移到 Windows 时,需额外花费精力寻找适配方案,甚至手动调整兼容逻辑;
    • 核心开发工具 Visual Studio 体积臃肿、配置项繁杂,对新手不友好,且易因配置不当引发环境问题。

经过我一段时间的摸索和测试,目前找到了一个比较好的解决方案:Windows + WSL(Ubuntu) + VSCode(WSL Plugin) 来作为开发环境,即有极高的兼容性,同时对于习惯于 linux 开发的我来说更顺手。最重要的是,这一套环境的配置远低于其他的环境配置,这里我们介绍一下怎么搭建这个开发环境。

阅读全文 »

本文是自己动手实现raft的前置文章,旨在分析raft协议的内部实现原理。作者在网上搜索了大量的文献和资料,向这些作者表达我的敬意。这里也推荐阅读本文的人可以参考这几篇经典的文章:

概述

在我们探讨 raft 算法之前,我们需要搞清楚为什么我们需要一个如此复杂的算法:我们用数据库举例:

在传统的单机数据库下,我们其实也会有所谓的一致性需求 -- 就是我们的事务。当我们在执行一个 SQL 语句时会存在 IO 操作,而这个 IO 操作和我们分布式系统的 IO 操作是没有本质区别的 -- 他们都有概率失败。例如:假设我们存在一个 SQL 写入语句,这个语句写入的字段中包含了一个超大字段,这个超大字段超过了操作系统的单次 IO 写入大小,那么操作系统必然会把这个写入语句拆分成多次 IO 操作。那现在我们面临的问题是:

  • 假设这个语句被拆分为两次 IO 操作:
    • 第一次 IO 操作成功了,第二次 IO 操作时磁盘满了无法写入怎么办?
    • 第一次 IO 操作成功了,在第二次 IO 操作之前机器重启了怎么办?
阅读全文 »

需要注意的是,linux下的网络编程是一个非常琐碎的过程,所以我们在这里只是实现了最简单的版本:

  • 只支持 IPv4
  • 只支持通过 Linux Bridge/Veth Pari
  • Subnet 的网段进行了限制,并且只允许通过后台默认分配,不允许用户自定义;

在实现的过程中,我们也有很多的地方因为太过于繁琐直接省略了:

  • 对于我们分配网段/分配IP的管理,只记录在内存中,并没有做持久化;
  • 虽然根据职责对系统进行了分层,但是整个的架构和接口设计都比较随意;
  • EndpointIdContainerId 没有分开,也就限制了我们每个容器只能接入一个网络;
  • 虽然对各个组件之间通过接口做了解耦,但是实际在代码实现中未实现完全解耦;
  • 错误处理不严谨,很多地方在失败时都没有进行回滚操作;
  • 没有增加跨子网路由,所以现在只能容器之间通信,而不能实现容器通过宿主机的网卡与外网通信。

在我们前面的文章 build my own docker from scratch 中,我们实现了:

  • 通过 namespace 对容器实现资源虚拟化;
  • 通过 cgroup 对容器的资源进行限制和管理;
  • 通过 daemon 进程来实现对整体资源的管理;

今天我们要开始实现一个对 docker 来说至关重要的特性:network namespace 的管理。从这一节,我们也可以体会到为什么我们要使用一个额外的 daemon 进程来对我们容器进行管理 -- 从之前的视角中,我们可以看到大量的缺点:

  • 引入了额外的复杂性;
  • 大量通过 UDS 通信导致的异步行为使得程序的编码和DEBUG都更加困难;
  • 中心化的服务降低了程序的健壮性。
阅读全文 »

发现问题

最近在尝试实现一个简单的 docker,在实现的过程中我试图将对linux的cgroup文件操作抽象为一个独立的接口 subsystem,但是对于不同的类型的文件他的结构完全不一样:

  • 对于 cgroup.procs,他的文件中存储的是一个以 \n 分割的 pid 数组;
  • 对于 cpu.max,他的文件中存储的是以空格分隔的两个数字;
1
2
3
4
5
cat /sys/fs/cgroup/system.slice/docker-0a7f5c9459f832ac7bb539e6e61dac51a4a4f66c5dbc1bbb963d0ec3f01d31ae.scope/cgroup.procs
#1435205

cat /sys/fs/cgroup/system.slice/docker-0a7f5c9459f832ac7bb539e6e61dac51a4a4f66c5dbc1bbb963d0ec3f01d31ae.scope/cpu.max
# max 100000

所以,我尝试直接将他做出如下抽象:

阅读全文 »

build my own docker from scratch

Docker is an OS‑level virtualization (or containerization) platform, which allows applications to share the host OS kernel instead of running a separate guest OS like in traditional virtualization.

前言

本文基于 OpenCloudOS 操作系统以及 x86_64 平台,go1.24.11 语言开发测试,全部代码位于 tiny-docker

1
2
3
4
5
6
7
8
uname -a
# Linux VM-0-10-opencloudos 6.6.117-45.1.oc9.x86_64 #1 SMP Tue Dec 16 11:49:47 CST 2025 x86_64 x86_64 x86_64 GNU/Linux

go version
# go version go1.24.11 linux/amd64

docker --version
# Docker version 29.1.3, build f52814d

Docker 是基于 Linux Namespace 和 Cgroup 实现的轻量级虚拟化平台。相较于传统虚拟机(如 Oracle VirtualBox),Docker 无需模拟硬件和运行独立内核,仅通过内核级的资源隔离与限制实现 “虚拟环境”,因此更轻量级(毫秒级启动)、资源占用更低 —— 同一台宿主机上的所有容器共享宿主机内核,这是容器与虚拟机的核心区别。

阅读全文 »

uCore并发

sequenceDiagram
    title 银行家算法 - 安全场景(sequenceDiagram)
    银行家算法 ->> 数据结构存储: 初始化请求(读取Available/Allocation/Need)
    数据结构存储-->>银行家算法: 返回初始数据(Available={}, Work={}, Finish=[F,F])
    
    %% 第一次循环:找满足条件的线程
    银行家算法 ->> 银行家算法: 遍历线程,检查条件(Need[i][j] ≤ Work[j] 且 Finish[i]=F)
    银行家算法 ->> 数据结构存储: 查询线程1的Need/Finish
    数据结构存储-->>银行家算法: 线程1:Need={200:1},Finish=F
    银行家算法 ->> 银行家算法: 线程1:Need > Work → 不满足
    银行家算法 ->> 数据结构存储: 查询线程2的Need/Finish
    数据结构存储-->>银行家算法: 线程2:Need={},Finish=F
    银行家算法 ->> 银行家算法: 线程2:Need ≤ Work → 满足
    
    %% 模拟线程2释放资源
    银行家算法 ->> 数据结构存储: 模拟线程2执行完毕,请求更新Work(Work += 线程2的Allocation{200:1})
    数据结构存储-->>银行家算法: Work更新成功(Work={200:1})
    银行家算法 ->> 数据结构存储: 请求标记线程2的Finish=true
    数据结构存储-->>银行家算法: Finish更新成功(Finish=[F,T])
    
    %% 第二次循环:找满足条件的线程
    银行家算法 ->> 银行家算法: 重新遍历线程(仅线程1未完成)
    银行家算法 ->> 数据结构存储: 查询线程1的Need/Work
    数据结构存储-->>银行家算法: 线程1:Need={200:1},Work={200:1}
    银行家算法 ->> 银行家算法: 线程1:Need ≤ Work → 满足
    
    %% 模拟线程1释放资源
    银行家算法 ->> 数据结构存储: 模拟线程1执行完毕,请求更新Work(Work += 线程1的Allocation{100:1})
    数据结构存储-->>银行家算法: Work更新成功(Work={100:1,200:1})
    银行家算法 ->> 数据结构存储: 请求标记线程1的Finish=true
    数据结构存储-->>银行家算法: Finish更新成功(Finish=[T,T])
    
    %% 判定结果
    银行家算法 ->> 银行家算法: 检查所有Finish是否为true
    银行家算法-->>外部: 输出判定结果 → 系统安全(无死锁风险)

引言

线程定义

简单地说,线程是进程的组成部分,进程可包含1 – n个线程,属于同一个进程的线程共享进程的资源, 比如 地址空间、打开的文件等。基本的线程由线程ID、执行状态、当前指令指针 (PC)、寄存器集合和栈组成。 线程是可以被操作系统或用户态调度器独立调度(Scheduling)和分派(Dispatch)的基本单位。进程是线程的资源容器, 线程成为了程序的基本执行实体。

阅读全文 »

进程间通信

基于文件的管道

1
2
3
4
5
6
7
// os/src/fs/pipe.rs

pub struct Pipe {
readable: bool,
writable: bool,
buffer: Arc<Mutex<PipeRingBuffer>>,
}

通过 buffer 字段还可以找到该管道端所在的管道自身。后续我们将为它实现 File Trait ,之后它便可以通过文件描述符来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// os/src/fs/pipe.rs

const RING_BUFFER_SIZE: usize = 32;

#[derive(Copy, Clone, PartialEq)]
enum RingBufferStatus {
FULL,
EMPTY,
NORMAL,
}

pub struct PipeRingBuffer {
arr: [u8; RING_BUFFER_SIZE],
head: usize,
tail: usize,
status: RingBufferStatus,
write_end: Option<Weak<Pipe>>,
}
阅读全文 »

文件系统

layout

整体逻辑

graph BT
    %% 核心模块:easy-fs(文件系统核心逻辑)
    subgraph easy-fs ["easy-fs(文件系统核心算法)"]
        direction BT
        subgraph BD ["抽象层:BlockDevice(块设备接口)"]
            direction TB
            read_block("read_block")
            write_block("write_block")
        end

        subgraph BC ["缓存层:BlockCache(块缓存)"]
            direction TB
            read("read")
            modify("modify")
            bc_other("其他缓存操作...")
        end

        subgraph LAY ["布局层:存储结构定义"]
            direction TB
            SB("SuperBlock(超级块)")
            BM("Bitmap(位图)")
            DI("DiskInode(磁盘索引节点)")
            DE("DirEntry(目录项)")
            DB("DataBlock(数据块)")
        end

        subgraph BMGR ["管理层:文件系统实例/接口"]
            direction TB
            EFS("EasyFileSystem(文件系统实例)")
            INODE("Inode(文件/目录操作接口)")
        end

        %% easy-fs 内部链路(修正方向)
        BD -->|读写物理块| BC
        BC -->|缓存读写| LAY
        LAY -->|基于布局实现| BMGR
    end

    %% 辅助工具:easy-fs 配套工具(宿主机侧)
    subgraph easy-fs-tools ["easy-fs-tools(宿主机工具)"]
        direction TB
        pack("easy_fs_pack(打包工具)")
        fuse("easy-fs-fuse(FUSE调试工具)")
    end
    easy-fs -->|依赖核心逻辑| pack
    pack -->|生成镜像| fs_img["fs.img(raw格式镜像)"]
    easy-fs -->|适配FUSE接口| fuse
    fuse -->|挂载调试| fs_img

    %% QEMU(虚拟硬件层)
    subgraph qemu ["QEMU(虚拟硬件)"]
        direction TB
        drive("-drive file=fs.img(后端存储)")
        virtio_dev("-device virtio-blk-device(虚拟块设备)")
    end
    fs_img -->|作为后端存储| drive
    drive -->|绑定到虚拟设备| virtio_dev

    %% os 内核(驱动+文件系统调用)
    subgraph os ["os(内核层)"]
        direction TB
        subgraph VIRTIO ["驱动层:VirtIOBlock"]
            direction TB
            virtio_bd("实现 BlockDevice 接口")
            virtio_ops("virtio-blk 读写操作")
        end
        subgraph FS_CALL ["应用层:文件系统调用"]
            direction TB
            fs_open("open/read/write")
            fs_exec("执行ELF文件")
        end
    end
    %% 内核链路
    qemu -->|暴露虚拟硬件| virtio_dev
    virtio_dev -->|内核枚举设备| VIRTIO
    VIRTIO -->|适配接口| easy-fs
    easy-fs -->|提供文件操作| FS_CALL

文件与文件描述符

阅读全文 »

uCore:进程及进程管理

我们将开发一个用户 终端 (Terminal) 或 命令行 (Command Line Application, 俗称 Shell ) , 形成用户与操作系统进行交互的命令行界面 (Command Line Interface)。

为此,我们要对任务建立新的抽象: 进程 ,并实现若干基于 进程 的强大系统调用。

flowchart LR
    subgraph init["初始化阶段:无用户任务"]
        direction LR
        registers_0("registers.cx: idle运行状态"):::pink
        current_0("current: None"):::purple
        idle_task_cx_0("idle_task_cx: zero.ctx(占位)"):::green
    end

    subgraph stage_1["阶段1:fetch_task"]
        direction LR
        registers_1("registers.cx: task.cx(用户任务状态)"):::pink
        current_1("current: Some(task)"):::purple
        idle_task_cx_1("idle_task_cx: 原registers.cx(idle休眠状态)"):::green
    end

    subgraph stage_2["阶段2:suspend"]
        direction LR
        registers_2("registers.cx: idle运行状态(从idle_task_cx_1加载)"):::pink
        current_2("current: None"):::purple
        idle_task_cx_2("idle_task_cx: 原registers_1(用户任务挂起状态)"):::green
    end

    subgraph stage_3["阶段3:exit→用户任务退出"]
        direction LR
        registers_3("registers.cx: idle运行状态(从idle_task_cx_1加载)"):::pink
        current_3("current: None"):::purple
        idle_task_cx_3("idle_task_cx: 原idle_task_cx_1(无变化)"):::green
    end

    subgraph TaskManager["TaskManager:就绪队列"]
        t_task("task.cx: 用户任务挂起状态(仅suspend后存在)"):::coral
    end

    %% 流转逻辑(补充关键动作标注)
    init -->|init| stage_1
    stage_1 -->|suspend| stage_2
    stage_1 -->|exit| stage_3

    %% 状态转移细节
    current_1 -->|take_current后add_task,共享task所有权| TaskManager
    registers_1 -->|__switch保存用户任务状态到task.cx| t_task
    idle_task_cx_1 -->|__switch加载idle状态到寄存器| registers_2
    idle_task_cx_1 -->|__switch加载idle状态到寄存器| registers_3

    %% 样式定义(沿用你的设计)
    classDef pink fill:#FFCCCC,stroke:#333, color: #fff, font-weight:bold;
    classDef green fill: #696,color: #fff,font-weight: bold;
    classDef purple fill:#969,stroke:#333, font-weight: bold;
    classDef error fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5;
    classDef coral fill:#f9f,stroke:#333,stroke-width:4px;
    classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;

与进程有关的重要系统调用

重要系统调用

阅读全文 »

在这个章节,我们会实现虚拟内存机制。

物理空间和虚拟空间

虚拟空间连续,物理空间不一定连续!

flowchart LR
    %% 样式定义(避开关键字,用合法语法)
    classDef structStyle fill:#f0f8ff,stroke:#2c3e50,stroke-width:2px,rounded:10px
    classDef vaStyle fill:#fdf2f8,stroke:#9b59b6,stroke-width:2px,rounded:10px
    classDef vpnStyle fill:#fef7fb,stroke:#9b59b6,stroke-width:1px,rounded:8px,padding:10px
    classDef paStyle fill:#e8f4f8,stroke:#3498db,stroke-width:2px,rounded:10px
    classDef ppnStyle fill:#f0f8ff,stroke:#3498db,stroke-width:1px,rounded:8px,padding:10px
    classDef fieldStyle font-weight:bold,color:#2c3e50
    classDef connStyle stroke:#7f8c8d,stroke-width:1.5px,arrowheadStyle:fill:#7f8c8d

    %% 上层:结构体(连续字段)
    subgraph Struct["📦 TimeVal 结构体(连续虚拟内存视图)"]
        direction TB
        sec["<span class='fieldStyle'>sec</span><br/>usize (8字节)"]:::fieldStyle
        usec["<span class='fieldStyle'>usec</span><br/>usize (8字节)"]:::fieldStyle
    end
    class Struct structStyle

    %% 中层:虚拟地址 VA(连续,跨2个虚拟页)
    subgraph VA["🌐 虚拟地址 VA(连续)"]
        direction LR
        subgraph VPN0["VPN0(虚拟页0)"]
            %% 修正:sec 位于 VPN0 末尾,VA = 页起始 + (4KB-8) = 0x40200000 + 0xFF8 = 0x40200FF8
            va_sec["0x40200FF8<br/>[页内偏移: 0xFF8 = 4KB-8]"]
        end
        subgraph VPN1["VPN1(虚拟页1)"]
            %% 修正:usec 位于 VPN1 起始,页内偏移 0x000
            va_usec["0x40201000<br/>[页内偏移: 0x000]"]
        end
        note_va["页大小:4KB (0x1000)<br/>VPN0范围:0x40200000~0x40200FFF<br/>VPN1范围:0x40201000~0x40201FFF"]
        class note_va vaStyle
    end
    class VA vaStyle

    %% 下层:物理地址 PA(离散)
    subgraph PA["💾 物理地址 PA(离散)"]
        subgraph PPN0["PPN0(物理页0)"]
            %% 物理页0起始 + 偏移0xFF8 = 0x8000 + 0xFF8 = 0x8FF8
            pa_sec["0x8FF8<br/>[页内偏移: 0xFF8]"]
        end
        subgraph PPN1["PPN1(物理页1)"]
            %% 物理页1起始 + 偏移0x000 = 0x4000 + 0x000 = 0x4000
            pa_usec["0x4000<br/>[页内偏移: 0x000]"]
        end
        note_pa["物理页独立分配,地址不连续<br/>PPN0范围:0x8000~0x8FFF<br/>PPN1范围:0x4000~0x4FFF"]
        class note_pa paStyle
    end
    class PA paStyle

    %% 映射关系(字段 → VA → PA)
    sec -.->|字段对应VA| va_sec:::connStyle
    usec -.->|字段对应VA| VPN1:::connStyle
    va_sec -.->|页表翻译(VPN0→PPN0)| pa_sec:::connStyle
    va_usec -.->|页表翻译(VPN1→PPN1)| pa_usec:::connStyle

    %% 核心结论标注(突出问题)
    conclusion["⚠️  核心问题:<br/>结构体VA连续(跨页边界),但映射的PA离散<br/>内核需拆为2段写入:<br/>1. PPN0的0x8FF8~0x8FFF(6字节?不,8字节刚好)<br/>2. PPN1的0x4000~0x4007"]:::structStyle
    PA -.->|体现| conclusion:::connStyle

管理SV39多级页表

阅读全文 »
0%