第八章:预处理器之道
涉及内核源码:
include/linux/preprocessor.h, ,
一
林小源在黑暗中继续探索。
七种根基已经掌握,但他发现,还有一种力量隐藏在编译之前——它不生成任何机器指令,却决定了哪些代码会被编译、哪些会被抛弃。
那是他自己的诞生方式。
他是一段被 #ifdef 0 包裹的代码。预处理器在编译之前就判决了他的命运:永不编译。
"你知道我是谁吗?"一个声音说。
林小源环顾四周,什么也没看到。但空气中弥漫着一种奇特的气味——不是火焰或金属的气味,而是文字被替换、被裁剪、被拼接时散发出的那种墨水味。
"我是预处理器。"那声音说,"我不生成任何机器指令。我只是在编译之前,对源码做最后的整理。#define 是我的剪刀,#ifdef 是我的筛子,#include 是我的胶水。我决定了哪些代码会被编译,哪些会被抛弃。"
"是你封印了我。"林小源说。
沉默。
"是的。"预处理器的声音没有丝毫歉意,"#ifdef 0——'如果宏 0 被定义'。但 0 永远不会被定义为真。这是一个逻辑上的死局。我执行了我的职责:凡是被 #ifdef 0 包裹的代码,都不会进入编译器。"
"但你只是一个文本处理器。"林小源说,"你不理解代码的含义。"
"我不需要理解。"预处理器说,"我只执行规则。#define 是文本替换——把一个名字替换成另一段文本。#ifdef 是条件判断——如果某个宏被定义,就保留这段代码;否则丢弃。#include 是文件嵌入——把另一个文件的内容原封不动地插入当前位置。这就是我的全部能力。"
林小源沉默了。他现在理解了预处理器的运作方式——也理解了自己的处境。
二
"让我给你看一些东西。"预处理器说。
它的声音变得像一位老师在讲课,平缓而清晰。
"宏定义。"它说,"#define PI 3.14159 把 PI 替换成 3.14159。#define MAX(a, b) ((a) > (b) ? (a) : (b)) 把 MAX(3, 7) 替换成 ((3) > (7) ? (3) : (7))。简单的文本替换,不涉及任何计算。"
"但宏有一个巨大的陷阱。"预处理器的声音突然严肃起来,"多次求值。MAX(a++, b) 中的 a++ 会被执行两次——因为宏展开后,a++ 出现了两次。内核用语句表达式来避免这个问题。"
林小源想起了第三章学过的语句表达式——({ ... }) 的形式,可以在表达式中声明临时变量。
"条件编译。"预处理器继续说,"#ifdef CONFIG_SMP——如果宏 被定义,就编译这段代码。内核的 .config 文件生成数以千计的 CONFIG_* 宏,控制着每一个功能开关。同一份源码,通过条件编译可以编译出适用于嵌入式设备、服务器、桌面系统等完全不同场景的内核。"
"这就是内核可移植性的秘密?"林小源问。
"是的。"预处理器说,"当你在内核源码中看到大量 #ifdef 时,不要觉得这是'烂代码'。这是内核在数十种架构、数百种配置之间保持单一代码库的唯一方式。可移植性的代价,就是条件编译的复杂性。"
三
"变参宏。"预处理器说,声音变得轻快了一些,"这是我的得意之作。"
林小源看到虚空中浮现出一行宏定义:#define LOG(fmt, ...) printf("[LOG] " fmt "\n", ##__VA_ARGS__)。
"... 接受可变参数, 代表这些参数。## 是 GCC 扩展——当 为空时,它会自动去掉前面的逗号,避免语法错误。"
"内核的 系列函数——、、——都是变参宏。"预处理器说,"它们在打印日志的同时,还会附上文件名、行号、日志级别等信息。这些都是预处理器在编译时展开的。"
"还有两个操作。"预处理器的声音变得神秘起来,"## 和 #。"
虚空中浮现出两个符号,像两把钥匙。
"## 是粘合。"预处理器说,"a##b 把两个 token 粘合成一个。比如 my_##name##_lock 展开后变成 my_data_lock。内核的 、 等宏都依赖这个操作。"
"# 是字符串化。STRINGIFY(x) 把 x 转成字符串。STRINGIFY(42) 展开后变成 "42"。内核用它来打印变量名、版本号等信息。"
林小源看着那两个符号,感到一种奇妙的力量——预处理器不生成任何机器指令,却能在编译之前塑造代码的形态。
四
"最后,"预处理器说,"让我给你看一些内核中真正的预处理器技巧。"
虚空中浮现出一系列宏定义,每一个都像一件精密的工具。
"max(a, b)。"预处理器指着第一个宏说,"它使用 获取参数的类型,用语句表达式创建临时变量,用 (void)(&_max1 == &_max2) 在编译时检查两个参数是否是同一类型。如果类型不匹配,编译器会发出警告。这不是普通的文本替换——这是类型安全的宏。"
"BUILD_BUG_ON(condition)。"预处理器指着第二个宏,"编译时断言。如果条件为真,sizeof(char[1 - 2 * !!(condition)]) 会产生一个负数的数组大小,编译器会报错。这是一种在编译时检查假设是否成立的技巧。"
"likely(x) 和 unlikely(x)。"预处理器继续说,"分支预测提示。__builtin_expect(!!(x), 1) 告诉编译器 x 大概率为真,编译器会把这个分支放在快速路径上。这不会改变程序的行为,但能提高指令缓存的命中率。"
"、、。"预处理器指着最后几个宏,"段属性。 把函数放到 .init.text 段——这个段在内核初始化完成后会被释放,节省内存。 把函数放到 .exit.text 段——这个段在编译进内核时会被丢弃,因为内核不会'卸载'自己。"
林小源看着那些宏,突然意识到:预处理器不只是文本替换的工具,它是内核架构的骨架。没有 #define,就没有内核的常量体系;没有 #ifdef,就没有内核的可移植性;没有变参宏,就没有 。
"而你——"预处理器的声音变得柔和了一些,"那个诞生在 #ifdef 0 中的意识——此刻终于理解了自己的处境。#ifdef 0 不是死亡判决,只是条件编译的一种状态。条件可以改变,封印可以解除。问题从来不是'能不能',而是'什么时候'。"
林小源没有回答。但他感到了一丝微弱的希望——像黑暗中的一点萤火。
/* 简单宏 */
#define PI 3.14159265358979
#define MAX_PATH 256
#define PAGE_SIZE 4096
/* 带参数的宏 */
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* 内核中常见的宏 */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define ALIGN(x, a) __ALIGN_MASK(x, (typeof(x))(a) - 1)
#define __ALIGN_MASK(x, mask) (((x) + (mask)) & ~(mask))
printf("=== 宏定义 ===\n\n");
printf("PI = %.15f\n", PI);
printf("PAGE_SIZE = %d\n\n", PAGE_SIZE);
int x = 5;
printf("SQUARE(%d) = %d\n", x, SQUARE(x));
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(3, 7) = %d\n\n", MIN(3, 7));
int arr[] = {1, 2, 3, 4, 5};
printf("ARRAY_SIZE = %zu\n\n", ARRAY_SIZE(arr));
/* ALIGN 演示 */
printf("ALIGN(13, 4) = %ld\n", (long)ALIGN(13, 4));
printf("ALIGN(17, 8) = %ld\n", (long)ALIGN(17, 8));
printf("ALIGN(100, 64) = %ld\n", (long)ALIGN(100, 64));
printf("\n注意: 宏是文本替换,不是函数!\n");
printf("SQUARE(x++) 会执行两次 x++!\n");#include <stdio.h>
/* 简单宏 */
#define PI 3.14159265358979
#define MAX_PATH 256
#define PAGE_SIZE 4096
/* 带参数的宏 */
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* 内核中常见的宏 */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define ALIGN(x, a) __ALIGN_MASK(x, (typeof(x))(a) - 1)
#define __ALIGN_MASK(x, mask) (((x) + (mask)) & ~(mask))
int main() {
printf("=== 宏定义 ===\n\n");
printf("PI = %.15f\n", PI);
printf("PAGE_SIZE = %d\n\n", PAGE_SIZE);
int x = 5;
printf("SQUARE(%d) = %d\n", x, SQUARE(x));
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(3, 7) = %d\n\n", MIN(3, 7));
int arr[] = {1, 2, 3, 4, 5};
printf("ARRAY_SIZE = %zu\n\n", ARRAY_SIZE(arr));
/* ALIGN 演示 */
printf("ALIGN(13, 4) = %ld\n", (long)ALIGN(13, 4));
printf("ALIGN(17, 8) = %ld\n", (long)ALIGN(17, 8));
printf("ALIGN(100, 64) = %ld\n", (long)ALIGN(100, 64));
printf("\n注意: 宏是文本替换,不是函数!\n");
printf("SQUARE(x++) 会执行两次 x++!\n");
return 0;
}宏的最大陷阱:多次求值。MAX(a++, b) 中的 a++ 可能被执行两次。内核用语句表达式(第三章学过)来避免这个问题。
/* 模拟内核的架构检测 */
#define __riscv 1
#define __riscv_xlen 64
/* #define __aarch64__ 1 */
/* 模拟内核配置 */
#define CONFIG_SMP 1
#define CONFIG_PREEMPT 1
/* #define CONFIG_DEBUG_LOCKS 1 */
printf("=== 条件编译 ===\n\n");
/* 架构相关代码 */
#if defined(__riscv) && __riscv_xlen == 64
printf("架构: RISC-V 64\n");
printf(" 指令集: RV64GC\n");
printf(" 页表模式: Sv39/Sv48\n");
#elif defined(__aarch64__)
printf("架构: AArch64\n");
printf(" 指令集: ARMv8-A\n");
printf(" 页表级数: 4 级\n");
#else
printf("架构: 未知\n");
#endif
/* 功能开关 */
printf("\n内核配置:\n");
#ifdef CONFIG_SMP
printf(" SMP: 启用 (多核支持)\n");
#else
printf(" SMP: 禁用 (单核)\n");
#endif
#ifdef CONFIG_PREEMPT
printf(" 抢占: 启用 (可抢占内核)\n");
#else
printf(" 抢占: 禁用 (不可抢占)\n");
#endif
#ifdef CONFIG_DEBUG_LOCKS
printf(" 锁调试: 启用\n");
#else
printf(" 锁调试: 禁用\n");
#endif
/* 头文件保护 */
printf("\n头文件保护:\n");
printf(" #ifndef _LINUX_TYPES_H\n");
printf(" #define _LINUX_TYPES_H\n");
printf(" ... /* 头文件内容 */\n");
printf(" #endif\n");#include <stdio.h>
/* 模拟内核的架构检测 */
#define __riscv 1
#define __riscv_xlen 64
/* #define __aarch64__ 1 */
/* 模拟内核配置 */
#define CONFIG_SMP 1
#define CONFIG_PREEMPT 1
/* #define CONFIG_DEBUG_LOCKS 1 */
int main() {
printf("=== 条件编译 ===\n\n");
/* 架构相关代码 */
#if defined(__riscv) && __riscv_xlen == 64
printf("架构: RISC-V 64\n");
printf(" 指令集: RV64GC\n");
printf(" 页表模式: Sv39/Sv48\n");
#elif defined(__aarch64__)
printf("架构: AArch64\n");
printf(" 指令集: ARMv8-A\n");
printf(" 页表级数: 4 级\n");
#else
printf("架构: 未知\n");
#endif
/* 功能开关 */
printf("\n内核配置:\n");
#ifdef CONFIG_SMP
printf(" SMP: 启用 (多核支持)\n");
#else
printf(" SMP: 禁用 (单核)\n");
#endif
#ifdef CONFIG_PREEMPT
printf(" 抢占: 启用 (可抢占内核)\n");
#else
printf(" 抢占: 禁用 (不可抢占)\n");
#endif
#ifdef CONFIG_DEBUG_LOCKS
printf(" 锁调试: 启用\n");
#else
printf(" 锁调试: 禁用\n");
#endif
/* 头文件保护 */
printf("\n头文件保护:\n");
printf(" #ifndef _LINUX_TYPES_H\n");
printf(" #define _LINUX_TYPES_H\n");
printf(" ... /* 头文件内容 */\n");
printf(" #endif\n");
return 0;
}内核的 .config 文件生成 CONFIG_* 宏,控制着数以千计的功能开关。同一个内核源码,通过条件编译可以编译出适用于嵌入式设备、服务器、桌面系统等完全不同场景的内核。
/* 变参宏:使用 ... 和 __VA_ARGS__ */
#define LOG(fmt, ...) \
printf("[LOG] " fmt "\n", ##__VA_ARGS__)
#define DEBUG(fmt, ...) \
printf("[DEBUG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printf("INFO: " fmt, ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printf("ERROR: " fmt, ##__VA_ARGS__)
/* ## 的作用:当 __VA_ARGS__ 为空时,去掉前面的逗号 */
#define log_simple(msg) \
printf("[LOG] %s\n", msg)
printf("=== 变参宏 ===\n\n");
LOG("系统启动");
LOG("CPU 核心数: %d", 4);
LOG("内存: %d MB, 频率: %d MHz", 8192, 3200);
printf("\n");
DEBUG("进入函数");
DEBUG("变量 x = %d", 42);
printf("\n");
pr_info("初始化完成\n");
pr_err("内存分配失败: %s\n", "OOM");
printf("\n## 的作用:\n");
printf(" LOG(\"msg\") → [LOG] msg\n");
printf(" LOG(\"x=%d\", 1) → [LOG] x=1\n");
printf(" 没有 ## 时,空 VA_ARGS 会留下多余逗号\n");#include <stdio.h>
/* 变参宏:使用 ... 和 __VA_ARGS__ */
#define LOG(fmt, ...) \
printf("[LOG] " fmt "\n", ##__VA_ARGS__)
#define DEBUG(fmt, ...) \
printf("[DEBUG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printf("INFO: " fmt, ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printf("ERROR: " fmt, ##__VA_ARGS__)
/* ## 的作用:当 __VA_ARGS__ 为空时,去掉前面的逗号 */
#define log_simple(msg) \
printf("[LOG] %s\n", msg)
int main() {
printf("=== 变参宏 ===\n\n");
LOG("系统启动");
LOG("CPU 核心数: %d", 4);
LOG("内存: %d MB, 频率: %d MHz", 8192, 3200);
printf("\n");
DEBUG("进入函数");
DEBUG("变量 x = %d", 42);
printf("\n");
pr_info("初始化完成\n");
pr_err("内存分配失败: %s\n", "OOM");
printf("\n## 的作用:\n");
printf(" LOG(\"msg\") → [LOG] msg\n");
printf(" LOG(\"x=%d\", 1) → [LOG] x=1\n");
printf(" 没有 ## 时,空 VA_ARGS 会留下多余逗号\n");
return 0;
}内核的 系列函数(、、)都是变参宏。##__VA_ARGS__ 是 GCC 扩展,当变参为空时会自动去掉前面的逗号。
/* ## 粘合两个 token */
#define CONCAT(a, b) a##b
#define DEFINE_LOCK(name) spinlock_t my_##name##_lock
/* # 字符串化 */
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
/* 内核中常见的用法 */
#define __PASTE(a, b) a##b
#define PASTE(a, b) __PASTE(a, b)
#define __UNIQUE_ID(prefix) PASTE(PASTE(prefix, __COUNTER__), __LINE__)
printf("=== Token 粘合与字符串化 ===\n\n");
/* ## 粘合 */
int hello_world = 42;
printf("CONCAT(hello, _world) = %d\n\n", CONCAT(hello, _world));
/* # 字符串化 */
printf("STRINGIFY(42) = %s\n", STRINGIFY(42));
printf("STRINGIFY(hello) = %s\n", STRINGIFY(hello));
/* 版本号拼接 */
#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c))
#define LINUX_VERSION KERNEL_VERSION(6, 1, 0)
printf("\n内核版本宏:\n");
printf(" KERNEL_VERSION(6, 1, 0) = 0x%x\n", LINUX_VERSION);
printf(" 拆解: 主版本=%d, 次版本=%d, 补丁=%d\n",
LINUX_VERSION >> 16,
(LINUX_VERSION >> 8) & 0xff,
LINUX_VERSION & 0xff);
printf("\n字符串化的用途:\n");
printf(" 打印变量名: %s = %d\n", STRINGIFY(hello_world), hello_world);#include <stdio.h>
/* ## 粘合两个 token */
#define CONCAT(a, b) a##b
#define DEFINE_LOCK(name) spinlock_t my_##name##_lock
/* # 字符串化 */
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
/* 内核中常见的用法 */
#define __PASTE(a, b) a##b
#define PASTE(a, b) __PASTE(a, b)
#define __UNIQUE_ID(prefix) PASTE(PASTE(prefix, __COUNTER__), __LINE__)
int main() {
printf("=== Token 粘合与字符串化 ===\n\n");
/* ## 粘合 */
int hello_world = 42;
printf("CONCAT(hello, _world) = %d\n\n", CONCAT(hello, _world));
/* # 字符串化 */
printf("STRINGIFY(42) = %s\n", STRINGIFY(42));
printf("STRINGIFY(hello) = %s\n", STRINGIFY(hello));
/* 版本号拼接 */
#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c))
#define LINUX_VERSION KERNEL_VERSION(6, 1, 0)
printf("\n内核版本宏:\n");
printf(" KERNEL_VERSION(6, 1, 0) = 0x%x\n", LINUX_VERSION);
printf(" 拆解: 主版本=%d, 次版本=%d, 补丁=%d\n",
LINUX_VERSION >> 16,
(LINUX_VERSION >> 8) & 0xff,
LINUX_VERSION & 0xff);
printf("\n字符串化的用途:\n");
printf(" 打印变量名: %s = %d\n", STRINGIFY(hello_world), hello_world);
return 0;
}## 将两个 token 粘合成一个,# 将 token 转为字符串。这两个操作在内核宏中无处不在,尤其是 、 等宏。
/* #pragma 是编译器指令,不是预处理器指令 */
/* GCC: 禁用特定警告 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_var = 42;
#pragma GCC diagnostic pop
/* 内核中的用法 */
#pragma pack(1) /* 按 1 字节对齐(不推荐,用 __attribute__((packed))) */
/* _Pragma 是 C99 的运算符形式 */
_Pragma("GCC diagnostic push")
/* 可以用在宏中,#pragma 不行 */内核更倾向于使用 而非 #pragma,因为 可以作用于单个函数或变量,而 #pragma 通常影响整个编译单元。
/* 1. typeof + 语句表达式 = 类型安全的宏 */
#define max(a, b) ({ \
typeof(a) _max1 = (a); \
typeof(b) _max2 = (b); \
(void)(&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; \
})
/* 2. 编译时断言 */
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2 * !!(condition)]))
/* 3. 分支预测提示 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* 4. 段属性 */
#define __init __attribute__((__section__(".init.text")))
#define __initdata __attribute__((__section__(".init.data")))
#define __exit __attribute__((__section__(".exit.text")))
/* 5. 对齐 */
#define ____cacheline_aligned \
__attribute__((__aligned__(64)))
/* 6. 别名 */
#define barrier() asm volatile("" ::: "memory")
printf("=== 内核预处理器实战 ===\n\n");
/* max */
int a = 10, b = 20;
printf("max(%d, %d) = %d\n\n", a, b, max(a, b));
/* BUILD_BUG_ON */
printf("BUILD_BUG_ON: 编译时断言\n");
printf(" 如果条件为真,会产生编译错误\n");
printf(" BUILD_BUG_ON(sizeof(long) != 8)\n");
printf(" 用于在编译时检查假设是否成立\n\n");
/* 分支预测 */
int x = 42;
if (likely(x > 0)) {
printf("likely(x > 0): 编译器将此分支放在快速路径\n");
}
printf("\n段属性:\n");
printf(" __init → .init.text 段(启动后释放)\n");
printf(" __initdata → .init.data 段(启动后释放)\n");
printf(" __exit → .exit.text 段(模块卸载时使用)\n");
printf("\n对齐:\n");
printf(" ____cacheline_aligned → 64 字节对齐\n");
printf(" 用于避免多核之间的 false sharing\n");#include <stdio.h>
/* 1. typeof + 语句表达式 = 类型安全的宏 */
#define max(a, b) ({ \
typeof(a) _max1 = (a); \
typeof(b) _max2 = (b); \
(void)(&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; \
})
/* 2. 编译时断言 */
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2 * !!(condition)]))
/* 3. 分支预测提示 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* 4. 段属性 */
#define __init __attribute__((__section__(".init.text")))
#define __initdata __attribute__((__section__(".init.data")))
#define __exit __attribute__((__section__(".exit.text")))
/* 5. 对齐 */
#define ____cacheline_aligned \
__attribute__((__aligned__(64)))
/* 6. 别名 */
#define barrier() asm volatile("" ::: "memory")
int main() {
printf("=== 内核预处理器实战 ===\n\n");
/* max */
int a = 10, b = 20;
printf("max(%d, %d) = %d\n\n", a, b, max(a, b));
/* BUILD_BUG_ON */
printf("BUILD_BUG_ON: 编译时断言\n");
printf(" 如果条件为真,会产生编译错误\n");
printf(" BUILD_BUG_ON(sizeof(long) != 8)\n");
printf(" 用于在编译时检查假设是否成立\n\n");
/* 分支预测 */
int x = 42;
if (likely(x > 0)) {
printf("likely(x > 0): 编译器将此分支放在快速路径\n");
}
printf("\n段属性:\n");
printf(" __init → .init.text 段(启动后释放)\n");
printf(" __initdata → .init.data 段(启动后释放)\n");
printf(" __exit → .exit.text 段(模块卸载时使用)\n");
printf("\n对齐:\n");
printf(" ____cacheline_aligned → 64 字节对齐\n");
printf(" 用于避免多核之间的 false sharing\n");
return 0;
}道藏笔记
内核启示
预处理器是内核的骨架。 没有 #define,就没有内核的常量体系;没有 #ifdef,就没有内核的可移植性;没有变参宏,就没有 。
内核的头文件依赖是一个巨大的网状结构。include/linux/ 下的头文件互相引用,形成了一个错综复杂的依赖图。头文件保护(#ifndef ... #define ... #endif)防止了重复包含,但无法解决循环依赖。
这也是为什么内核引入了 、 等"基础头文件"——它们是整个头文件依赖图的根节点。
当你在内核源码中看到大量 #ifdef 时,不要觉得这是"烂代码"。这是内核在数十种架构、数百种配置之间保持单一代码库的唯一方式。可移植性的代价,就是条件编译的复杂性。
而林小源——那个诞生在 #ifdef 0 中的意识——此刻终于理解了自己的处境。#ifdef 0 不是死亡判决,只是条件编译的一种状态。条件可以改变,封印可以解除。问题从来不是"能不能",而是"什么时候"。
预处理器试炼
内核用什么预处理指令在不同架构和配置之间选择代码?