Linux 内核代码中广泛使用宏来简化复杂操作、提高代码效率和可维护性。container_of 之前的博客里有过单独说明,这篇博客里单独介绍一下其他 常见的宏
一、数据结构操作相关宏
这类宏主要用于处理数据结构、成员偏移、数组大小等。
- offsetof(TYPE, MEMBER)
- 作用: 计算结构体 TYPE 中成员 MEMBER 相对于结构体起始地址的字节偏移量。
- 使用场景:
- 是 container_of 宏实现的基础。
- 在需要手动计算成员地址或进行底层数据布局时非常有用,例如与硬件直接交互或进行数据序列化。
- 示例:
struct my_struct {
int id;
char name[32];
unsigned long value;
};
size_t offset_of_value = offsetof(struct my_struct, value);
// offset_of_value 的值将是 value 成员的偏移量
- ARRAY_SIZE(arr)
- 作用: 计算静态分配数组 arr 中的元素个数。
- 使用场景:
- 遍历数组时避免硬编码数组大小,使代码对数组维度变化更具鲁棒性。
- 向函数传递数组大小时。
- 示例:
int scores[] = {90, 85, 92, 78};
for (int i = 0; i < ARRAY_SIZE(scores); i++) {
// 处理 scores[i]
}
二、位操作相关宏
这类宏用于高效地进行位运算,常用于硬件寄存器操作、状态标志管理等。
- BIT(nr)
- 作用: 生成一个只有第 nr 位(0索引)为1的位掩码 (等价于 1UL << nr)。
- 使用场景:
- 设置、清除或测试硬件寄存器或状态标志中的单个位。
- 定义位标志常量。
- 示例:
#define STATUS_ENABLE BIT(0) // 0x00000001
#define STATUS_ERROR BIT(5) // 0x00000020
unsigned int reg_status = 0;
reg_status |= STATUS_ENABLE; // 设置使能位
if (reg_status & STATUS_ERROR) {
// 检查错误位
}
- GENMASK(h, l)
- 作用: 生成一个从第 l 位(低位)到第 h 位(高位)都为1的位掩码(包含 l 和 h)。
- 使用场景:
- 处理硬件寄存器中,连续多个位代表一个字段的情况。
- 提取或设置多位字段。
- 示例:
// 假设寄存器中 bits 7:4 代表一个4位的配置值
#define CONFIG_FIELD_MASK GENMASK(7, 4) // 生成掩码 0b11110000 (0xF0)
unsigned int reg_value = 0xA5; // 假设寄存器值为 0b10100101
unsigned int config_val = (reg_value & CONFIG_FIELD_MASK) >> 4; // 提取字段值 (结果为 0xA)
- FIELD_PREP(mask, val) 和 FIELD_GET(mask, reg)
- 作用:
- FIELD_PREP(mask, val): 将值 val 准备好以便写入由 mask 定义的寄存器字段。它会将 val 左移到正确的位置,并确保其值不会超出 mask 的范围。
- FIELD_GET(mask, reg): 从寄存器值 reg 中提取由 mask 定义的字段的值。
- 使用场景:
- 更安全、更可读地设置和获取硬件寄存器中的多位字段,通常与 GENMASK 配合使用。
- 示例:
#define ADC_CHANNEL_MASK GENMASK(3, 0) // ADC通道选择,bits 0-3
unsigned int control_reg = 0x100; // 假设寄存器初始值
unsigned int channel_to_set = 5; // 要设置的通道号
// 设置ADC通道
control_reg &= ~ADC_CHANNEL_MASK; // 清除旧值
control_reg |= FIELD_PREP(ADC_CHANNEL_MASK, channel_to_set);
// 获取当前ADC通道
unsigned int current_channel = FIELD_GET(ADC_CHANNEL_MASK, control_reg);
三、错误处理与调试宏
内核中有一套完善的错误处理和调试机制,宏在其中扮演了重要角色。
- IS_ERR(ptr), PTR_ERR(ptr), ERR_PTR(errnum)
- 作用: 内核中标准的错误指针处理机制。一些函数在失败时会返回一个编码了错误号的特殊指针(错误指针),而不是 NULL。
- ERR_PTR(errnum): 将一个负的错误码 (如 -ENOMEM) 转换为错误指针。
- IS_ERR(ptr): 检查指针 ptr 是否是一个错误指针。
- PTR_ERR(ptr): 如果 ptr 是错误指针,则提取原始的负错误码。
- 使用场景:
- 处理那些可能返回错误指针的内核函数(如 kmalloc 在不使用 GFP_NOFAIL 时,许多驱动的 probe 函数)。
- 示例:
struct device_node *node;
node = of_find_node_by_name(NULL, "my-device");
if (IS_ERR(node)) {
long err_code = PTR_ERR(node);
pr_err("Failed to find device node, error: %ld\n", err_code);
return err_code;
}
// 成功获取 node,继续使用
- WARN_ON(condition) 和 WARN_ON_ONCE(condition)
- 作用: 如果 condition 为真,则在内核日志中打印一条警告信息(包含调用栈)。WARN_ON_ONCE 则只在条件第一次满足时打印。
- 使用场景:
- 用于提示开发或运行时出现的非致命但异常的情况。
- 帮助识别潜在的bug或配置错误。
- 示例:
int process_data(void *data, size_t size) {
if (WARN_ON(size == 0)) {
pr_warn("process_data called with zero size!\n");
return -EINVAL; // 即使打印了警告,也可能需要处理
}
// ...
return 0;
}
- BUG_ON(condition) 和 BUG()
- 作用:
- BUG_ON(condition): 如果 condition 为真,则触发一个内核 “BUG”,通常会导致内核 “oops” (打印详细错误信息和栈回溯,系统可能不稳定或宕机)。
- BUG(): 无条件触发一个内核 “BUG”。
- 使用场景:
- 用于断言那些绝对不应该发生的情况或关键的不变条件被破坏。
- 应谨慎使用,因为它可能导致系统崩溃。主要用于调试和发现严重逻辑错误。
- 示例:
struct list_head *entry;
// 假设 entry 必须非空
if (BUG_ON(!entry)) {
// 此处会触发 BUG,表明发生了严重错误
return; // 可能不会执行到这里
}
- pr_err(fmt, …), pr_warn(fmt, …), pr_info(fmt, …), pr_debug(fmt, …) 等
- 作用: 内核主要的打印函数 printk 的封装,提供了不同的日志级别。还有针对设备的 dev_err(), dev_info() 等。
- 使用场景:
- 记录调试信息、普通信息、警告和错误。
- pr_debug 的信息通常只有在定义了 DEBUG 或启用了动态调试时才会输出。
- 示例:
if (init_module_failed) {
pr_err("MyModule: Failed to initialize, error code %d\n", err);
} else {
pr_info("MyModule: Initialized successfully.\n");
}
四、并发与同步相关宏
处理并发访问和同步时,一些宏可以提供便利或性能优化。
- likely(x) 和 unlikely(x)
- 作用: 向编译器提供关于条件 x 为真或为假的概率提示。编译器可以利用此信息优化分支预测和指令缓存,例如将更可能执行的代码路径放在前面。
- 使用场景:
- 优化性能敏感代码路径中,条件的一个分支远比另一个分支更常被执行的情况。
- 常用于错误检查路径。
- 示例:
int my_func(int *ptr) {
if (unlikely(!ptr)) { // ptr 为 NULL 是不常见(错误)的情况
return -EFAULT;
}
// 正常处理 ptr
return ptr[0];
}
- ACCESS_ONCE(x), READ_ONCE(x), WRITE_ONCE(x)
- 作用: 确保变量 x 的访问(读或写)只发生一次,并且编译器不会对此访问进行可能导致问题的优化(如重新排序、合并访问、从寄存器缓存读取等)。它们主要防止编译器级别的优化。
- 使用场景:
- 在无锁算法中访问共享变量,或与内存映射I/O (MMIO) 交互时,访问的顺序和次数很重要。
- 当 volatile 过于宽泛或不够精确时,这些宏提供更细粒度的控制。
- 注意: 这些宏主要解决编译器优化问题,对于CPU级别的内存乱序,还需要配合内存屏障 (memory barriers) 使用。
- 示例:
// 假设 event_flag 由中断处理程序设置,由主线程读取
extern int event_flag; // 假设在别处定义并可能被并发修改
// 中断处理程序中:
WRITE_ONCE(event_flag, 1);
// 主线程中:
if (READ_ONCE(event_flag)) {
// 处理事件
WRITE_ONCE(event_flag, 0); // 清除标志
}
五、内存管理相关宏
- virt_to_phys(address) 和 phys_to_virt(address)
- 作用: 在虚拟地址和物理地址之间进行转换。具体实现是体系结构相关的。
- 使用场景:
- 设备驱动程序需要向硬件提供物理地址时(例如DMA操作)。
- 底层内存管理代码。
- 示例 (概念性):
void *kernel_buffer = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (kernel_buffer) {
phys_addr_t physical_address = virt_to_phys(kernel_buffer);
setup_dma_controller(physical_address, PAGE_SIZE);
kfree(kernel_buffer);
}
- PAGE_ALIGN(addr)
- 作用: 将地址 addr 向上舍入到最近的页边界。
- 使用场景:
- 分配需要页对齐的内存。
- 涉及内存页的计算。
- 示例:
// 假设 PAGE_SIZE 为 4096 (0x1000)
unsigned long my_addr = 0x1234;
unsigned long aligned_addr = PAGE_ALIGN(my_addr); // aligned_addr 将是 0x2000
六、编译时检查与类型安全宏
- BUILD_BUG_ON(condition)
- 作用: 如果 condition 在编译时为真,则产生一个编译错误。这是一种静态断言。
- 使用场景:
- 在编译阶段确保某些不变量,如结构体大小、类型兼容性或配置约束。
- 尽早捕获错误。
- 示例:
struct fixed_size_struct {
int a;
char b[12]; // 期望总大小为16字节
};
// 确保结构体大小符合预期
BUILD_BUG_ON(sizeof(struct fixed_size_struct) != 16);
- __must_check
- 作用: 这是一个函数属性(通常通过宏定义),它告诉编译器如果函数的返回值被忽略,就发出警告。
- 使用场景:
- 用于那些返回错误码或重要状态值的函数,调用者必须检查其返回值。
- 鼓励更安全的编程实践。
- 示例 (概念性,实际为属性):
// 在头文件中可能这样定义:
#define __must_check __attribute__((warn_unused_result))
int __must_check important_operation(void);
void caller_function(void) {
important_operation(); // 编译器会在此处给出警告
if (important_operation() < 0) { /* 处理错误 */ } // 正确使用
}
七、通用工具宏
- min(x, y) 和 max(x, y)
- 作用: 分别返回两个值中的较小者和较大者。它们经过精心实现,以避免参数的多次求值,并且是类型安全的(通常使用GCC的 typeof 扩展)。
- 使用场景:
- 常见的数学或逻辑比较。
- 示例:
int val1 = 100, val2 = 200;
int lower_val = min(val1, val2); // lower_val 为 100
- roundup(x, y) 和 rounddown(x, y)
- 作用:
- roundup(x, y): 将 x 向上舍入到 y 的最接近倍数。
- rounddown(x, y): 将 x 向下舍入到 y 的最接近倍数。
- 使用场景:
- 将大小或地址对齐到特定边界(如缓存行大小、磁盘块大小)。
- 示例:
#define CACHE_LINE_SIZE 64
size_t buffer_needed = 100;
size_t aligned_buffer_size = roundup(buffer_needed, CACHE_LINE_SIZE); // 128
- DIV_ROUND_UP(n, d)
- 作用: 计算 n / d 并将结果向上舍入到最近的整数。等价于 (n + d – 1) / d (对于正整数)。
- 使用场景:
- 计算存储 n 个项目需要多少个大小为 d 的块。
- 示例:
int items_per_block = 16;
int total_items = 65;
int blocks_needed = DIV_ROUND_UP(total_items, items_per_block); // blocks_needed 为 5
本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。