0%

1. 前言

我们在调试 release 版本的程序时,由于缺乏符号信息,所以需要通过寄存器来查看函数的参数、返回值等。

2. 寄存器

2.1. 通用寄存器 (General Purpose Registers)

寄存器名 英文名称 作用
rax Accumulator 累加器,通常用于算术运算和函数返回值存储。
rbx Base 基址寄存器,常用于存储数据或指针。
rsi Source Index 源索引寄存器,常用于字符串操作中的源地址指针(函数第一个参数)。
rdi Destination Index 目标索引寄存器,常用于字符串操作中的目标地址指针或结构体指针(函数第二个参数)。
rdx Data 数据寄存器,常用于 I/O 操作或乘除法运算中的扩展数据存储(函数第三个参数)。
rcx Counter 计数器寄存器,常用于循环计数或字符串操作中的计数(函数第四个参数)。
rsp Stack Pointer 栈指针寄存器,指向当前栈顶。
rbp Base Pointer 基址指针寄存器,指向当前栈帧的基址。
r8~r15 General Purpose 通用寄存器,扩展的 64 位寄存器之一,用于存储数据或指针(r8r9 常用于保存函数第五六个参数)。

2.2. 特殊用途寄存器 (Special Purpose Registers)

寄存器名 英文名称 作用
rip Instruction Pointer 指令指针寄存器,存储当前执行指令的地址。
rflags Flags 标志寄存器,存储状态标志位(如进位、溢出、零标志等)。

2.3. 段寄存器 (Segment Registers)

寄存器名 英文名称 作用
cs Code Segment 代码段寄存器,指向当前代码段的基址。
ds Data Segment 数据段寄存器,指向当前数据段的基址。
es Extra Segment 额外段寄存器,指向额外数据段的基址。
fs FS Segment 特殊用途段寄存器,常用于线程本地存储等。
gs GS Segment 特殊用途段寄存器,常用于线程本地存储等。
ss Stack Segment 栈段寄存器,指向当前栈段的基址。

2.4. 浮点与向量寄存器 (Floating Point and Vector Registers)

寄存器名 英文名称 作用
xmm0-xmm15 SIMD Registers 用于 SSE 指令集的 128 位向量运算。
ymm0-ymm15 AVX Registers 用于 AVX 指令集的 256 位向量运算。
zmm0-zmm31 AVX-512 Registers 用于 AVX-512 指令集的 512 位向量运算。

2.5. 函数调用时的参数传递

在 x86_64 架构中,函数调用时的参数传递遵循 System V AMD64 ABI(Linux/Unix 系统的标准调用约定)。

前六个整数或指针类型的参数依次存储在以下寄存器中:

  1. rdi - 第一个参数
  2. rsi - 第二个参数
  3. rdx - 第三个参数
  4. rcx - 第四个参数
  5. r8 - 第五个参数
  6. r9 - 第六个参数

对于浮点类型的参数(如 floatdouble),前八个参数存储在以下 SSE 寄存器 中:

  1. xmm0 - 第一个浮点参数
  2. xmm1 - 第二个浮点参数
  3. xmm2 - 第三个浮点参数
  4. xmm3 - 第四个浮点参数
  5. xmm4 - 第五个浮点参数
  6. xmm5 - 第六个浮点参数
  7. xmm6 - 第七个浮点参数
  8. xmm7 - 第八个浮点参数

溢出参数(超过寄存器数量)会依次存储在 中:

  • 超过寄存器数量(整数参数超过 6 个,浮点参数超过 8 个)的参数会依次压入栈中。
  • 栈需要保持 16 字节对齐,可能会插入填充字节。
  • 可以通过访问栈指针(rsp)或基址指针(rbp)来找到栈上的参数。
    • 使用 rsp(栈指针)
      • 在函数入口时,rsp 指向栈顶(即返回地址的下一个位置)。
      • 栈上的第一个参数位于 [rsp + 8](跳过返回地址)。
      • 第二个参数位于 [rsp + 16],依此类推。
    • 使用 rbp(基址指针)
      • 如果函数使用了帧指针(rbp),rbp 通常指向调用者的栈帧基址。
      • 栈上的第一个参数位于 [rbp + 16](跳过返回地址和保存的 rbp)。
      • 第二个参数位于 [rbp + 24],依此类推。

3. 栈

3.1. 理解栈布局

在函数调用时,栈的布局通常如下(从高地址到低地址):

  1. 返回地址:调用函数时,call 指令会将返回地址(下一条指令的地址)压入栈中。
  2. 溢出参数:如果参数超过寄存器数量,多余的参数会依次压入栈中。
  3. 栈对齐填充:为了满足 16 字节对齐要求,可能会有额外的填充字节。
  4. 局部变量和保存的寄存器:函数内部可能会在栈上分配空间用于局部变量或保存调用者的寄存器。

3.2. 函数调用时的压栈过程

在x86_64架构中,函数调用时会涉及到栈的操作,包括压栈和出栈。这些操作主要用于保存调用者的上下文(如返回地址、寄存器值)以及为被调用函数分配栈帧。

3.2.1. 调用者(Caller)的操作

  1. 压入返回地址
    当调用者使用 call 指令调用函数时,CPU会自动将返回地址(下一条指令的地址)压入栈中。此时,rsp(栈指针)会减少8字节(64位系统)。

    1
    2
    3
    4
    call function
    # 等价于:
    push rip ; 将返回地址压入栈
    jmp function
  2. 压入溢出参数(如果有)
    如果函数的参数超过了寄存器数量(整数参数超过6个,浮点参数超过8个),多余的参数会从右到左依次压入栈中。rsp 会随着每个参数的压入减少。

  3. 对齐栈
    为了满足 16字节对齐 的要求,调用者可能会插入额外的填充字节,使得 rsp 在调用函数前保持16字节对齐。

3.2.2. 被调用者(Callee)的操作

  1. 保存调用者的栈帧基址
    被调用者通常会保存调用者的栈帧基址(rbp),以便在函数返回时恢复调用者的栈帧。

    1
    2
    push rbp       ; 保存调用者的 rbp
    mov rbp, rsp ; 设置当前函数的栈帧基址
  2. 分配栈空间
    被调用者会根据函数内部局部变量的需求,在栈上分配空间。rsp 会减少相应的字节数。

    1
    sub rsp, <size>  ; 为局部变量分配栈空间

3.3. 2. 函数返回时的出栈过程

3.3.1. 被调用者(Callee)的操作

  1. 释放局部变量的栈空间
    被调用者在返回前会释放为局部变量分配的栈空间。

    1
    add rsp, <size>  ; 恢复 rsp
  2. 恢复调用者的栈帧基址
    被调用者会恢复调用者的 rbp,以确保调用者的栈帧完整。

    1
    pop rbp  ; 恢复调用者的 rbp
  3. 返回到调用者
    被调用者使用 ret 指令从栈中弹出返回地址,并跳转到该地址。

    1
    ret  ; 等价于:pop rip

3.3.2. 调用者(Caller)的操作

  1. 清理栈上的参数(如果需要)
    如果调用约定要求调用者清理栈上的参数(如 cdecl 调用约定),调用者会调整 rsp
    1
    add rsp, <size>  ; 清理栈上的参数

3.4. 3. 栈指针(rsp)和基址指针(rbp)的变化

以下是一个函数调用的栈布局示例:

C代码

1
2
3
4
5
6
7
8
void example(int a, int b) {
int x = a + b;
}

int main() {
example(1, 2);
return 0;
}

汇编代码(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# main 函数
main:
sub rsp, 16 ; 对齐栈
mov edi, 1 ; 第一个参数 -> rdi
mov esi, 2 ; 第二个参数 -> rsi
call example ; 调用 example 函数
add rsp, 16 ; 恢复栈
ret

# example 函数
example:
push rbp ; 保存调用者的 rbp
mov rbp, rsp ; 设置当前栈帧基址
sub rsp, 16 ; 为局部变量分配栈空间
mov eax, edi ; a -> eax
add eax, esi ; a + b
leave ; 恢复栈帧(等价于:mov rsp, rbp; pop rbp)
ret ; 返回调用者

栈布局变化

操作 rsp 变化 栈内容(从高地址到低地址)
call example rsp -= 8 返回地址
push rbp rsp -= 8 保存调用者的 rbp
sub rsp, 16 rsp -= 16 为局部变量分配空间
leave rsp += 16 释放局部变量空间
ret rsp += 8 弹出返回地址

3.5. 总结

  1. 函数调用的栈操作
    • 调用者负责压入返回地址和溢出参数。
    • 被调用者负责保存 rbp 和分配局部变量空间。
    • 函数返回时,释放局部变量空间并恢复调用者的栈帧。
  2. rsprbp 的变化
    • rsp 指向栈顶,动态变化。
    • rbp 指向栈帧基址,通常固定不变。

4. 使用 gdb 查看寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看所有寄存器
info registers

# 查看指定寄存器
info registers rdi
# 或简写
i r rdi

# 查看十六进制
p/x $rdx # 十六进制
p/d $rdx # 十进制
# 或简写
p $rdx

其中,info registers 会打印三列:

  • 第一列:寄存器名称
  • 第二列:寄存器的值(十六进制)
  • 第三列:寄存器的值(十进制;也可能是十六进制,用 0x 开头)

info registers rdip $rdi 效果相同。

从寄存器查到的内存地址,可以用 x (examinze)命令来查看内存的值:

1
2
3
4
5
6
7
8
9
10
# 查看指令
x/i $rip
# 查看栈顶
x/16x $rsp

# 查看内存
p $rdi
x/2gx $rdi
# 或先用 $rdi 查出内存地址,直接用地址访问
x/2gx 47926411878160

x 命令的说明:

1
x/FMT ADDRESS

其中:

  • x:表示“examine memory”(查看内存)
  • 2:数字,表示要查看的单元数
  • g:表示每个单元的 size,有 b(byte), h(halfword), w(word), g(giant, 8 bytes)
  • x:表示值的格式,有 o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string)
    and z(hex, zero padded on the left).

在 gdb 命令行中使用 help 命令,可以查看命令的说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) help x
Examine memory: x/FMT ADDRESS.
ADDRESS is an expression for the memory address to examine.
FMT is a repeat count followed by a format letter and a size letter.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), c(char), s(string)
and z(hex, zero padded on the left).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
The specified number of objects of the specified size are printed
according to the format. If a negative number is specified, memory is
examined backward from the address.

Defaults for format and size letters are those previously used.
Default count is 1. Default address is following last thing printed
with this command or "print".

5. 使用 gdb 查看栈

  • bt
  • frame
  • args
  • locals
  • x

TDODO

5.1. frame 与寄存器的值

  • GDB 中的寄存器值(如 $rax, $rdi, $rsp 等)是当前 CPU 执行上下文的快照。
  • 当你切换到 frame 0(最内层栈帧)时,寄存器值是最真实的,因为这是程序当前正在执行的地方。
  • 当你切换到 外层栈帧(frame 1, 2, …)时,GDB 会尝试还原当时的寄存器状态,但这依赖于:
    • 编译器是否保存了寄存器值(如 callee-saved)
    • 是否有调试符号或 unwind 信息
    • GDB 是否能推断出寄存器的保存位置

寄存器值可能出现的情况

情况 表现
寄存器是 caller-saved(如 rdi, rsi, rax) 可能显示 或错误值
寄存器是 callee-saved(如 rbx, rbp, r12~r15) 通常能正确还原
没有调试信息或优化严重 GDB 无法还原,显示当前值或

建议

  • 如果你要分析寄存器状态,最好在 frame 0 或断点处进行。
  • 如果你在分析 core dump 或栈破坏问题,寄存器值只能作为参考,不要完全依赖外层 frame 的寄存器快照。
  • 使用 info args 和 info locals 更可靠地查看参数和局部变量(如果有符号信息)。

6. 在特定线程中设置断点

6.1. 断点只作用于某线程

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看所有线程 ID 和当前线程 ID(gdb 中会使用 * 标注当前线程)
(gdb) info threads
# 切换当前上下文到指定线程
(gdb) thread <THREAD_ID>
# 通过查看当前堆栈是不是自己要断点的线程
(gdb) bt
(gdb) break LOCATION thread THREADNUM
# 条件断点
(gdb) break source.c:123 thread 5 if fds[0].fd == 7
# 如果没有 debug 符号,可以利用函数返回值寄存器断点
(gdb) break poll thread 2 if $rdx > 0
# 完整格式
break [PROBE_MODIFIER] [LOCATION] [thread THREADNUM] [if CONDITION]

6.2. 锁定调度器,只让当前线程运行

默认情况下,GDB 会让所有线程一起运行(比如你执行 continue 时)。如果你只想让当前线程运行,其它线程保持暂停,可以使用:

1
(gdb) set scheduler-locking on

这表示:只有当前线程会执行,其他线程全部暂停。

其中模式还有:

模式 说明
off 默认值,所有线程都可以运行
on 只有当前线程运行,其他线程暂停
step 单步调试时只运行当前线程,continue 时其他线程也会运行

你可以随时切换:

1
(gdb) set scheduler-locking step
  • 如果你在调试死锁、竞态或线程间通信问题,锁定调度器是非常有效的方式。
  • 如果你在调试某个 poll() 或 epoll_wait() 调用,只想观察某个线程的行为,可以结合 catch syscall 和 thread 命令一起使用。

7. 查看汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看汇编代码,其中 "=>" 标记的是当前执行位置
(gdb) disassemble
# 反汇编指定地址范围
# 这会显示从当前指令开始的 32 字节范围内的汇编代码。
(gdb) disassemble $rip, $rip+32

# 查看当前指令(x86)
(gdb) x/i $pc
# 或在 x86-64 架构下:
(gdb) x/i $rip

# Bonus: 默认 GDB 使用 AT&T 风格(如 %rax),你可以切换为 Intel 风格
# 这样输出会更接近你在汇编教材或 IDA Pro 中看到的格式
(gdb) set disassembly-flavor intel

8. 位置无关代码(PIC)

8.1. 什么是 PIC

  • PIC(Position Independent Code,位置无关代码)是一种编译方式,使得生成的代码可以在内存中的任意位置运行,而无需硬编码绝对地址。
  • 在动态链接库(shared libraries)中,通常需要使用 PIC,以便库可以被加载到任意内存地址。
  • 节省空间:相比使用 64 位绝对地址,RIP 相对寻址只需要一个 32 位偏移量。
  • 更安全:支持地址随机化(ASLR),提高程序的安全性。

8.2. PIC 的实现

  1. 访问全局变量
    在 PIC 模式下,代码通过 全局偏移表(GOT, Global Offset Table)过程链接表(PLT, Procedure Linkage Table) 访问全局变量和函数地址。

  2. 寄存器 rip 的使用
    x86_64 支持基于 rip(指令指针)的寻址方式,PIC 会利用 rip 相对寻址来访问全局变量或函数地址,而不是使用绝对地址。

在 x86-64 架构中,传统的绝对地址寻址方式不再适用于位置无关代码。于是引入了 RIP(指令指针)相对寻址:

假设你有一个全局变量 int x = 42;,在汇编中访问它可能会变成:

1
2
asm
mov eax, DWORD PTR [rip + offset_to_x]

这里的 offset_to_x 是编译器计算出来的 x 相对于当前指令的偏移量。

寻址方式 描述 是否位置无关
绝对地址寻址 使用固定地址,如 [0x400123] ❌ 否
寄存器间接寻址 如 [rax],地址由寄存器决定 ✅ 是
RIP 相对寻址 如 [rip + offset],相对当前指令位置 ✅ 是

8.3. PIC 的优化

  • 减少重定位:通过 rip 相对寻址,避免了加载时的重定位操作,提高了加载速度。
  • 共享内存:多个进程可以共享同一段动态库代码,而无需为每个进程生成独立的副本。

8.4. 示例

1
2
mov rax, [rip + global_var@GOTPCREL]  ; 通过 GOT 表访问全局变量
call [rip + func@PLT] ; 通过 PLT 表调用函数

举个 gdb 调试的例子:

1
2
3
4
(gdb) x/i $rip
=> 0x2ac084b5ec10 <poll>: cmpl $0x0,0x2d939d(%rip) # 0x2ac084e37fb4 <__libc_multiple_threads>
(gdb) p (bool)$__libc_multiple_threads
true
  • cmpl $0x0, 0x2d939d(%rip) 是一条比较指令(cmp),用于将某个内存地址中的值与立即数 0 进行比较。
  • (%rip) 表示使用 RIP 相对寻址,这是 x86-64 架构中常见的一种寻址方式。
  • 实际比较的是地址 0x2ac084e37fb4 处的值,也就是 __libc_multiple_threads 这个变量。

__libc_multiple_threads 是什么?

  • 这是 GNU C 库(glibc)中的一个内部变量,用来标记当前进程是否启用了多线程。
  • 如果这个值是 0,说明当前进程是单线程。
  • 如果是非零,说明进程中有多个线程。

所以这条指令的作用是:判断当前进程是否是多线程环境,可能用于决定是否启用线程安全的行为。

8.5. 为什么使用 RIP 相对寻址?

  1. RIP 是唯一始终已知的寄存器
  • 在执行指令时,CPU 总是知道当前指令的地址(即 RIP)。
  • 所以可以在编译时计算出目标数据与当前指令之间的偏移量,而不需要知道数据的绝对地址。

这就允许编译器生成位置无关代码,即使程序被加载到不同的内存地址,偏移量仍然有效。

  1. 其他寄存器值是动态的,不可预测
  • 比如 RBX、RAX、RDI 等寄存器,它们的值在运行时可能被程序修改。
  • 如果用这些寄存器做基址寻址,编译器就无法提前知道它们的值,也就无法生成稳定的偏移量。
  1. 支持共享库和地址空间布局随机化(ASLR)
  • RIP 相对寻址让代码段不依赖固定地址,可以被多个进程共享。
  • 也支持操作系统在运行时随机加载地址,提高安全性(ASLR)。
  1. 节省指令空间
  • 使用 RIP 相对寻址只需要一个 32 位偏移量。
  • 如果使用绝对地址,需要嵌入完整的 64 位地址,指令长度更长,效率更低。

8.6. 为什么使用 RIP 相对寻址只需要一个 32 位偏移量

在 x86-64 架构中,RIP 相对寻址的偏移量被设计为一个有符号的 32 位整数,也就是一个 displacement(位移)字段,它在机器码中只占用 4 个字节。

  • RIP 是 64 位的指令指针,表示当前指令的地址。

  • RIP 相对寻址的目标地址是通过:

    目标地址 = 下一条指令地址(RIP) + 32 位偏移量

  • 这个偏移量是一个 有符号整数,所以它的范围是:

    从 −2³¹ 到 +2³¹−1,即 ±2GB 的寻址范围。

这意味着,当前指令附近 ±2GB 范围内的任何数据都可以通过 RIP 相对寻址访问。

优点 说明
✅ 节省空间 只用 4 字节表示偏移,比使用完整 64 位地址节省指令长度
✅ 支持位置无关代码 编译器只需计算偏移,不依赖绝对地址
✅ 高效 CPU 执行时只需加法运算,无需查表或重定位
✅ 安全 支持地址空间布局随机化(ASLR),提高安全性

8.7. 为什么可以被多个进程共享?

因为代码中不再硬编码具体地址,多个进程可以:

  • 使用同一份物理内存中的代码段。
  • 每个进程有自己的数据段,但共享同一份只读代码。

这大大节省了内存,提高了系统效率。

举个例子:

进程 加载地址 使用的代码段
A 0x400000 使用共享代码段
B 0x500000 使用共享代码段

两者的代码段内容完全一样,因为里面的寻址是相对 RIP 的,不依赖于加载地址。

为什么绝对寻址不可以被多进程共享?

  • 每个进程的虚拟地址空间是独立的
    • 操作系统为每个进程分配独立的虚拟地址空间。
    • 即使两个进程都加载了同一个程序,它们的地址空间可能完全不同。
    • 如果代码中使用绝对地址,加载到不同地址空间后,这些地址就不再有效。

所以,绝对地址在一个进程中是有效的,在另一个进程中可能就指向错误的地方或根本不存在。

  • 需要重定位,无法直接共享物理页

    • 如果使用绝对地址,操作系统必须在每个进程加载时对代码进行“重定位”,修改指令中的地址。
    • 一旦修改,代码段就变成了进程私有,不能共享同一份物理内存。
    • 而位置无关代码(如使用 RIP 相对寻址)不需要修改,可以直接映射到多个进程的地址空间。
  • 违反共享库的设计原则

    • 动态链接库(如 .so.dll)的核心优势就是可以被多个进程共享。
    • 如果库中使用绝对地址,每个进程都要有自己的副本,失去了共享的意义。
    • 正确做法是使用位置无关代码(PIC),让库在任意地址都能运行。
区域 是否可共享 原因说明
代码段 ✅ 是 只读 + 位置无关,多个进程可映射同一物理页
数据段 ❌ 否 每个进程的数据不同,需独立副本
❌ 否 动态分配,地址空间不同
❌ 否 私有调用栈,不能混用
共享内存段 ✅ 是 显式创建,专门用于共享

如果你想深入了解某个进程的内存布局,可以分析 /proc/[pid]/maps 或用工具如 pmapvmmap

9. 实际 debug 例子:在多线程中查看 poll 的事件

先复习下 poll 函数:

1
2
3
4
5
6
7
8
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// 第一个参数 fds 的类型
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

FIXME:这种在汇编代码 ret 前断点,并依据 raxrdi 设置条件断点的方式不可靠,因为可能进入了 libc 层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 查看 polll 的汇编代码
(gdb) disass poll
Dump of assembler code for function poll:
0x00002ac084b5ec10 <+0>: cmpl $0x0,0x2d939d(%rip) # 0x2ac084e37fb4 <__libc_multiple_threads>
0x00002ac084b5ec17 <+7>: jne 0x2ac084b5ec29 <poll+25>
0x00002ac084b5ec19 <+0>: mov $0x7,%eax
0x00002ac084b5ec1e <+5>: syscall
0x00002ac084b5ec20 <+7>: cmp $0xfffffffffffff001,%rax
0x00002ac084b5ec26 <+13>: jae 0x2ac084b5ec59 <poll+73>
0x00002ac084b5ec28 <+15>: ret
0x00002ac084b5ec29 <+25>: sub $0x8,%rsp
0x00002ac084b5ec2d <+29>: call 0x2ac084b77600 <__libc_enable_asynccancel>
0x00002ac084b5ec32 <+34>: mov %rax,(%rsp)
0x00002ac084b5ec36 <+38>: mov $0x7,%eax
0x00002ac084b5ec3b <+43>: syscall
0x00002ac084b5ec3d <+45>: mov (%rsp),%rdi
0x00002ac084b5ec41 <+49>: mov %rax,%rdx
0x00002ac084b5ec44 <+52>: call 0x2ac084b77660 <__libc_disable_asynccancel>
0x00002ac084b5ec49 <+57>: mov %rdx,%rax
0x00002ac084b5ec4c <+60>: add $0x8,%rsp
0x00002ac084b5ec50 <+64>: cmp $0xfffffffffffff001,%rax
0x00002ac084b5ec56 <+70>: jae 0x2ac084b5ec59 <poll+73>
=> 0x00002ac084b5ec58 <+72>: ret
0x00002ac084b5ec59 <+73>: mov 0x2d31f0(%rip),%rcx # 0x2ac084e31e50
0x00002ac084b5ec60 <+80>: neg %eax
0x00002ac084b5ec62 <+82>: mov %eax,%fs:(%rcx)
0x00002ac084b5ec65 <+85>: or $0xffffffffffffffff,%rax
0x00002ac084b5ec69 <+89>: ret

# 找到所有的 ret 指令,设置条件断点
# 注意:最好是在 ret 指令之前的指令上也加上断点,
# 因为 ret 的时候,可能已经把当前栈(除 rsp / rbp 外)都弹出了,寄存器中将看不到当前栈的信息
#
# $rax 是返回值寄存器,也就是返回大于 0 时,让进程暂停
# 这里的 * 表示取内存的值(存放的是指令),* 断不可少,不然会被认为是 Function name
#
(gdb) b *0x00002ac084b5ec26 thread 4 if $rax > 0
(gdb) b *0x00002ac084b5ec28 thread 4 if $rax > 0
(gdb) b *0x00002ac084b5ec56 thread 4 if $rax > 0
(gdb) b *0x00002ac084b5ec58 thread 4 if $rax > 0
(gdb) b *0x00002ac084b5ec65 thread 4 if $rax > 0
(gdb) b *0x00002ac084b5ec69 thread 4 if $rax > 0

# 继续运行
(gdb) c

# 当 IO 事件发生,程序会被暂停

源码

https://github.com/dmtcp/dmtcp/tree/main/jalib

malloc 的特性和局限

malloc/free 是操作系统(或 C 库)提供的通用堆分配器。

  • 它通常会采用 “内存池 + 分块 + 空闲链表” 等技术,但它为了通用性和线程安全,设计得很复杂,开销较大。
  • 在高并发 / 频繁小块分配释放的场景下,malloc 的性能和碎片控制未必理想。

malloc 很难让你:

  • 控制分配内存的位置(如 DMTCP 需要特殊内存区域)
  • 统计 / 追踪所有分配的内存块
  • 实现定制的分配策略(如无锁、分层小块池、预扩展等)
  • 轻松调试和隔离内存问题

内存碎片

malloc 的确在其实现内部也维护着自己的 “内存池”,并且会对小块内存(small bins/tcache/fast bins 等)做优化和分组管理。比如在 glibc 的 malloc(ptmalloc)中,就有针对小块内存的快速分配机制。

  1. malloc 是 “通用分配器”
    malloc 需要支持所有应用场景,包括大 / 小 / 奇异尺寸的分配、跨多线程、兼容各种系统调用和 ABI。
    为了兼容性和健壮性,malloc 实现复杂,包含很多额外的元数据和检查,导致分配 / 释放开销更大。

  2. malloc 的小块管理是 “全局的”
    malloc 管理的小块是全进程共享的,所有线程 / 模块都会竞争同一套管理结构(如 fastbin、tcache、small bin)。
    在高并发、频繁小块分配 / 释放的场景下,锁竞争和同步成本变高,可能成为性能瓶颈。

  3. 自定义分配器(如 jalib)“更窄、更专用”
    jalib 只服务 DMTCP 内部的特殊内存分配需求,只关注固定几种典型的小块尺寸(如 64/256/1024…)。
    可以用更简单、更高效的 “无锁链表 + 内存对齐块” 来管理池,分配和释放几乎都是 O(1)的原子操作。
    不需要兼容所有 malloc 的场景(如 realloc、跨模块释放等),所以能极致优化。

  4. 控制权和可观测性
    jalib 可以完全掌控池的生命周期、分配区域、分配策略(如预扩展、定制回收),还可以追踪统计、调试。
    malloc 的内部状态你无法直接控制或感知,也无法方便地和 DMTCP 的 checkpoint、回滚等功能集成。

  5. 内存碎片和确定性
    专用分配器能保证分配块 “定长、对齐”,几乎无碎片,分配和回收都是确定性的。
    malloc 需要兼容各种尺寸,碎片和内存抖动不可避免。

jalib(自定义分配器)的设计动机

性能优化

  • DMTCP 频繁地分配和释放小块内存(如元数据、临时缓存等),如果每次都用 malloc,性能损耗大。
  • jalib 采用分级固定块池,每次分配 / 释放只需操作链表和原子变量,比 malloc 更快、更少碎片。

线程安全的高效实现

  • jalib 用无锁(128 位 CAS)或轻量级互斥方案,适合高并发分配 / 释放。
  • malloc 虽然线程安全,但实现方式更重,适用范围更广,未必最优。

可控性和可追踪性

  • jalib 可以统计分配次数、追踪所有内存池区,方便调试、分析和 checkpoint 恢复。
  • 可根据实际需求预分配或批量扩展,避免运行时大规模内存抖动。

适应 DMTCP 的特殊需求

  • DMTCP 需要在 checkpoint/restore 时管理所有内存区域,jalib 可以定制分配区域、分配方式,malloc 无法满足。
  • 可实现特定平台的优化,如 mmap 固定地址分配等。

故障隔离和调试

  • jalib 可以在有 bug 或内存泄漏时,帮助定位具体的分配 / 释放流程。
  • 可以方便地记录所有 arena 信息,甚至实现特殊的调试模式。

总结

虽然 malloc 也是内存池管理,但它是为通用用途设计的,不能满足 DMTCP 这类高性能、高可控性、特殊内存管理需求场景。自定义 jalib 分配器可以更高效地管理小块内存,优化多线程性能,便于调试和适配特定需求。

可以归纳为三点:

  • 性能更高,碎片更少
  • 更好地适应 DMTCP 的需求
  • 更易于调试和控制

jalloc 设计思路

多层级固定大小块分配(层级分配器)

  • 设计了 5 个分配层级(lvl1~lvl5),每层负责不同大小的定长内存块(如 64、256、1024、4096、16384 字节)。
  • 小于等于这 5 个等级的分配请求,会被分配到各自的层级。
  • 超过最大层级的请求,则直接调用 _alloc_raw(通常是 mmap)。

优点:

  • 小块内存可以复用,减少系统调用和碎片。
  • 大块内存仍可直接用系统接口,兼顾通用性。

固定块分配器 JFixedAllocStack

每个层级对应一个 JFixedAllocStack<N>,其核心是无锁栈式管理:

  • 内部维护一个空闲块栈(LIFO 链表)。
  • allocate 时从栈取出一个空闲块,栈空时调用 expand 申请一批新块。
  • deallocate 时将块归还到栈顶。

核心技术点

  • 原子双字比较交换(128 位 CAS)

    为了线程安全,栈顶指针 _top 需要原子更新。这里用到了 128 位 CAS(Compare-And-Swap),保证 node 指针和计数器同时更新,避免 ABA 问题。

  • CAS 不可用时的降级方案

    对于不支持 128 位原子操作的平台,采用 futex+memcpy 的方式手动实现互斥和原子性。

线程安全设计

分配和释放都用原子操作保护,无需锁,性能高。
多线程环境下不会出现竞争条件或内存破坏。

Arena 记录和调试

分配的内存区域(arena)可以记录到全局数组中,方便调试和统计。
通过 JAlloc::getAllocArenas() 可获得分配区域列表。

全局 new/delete 重载(可选)

如果定义了 OVERRIDE_GLOBAL_ALLOCATOR,会重载 operator newoperator delete,让全局 new/delete 也用这个分配器。

灵活切换

可以通过宏 JALIB_ALLOCATOR 切换:

  • 启用时用自定义分配器
  • 否则回退为标准 malloc/free

总结

本内存分配器的设计核心在于:

  • 采用多级固定块内存池 + 无锁算法,高效服务于小块高频分配 / 释放;
  • 通过 128 位原子操作或 futex 确保并发安全,适用多平台;
  • 提供 arena 管理和统计,方便调试与维护;
  • 兼容传统分配方式,易于集成和切换。

这种设计非常适合像 DMTCP 这样对性能和内存管理有特殊要求的系统级软件。

Memory

Memory = 存储 + 访问逻辑

存储

  • 在仿真里,memory 本质上就是一个数组(Array)或者向量(Vector),每个元素对应一个存储单元(bit/byte/word)。
  • 例:一个 8 位 × 1024 深度的 RAM,可以在仿真里用 uint8_t mem[1024]; 表示。

访问逻辑

  • 读(Read):根据地址返回对应的数据。
  • 写(Write):根据地址和写使能信号,将数据写入存储单元。
  • 可能涉及 时序:同步(clock 边沿写入)或异步(立即生效)。

时序和延迟

  • 在硬件里,memory 访问不是瞬间的:存在 读延迟、写延迟。
  • 仿真时,可以用 延时事件 或 clock 边沿触发 来模拟。

仿真代码

功能说明

  • 多端口读写:支持同时多个端口访问 memory
  • 写冲突仲裁:写优先策略或延迟写,可扩展读优先 / 轮询
  • 读延迟 pipeline:延迟由 read_delay 控制
  • Burst / wrap-around:访问超出末尾自动回绕
  • 简单 Cache/Tag:模拟命中 / 未命中
  • 异步端口:不同端口调用 read/write 可在不同 tick,模拟异步时钟
  • 随机 bit flip / SEU:1% 概率错误注入
  • 统计与功耗估算基础:记录读写次数、命中数、平均延迟
EdaMemoryFull.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <random>
#include <map>
#include <algorithm>
#include <fstream>

struct Event {
int time;
std::function<void()> action;
bool operator<(const Event& other) const { return time > other.time; }
};

class EdaMemoryFull {
std::vector<uint8_t> mem;
std::priority_queue<Event> event_queue;
int sim_time = 0;
int read_delay;
std::mt19937 gen;

int cache_size;
struct CacheLine { bool valid=false; int tag=-1; };
std::vector<CacheLine> cache;

int max_bus_access_per_cycle = 2;
int current_cycle_access = 0;

// 统计信息
std::map<int,int> read_count, write_count, read_hits, write_hits;
std::map<int,int> read_delay_total;
int dynamic_power = 0; // 动态功耗
int static_power; // 静态功耗
std::vector<int> dynamic_power_per_cycle; // 每周期动态功耗

// 动态功耗公式(单位:动态功耗单位 = 每bit切换一次算1)
//
// P_dyn = α * C * V^2 * f
// 其中,α 开关活动因子(switching activity)
// C 负载电容
// V 电源电压
// f 时钟频率
// 简化为每次 bit 改变增加一个单位动态功耗
//
// 静态功耗公式(单位:静态功耗单位 = 每8bit存储单元算1)
//
// P_static = I_leak * V * N
// 简化为每8bit存储单元增加1单位静态功耗
//
// 总功耗
//
// P_total = P_dyn + P_static

public:
enum WritePriority { WRITE_FIRST, READ_FIRST, ROUND_ROBIN } write_prio;

EdaMemoryFull(size_t size, int delay, int c_size, WritePriority prio=WRITE_FIRST)
: mem(size,0), read_delay(delay), gen(std::random_device{}()),
cache_size(c_size), cache(c_size), static_power(size/8), write_prio(prio)
{
std::uniform_int_distribution<> dis(0,255);
for(auto &v: mem) v = dis(gen);
}

void write(int port,int addr,const std::vector<uint8_t>& data){
write_count[port]++;
if(current_cycle_access>=max_bus_access_per_cycle){
event_queue.push({sim_time+1,[this,port,addr,data](){ write(port,addr,data); }});
return;
}
current_cycle_access++;
if(write_prio==WRITE_FIRST){
apply_write(addr,data);
} else {
event_queue.push({sim_time+1,[this,addr,data](){ apply_write(addr,data); }});
}
update_cache(port, addr,data,true);
}

void read(int port,int addr,size_t length,std::function<void(std::vector<uint8_t>)> callback){
read_count[port]++;
if(current_cycle_access>=max_bus_access_per_cycle){
event_queue.push({sim_time+1,[this,port,addr,length,callback](){ read(port,addr,length,callback); }});
return;
}
current_cycle_access++;
int trigger_time = sim_time+read_delay;
bool hit = check_cache(port, addr,length);
if(hit) read_hits[port]++;
event_queue.push({trigger_time,[this,addr,length,callback,port,trigger_time]() {
std::vector<uint8_t> data;
for(size_t i=0;i<length;i++){
uint8_t val = mem[(addr+i)%mem.size()];
if(random_bit_flip()) val ^= (1<<(gen()%8));
dynamic_power += count_bit_changes(val, mem[(addr+i)%mem.size()]);
data.push_back(val);
}
read_delay_total[port] += (trigger_time - sim_time);
callback(data);
}});
}

void tick(){
sim_time++;
current_cycle_access = 0;
int cycle_dyn_power = 0;

while(!event_queue.empty() && event_queue.top().time <= sim_time){
auto e = event_queue.top(); event_queue.pop();
int before = dynamic_power;
e.action();
cycle_dyn_power += (dynamic_power - before);
}

dynamic_power_per_cycle.push_back(cycle_dyn_power);

// ASCII 可视化每周期动态功耗
int scale = 50;
int bar_len = *std::max_element(dynamic_power_per_cycle.begin(), dynamic_power_per_cycle.end())>0 ?
cycle_dyn_power*scale/(*std::max_element(dynamic_power_per_cycle.begin(), dynamic_power_per_cycle.end())) : 0;
std::cout << "Cycle " << sim_time << " dyn power: " << cycle_dyn_power
<< " total power: " << dynamic_power + static_power << " ";
for(int i=0;i<bar_len;i++) std::cout<<"#";
std::cout << std::endl;
}

void print_stats() const {
std::cout<<"Simulation stats:\n";
for(auto& [port,cnt]: read_count)
std::cout<<"Port "<<port<<" read count: "<<cnt
<<", hits: "<<read_hits.at(port)
<<", avg delay: "<<(cnt?read_delay_total.at(port)/cnt:0)<<"\n";
for(auto& [port,cnt]: write_count)
std::cout<<"Port "<<port<<" write count: "<<cnt
<<", hits: "<<write_hits.at(port)<<"\n";
std::cout<<"Dynamic power units: "<<dynamic_power<<"\n";
std::cout<<"Static power units: "<<static_power<<"\n";
std::cout<<"Total power units: "<<dynamic_power + static_power<<"\n";
}

void export_power_csv(const std::string &filename) const {
std::ofstream ofs(filename);
if(!ofs.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return;
}
ofs << "Cycle,DynamicPower,StaticPower,TotalPower\n";
for(size_t i=0;i<dynamic_power_per_cycle.size();i++){
int dyn = dynamic_power_per_cycle[i];
int total = dyn + static_power;
ofs << (i+1) << "," << dyn << "," << static_power << "," << total << "\n";
}
ofs.close();
std::cout << "Power data exported to " << filename << std::endl;
}

private:
bool random_bit_flip(){
std::uniform_real_distribution<> dis(0.0,1.0);
return dis(gen)<0.01;
}

int count_bit_changes(uint8_t a,uint8_t b){
uint8_t diff = a^b;
int count=0;
while(diff){ count+=diff&1; diff>>=1; }
return count;
}

bool check_cache(int port, int addr,size_t length){
int line = addr % cache_size;
int tag = addr / cache_size;
return cache[line].valid && cache[line].tag==tag;
}

void update_cache(int port, int addr,const std::vector<uint8_t>& data,bool write=false){
int line = addr % cache_size;
int tag = addr / cache_size;
cache[line].valid=true;
cache[line].tag=tag;
if(write) write_hits[port]++;
}

void apply_write(int addr,const std::vector<uint8_t>& data){
for(size_t i=0;i<data.size();i++) mem[(addr+i)%mem.size()]=data[i];
}
};

// 示例主程序
int main(){
EdaMemoryFull mem(1024,2,16,EdaMemoryFull::WRITE_FIRST);

mem.write(0,10,{42,43,44});
mem.write(1,11,{99});
mem.read(0,10,3,[](std::vector<uint8_t> data){
std::cout<<"Port0 read burst: ";
for(auto v:data) std::cout<<(int)v<<" ";
std::cout<<std::endl;
});
mem.read(1,11,1,[](std::vector<uint8_t> data){
std::cout<<"Port1 read: "<<(int)data[0]<<std::endl;
});

for(int i=0;i<10;i++) mem.tick();
// 导出功耗数据
mem.export_power_csv("memory_power.csv");

mem.print_stats();
return 0;
}

输出的 memory_power.csv 文件内容示例:

1
2
3
4
5
6
7
8
9
10
11
Cycle,DynamicPower,StaticPower,TotalPower
1,3,128,131
2,0,128,128
3,5,128,133
4,2,128,130
5,7,128,135
6,1,128,129
7,4,128,132
8,0,128,128
9,2,128,130
10,3,128,131

每列含义:

  • Cycle:周期号
  • DynamicPower:每周期动态功耗单位
  • StaticPower:静态功耗单位(固定)
  • TotalPower:总功耗单位

功耗分析

report_power.pyview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import pandas as pd
import matplotlib.pyplot as plt

# 读取 CSV
df = pd.read_csv('memory_power.csv')

# 找到动态功耗峰值周期
peak_cycle = df['DynamicPower'].idxmax() + 1
peak_value = df['DynamicPower'].max()

plt.figure(figsize=(12,6))

# 绘制条形背景(ASCII风格效果)
for i, val in enumerate(df['DynamicPower']):
bar_len = int(val / peak_value * 50) # 50字符最大长度
plt.text(i+1, 0, '#' * bar_len, fontsize=8, color='grey', va='bottom')

# 绘制曲线
plt.plot(df['Cycle'], df['DynamicPower'], label='Dynamic Power', marker='o', color='blue')
plt.plot(df['Cycle'], df['StaticPower'], label='Static Power', linestyle='--', color='orange')
plt.plot(df['Cycle'], df['TotalPower'], label='Total Power', linestyle='-.', color='green')

# 高亮峰值
plt.scatter(peak_cycle, peak_value, color='red', s=100, label='Peak Dynamic Power')

plt.title('Memory Power Simulation with Peak Highlight & ASCII-style Bars')
plt.xlabel('Cycle')
plt.ylabel('Power Units')
plt.grid(True, linestyle=':')
plt.legend()
plt.tight_layout()
plt.show()

burst/multi-port 总线冲突

在多端口系统中,尤其是在使用总线结构的系统中,总线冲突(Bus contention)是一个常见的问题。总线冲突通常发生在多个设备尝试同时访问总线上的同一资源时。这种情况可能会导致数据损坏、系统性能下降或甚至系统崩溃。下面是一些解决和缓解总线冲突的策略:

  1. 仲裁机制
    仲裁是解决总线冲突的一种常用方法。它通过一个仲裁器(Arbiter)来决定哪个设备可以访问总线。常见的仲裁策略有:

优先级仲裁:根据预先设定的优先级顺序决定哪个设备可以访问总线。

轮询仲裁:轮流让每个设备访问总线。

基于请求的仲裁(如请求共享(Request-for-Shared, RFS)和请求独占(Request-for-Exclusive, RFE)):设备首先请求对资源的访问,然后根据请求的类型(共享或独占)来决定访问权限。

  1. 分时复用
    通过时间分割(Time Division Multiplexing, TDM)或频率分割(Frequency Division Multiplexing, FDM),可以允许多个设备在不同的时间或频率上使用总线,从而减少冲突。例如,可以使用时分多路复用将总线的不同时间段分配给不同的设备。

  2. 编码和解码技术
    使用特殊的编码和解码技术,如霍纳编码(Hornar code)或格雷码(Gray code),可以减少在总线上传输数据时的错误,并帮助检测和纠正数据冲突。

  3. 总线锁定
    在访问总线期间,通过总线锁定机制确保没有其他设备可以访问总线。这可以通过在总线上设置一个锁定信号来实现,该信号在访问期间保持激活状态。

  4. 缓存和缓冲
    为每个设备提供局部缓存或缓冲机制,可以减少对总线的直接访问次数,从而降低冲突的可能性。当一个设备需要与总线上的另一个设备通信时,它可以先将数据写入自己的缓存,然后再由缓存同步到总线上。

  5. 使用更宽的总线
    增加总线的宽度可以允许在同一时间内传输更多的数据,从而减少对总线的需求,降低冲突的可能性。

实施步骤
评估系统需求:确定哪些类型的设备将使用总线,以及它们对带宽的需求。

选择仲裁策略:根据设备的优先级和带宽需求选择合适的仲裁策略。

设计硬件:根据选定的策略设计硬件,包括添加仲裁器、缓存和适当的控制逻辑。

测试和优化:实施后进行系统测试,根据测试结果调整策略或硬件设计。

通过上述方法,可以有效管理和减少多端口系统中的总线冲突问题,提高系统的稳定性和性能。

Cache Tag(缓存标记)

Cache Tag(缓存标记)是高速缓存(Cache)中的关键组成部分,用于存储数据在主存中的地址信息,以便快速定位数据位置。 ‌

核心功能
Tag字段存储了主存中数据的地址信息,当CPU访问主存时,首先通过Tag字段判断数据是否存在于Cache中。若存在,则直接从Cache读取;若不存在,则访问主存。 ‌

结构组成

  • ‌Tag‌:记录数据在主存的地址信息。
  • ‌Data‌:存储实际数据。
  • ‌Valid Bit‌:标记数据是否有效。
  • ‌Dir‌:目录信息,用于区分不同数据块。 ‌

应用场景

现代处理器通常采用多级Cache结构(如L1、L2、L3),其中Tag与Data共同构成Cache Line,用于快速访问和存储数据。例如,ARMv8-A架构的处理器包含独立的I-Cache和D-Cache,分别存储指令和数据。

Cache Tag 仿真代码

FIXME: 该代码会 coredump 。

cache_simulator.hview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#ifndef CACHE_SIMULATOR_H
#define CACHE_SIMULATOR_H

#include <vector>
#include <unordered_map>
#include <mutex>
#include <atomic>

// 缓存行结构体
struct CacheLine {
bool valid = false;
bool dirty = false;
uint32_t tag = 0;
uint64_t last_used = 0; // 用于LRU替换策略
std::vector<uint8_t> data;
};

// 端口访问请求结构体
struct PortRequest {
uint32_t port_id;
bool is_write;
uint32_t addr;
uint8_t* data_ptr;
size_t data_size;
};

class CacheSimulator {
public:
CacheSimulator(uint32_t line_size, uint32_t num_lines, uint32_t num_ports);

// 多端口访问接口
void process_request(const PortRequest& req);

// 缓存配置
void set_write_policy(bool write_back);
void set_replacement_policy(int policy); // 0:LRU, 1:FIFO, 2:Random

private:
// 内部缓存操作
bool access_cache(uint32_t port_id, uint32_t addr, bool is_write, uint8_t* data, size_t size);
void handle_miss(uint32_t port_id, uint32_t addr);
void evict_line(uint32_t set_idx, uint32_t way_idx);

// 多端口同步
std::atomic<uint64_t> global_counter_{0}; // 原子计数器
std::vector<std::mutex> port_locks_;

// 缓存结构
uint32_t line_size_;
uint32_t num_sets_;
std::vector<std::vector<CacheLine>> cache_;

// 策略配置
bool write_back_;
int replacement_policy_;
};
#endif
cache_simulator.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "cache_simulator.h"
#include <algorithm>
#include <random>
#include <cstring>

CacheSimulator::CacheSimulator(uint32_t line_size, uint32_t num_lines, uint32_t num_ports)
: port_locks_(num_ports), line_size_(line_size) {
num_sets_ = num_lines; // 简单实现,可扩展为组相联
cache_.resize(num_sets_, std::vector<CacheLine>(1)); // 直接映射
}

void CacheSimulator::process_request(const PortRequest& req) {
std::lock_guard<std::mutex> lock(port_locks_[req.port_id]);
access_cache(req.port_id, req.addr, req.is_write, req.data_ptr, req.data_size);
}

bool CacheSimulator::access_cache(uint32_t port_id, uint32_t addr, bool is_write,
uint8_t* data, size_t size) {
uint32_t tag = addr / line_size_;
uint32_t set_idx = tag % num_sets_;

// 查找命中
for (auto& line : cache_[set_idx]) {
if (line.valid && line.tag == tag) {
line.last_used = ++global_counter_;
if (is_write) {
memcpy(line.data.data(), data, size);
line.dirty = true;
} else {
memcpy(data, line.data.data(), size);
}
return true;
}
}

// 未命中处理
handle_miss(port_id, addr);
return false;
}

void CacheSimulator::handle_miss(uint32_t port_id, uint32_t addr) {
uint32_t tag = addr / line_size_;
uint32_t set_idx = tag % num_sets_;

// 查找可替换行
auto& lines = cache_[set_idx];
auto victim = std::min_element(lines.begin(), lines.end(),
[](const CacheLine& a, const CacheLine& b) {
return a.last_used < b.last_used; // LRU策略
});

// 写回脏数据
if (write_back_ && victim->dirty) {
// 模拟写回主存操作
}

// 加载新数据
victim->valid = true;
victim->tag = tag;
victim->dirty = false;
victim->last_used = ++global_counter_;
// 模拟从主存加载数据
victim->data.resize(line_size_);
}
main.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "cache_simulator.h"
#include <thread>
#include <iostream>

void port_thread(CacheSimulator& cache, uint32_t port_id) {
for (int i = 0; i < 1000; ++i) {
PortRequest req;
req.port_id = port_id;
req.is_write = (i % 3 == 0);
req.addr = rand() % 0xFFFF;
uint8_t data[64] = {0};
req.data_ptr = data;
req.data_size = sizeof(data);

cache.process_request(req);
}
}

int main() {
CacheSimulator cache(64, 1024, 4); // 64B行, 1024行, 4端口

std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(port_thread, std::ref(cache), i);
}

for (auto& t : threads) {
t.join();
}

std::cout << "Cache simulation completed" << std::endl;
return 0;
}
Makefileview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 编译器配置
CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -O3 -pthread
LDFLAGS := -pthread

# 项目结构
SRC_DIR := .
BUILD_DIR := build
TARGET := $(BUILD_DIR)/cache_simulator

# 源文件列表
SRCS := $(wildcard $(SRC_DIR)/*.cpp)
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)

# 默认目标
all: $(BUILD_DIR) $(TARGET)

# 创建构建目录
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)

# 主目标链接
$(TARGET): $(OBJS)
$(CXX) $(LDFLAGS) $^ -o $@

# 编译规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CXXFLAGS) -MMD -c $< -o $@

# 包含依赖关系
-include $(DEPS)

# 清理
clean:
rm -rf $(BUILD_DIR)

.PHONY: all clean

IO + CPU 密集 + IO

tasks

tasks.hppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 模拟 IO 和 CPU 任务
*/

#include <unistd.h>

#include <iostream>
#include <vector>

struct DataChunk {
std::vector<char> data;
DataChunk() = default;
explicit DataChunk(size_t size) : data(size) {}
};

struct CompressedChunk {
std::vector<char> data;
CompressedChunk() = default;
explicit CompressedChunk(size_t size) : data(size) {}
};

// 模拟数据读取函数
bool read_from_network(DataChunk& chunk) {
sleep(3); // 模拟 IO 延迟

chunk.data.resize(100); // 模拟每个数据块有 100 个字节

static int count = 0;
if (count++ >= 10) return false; // 模拟读取 10 个数据块后结束

std::generate(chunk.data.begin(), chunk.data.end(),
[]() { return rand() % 256; });
return true;
}

// 模拟压缩函数
char compress_byte(char byte) {
for (int i = 0; i < 10'000ll; ++i)
; // 模拟 CPU busy
return byte % 128; // 简单压缩算法示例
}

// 模拟写入函数
void write_to_file(const CompressedChunk& chunk) {
sleep(3); // 模拟 IO 延迟
std::cout << "Writing chunk of size " << chunk.data.size() << "\n";
}

方案一

1_message_queue.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 方案 1:异步队列 + TBB 流水线
*/

#include <tbb/tbb.h>

#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
#include <atomic>

#include "tasks.hpp"

// 全局队列
std::queue<std::vector<char>> readQueue;
std::mutex mtx;
std::condition_variable cv;
std::atomic_bool stop = false;

// 读线程
void networkReader() {
while (!stop) {
DataChunk chunk;

if (!read_from_network(chunk)) // 阻塞 I/O
{
stop.store(true, memory_order::releaxed);
return;
}

{
std::lock_guard<std::mutex> lock(mtx);
readQueue.push(std::move(chunk.data));
}
cv.notify_one();
}
}

// 压缩任务
void compressor() {
while (!stop) {
DataChunk chunk;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !readQueue.empty(); });
chunk.data = std::move(readQueue.front());
readQueue.pop();
}

// CPU 密集计算,使用 TBB 并行
CompressedChunk compressed(chunk.data.size());
tbb::parallel_for(size_t(0), chunk.data.size(), [&](size_t i) {
compressed.data[i] = compress_byte(chunk.data[i]); // 假设单字节压缩
});

write_to_file(compressed); // 可以异步
}
}

int main() {
std::thread reader(networkReader);
std::thread worker(compressor);

reader.join();
worker.join();
}

方案二

2_flow_graph.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 方案 2:TBB Flow Graph
*/

#include <tbb/flow_graph.h>
#include <tbb/tbb.h>

#include <iostream>
#include <vector>

#include "tasks.hpp"

using namespace tbb;
using namespace tbb::flow;

int main() {
graph g;

// 1. 读取节点(串行)
input_node<DataChunk> reader(
g,
[](flow_control& fc) -> DataChunk {
DataChunk chunk(1024); // 1KB数据块
if (!read_from_network(chunk)) {
fc.stop();
return DataChunk();
}
return chunk;
});

// 2. 并行处理节点(无限制并发)
function_node<DataChunk, CompressedChunk> processor(
g, unlimited,
[](const DataChunk& input) -> CompressedChunk {
CompressedChunk output(input.data.size());

tbb::parallel_for(
tbb::blocked_range<size_t>(0, input.data.size()),
[&](const tbb::blocked_range<size_t>& r) {
for (size_t i = r.begin(); i != r.end(); ++i) {
output.data[i] = compress_byte(input.data[i]);
}
});

return output;
});

// 3. 写入节点(串行保证写入顺序)
function_node<CompressedChunk> writer(
g, serial,
[](const CompressedChunk& output) {
write_to_file(output);
});

// 构建数据流管道
make_edge(reader, processor);
make_edge(processor, writer);

// 启动管道
reader.activate();
g.wait_for_all();

return 0;
}

方案三

3_pipeline.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 方案 3: TBB 流水线
*/

#include <tbb/tbb.h>

#include <iostream>
#include <vector>

#include "tasks.hpp"

int main() {
tbb::parallel_pipeline(
/* max_number_of_live_token */ 4,
// Stage 1: 读网络数据
tbb::make_filter<void, DataChunk>(
tbb::filter_mode::serial_in_order,
[](tbb::flow_control& fc) -> DataChunk {
DataChunk chunk;
if (!read_from_network(chunk)) { // 返回 false 时结束
fc.stop();
}
return chunk;
}) &
// Stage 2: CPU 压缩
tbb::make_filter<DataChunk, CompressedChunk>(
tbb::filter_mode::parallel,
[](DataChunk chunk) -> CompressedChunk {
CompressedChunk out(chunk.data.size());
tbb::parallel_for(size_t(0), chunk.data.size(), [&](size_t i) {
out.data[i] = compress_byte(chunk.data[i]);
});
return out;
}) &
// Stage 3: 写文件
tbb::make_filter<CompressedChunk, void>(
tbb::filter_mode::serial_in_order,
[](CompressedChunk out) {
write_to_file(out); // 可以异步
}));

return 0;
}

IO模型概念

IO 模型通常按两条维度划分:

  1. 阻塞 vs 非阻塞

    • 阻塞 IO(Blocking IO):调用 read/recv 等函数时,如果数据没准备好,进程会被挂起,直到数据就绪。
    • 非阻塞 IO(Non-blocking IO):调用 read/recv 时,如果数据没准备好,直接返回 EAGAIN 或 EWOULDBLOCK,进程继续做别的事情。
  2. 同步 vs 异步

    • 同步(Synchronous):调用者要等待操作完成才能继续。

      • 阻塞 IO + 同步:最常见,比如普通 read(fd, buf, n)
      • 非阻塞 IO + 同步:调用立即返回,如果没数据则报错或返回 0
    • 异步(Asynchronous):调用者发起操作后,不需要等待,操作完成时通过回调、信号、事件通知等告知结果。

⚡ 关键:异步 IO 的核心是不阻塞当前线程,而结果通知是通过事件或回调完成的。

Linux 常见异步 IO 方式

Linux 下主要有四种机制:

  1. POSIX AIO(aio_* 系列)

    • 系统调用:aio_read(), aio_write()

    • 完成通知方式:

      • 轮询 aio_error()
      • 信号通知 SIGIO
      • 回调函数 sigevent.sigev_notify = SIGEV_THREAD
    • 使用场景:文件 IO,可以在后台发起读写请求,主线程继续工作。

    • ⚠️ 目前性能不如 epoll + 线程池模拟异步。

  2. 信号驱动 IO(SIGIO)

    • 进程或文件描述符注册 F_SETOWN,开启 O_ASYNC
    • 当 fd 可读写时,内核发信号给进程
    • 通常用于少量 fd 的异步事件
  3. I/O 多路复用(select, poll, epoll)

    • 本质是非阻塞 + 事件通知

    • Epoll + 非阻塞 IO 可以模拟高效的异步 IO

    • 适合网络服务器、socket 编程

    • 典型流程:

      • 设置 fd 为非阻塞(否则 read/write 可能阻塞,因为 epoll 本质是同步的)
      • 注册 fd 到 epoll,关注 EPOLLIN / EPOLLOUT
      • 调用 epoll_wait 等待事件
      • 事件触发时读取或写入数据
  4. Linux AIO(io_uring)

    • 新一代高性能异步 IO 接口
    • 支持文件、网络 IO
    • 提供 提交队列 + 完成队列,几乎零系统调用开销
    • 可以真正做到线程几乎不阻塞等待

异步 IO 的优点

  • 不阻塞主线程,提高吞吐量
  • 可同时处理大量 IO(特别是网络/文件服务器)
  • 与多线程相比,降低线程上下文切换开销

异步 IO 的缺点

  • 编程复杂度高(需要事件驱动、回调或状态机)
  • 错误处理和信号安全问题复杂
  • 文件异步 IO 性能在传统 AIO 下不一定比多线程高

Linux 下常见异步 I/O 机制对比

特性 / 机制 POSIX AIO epoll + 非阻塞 IO io_uring 信号驱动 IO (SIGIO)
类型 异步文件 IO 多路复用 + 非阻塞网络 IO 高性能异步 IO 异步事件通知
支持对象 文件 文件描述符(socket、管道等) 文件 + 网络 + 其他 IO 文件描述符(socket、pipe)
用户态/内核态 系统调用提交,内核异步处理 用户态轮询/等待事件,内核检查 fd 用户态 SQ + 内核 CQ 用户注册 fd,内核通过信号通知
提交方式 aio_read/aio_write 写入 fd 并通过 epoll_wait 检查 写入 SQ(批量可提交) 设置 O_ASYNC + F_SETOWN
完成通知 信号 / 回调 / aio_error轮询 epoll_wait 返回就绪事件 完成队列 (CQ),阻塞或非阻塞读取 信号处理函数 (SIGIO)
性能 中等,系统调用多 高,单线程处理大量 fd 很高,几乎零系统调用,批量提交 较低,信号开销大,适合少量 fd
编程复杂度 中等偏复杂 中等,需要状态机处理 高,但灵活,可批量和链式操作 高,信号处理函数限制多,必须信号安全
适合场景 文件异步读写 高并发网络服务器 高性能文件和网络 IO 少量异步事件或控制信号触发场景

Linux io_uring

io_uring 是 Linux 内核自 5.1 版本引入的一个异步 I/O 框架,它提供了 低延迟、高吞吐的异步文件和网络 I/O。它的特点是:

  • 零拷贝提交:应用程序可以直接向内核提交 I/O 请求,无需系统调用每次阻塞。
  • 环形队列机制:通过共享内存的 提交队列(Submission Queue, SQ) 和 完成队列(Completion Queue, CQ),用户态和内核态可以高效交互。
  • 支持多种 I/O 类型:文件读写、网络收发、文件同步、缓冲区操作等。
  • 批量提交和完成:可以一次提交多个 I/O 请求,并批量获取完成结果。

简单理解:它把传统阻塞 I/O 的 “系统调用来回” 改成了 共享环形队列 + 异步通知。

安装

  1. 方法一:从 APT 安装
1
2
sudo apt update
sudo apt install liburing-dev

检查安装路径

1
ls /usr/include/liburing.h
  1. 方法二:从源码安装
1
2
3
4
git clone https://github.com/axboe/liburing.git
cd liburing
make
sudo make install

io_uring 的核心数据结构

Submission Queue(SQ)

  • 用户态将 I/O 请求放入 SQ。
  • SQ 是一个环形数组,存放 io_uring_sqe(I/O 请求条目)。
  • 用户通过 系统调用 io_uring_enter 将 SQ 中的新请求通知内核。
  • 内核会按顺序处理 SQ 中的 I/O 请求。
字段 作用
opcode I/O 类型,如读、写、fsync、accept、sendmsg
fd 文件描述符
off 偏移量(文件 I/O)
addr 用户缓冲区地址
len I/O 数据长度
flags 请求标志,如 IOSQE_FIXED_FILEIOSQE_IO_LINK

Completion Queue(CQ)

  • 内核完成 I/O 后,将结果写入 CQ。
  • CQ 也是一个环形数组,存放 io_uring_cqe(完成条目)。
  • 用户可以轮询或等待 CQ 获取完成结果。
字段 作用
res I/O 结果,成功为正数(读写字节数),失败为负错误码
user_data 用户自定义数据,方便识别请求

io_uring 工作流程

1
2
3
4
5
6
7
8
9
10
11
12
+-----------+          +-----------+
| User App | <-----> | Kernel |
+-----------+ +-----------+
| |
| write SQE to SQ | <- Submission Queue
|-------------------->|
| |
| io_uring_enter | <- 通知内核处理
|-------------------->|
| |
| <------------------ | <- CQE 放入 CQ
| read CQE from CQ |
  • 用户态填充 SQE(Submission Queue Entry)。
  • 调用 io_uring_enter() 提交 SQE (不阻塞)。
  • 内核处理 I/O 请求。
  • 内核把完成结果写入 CQ。
  • 用户态可以:
    • 轮询 CQ:主动读取 CQE(Completion Queue Entry)
    • 注册回调(liburing 新版本支持 IORING_SETUP_IOPOLL + IORING_SETUP_SQPOLL 或自己封装)

注意:

  • 异步 I/O ≠ 必须用回调。关键是提交后不阻塞等待,可以同步轮询完成结果,也可以异步触发回调。
  • 回调是一种可选的使用方式。
  • 最核心的是 共享环形队列 + 完成队列,用户可以同步取结果也可以异步通知。

为什么 io_uring 没有强制回调

传统异步 I/O(比如 Windows IOCP)必须注册回调或事件句柄,因为内核不会给你“主动通知”。

Linux io_uring 的设计哲学是:

  • 用户态和内核共享内存 → 用户态可以自己轮询完成队列。
  • 减少系统调用次数 → 不依赖信号或回调触发。
  • 需要回调时,用户可以自己封装一个事件循环。

所以你看到 io_uring 的官方示例都是 顺序写代码,但是仍然是异步 I/O,因为:

  • 提交后内核可以并行处理多个 I/O。
  • 用户态无需阻塞等待内核完成处理(可以去做别的事)。

io_uring 的使用示例(C 语言)

io_uring_hello.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>

#define QUEUE_DEPTH 8
#define BUFFER_SIZE 1024

int main() {
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
int ret, fd;
char buf[BUFFER_SIZE];

// 打开文件
fd = open("test.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}

// 初始化 io_uring
ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
if (ret < 0) {
perror("io_uring_queue_init");
return 1;
}

// 获取提交队列条目
sqe = io_uring_get_sqe(&ring);
if (!sqe) {
fprintf(stderr, "io_uring_get_sqe failed\n");
return 1;
}

// 准备读取操作
io_uring_prep_read(sqe, fd, buf, BUFFER_SIZE, 0);

// 提交到内核
ret = io_uring_submit(&ring);
if (ret < 0) {
perror("io_uring_submit");
return 1;
}

// 等待完成
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
perror("io_uring_wait_cqe");
return 1;
}

// 读取结果
if (cqe->res < 0) {
fprintf(stderr, "Async read failed: %d\n", cqe->res);
} else {
printf("Read %d bytes: %.*s\n", cqe->res, cqe->res, buf);
}

// 通知内核完成
io_uring_cqe_seen(&ring, cqe);

// 关闭
io_uring_queue_exit(&ring);
close(fd);

return 0;
}

这个例子展示了 最基本的异步文件读取:

  • 初始化 ring。
  • 获取一个 SQE 并填充读请求。
  • 提交 SQE。
  • 等待 CQE 获取结果。
  • 标记完成并清理。

Boost asio

要区分 操作系统级别的异步 I/O 和 asio 的抽象,因为 asio 并不是单一机制,而是根据平台选择最优实现。具体分析如下:

asio 的工作原理

asio 提供 异步接口(async_read, async_write 等),程序不会阻塞线程

内部实现方式根据平台不同而不同:

平台 异步方式
Linux 基于 epoll / io_uring / AIO,是真正的内核异步 I/O(零拷贝,内核通知完成)
Windows 基于 IOCP(I/O Completion Ports),内核异步 I/O
Mac / BSD 基于 kqueue / poll,有些情况下是模拟异步(多线程或事件轮询)

要确认 asio 在你的 Linux 机器上选择了哪种底层 I/O 机制,可以按下面几个方法操作:

  1. 查看 asio 使用的 I/O 对象

asio 有两个主要 I/O 后端:

  • 旧版 AIO / epoll(select_reactor / epoll_reactor)
  • io_uring(在新版本 Boost.Asio 或 standalone Asio 支持)

在 编译时,asio 会检测系统特性:

  • 如果 Linux 内核 ≥ 5.1,asio 默认启用 io_uring
  • 否则使用 epoll
  1. 通过宏或配置查看

在你的 asio 头文件中,可能有如下宏:

1
2
3
4
5
#if defined(BOOST_ASIO_HAS_IOURING)
std::cout << "asio will use io_uring\n";
#elif defined(BOOST_ASIO_HAS_EPOLL)
std::cout << "asio will use epoll\n";
#endif

这些宏在 boost/asio/detail/config.hpp 或 asio/config.hpp 中定义,表示底层机制。

  1. 运行时确认

asio 本身没有公开 API 显示底层 I/O 类型,但可以通过系统调用监控判断:

使用 strace 观察程序 I/O:

1
strace -f ./your_program 2>&1 | grep io_uring
  • 如果看到 io_uring_setup、io_uring_enter 系统调用,就说明启用了 io_uring
  • 如果没有,只看到 epoll_wait / epoll_ctl,说明使用的是 epoll

对 epoll,strace 会显示 epoll_create1 / epoll_ctl / epoll_wait

阻塞 vs 异步

类型 描述 asio 中的表现
阻塞 I/O 调用时线程被挂起,等待 I/O 完成 不使用,线程会被阻塞,TBB 线程占用
模拟异步 内核不支持真正异步,用线程轮询或线程池实现 Mac/BSD 某些场景下可能是模拟
真正异步 I/O 内核支持,操作提交后立即返回,完成由内核通知 Linux/io_uring、Windows IOCP 就是真正异步

线程安全性

malloc 是线程安全的,但有一些细节需要注意。

  1. 线程安全含义

线程安全:多个线程同时调用 malloc/free 不会破坏堆管理结构,也不会导致内存管理崩溃。

实现方法:

glibc malloc 在内部使用 锁(mutex 或 spinlock) 保护全局堆管理数据结构

不同线程同时申请或释放内存,内核保证堆表一致

  1. 限制与注意事项

性能问题

多线程频繁 malloc/free,锁竞争可能成为瓶颈

对性能敏感的程序可能使用:

线程本地缓存(thread-local cache) 的 jemalloc / tcmalloc

避免全局锁竞争

信号处理上下文不安全

虽然线程安全,但 malloc 在信号处理函数里不安全

原因:

信号可能打断正在执行的 malloc

malloc 内部锁可能被持有 → 再次调用可能死锁

所以 signal handler 中不能直接调用 malloc/free

特性 malloc/glibc
多线程调用安全 ✅ 是线程安全的
信号处理函数调用安全 ❌ 不安全
性能 可能锁竞争,需要优化(jemalloc/tcmalloc)

信号安全

malloc / free 内部会修改全局堆管理结构(如 free list)

如果信号到达时主程序正在调用 malloc 或 free,信号处理函数里再次调用 malloc/free → 堆数据结构可能被破坏,
可能导致崩溃或内存泄漏

虽然线程安全,但 malloc 在信号处理函数里不安全

原因:

  • 信号可能打断正在执行的 malloc
  • malloc 内部锁可能被持有 → 再次调用可能死锁

所以 signal handler 中不能直接调用 malloc/free

安全做法

信号处理函数里只做:

  • 设置标志位(sig_atomic_t flag = 1;)
  • 写入 pipe / eventfd
  • 调用 async-signal-safe 系统调用(write(), _exit())

之后由主程序在安全上下文处理 malloc/free 或其他复杂操作

异步信号

锁、malloc 等不能在信号处理函数里用。这里涉及到 异步信号安全 (async-signal-safe) 的概念。

  1. 信号处理函数的执行环境

当一个信号到达进程时,内核 异步中断当前执行流,立即跳转到信号处理函数执行。

这意味着:

当前线程可能 正在持有锁(mutex、spinlock 等)

当前线程可能 正在使用 malloc/free,操作堆数据结构

  1. 为什么不能调用这些函数

锁(mutex 等)

如果信号处理函数里调用 pthread_mutex_lock():

线程可能已经在信号到达前持有这个锁

信号处理函数再次尝试加锁 → 死锁

malloc / free

malloc 内部会修改全局堆管理结构(如 free list)

如果信号到达时主程序正在调用 malloc 或 free,信号处理函数里再次调用 malloc/free → 堆数据结构可能被破坏

可能导致崩溃或内存泄漏

  1. async-signal-safe 函数

POSIX 定义了一组 “异步信号安全函数”(async-signal-safe functions)

信号处理函数中 只允许调用这些函数

常用安全函数示例:

_exit()

write()(低级系统调用,不会锁堆)

sig_atomic_t 类型变量赋值

总结

函数类型 可在信号处理函数里用? 原因
pthread_mutex_lock 可能已持锁 → 死锁
malloc/free 可能正在操作堆 → 数据结构破坏
write(fd, buf, n) 系统调用,不会破坏用户态结构
_exit() 安全终止进程

核心思想:信号是异步的,中断当前执行流,调用非 async-signal-safe 函数可能破坏正在执行的操作,导致不可预测的行为。

  1. thread_local 的基本语义

C++11 引入的存储类型说明符:

thread_local int x = 0;

表示 每个线程都有一份独立的 x,互不干扰。

生命周期:跟普通静态变量类似(全局存活直到线程退出)。

  1. 底层实现原理

它的实现依赖于 TLS (Thread Local Storage, 线程局部存储) 机制。

在 ELF/Linux 下:

编译器在 .tdata / .tbss 段里为 thread_local 变量分配空间(就像全局变量在 .data / .bss 段里)。

程序加载时,动态链接器(ld.so)会记录这些 TLS 变量的“模板布局”。

每个线程启动时,线程库(glibc/pthread)会:

给这个线程分配一块 TLS 块(通常放在线程栈附近,或者专门的内存页)。

把 .tdata 里的初始值拷贝到这个线程的 TLS 块。

.tbss 部分(未初始化的 thread_local)则清零。

线程访问 thread_local 时,编译器生成的代码会通过 TLS 寄存器(如 x86-64 的 FS/GS 段寄存器)+ 偏移量,找到对应线程的存储单元。

例如 x86-64 Linux 上,errno 就是:

#define errno (*__errno_location())

而 __errno_location() 内部就是通过 %fs:offset 找到 TLS 块里的 errno。

  1. 存放在哪里?

Windows:在 TEB (Thread Environment Block) 里有 TLS 指针,__declspec(thread) 就用它。

Linux/ELF:在每个线程的 TLS 块里(通常分配在线程栈附近的一片内存区域)。访问通过 FS/GS 寄存器。

编译器细节:

GCC/Clang 默认用 “动态 TLS 模型”(访问时通过动态链接器查询 TLS 偏移)。

如果加 -ftls-model=initial-exec,编译器会直接用固定偏移访问 TLS,速度更快(但要求变量在主程序或静态库里)。

  1. 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
thread_local int counter = 0;

void worker(int id) {
for (int i = 0; i < 3; i++) {
counter++;
std::cout << "Thread " << id << ": counter = " << counter << "\n";
}
}

int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
}

输出大致是:

1
2
3
4
5
6
Thread 1: counter = 1
Thread 1: counter = 2
Thread 1: counter = 3
Thread 2: counter = 1
Thread 2: counter = 2
Thread 2: counter = 3

说明 counter 在不同线程里独立。

✅ 总结:

thread_local 变量放在 TLS 段,每个线程有自己的拷贝。

访问是通过 线程局部存储寄存器(FS/GS)+ 偏移量 实现的。

存储空间由线程库在创建线程时分配和初始化。

.
├── AT&T Assembly Manual.pdf
├── C++ Primer Plus 第五版中文版.pdf
├── C++.Primer.5th.Edition_2013.pdf
├── CplusplusConcurrencyInAction_PracticalMultithreading.pdf
├── Debug Hacks中文版:深入调试的技术和工具.pdf
├── IB-Host-and-Ports.pdf
├── IBTA-Overview-of-IBTA-Volume-1-Release-1.6-2022-07-15.pdf
├── InifiniBand Guide.pdf
├── LDAP Authentication Guide.pdf
├── Linux Kernel Networking - Implementation and Theory.pdf
├── Linux-UNIX系统编程手册(上、下册).pdf
├── Linux内核设计与实现(第三版中文高清带目录).pdf
├── Linux多线程服务端编程 - 陈硕.pdf
├── Linux多线程服务端编程:使用muduo C++网络库.pdf
├── Linux高性能服务器编程.pdf
├── OKTA Multifactor Authentication (MFA) FAQ.pdf
├── Shell脚本学习指南.pdf
├── TBB
│   ├── Intel TBB.pdf
│   ├── TBBtutorial.pdf
│   ├── intel-tbb.pdf
│   ├── oneTBB-master.zip
│   └── onetbb_developer-guide-api-reference_2021.6-772616-772617.pdf
├── UNIX环境高级编程(第三版).pdf
├── UNIX网络编程卷1:套接字联网API(第3版).pdf
├── ccia_code_samples-master.zip
├── dmtcp
│   ├── Be Kind, Rewind —Checkpoint & Restore Capability for Improving Reliability of Large-scale Semiconductor Design.pdf
│   ├── dmtcp-mug-17.pdf
│   └── plugin-tutorial.pdf
├── gdb-and-assembly.pdf
├── gdb-refcard.pdf
├── gdb.pdf
├── p4-p4v-cheat-sheet_cn.pdf
├── thread-analyzer-193426.pdf
├── 并行编程
│   ├── C++ Concurrency in Action.pdf
│   ├── C++-Concurrency-In-Action-2ed-zh-v0.2.pdf
│   ├── C++并发编程实战(中文版).pdf
│   ├── C++并发编程实战.pdf
│   ├── C++并发编程实战源代码
│   ├── C++并发编程实战源代码.zip
│   ├── Patterns_for_Parallel_Programming.pdf
│   └── 高并发网络模型.png
└── 编译原理(龙书) 第二版.pdf

1. Github Pages

本文解释利用github pages搭建个人主页/项目主页的方法。

github pages简介:官方链接

github pages使用了CNAME record技术,参考:链接1链接2Custom domains in Github Pages

注:Read the Docs也是一个很好的搭建个人主页的网站。

Github Pages 站点类型

有3种类型的 Github Pages 站点(sites):project, user 和 organization 。

Project sites 连接到 github 上特定 project ,比如 Javascript library 或 recipe collection。user 或 organization sites 连接到 github.com 的特定账户。

发布 user site ,你必须创建一个你的个人账户下的一个名为 <username>.github.io 的 repository 。发布 organization site ,你必须创建一个组织所有的名为 <organization>.github.io 的 repository 。除非你使用 custom domain ,否则 user 和 organization sites 将位于 http(s)://<username>.github.iohttp(s)://<organization>.github.io

project site 的源文件存储在作为 project 的相同的 repository 中。除非使用 custom domain , 否则 project sites 将位于 http(s)://<username>.github.io/<repository>http(s)://<organization>.github.io/<repository>

有关如何自定义影响您网站的域名的更多信息,参见”About custom domains and GitHub Pages“。

每个 github 账户允许创建 1 个 user 或 organization 站点。无论是被组织还是个人所有,project 站点的个数不限制。

GitHub Pages 访问方法

参考官方文档

例如,你的project站点配置的发布源是gh-pages分支,然后在gh-pages分支上创建了一个about/contact-us.md文件,你将可以在https://<user>.github.io/<repository>/about/contact-us.html访问它。

你也可以使用Jekyll等静态站点生成器来给你的github page配置一个主题。

站点发布常见问题的解决方法

Github workflows

参考官方文档

2. 配置前准备

2.1. Markdown编辑器

推荐的markdown编辑器

  • VSCode:免费。VSCode原生支持Markdown,安装一些插件可以帮助更快地编写markdown文件。
  • Typora:现在已经开始收费。

VSCode markdown插件:

  • Mardown All in One: 提供快捷键,帮助更快的编写markdown文件。
  • Markdown+Math:提供数学公式支持。
  • Markdown Preview Enhanced: 将原生markdown预览的黑色背景改成白色。
  • Markdown Preview Github Styling:提供Github风格的预览。

在线表格生成器:可以生成Markdown、Text、HTML、LaTex、MediaWiki格式的表格。

2.2. 轻量级虚拟机WSL

WSL,Windows Subsystem for Linux,是Windows提供的轻量级Linux虚拟机。

安装教程:见链接

2.2.1. WSL默认没有启用systemctl:

启用systemctl的方法:链接

替代方法:不需要启动systemctl,因为会比较占用资源,启动也会变慢。可以使用service命令替代。

2.2.2. WSL默认没有安装openssl-server:

使用ssh连接到服务器时,需要服务器运行着sshd程序,否则连接不上,会出现”Connection refused“错误。

参考链接

查看openssh-server有没有安装:

1
dpkg --list | grep ssh

注:如果安装了openssh-server,执行which sshd可以看到路径。

WSL默认没有安装openssh-server,安装方法:

1
sudo apt-get install openssh-server

启动ssh:

1
sudo service ssh start

2.2.3. 通过https登录到github

git push不再支持输入用户名和密码,当提示输入密码时,需要输入personal access token.

步骤1:在github上创建personal access token

步骤2:在命令行上使用personal access token

步骤3:为了避免每次都需要输入personal access token,可以将其缓存在git client上

1
gh auth login

注:使用gh命令需要先安装GitHub CLI:

1
sudo apt-get install gh

2.2.4 执行git pull/push时,出现”The authenticity of host ‘github.com (20.205.243.166)’ can’t be established. ED25519 key fingerprint is SHA256:…”错误

报错解释:

这个报错信息通常出现在使用SSH连接到一个新的主机时。它表示你的计算机无法验证远程服务器的身份,因为服务器的公钥不在你本地计算机的known_hosts文件中。这是SSH为了防止”中间人”攻击而进行的安全检查。

解决方法:

验证指纹信息:你可以查看远程主机的指纹信息,并与服务器gitee.com的公钥指纹进行对比,确保它们匹配。你可以在~/.ssh/known_hosts文件中找到已知主机的公钥指纹。

如果确认指纹正确无误,且你信任这个服务器,可以添加这个主机及其公钥到你的known_hosts文件中,以便SSH不再警告。执行以下命令:

ssh-keyscan -H gitee.com >> ~/.ssh/known_hosts

如果你不想添加到known_hosts文件中,可以在第一次连接时使用ssh -o StrictHostKeyChecking=no来跳过这个检查。但这样做会降低安全性。

如果你是在多个服务器上使用相同的IP地址,并且之前已经添加过这个IP的记录,那么可能是服务器的公钥发生了变化,这种情况下你应该联系服务器管理员确认公钥的变更。

注意:在实际生产环境中,不建议无条件信任新的SSH指纹,除非你完全了解这个服务器的来源和身份。

3. 静态站点生成器

以下几种静态站点生成器都可以用来搭建个人主页。如果使用除JekyII外的工具,则需要配置Github Actions以构建和发布你的站点。

3.1. mkdocs

mkdocs是一个快速的静态网页生成器。

发布个人网站的方法:参见mkdocs-material官网

3.2. JekyII

Jekyll 是一个静态站点生成器,内置对 GitHub Pages 的支持和简化的构建进程。

参见 About GitHub Pages and Jekyll