0%

函数

ibv_fork_init

文档
ibv_fork_init

模板: int ibv_fork_init(void)

输入参数: 无

输出参数: 无

返回值: 0 on success, -1 on error. If the call fails, errno will be set to indicate the
reason for the failure.

描述: ibv_fork_init 初始化 libverbs 的数据结构来安全地处理 fork() 并避免数据损坏,不论
fork() 是被显式调用还是隐式调用(比如在 system() 中被调用)。如果所有的父进程总是阻塞直至所有的
子进程结束或使用 exec() 改变地址空间,那么 ibv_fork_init 可以不被调用。

该函数在支持 madvise 的 MADV_DONTFORK 标记的 Linux 内核(2.6.17 或更高)上可以工作。

设置环境变量 RDMAV_FORK_SAFE 或 IBV_FORK_SAFE 环境变量为任意值,有着与 ibv_fork_init 相同的效
果。

设置 RDMAV_HUGEPAGES_SAFE 为任意值,以告诉库需要检查内核为内存域(memory regions)使用的底层内存页
的大小。如果应用程序直接或通过库(如 libhugtlbfs )间接使用大内存页(博主注:即大于 4KB)时,该环境
变量是必须的。(博主注:ibv_fork_init 将检查 RDMAV_HUGEPAGES_SAFE )

调用 ibv_fork_init 将降低性能,因为每个内存注册都将有一个额外的系统调用和分配附加的内存以追踪内存
域(memory regions)。确切的性能损失取决于工作负载,通常不会很大。

设置 RDMAV_HUGEPAGES_SAFE 会为所有的内存注册增加更多的开销。

文档

Documentation: RDMA Aware Networks Programming User Manual v1.6

Local Documentation

Sun Network QDR InfiniBand Gateway Switch Topic Set

RDMA 知乎专栏

用户态的 Verbs API 手册跟代码在一个仓库维护,手册地址
:https://github.com/linux-rdma/rdma-core/tree/master/libibverbs/man

有很多在线的 man page 网站可以查阅这些接口的说明,比如官方的连接
:https://man7.org/linux/man-pages/man3/ibv_post_send.3.html

也有一些其他非官方网页,支持在线搜索:https://linux.die.net/man/3/ibv

查阅系统 man page 如果你使用的商用 OS 安装了 rdma-core 或者 libibverbs 库,那么可以直接用 man 命令查
询接口:

1
man ibv_post_send

查询 Mellanox 的编程手册
《RDMA Aware Networks Programming User Manual Rev 1.7》
,最新版是 2015 年更新的。该手册写的比较详尽,并且附有示例程序,但是可能与最新的接口有一些差异
。Mellanox VPI®(Virtual Procotol Interconnect)架构为同时支持 InfiniBand 和以太网语义的网络适配器和
交换机提供高性能、低延迟和可靠的方法。

Book:
Linux Kernel Networking - Implementation and Theory

Dotan’s blog: Dotan Barak, an InfiniBand Expert. Dotan is a Senior
Software Manager at Mellanox Technologies working on RDMA Technologies.

性能分析工具(profiling)

Blog:
Tips and tricks to optimize your RDMA code

libibprof

IB 简介

RDMA - Remote Direct Memory Access 远程直接内存存取。

InfiniBand 是一种高性能计算机网络通信标准,它具有极高的吞吐量和极低的延迟。如果您需要使用 InfiniBand
进行编程,您需要使用支持 InfiniBand 的编程语言(如 C++)来编写代码。

机构和组织:

OFA: Open Fabrics Alliance.

IBTA: InfiniBand Trade Association.

概念

  • CQ - Complete Queue 完成队列
  • WQ - Work Queue 工作队列
  • WR - Work Request 工作请求
  • QP - Queue Pairs 队列对(Send-Receive)
  • SQ - Send Queue 发送队列
  • RQ - Receive Queue 接收队列
  • PD - Protection Domain 保护域,将 QP 和 MR 结合在一起
  • MR - Memory Region 内存区域。一块经注册过的且本地网卡可以读写的内存区域。包含 R_Key 和 L_Key。
  • SGE - Scatter/Gather Elements 分散/聚集元素。
  • R_Key - Remote Key
  • L_Key - Local Key
  • CA - (Host) Channel Adapter, an inifiniband network interface card.
  • NIC - Network Interface Card 网卡。
  • LID - Local Identifier.
  • CM - Connection Manager.

其他常见缩写:

  • RC - reliable connected.
  • SCSI - Small Computer System Interface 小型计算机系统接口。
  • SRP - SCSI RDMA Protocol. / Secure Remote Password.

博客:https://blog.51cto.com/liangchaoxi/4044818

安装

InfiniBand 和 RDMA 相关软件包

sudo apt-get install infiniband-diags
sudo apt install ibverbs-utils

API

以下是一些支持 InfiniBand 的 C++库:

Infinity:这是一个轻量级的 C++ RDMA 库,用于 InfiniBand 网络。它提供了对两侧(发送/接收)和单侧(读/
写/原子)操作的支持,并且是一个简单而强大的面向对象的 ibVerbs 抽象。该库使用户能够构建使用 RDMA 的复
杂应用程序,而不会影响性能1。

OFED:这是一个开放式 Fabrics Enterprise Distribution,它提供了对 InfiniBand 和 RoCE(RDMA over
Converged Ethernet)技术的支持。OFED 提供了一组用户空间库和驱动程序,可用于构建支持 RDMA 的应用程
序2。

以下是使用 Infinity 库编写支持 InfiniBand 的 C++代码示例:

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
// 创建新上下文
infinity::core::Context *context = new infinity::core::Context();

// 创建队列对
infinity::queues::QueuePairFactory *qpFactory = new infinity::queues::QueuePairFactory(context);
infinity::queues::QueuePair *qp = qpFactory->connectToRemoteHost(SERVER_IP, PORT_NUMBER);

// 创建并向网络注册缓冲区
infinity::memory::Buffer *localBuffer = new infinity::memory::Buffer(context, BUFFER_SIZE);

// 从远程缓冲区读取(单向)并等待完成
infinity::memory::RegionToken *remoteBufferToken = new infinity::memory::RegionToken(REMOTE_BUFFER_INFO);
infinity::requests::RequestToken requestToken(context);
qp->read(localBuffer, remoteBufferToken, &requestToken);
requestToken.waitUntilCompleted();

// 将本地缓冲区的内容写入远程缓冲区(单向)并等待完成
qp->write(localBuffer, remoteBufferToken, &requestToken);
requestToken.waitUntilCompleted();

// 将本地缓冲区的内容通过队列对发送(双向)并等待完成
qp->send(localBuffer, &requestToken);
requestToken.waitUntilCompleted();

// 关闭连接
delete remoteBufferToken;
delete localBuffer;
delete qp;
delete qpFactory;
delete context;

以下是使用 OFED 库编写支持 InfiniBand 的 C++代码示例:

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
// 创建新上下文
struct ibv_context *context = ibv_open_device(*device);

// 创建完成端口
struct ibv_pd *pd = ibv_alloc_pd(context);

// 创建队列对
struct ibv_qp_init_attr qp_init_attr;
memset(&qp_init_attr, 0, sizeof(qp_init_attr));
qp_init_attr.send_cq = cq;
qp_init_attr.recv_cq = cq;
qp_init_attr.qp_type = IBV_QPT_RC;
qp_init_attr.cap.max_send_wr = 1;
qp_init_attr.cap.max_recv_wr = 1;
qp_init_attr.cap.max_send_sge = 1;
qp_init_attr.cap.max_recv_sge = 1;
struct ibv_qp *qp = ibv_create_qp(pd, &qp_init_attr);

// 创建并向网络注册缓冲区
char *localBuffer = (char *)malloc(BUFFER_SIZE);
struct ibv_mr *mr = ibv_reg_mr(pd, localBuffer, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE);

// 连接到远程主机
struct sockaddr_in remoteAddress;
memset(&remoteAddress, 0, sizeof(remoteAddress));
remoteAddress.sin_family = AF_INET;
remoteAddress.sin_port = htons(PORT_NUMBER);
inet_pton(AF_INET, SERVER_IP, &remoteAddress.sin_addr);
struct rdma_cm_id *cmId;
rdma_create_id(*eventChannel, &cmId, NULL, RDMA_PS_TCP);
rdma_resolve_addr(cmId, NULL, (struct sockaddr *)&remoteAddress, RESOLVE_TIMEOUT_MS);

// 等待连接完成
rdma_wait_event(*eventChannel, RDMA_CM_EVENT_ESTABLISHED);
rdma_ack_cm_event(cmEvent);

// 获取远程缓冲区信息
struct ibv_wc wc;
ibv_post_recv(qp, &recvWr, &badRecvWr);
do {
ibv_poll_cq(cq, 1, &wc);
} while (wc.status != IBV_WC_SUCCESS || wc.opcode != IBV_WC_RECV_RDMA_WITH_IMM || wc.imm_data != htonl(IMM_DATA));
remoteBufferInfo.rkey = ntohl(wc.imm_data >> 8);
remoteBufferInfo.vaddr = wc.wr_id;

// 将本地缓冲区的内容写入远程缓冲区(单向)
struct ibv_send_wr sendWr;
memset(&sendWr, 0, sizeof(sendWr));
sendWr.wr_id = 0;
sendWr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
sendWr.sg_list = &localSge;
sendWr.num_sge = 1;
sendWr.send_flags = IBV_SEND_SIGNALED;
sendWr.wr.rdma.remote_addr = remoteBufferInfo.vaddr;
sendWr.wr.rdma.rkey = remoteBufferInfo.rkey;
localSge.addr = (uintptr_t)localBuffer;
localSge.length = BUFFER_SIZE;
localSge.lkey = mr->lkey;
ibv_post_send(qp, &sendWr, &badSendWr);

// 关闭连接
ibv_dereg_mr(mr);
free(localBuffer);
ibv_destroy_qp(qp);
ibv_dealloc_pd(pd);
ibv_close_device(context);

Linux manual page

Command-Line

文档:https://docs.nvidia.com/networking/pages/viewpage.action?pageId=43719572

  • ibstat

  • ibhosts - 查看所有的 IB hosts。

  • ibnetdiscover - discover InfiniBand topology.

  • ibv_devices - list RDMA devices.

  • ibv_devinof - Print information about RDMA devices available for use from userspace.

  • ibv_rc_pingpong - Run a simple ping-pong test over InfiniBand via the reliable connected (RC)
    transport.

  • targetcli - administration shell for storage targets

    targetcli is a shell for viewing, editing, and saving the configuration of the kernel’s target
    subsystem, also known as LIO. It enables the administrator to assign local storage resources
    backed by either files, volumes, local SCSI devices, or ramdisk, and export them to remote systems
    via network fabrics, such as iSCSI or FCoE.

  • srp_daemon - Discovers and connects to InfiniBand SCSI RDMA Protocol (SRP) targets in an IB
    fabric.

  • ibsrpdm - List InfiniBand SCSI RDMA Protocol (SRP) targets on an IB fabric.

liraries

devid: device ID library. Refer to
here.

ibverbs: 使得用户空间进程能够使用 RDMA verbs(即进行 RDMA 操作)。Refer to
here.

dl: Dynamic Loader.

Makefile默认shell

Makefile 默认的 shell 是 /bin/sh:
查看 Makefile 默认的 shell:
$(warning ${SHELL}) # 查看默认 shell

如果在 Makefile 开头加上以下语句,可以指定 shell:
SHELL := /bin/bash

Makefile 的 shell 命令似乎每一条都会新开辟一个 shell 环境来执行,因为每条 shell 命令似乎都执行了一次 .cshrc 脚本。
要想在同一个 shell 环境中执行所有命令,则需要使用分号分割并转义换行符。

Makefile环境变量

你可以通过设置环境变量 MAKEFLAGS 来让所有的子shell都将 make 认作 make -n:
export MAKEFLAGS=“-n”
注:-n, --dry-run 表示只打印将要执行的命令,但是不真正执行它们。

Makefile 设置 PATH 环境变量:
PATH := mypath:$(PATH)
必须使用 “:=” ,因为 “:=” 是简单赋值,但是 “=” 会递归展开,会导致报错 “Recursive variable `PATH’ references itself (eventually)”。

Makefile 定义多行变量:使用 define 语句。
Makefile 接收命令行参数

非常奇特的现象:
$ make clean
cat clean.sh >clean
chmod a+x clean
因为 Makefile 文件中没有 clean 的 recipe,但是当前目录下有个 clean.sh 文件。
但是,当再次执行 make clean,clean 脚本还是不会被执行:
$ make clean
make: `clean’ is up to date.

特殊符号

空格

Makefile对空格的处理,似乎是:从第一个非空格开始,到明确的截止符(换行、逗号、括号、注释标记’#'等)为止。
a = b #注意末尾有3个空格
(warninga=(warning a=(a)c)

结果:
a= b c

Makefile转义符:
字符 转义方法
$ $$

#

\ \

注意:这里说的是Makefile中的转义符,不是bash中的转义符。

括号

引用变量时,Shell使用大括号,Makefile则大括号和小括号都行。但是在命令中使用Shell变量就需要使用大括号。

参考

Makefile调试方法

要调试 Makefile 并查看其执行过程,可以使用以下几种方法:
1. 使用 make 的 -n 或 --dry-run 选项:这将显示 Makefile 中的所有命令,而不会真正执行它们。
make -n
1. 使用 make 的 -d 或 --debug 选项:这将显示详细的调试信息,包括变量的展开和规则的匹配过程。
make -d
1. 使用 make 的 -p 或 --print-data-base 选项:这将打印所有的变量、规则和隐含规则。
make -p
1. 在 Makefile 中添加调试信息:你可以在 Makefile 中添加一些调试信息,例如使用 (info...)来打印变量的值。print:@(info ...) 来打印变量的值。 print: @(foreach V, $(.VARIABLES), $(info $(V) = (((V))))
1. 使用 make 的 -j 选项:如果你使用并行执行,可以使用 -j 选项来限制并行任务的数量,并更容易地跟踪输出。
make -j1

makefile打印所有变量的值:
debug:
@$(foreach V, $(.VARIABLES), $(info $(V) = (((V))))
然后在命令行中运行:
make debug
这将打印所有变量及其值。

print-%:
@echo $* = ((*)

print-% 是一个 Makefile 目标,用于打印变量的值。具体步骤如下:
1. 定义一个目标 print-%,其中 % 是一个通配符,表示任意变量名。
2. 使用 @echo $* = ((*) 打印变量名和变量值。

示例代码:
print-%:
@echo $* = ((*)
使用方法:
在命令行中运行:
make print-VARIABLE_NAME
例如:
make print-XTENSA_SW_RELEASE
这将打印 XTENSA_SW_RELEASE 变量的值。

命令行参数

See here.

@: See here.

This works fine for me:

1
2
3
4
5
6
7
8
9
10
11
12
# If the first argument is "run"...
ifeq (run,$(firstword $(MAKECMDGOALS)))
# use the rest as arguments for "run"
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
# ...and turn them into do-nothing targets
# TODO: What does the following line mean?
# $(eval $(RUN_ARGS):;@:)
endif

# "cmd" refers to any command
run:
cmd $(RUN_ARGS)

makefile同名目标处理方式

参考:

makefile将命令结果赋值给变量

Makefile中短划线

1
2
all:
-/bin/rm -rf *.log

其中,“-/bin/rm"的短划线”-"是一个特殊前缀,表示忽略命令执行过程的错误。

为每个源文件生成一个可执行程序

1
2
3
4
5
6
7
SRCS = $(wildcard *.c)

all: $(SRCS:.c=)

# Unnecessary, as the default rules are adequate.
.c:
gcc $(CPFLAGS) $< -o $@

最后两行其实不需要,默认规则已经足够了。

其中,$(SRCS:.c=.o)表示将变量SRCS中的每个单词(以空格分割)中的.c替换为.o。以上代码则是将所有.c都去掉。

Makefile保留中间文件

🛡️ 方法一:使用 .PRECIOUS 保留中间文件
这是最直接的方式,告诉 make 不要删除某些目标,即使构建失败:

1
.PRECIOUS: %.o %.d

你可以指定具体的文件名或模式(如 %.o 表示所有 .o 文件)。

🛡️ 方法二:使用 .SECONDARY 保留中间文件但不强制重建
如果你希望保留中间文件,但又不希望它们被视为最终目标,可以用:

1
.SECONDARY: intermediate.o

或者:

1
.SECONDARY:

这会保留所有中间文件。

🧠 方法三:避免使用“中间目标”语法
如果你写了类似:

1
intermediate: ;

这种空规则会让 make 把 intermediate 视为中间目标,构建失败时自动删除。避免这种写法,或者配合 .PRECIOUS 使用。

🛠️ 方法四:将中间文件输出到特定目录
你可以将中间文件集中到一个目录中,便于管理和保留:

1
2
3
4
OBJ_DIR := build/obj
$(OBJ_DIR)/%.o: %.cpp
$(CXX) -c $< -o $@
然后用 .PRECIOUS: $(OBJ_DIR)/%.o 保留它们。

📌 示例:保留 .o 和 .d 文件

1
2
3
4
5
6
7
.PRECIOUS: %.o %.d

main: main.o
$(CC) $^ -o $@

main.o: main.c
$(CC) -c $< -o $@

gcc可以使用 -save-temps 选项保留中间文件:

1
gcc -save-temps -c main.c

🧠 补充:-save-temps=obj

如果你只想把中间文件保存在目标文件所在目录,而不是当前目录,可以使用:

1
gcc -save-temps=obj main.c -o main

Color Scheme

Refer to the link.

Add these three lines to ~/.bashrc

1
2
3
4
5
$ vi ~/.bashrc
export LS_OPTIONS='--color=auto'
# dircolors - color set for ls
eval "$(dircolors -b)"
alias ls='ls $LS_OPTIONS'

1. 术语

resident set size (RSS)

2. 文件系统

/proc/self/statm

3. 相关命令

3.1. top

3.1.1. Fields

TIME+1 TIME
5432:01 means “5432 minutes and 1 second” 90,32 means “90 hours and 32 minutes”
25:15.20 means “25 minutes, 15 seconds and 20% of 1 second” 25:15 means “25 minutes and 15 seconds”

3.2. ps

3.2.1. 输出说明

TIME: the cumulated CPU time in [DD-]hh:mm:ss format (time=TIME)

Field value & means
TIME 1-18:09:38 means “1 day, 18 hours, 9 minutes and 38 seconds”

4. 相关系统调用

4.1. fork

子进程是父进程的副本,获取了父进程的数据空间、堆和栈的副本,但是他们共享正文段。

写时复制(Copy-On-Write,COW):不复制父进程的完全副本,只有在父或子进程尝试修改这些区域时,则为修改区域的那块内存制作一个副本,通常是虚拟内存的一页。

博主注: 如果父进程尝试修改共享的内存页,内核会为父进程同样制作副本,而把原本的内存页留给子进程使用,这会导致即使在父进程中,原本的内存页的物理地址也会发生改变,从而使得 RDMA 等机制发生错误(要求物理地址保持不变),所以此时需要使用 madise 设置 MADV_DONTFORK 标志。

在 Linux 中,写时复制(Copy-On-Write, COW)机制主要通过页表和内存管理单元(MMU)来实现。以下是 COW 在 Linux 中的具体实现步骤:

页表标记:当进程调用 fork() 创建子进程时,父进程和子进程的页表会标记共享的内存页为只读。这意味着这些页在初始状态下是共享的,且无法被写入。

页错误处理:如果父进程或子进程尝试写入这些只读页,会触发页错误(page fault)。操作系统内核会捕获这个页错误,并执行 COW 机制。

内存页复制:在页错误发生时,操作系统会分配一个新的物理内存页,并将原始页的内容复制到这个新页中。然后,页表会更新,以指向新的可写内存页。

页表更新:操作系统更新进程的页表,使得写入操作可以在新的内存页上进行,而不会影响其他进程共享的原始页。

引用计数:操作系统维护每个内存页的引用计数,以跟踪有多少进程共享该页。当引用计数减少到零时,内存页可以被释放。

4.2. mmap

TODO

4.3. madvise

madvise 是一个系统调用,用于向内核提供关于内存使用的建议。

1
2
3
#include <sys/mman.h>

int madvise(void *addr, size_t length, int advice);

4.3.1. 选项 advice:

4.3.1.1. MADV_DONTFORK

MADV_DONTFORK 阻止 fork() 后的子进程看见这些范围的内存页。

这意味该内存不会被复制,即在 fork() 调用时,指定的内存区域不会被子进程继承,避免写时复制导致的页物理地址发生变化。在 fork() 之后,如果父进程对共享内存页进行写操作,写时复制(COW)机制会将这些页复制到新的物理位置。这会导致 RDMA 操作使用的内存地址不一致,从而引发数据错误。

拓展: RDMA 中,ibv_fork_init 和 RDMAV_HUGEPAGES_SAFE 会调用 madvise() 来为 IB 卡的 DMA 内存页设置 MADV_DONTFORK ,以避免数据损坏。

5. 参考

what-does-virtual-memory-size-in-top-mean
[2]: https://blog.csdn.net/weixin_42319496/article/details/125940896
[3]: https://docs.nvidia.com/networking/display/rdmaawareprogrammingv17/ibv_fork_init

See the article: Prefixes for binary multiples

Table:


Prefixes for binary multiples
 Factor  Name  Symbol  Origin Derivation 
 210 kibi Ki kilobinary: (210)1 kilo: (103)1
 220 mebi Mi megabinary: (210)2  mega: (103)2
 230 gibi Gi gigabinary: (210)3 giga: (103)3
 240 tebi Ti terabinary: (210)4 tera: (103)4
 250 pebi Pi petabinary: (210)5 peta: (103)5
 260 exbi Ei exabinary: (210)6 exa: (103)6

Examples and comparisons with SI prefixes
one kibibit  1 Kibit = 210 bit = 1024 bit
one kilobit  1 kbit = 103 bit = 1000 bit
one byte  1 B = 23 bit = 8 bit
one mebibyte  1 MiB = 220 B = 1 048 576 B
one megabyte  1 MB = 106 B = 1 000 000 B
one gibibyte  1 GiB = 230 B = 1 073 741 824 B
one gigabyte  1 GB = 109 B = 1 000 000 000 B

printf、scanf 这类函数参数个数不固定,靠的是变参函数:形参列表里用 ... 表示后面还可以跟任意个实参。要在自己的函数里读这些参数,靠的是 <stdarg.h>(C++ 里是 <cstdarg>)提供的一整套 va_* 接口:va_list、va_start、va_arg、va_end,以及需要复制状态时用的 va_copy。

本文以 va_* 的原理为主线:它们如何协同工作、背后依赖什么假设,再落到用法、完整示例,以及几个容易卡住的理解点。

va_* 在解决什么问题?

对普通函数,形参个数、类型在声明里写死,编译器按调用约定为每个参数安排寄存器或栈槽。变参函数多了 ...:调用方可多传任意个实参,但被调函数一侧没有名字、没有类型信息。

va_* 就是为此准备的一套「运行时游标」:

  1. 用 va_list 保存「当前读到哪」的状态
  2. 用 va_start 把状态对准第一个可变参数
  3. 用 va_arg 按你给出的类型取出一个参数,并前进状态
  4. 用 va_end 结束这次遍历、做清理
  5. 需要同一份参数再扫一遍或交给别人时,用 va_copy 复制状态

它们大多是宏(现代编译器上常落到 __builtin_va_*),不是普通库函数。整套接口要一起看:缺了初始化,后面无法读;读完不清理,在部分 ABI 上是未定义行为。

还有一点贯穿始终:编译器不知道 ... 里有几个参数、各是什么类型。因此业务代码必须另有约定——固定参数里带个数、格式串(如 "%d%s")、或哨兵值(如 -1 / NULL)。va_* 只负责「按你的指示往前读」,不负责猜协议。

原理:游标如何跟着参数走

经典栈模型

传统 cdecl 一类约定里,参数从右往左压栈,固定参数与可变参数在栈上往往连成一片。此时可以把 va_* 想成在这块连续内存上移动指针。

先说清「最后一个固定参数」指什么。变参函数里,写在 ... 前面、带名字的形参叫固定参数;... 代表的那些没有名字的实参叫可变参数。固定参数里紧挨着 ...、排在最末尾的那一个,就叫最后一个固定参数。例如:

1
2
3
4
5
6
7
8
9
int printf(const char *format, ...);
/* ↑ format 是唯一的固定参数,也是「最后一个固定参数」 */

double calculate_average(int count, ...);
/* ↑ count 是最后一个(也是唯一的)固定参数 */

void log_message(const char *level, const char *format, ...);
/* ↑ format 才是最后一个固定参数;
level 也是固定参数,但不是「最后」那个 */

va_start 的第二个参数必须写这个名字(如 format / count),用来标定可变参数从哪开始。

1
2
3
4
5
6
地址由低到高 --->

[ 最后一个固定参数 ] [ 可变参数1 ] [ 可变参数2 ] ...
▲ ▲
│ └── va_start 之后游标对准这里
└── 定位基准(va_start 的第二个参数)

各接口在这个模型里的分工:

接口 原理上在做什么
va_list 游标本身:保存当前读位置(教学实现里常是 char*)
va_start(ap, last) 以 last(最后一个固定形参)为基准,算出第一个可变参数的地址,写入 ap
va_arg(ap, type) 按 type 从当前位置读值,再把游标前进 sizeof(type)(含对齐)
va_end(ap) 重置/清空游标,结束本次使用
va_copy(dst, src) 复制一份游标状态,便于再次遍历或交给别的函数

概念上的朴素实现(仅用于理解,勿当可移植代码):

1
2
3
4
5
6
7
8
9
typedef char *va_list;

#define va_start(ap, last) \
(ap = (va_list)&(last) + sizeof(last))

#define va_arg(ap, type) \
(*(type *)((ap += sizeof(type)) - sizeof(type)))

#define va_end(ap) (ap = (va_list)0)

读一遍就能看清协作关系:va_start 赋初值,va_arg 读并前进,va_end 收尾。真正实现里对齐、提升类型等细节更复杂,但骨架如此。

现代 ABI:寄存器 + 结构体游标

x86-64、ARM64 上,不少参数先放进寄存器,装不下的再进栈。此时:

  • va_list 往往是带多个字段的结构(或其一元素数组),而不只是 char*
  • 常见字段类似 gp_offset、fp_offset、reg_save_area、overflow_arg_area,分别跟踪通用/浮点寄存器保存区与栈上溢出区
  • va_start / va_arg / va_end 常落到编译器内建,按 ABI 填表、取数、清理

原理没有变:仍然是一套状态机,记录「下一个参数从哪取」;变的是状态里记的是简单地址,还是寄存器偏移加栈指针。可移植代码应把 va_list 当不透明类型,只用 va_* 操作。

用法与完整示例

标准用法(自己解析每一个参数时):

  1. 声明 va_list ap
  2. va_start(ap, last_named_param)
  3. 循环或按协议调用 va_arg(ap, type)
  4. va_end(ap)

示例一:固定参数给出个数

title:按 count 求平均view 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
#include <stdio.h>
#include <stdarg.h>

/* count 为固定参数,表示后面跟随的可变参数个数 */
double calculate_average(int count, ...) {
if (count <= 0) {
return 0.0;
}

va_list args;
double sum = 0.0;

va_start(args, count);
for (int i = 0; i < count; i++) {
int value = va_arg(args, int);
sum += value;
}
va_end(args);

return sum / count;
}

int main(void) {
double avg1 = calculate_average(4, 10, 20, 30, 40);
printf("Average 1: %.2f\n", avg1); /* 25.00 */

double avg2 = calculate_average(3, 5, 15, 25);
printf("Average 2: %.2f\n", avg2); /* 15.00 */

return 0;
}

示例二:哨兵标记结尾

title:以 -1 为结束哨兵view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdarg.h>

/* 打印非负整数序列,约定以 -1 作为结束哨兵 */
void print_numbers(int first, ...) {
va_list args;
va_start(args, first);

int current = first;
while (current != -1) {
printf("%d ", current);
current = va_arg(args, int);
}

va_end(args);
printf("\n");
}

int main(void) {
print_numbers(10, 20, 30, 40, -1); /* 10 20 30 40 */
return 0;
}

示例三:不自己解析,整份转交

若本函数只是包装器,可以把已初始化的 va_list 交给 vprintf、vsnprintf 等 v* 接口——它们内部会走自己的 va_arg。这时你仍需要 va_start / va_end,但不必手写 va_arg:

title:转交 vprintfview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdarg.h>

/* 包装器:只做 va_start / va_end,参数解析交给 vprintf */
void log_message(const char *level, const char *format, ...) {
va_list args;
va_start(args, format);

printf("[%s]: ", level);
vprintf(format, args);
printf("\n");

va_end(args);
}

int main(void) {
log_message("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
return 0;
}

需要把同一份参数交给多个函数、或扫完再扫一遍时,用 va_copy 复制状态,并分别 va_end。一份 va_list 被消费后,一般不能假定还能原样再扫。

理解点一:va_list(ap)到底是什么类型?

ap 的类型就是 va_list。标准只说它是实现定义类型,用来保存遍历所需信息。

  • 简化 / 老平台:常是 typedef char *va_list,ap 就是指向参数内存的字节指针;va_start 展开后类似 ap = (va_list)&count + sizeof(count)。
  • 现代 x86-64 等:常是结构体(有的再 typedef 成一元素数组,便于按「数组退化为指针」传递,避免整份状态被错误拷贝)。va_start 由内建填好各字段。

两种形态都服务于同一原理:va_list 是游标状态,不是业务数据本身。想看本机定义,打开系统 stdarg.h 或对源文件预处理后搜 va_list 即可。

理解点二:va_start 为何传「名字」而不是指针?

1
va_start(ap, last_fix_param);

第二个参数必须是形参列表里 ... 前面最后一个固定参数的标识符(例如 format),用来标定可变参数从哪开始。

因为 va_start 是宏:内部需要对这个名字做定位(朴素实现里是 &(last) 再加 sizeof;现代实现里是 __builtin_va_start 按 ABI 分析该标识符)。若你传入 &count,展开后会对指针再取地址,位置全错。

同理,第一个参数写 ap 而不是 &ap:宏展开后直接对当前作用域的 ap 赋值,不必像函数出参那样传指针。

理解点三:什么时候可以不写 va_arg?

va_arg 只是「亲手取下一个参数」这一环。若你不亲手解析,就可以不写它,但通常仍要理解整套 va_*:

场景 仍用哪些 可否省 va_arg
自己逐个解析 va_list + start + arg + end 否
转发给 vprintf 等 start + end(中间把 ap 交出) 是
va_copy 后整体交给别人 start / copy / end 当前函数可以不写
协议表明没有额外参数 视实现:有时仍 start/end,逻辑上跳过提取 可跳过调用
改用 C++ 变参模板 不用 <cstdarg> 整套都不需要

C++11 起的变参模板(以及 C++17 折叠表达式)是另一条类型安全的路线,与 va_* 运行时游标不是同一套机制。

常见坑点

  • 类型提升:经 ... 传入时,char / short 升为 int,float 升为 double。va_arg(ap, float) 错误,应 va_arg(ap, double)。
  • 成对使用:va_start(或 va_copy)之后必须 va_end;在部分 ABI 上 va_end 会做真实清理,省略是未定义行为。
  • 类型和个数要自己保证正确:va_arg 写的类型必须和调用方实际传入的一致(并考虑上面的类型提升);循环取参的次数也不能超过真实传入的个数。这些编译器都检查不了,写错了运行时才会表现为读到莫名其妙的值,甚至越界崩溃。

小结

va_* 是一套围绕「可变参数游标」的协作接口:va_list 存状态,va_start 定位起点,va_arg 按类型读取并前进,va_end 收尾,va_copy 复制状态。原理上,它们把调用约定里排布好的可变实参,变成函数体内可逐步消费的序列;栈模型用指针加减理解,现代 ABI 用结构体记寄存器与栈位置,骨架相同。

业务侧仍要自己约定个数与类型;需要转发时,把整份 va_list 交给 v* 即可,不必在每个包装函数里重复 va_arg。

inline namespace

You can define and specialize members of an inline namespace as if they were also members of the enclosing namespace.
inline namespace 表示该命名空间也是上级命名空间的成员。
参考: Inline namespace definitions (C++11)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace A {
#if USE_INLINE_B
inline
#endif
namespace B {
int foo(bool) { return 1; }
}
int foo(int) { return 2; }
}

int main(void) {
return A::foo(true); // 如果USE_INLINE_B为0,则该句出错:找不到定义。
}

Unnamed / anonymous namespaces vs static in namespace

Refer to: Unnamed / anonymous namespaces vs static in namespace

  1. at a drop of a hat: 毫不犹豫。见Cambridge dict.
  2. beyond your ken: 超过你的认知范畴
    ken: the range of perception, understanding or knowledge. 参考词典。

Sheldon: Woman, you’re playing with forces beyond your ken.
Penny: Yeah, your ken can kiss my Barbie.

Penny想要气气Sheldon,她故意把ken曲解成玩偶,说:你的ken可以和我芭比娃娃谈恋爱。
出处: word reference。

  1. a rule of thumb: 经验法则。见柯林斯词典。
  2. vice versa: 反之亦然。近义词:conversely.
  3. on (a) par with: 相当,对等,可媲美。See more: on (a) par with
  4. IMHO: In my humble opinion 恕我直言。humble 谦逊的。
  5. AFAIK: As far as I know 据我所知。
  6. miss the forest for the trees. / not see the forest for the trees. 只见树木、不见森林。参见剑桥词典:to be unable to get a general understanding of a situation because you are too worried about the details 因太注重细节而无法从整体角度看待问题。
  7. irrespective of 不论
  8. sell sb out. 出卖某人。
  9. mood swings. 情绪波动。 swing v. 摇摆;swings n. 秋千。
  10. buy that… 相信…… e.g. I didn’t think the doctor was gonna buy that…
  11. Roger that. / Copy that. 参考