Skip to content

System Programming

打开文件

fstream

C++ 标准库自带的 <fstream> 是最常用的方式。它是跨平台的,不依赖 Linux/Windows 特性。

#include <fstream>
#include <string>

int main() {
    std::ofstream file("output.txt"); // 打开文件(会覆盖)
    if (!file) {
        return 1; // 打开失败
    }

    file << "Hello, Modern C++!\n";
    file << "当前时间:" << 2025 << "-11-07\n";
    file.close(); // 自动关闭也可以省略

    return 0;
}

可以用

std::fstream myfile{"myfile.txt", std::ios::in | std::ios::out};

指定模式,默认 std::ios::in | std::ios::out 要求文件 必须存在。如果文件不存在,会打开失败。要让它自动创建文件,需要加上 std::ios::truncstd::ios::app

二进制写入

std::vector<unsigned char> data = {0xDE, 0xAD, 0xBE, 0xEF};

std::ofstream file("data.bin", std::ios::binary);
file.write(reinterpret_cast<const char*>(data.data()), data.size());

write的函数签名是 const char* 所以要类型转换

Filesystem

C++17 引入的 <filesystem> 是现代 C++ 标准库中非常强大的模块之一:

类型 说明
fs::path 表示路径对象(跨平台、自动处理斜杠)
fs::directory_entry 表示文件或目录的一个条目
fs::file_status 保存文件类型和权限信息
fs::filesystem_error 抛出的异常类型
fs::space_info 存储磁盘空间信息(总量、可用空间)

遍历文件夹

fs::path dir = "example_folder";

for (const auto& entry : fs::directory_iterator(dir)) {
    std::cout << entry.path() << "\n";
}
for (const auto& entry : fs::recursive_directory_iterator(root)) {
    std::cout << entry.path() << "\n";
}

分块读取文件

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <filesystem>
#include <cstring>

namespace fs = std::filesystem;

constexpr int CHUNK_SIZE = 4 * 1024 * 1024;

std::size_t get_file_size(fs::path file) {
    try {
        return fs::file_size(file);
    } catch (const fs::filesystem_error& e) {
        std::cerr << e.what() << std::endl;
        return 0;
    }
}

auto calc_num(const std::string& file_name) {
    std::size_t size = get_file_size(fs::path{file_name});
    std::size_t num = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
    return num;
}


struct FileDescriptor {
    int fd = -1;
    FileDescriptor(const std::string& path, int flag) {
        fd = open(path.c_str(), flag);
    }
    ~FileDescriptor() {
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
    }
};

struct MMapper {
    char* data = nullptr;
    std::size_t len;
    MMapper(int fd, std::size_t length, std::size_t offset) : len(length) {
        data = static_cast<char*>(mmap(
            0,
            length,
            PROT_READ, // PROTECTION
            MAP_SHARED, // SHARED
            fd,
            offset
        ));
    }
    ~MMapper() {
        if (data != nullptr && data != MAP_FAILED) {
            munmap(data, len);
            data = nullptr;
        }
    }
};


auto read_chunk(std::size_t offset, std::size_t length) -> char* {
    std::string file_name = "poly.cpp";
    int fd = FileDescriptor(file_name, O_RDONLY).fd;

    auto size = get_file_size(fs::path{file_name});
    char* data = MMapper(fd, length, offset).data;
    std::byte buf[CHUNK_SIZE];
    memcpy(buf, data, CHUNK_SIZE);
    return data;
}

网络

可以安装boost库

sudo apt install libboost-all-dev

其它可以看boost的那一章节

Epoll

mmap

page fault 行为

当 CPU 要访问某个虚拟地址时,会走 MMU 查页表:

  • 如果 页表里没有这个虚拟页的有效映射,或者
  • 有映射但权限不允许(比如写了只读页)

CPU 就会触发一个异常:page fault,陷入内核。

也就是说:

page fault = “这页我现在用不了,你(内核)来处理一下” 这个硬件异常。

情况 A:地址合法,只是“还没真正分配物理页”

第一次访问这块区域时:

  • 触发 page fault;
  • 内核发现:这个地址确实在某个匿名 VMA 里面,而且权限 OK;
  • 就:
  • 从伙伴系统里 分配一个物理页
  • 把这页清零(匿名映射)或从文件中读数据(文件映射);
  • 在页表里建立 虚拟页 → 物理页 的映射;
  • 返回用户态,重新执行刚刚那条指令,这次访问就成功了。

情况 B:地址对应的页在磁盘(swap 或文件),需要调回来

比如:

  • 某页之前被换出到 swap(交换分区)了;
  • 或者是可执行文件 / 动态库里尚未加载的那部分;

访问时:

  1. page fault 发生;
  2. 内核看到:这页有记录,但当前不在内存里;
  3. 从磁盘(swap 或文件)把这页读回来;
  4. 建立页表映射;
  5. 程序继续执行。

这是 “主要缺页(major fault)”:需要磁盘 IO,比较慢。

情况 C:地址压根不属于进程(或权限不允许)

  • 这个地址 根本不在任何合法区域里,或者
  • 权限不允许(比如只读区域被写)

结论:这是程序 bug,没法“自动修复”

内核给进程发一个信号(Linux 中是 SIGSEGVSIGBUS

如果程序没特别处理这个信号,就默认 被杀掉 —— 我们看到的就是:

perf

valgrind, ASan, cachegrind

Conan包管理器

Process

进程和线程

  • 进程是资源分配的最小单位
  • Thread是scheduling的最小单位
  • 线程共享进程空间,创建成本低,contextswitch的成本低
  • Process用于隔离,

IPC方式

  • pipe:
  • Named pipe FIFO
  • 共享内存
  • 消息队列
  • signal
  • Unix domain socket
  • TCP socket

进程的创建

我们可以把进程创建看作是 OS 在准备一个全新的工作环境。

  • OS 找到程序文件(例如,一个可执行文件),并开始将代码和数据从磁盘加载到物理内存中的某个位置。
  • OS 创建一个数据结构来管理这个新进程,这就是 PCB。PCB 中包含一个指向新创建的页表的指针
  • 分配和初始化资源

Memory

虚拟内存

解决了几个核心问题:

  1. 内存抽象 (Abstraction):让每个程序都感觉自己独占了连续、完整的内存空间,简化了编程。
  2. 内存保护 (Protection):隔离了不同程序,一个程序的错误不会影响到其他程序或操作系统。
  3. 共享与并发 (Sharing & Concurrency):允许多个进程共享库文件,并支持高效的多任务执行。
  4. 当多个程序(进程 A、进程 B)都使用了同一个动态链接库
  5. 物理内存只加载一份
  6. 对于 进程 A,它的页表会将它虚拟地址空间中的某个范围,对于 进程 B,它的页表会将它虚拟地址空间中的另一个范围

每个独立的进程 🏘️ 都有自己独立的 页表 (Page Table)

地址转换

x86_64 平台使用4级页表,页大小为4KiB,每个页表均具有512个条目,每个条目占用8字节,所以每个页表固定占用 512 * 8B = 4KiB。

CR3 寄存器中存储着指向4级页表的物理地址,而在每一级的页表(除一级页表外)中,都存在着指向下一级页表的指针

当进程访问一个虚拟地址时,硬件分页机构会查页表 / TLB:

  • 如果:
  • 该页表项不存在,或者
  • 标记为“not present”(不在内存),或者
  • 当前访问权限不允许(比如写一个只读页) → CPU 产生一个 page fault 异常,切到内核态

  • 如果地址不在该进程的合法虚拟地址空间范围内:

  • 比如访问了空指针附近的地址、越界到别的区域
  • 或者权限不对(写只读、执行不可执行区等) → OS 通常会:
  • 在 Linux 里给进程发 SIGSEGV(段错误),进程被杀掉,常见的 Segmentation fault (core dumped)
  • 如果地址是合法的,只是还没在内存:
  • 比如这是刚分配还没真正“映射”的匿名内存,或者映射文件的页面还没加载
  • 这才是“正常的缺页”,OS 会尝试把这一页“弄进来”。

Page Table Entry

当 CPU 尝试访问一个虚拟地址时,它首先会通过内存管理单元 (MMU) 去查找对应的页表项 (Page Table Entry)

Valid Bit

  • 有效位 (Valid Bit) 表示这个虚拟页已经存在于物理内存 (RAM) 中,MMU 可以正常进行地址转换。
  • 如果 有效位 = 0 (不存在),则表示这个虚拟页目前不在物理内存中,它很可能被存储在磁盘 (Disk) 上的一个特殊区域,这就是会触发缺页中断 (Page Fault) 的信号。,页表中的其他位(剩余部分)*通常被操作系统用来存储该页在*磁盘上的地址

在将数据加载进来之前,OS 需要在物理内存 (RAM) 中找到一个空闲的空间来存放这个新的页。如果物理内存中没有空闲的页框 (Free Frame) 了操作系统必须做出一个艰难的决定:牺牲(或称换出)内存中现有的一个页,为新的页腾出空间。使用LRU算法!

LRU 的核心思想:基于时间局部性原理,它假设如果一个页最近没有被使用,那么将来一段时间内它被使用的可能性也很低。

修改位 (Dirty Bit)

如果脏位 = 1,说明程序已经修改了物理内存中的这个页,导致 RAM 里的数据和磁盘上存储的旧数据不一致。因此,OS 必须将这个修改过的页写回磁盘

CPU 线程上下文切换

当 OS 启动磁盘 I/O 后,它会执行以下操作

  • 将引发缺页中断的线程/进程运行态 (Running) 变成阻塞态 (Blocked/Waiting)
  • OS 调度器(Scheduler)立即选择另一个就绪态 (Ready) 的线程,执行上下文切换

  • 磁盘 I/O 操作终于完成,缺失的页数据已经成功加载到了物理内存中。磁盘控制器会向 CPU 发送一个I/O 完成中断 (I/O Completion Interrupt) 来通知 OS。

  • OS 必须找到这个虚拟页对应的页表项,将 有效位 (Valid/Present Bit) 设置为 1,并将页表项中的物理页框号 (PFN) 填上新加载页的内存地址。
  • 更新线程状态 🔄:将之前因等待 I/O 而被阻塞的线程状态从阻塞态改为就绪态,将其放入就绪队列等待 CPU 调度。

页的管理由 伙伴系统 完成

页里的小格子(对象)由 slab 系统 细分和复用

底层:伙伴系统(buddy system)——按“2^k 个页”的块来分

所有物理页用 struct page 表示;这些页按物理地址被划到不同的 zone;每个 zone 内部,用“2^order 页”的块(free_area[order])来管理空闲内存。

Linux 把 整个物理内存按页(page)划分,每一页(通常 4KB)都会对应一个 struct page 结构体

典型信息包括(简化理解):

  • 这一页:
  • 在不在用?(free / allocated)
  • 属于哪个 zone?
  • 属于哪个伙伴块 / slab?
  • 被哪个匿名页、文件映射、page cache 等占用?
  • 链表指针:
  • 在空闲链表里时,用来挂在 free list
  • 在 LRU 上时,用来挂在回收队列

zone:不同物理内存“区域”的划分

zone = 把一整片物理内存按用途/访问限制做“物理分区”,每个分区内部再用伙伴系统管理空闲页。

不是所有物理内存都一样用法,比如:

  • 低地址的一段,要留给某些老式 DMA 设备(只能访问低 16MB、低 4GB 等)

Linux 把一部分物理内存划为一个 zone,常见的有:

  • ZONE_DMA:DMA 设备可访问的低端内存
  • ZONE_DMA32:某些只能访问 32bit 地址的设备用
  • ZONE_NORMAL:正常直接映射的区域(主要战场)
  • ZONE_HIGHMEM:32 位时代的高端内存(现在 64 位上一般不用)

order:按“2^order 页”为单位的块大小

伙伴系统不会一页一页地管理,而是按 2 的幂次方个页组成的块 来管理:

  • order = 0\(2^0 = 1\) 页 → 1 * 4KB
  • order = 1\(2^1 = 2\) 页 → 8KB
  • order = 2 → 4 页 → 16KB
  • 一直到 MAX_ORDER(比如 10:1024 页)

在每个 zone 里,会有一个数组:

free_area[order]

表示这个 zone 中:

  • 所有大小为 2^order 页的“空闲块” 的链表;
  • 有的实现还配一个 bitmap 来加速判断某个 order 有没有空闲块。

slab / slub 分配器——按“对象”来分

kmalloc(sizeof(struct foo)) 这种场景,直接用伙伴系统分页太粗了:

  • 你只要几十字节,却拿了一整页 4KB,会浪费很多
  • 而且频繁向伙伴系统申请/释放页,会导致碎片 & 开销变大

于是 Linux 在页之上再套一层 “对象缓存分配器”

概念:

  • cache(kmem_cache):缓存某种“统一大小对象”的池子,比如:
  • 专门管理 32 字节对象的 cache
  • 专门管理 struct inode 的 cache
  • slab:每个 cache 由多块 slab 组成;一个 slab = 若干页 + 管理元数据 在 slab 中切出很多个等大小的“对象槽位”

SLUB:现在主流默认

  • 目标是“简单 + 快”
  • 简化元数据,用每 CPU 的 freelist、减少锁争用

Malloc

我们平时在 C 里用的 malloc/free,在 Linux + glibc 环境下,默认实现就是 ptmalloc(“pthreads

ptmalloc 完全是“用户空间”的内存分配器,实现在 glibc 里面

VMA

VMA = 进程虚拟地址空间里的一段“连续、属性相同的区间”的描述结构。 比如:一段代码、一段堆、一段 mmap 文件、一段栈……每一段就是一个 VMA。

每一块“连续 + 权限/用途属性一样”的区间,在内核里就是一个 vm_area_struct,即一个 VMA。

每个进程有一个 mm_struct,它里面有:

struct mm_struct {
    struct vm_area_struct *mmap;   // 所有 VMA 按地址升序连成的链表头
    struct rb_root mm_rb;          // 所有 VMA 按 vm_start 排序的红黑树根
    unsigned long mmap_base;       // mmap 区起始地址
    unsigned long start_brk, brk;  // 堆的起始 & 当前结束地址
    unsigned long start_stack;     // 栈顶地址
    // ... 还有页表根、rss统计等...
};

可以理解为:

一个进程的地址空间 = mm_struct + 一堆挂在上面的 VMA + 页表。

其中:

  • 链表 mmap:遍历所有 VMA(比如 /proc/pid/maps 就是这么打印的)
  • 红黑树 mm_rb:按起始地址有序,用来快速查找“某个地址属于哪个 VMA”

epoll

#include <sys/epoll.h>

int epfd = epoll_create1(0);
epoll_event ev[64] = {}; 

int n = epoll_wait(epfd, ev, 64, 15);
if (n > 0) {
    // 有 n 个就绪事件,存在 events[0..n-1]
} else if (n == 0) {
    // 超时,没有事件
} else {
    // n == -1,出错
}

绑定事件

wakeupfd_ = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = wakeupfd_;
epoll_ctl(epfd_, EPOLL_CTL_ADD, wakeupfd_, &ev);

什么是 “交易基础设施 (Trading Infrastructure)”

大体包括以下模块:

  • 🧩 Market Data Feed Handlers(行情接入、解析)
  • ⚙️ Order Gateways / FIX Engine(订单接口、撮合)
  • 🧠 Strategy Engine / Algo Framework(算法执行)
  • 📊 Backtesting / Simulation(回测仿真)
  • 🧰 Infrastructure Services(日志、配置、消息分发、监控)
  • 🌐 Networking & Middleware(消息总线、队列、数据库访问)