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::trunc 或 std::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(交换分区)了;
- 或者是可执行文件 / 动态库里尚未加载的那部分;
访问时:
- page fault 发生;
- 内核看到:这页有记录,但当前不在内存里;
- 从磁盘(swap 或文件)把这页读回来;
- 建立页表映射;
- 程序继续执行。
这是 “主要缺页(major fault)”:需要磁盘 IO,比较慢。
情况 C:地址压根不属于进程(或权限不允许)
- 这个地址 根本不在任何合法区域里,或者
- 权限不允许(比如只读区域被写)
结论:这是程序 bug,没法“自动修复”
内核给进程发一个信号(Linux 中是 SIGSEGV 或 SIGBUS)
如果程序没特别处理这个信号,就默认 被杀掉 —— 我们看到的就是:
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
虚拟内存
解决了几个核心问题:
- 内存抽象 (Abstraction):让每个程序都感觉自己独占了连续、完整的内存空间,简化了编程。
- 内存保护 (Protection):隔离了不同程序,一个程序的错误不会影响到其他程序或操作系统。
- 共享与并发 (Sharing & Concurrency):允许多个进程共享库文件,并支持高效的多任务执行。
- 当多个程序(进程 A、进程 B)都使用了同一个动态链接库
- 物理内存只加载一份
- 对于 进程 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 * 4KBorder = 1→ \(2^1 = 2\) 页 → 8KBorder = 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(消息总线、队列、数据库访问)