LINUX对齐Align原理

本文深入探讨了Linux内核中实现内存地址对齐的巧妙方法,通过宏定义和位运算,确保数据结构在内存中以特定大小对齐,以提升性能。详细解析了alignment_down和alignment_up宏的工作原理,并提供了多个实例,如页面对齐和skb数据对齐。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在Linux内核中,经常会遇到类似于如下的代码

#define __DATA_ALIGN(end, align) (((unsigned int)end+align-1)&(~(align-1)))
#define __MSG_ALIGNED(end)       __DATA_ALIGN(end, 0x10)

光看字面意思,可以知道是字节align大小对齐的作用。原理上是怎么一回事,转载了一篇文章:https://blog.youkuaiyun.com/supperwangli/article/details/5142956

1. 引子 
int a;   
int size = 8;
    
如果让a为(size =8)的整数倍表示成二进制应是什么样子呢?那就是让这个数表示成二进制时的最后三位为0.
而要达到这一目标,只要下面这个数与它进行与运算就可以了:
  11111111 11111111 11111111 11111000   
而上面这个数实际下就是 ~ (size - 1),可以将该数称为size的对齐掩码size_mask.   
    
可这样做求出的是比a小的那个最大的8的倍数. 如果要求出比a大的是不是需要加上8就可以了? 
可是如果a本身就是8的倍数, 这样加8不就错了吗, 所以在a基础上加上 size - 1, 然后与size的对齐掩码进行与运算. 

这样, 我们可以定义下面的宏, 用于计算一个数a以size为倍数的前后两个值:
#define alignment_down(a, size) (a & (~(size-1)) )
#define alignment_up(a, size) ((a+size-1) & (~ (size-1))) 
  
例如: 
a=0, size=8,  则alignment_down(a,size)=0, alignment_up(a,size)=0.
a=6, size=8,  则alignment_down(a,size)=0, alignment_up(a,size)=8.
a=8, size=8,  则alignment_down(a,size)=8, alignment_up(a,size)=8.
a=14, size=8, 则alignment_down(a,size)=8, alignment_up(a,size)=16. 
  
注意:size应当为2的n次方, 即2, 4, 8, 16, 32, 64, 128, 256, 1024, 2048, 4096..... 
  
2. 在linux中的应用
上面的计算方法在linux等代码中也常常可以看到,下面给出几个例子. 
  
例1:当分配地址addr时, 要将该地址以size为倍数对齐, 而且要得到是比addr大的值, 则使用_ALIGN宏:
/* align addr on a size boundary - adjust address up if needed -- Cort */
#define _ALIGN(addr,size)   (((addr)+(size)-1)&(~((size)-1))) 

例2:与页面对齐相关的宏
#define PAGE_SIZE               4096
#define PAGE_MASK               (~(PAGE_SIZE-1)) 
/* to align the pointer to the (next) page boundary */
#define PAGE_ALIGN(addr)        (((addr)+PAGE_SIZE-1) & PAGE_MASK) 
  
例3:与skb分配时对齐相关的宏
#define SKB_DATA_ALIGN(X)   (((X) + (SMP_CACHE_BYTES - 1)) & ~(SMP_CACHE_BYTES - 1)) 
  
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int fclone)
{
    ......
 
    /* Get the HEAD */
    skb = kmem_cache_alloc(cache, gfp_mask & ~__GFP_DMA);
    if (!skb)
         goto out; 
  
    /* Get the DATA. Size must match skb_add_mtu(). */
    size = SKB_DATA_ALIGN(size);
    data = kmalloc_track_caller(size + sizeof(struct skb_shared_info),  gfp_mask);
    if (!data)
         goto nodata; 
    ......
}

<think>我们被要求实现一个内存对齐的宏ALIGN。内存对齐是指将给定的地址或大小调整为特定对齐值的倍数。对齐值通常是2的幂(如4、8、16等)。 常见的对齐操作有两种: 1. 向上对齐:将给定的值调整为大于或等于该值的最小对齐倍数。例如,将7向上对齐到4的倍数得到8。 2. 向下对齐:将给定的值调整为小于或等于该值的最大对齐倍数。例如,将7向下对齐到4的倍数得到4。 通常,ALIGN宏用于向上对齐。我们可以通过以下数学公式实现: ALIGN(x, a) = ((x + a - 1) / a) * a (整数除法) 但是,由于整数除法会截断,我们可以用位运算来优化(当a是2的幂时): ALIGN(x, a) = (x + a - 1) & ~(a - 1) 解释: ~(a-1) 产生一个掩码,该掩码的低log2(a)位为0,其余为1。例如,当a=4(二进制100)时,a-1=3(二进制011),取反得到...11111100(假设32位系统)。 然后,将x加上a-1,这样如果x不是a的倍数,加上a-1就会进位到下一个对齐边界,然后通过与掩码进行按位与操作,将低位清零。 注意:这个宏要求对齐值a必须是2的幂。 在Linux内核中,我们经常看到类似的宏,例如: #define ALIGN(x, a) __ALIGN_KERNEL((x), (a)) #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1) #define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask)) 因此,我们可以这样定义: #define ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) 但是,这里有一个细节:当a不是2的幂时,上述宏的行为可能不正确。因此,我们通常要求a是2的幂。 另外,我们也可以实现向下对齐的宏: #define ALIGN_DOWN(x, a) ((x) & ~((a) - 1)) 现在,我们根据要求,在Linux环境下(用户空间或内核空间)实现一个ALIGN宏。注意,在用户空间的C程序中,我们同样可以这样定义。 然而,我们还需要注意类型问题。上面的宏假设x和a都是整数类型(地址或大小通常是无符号整数)。为了通用性,我们可以使用无符号长整型,但最好使用适当的类型转换。 另外,在C语言中,我们可能希望这个宏能处理各种整数类型,包括指针。指针需要先转换为整数再进行运算。 在用户空间,我们通常使用`<stdint.h>`中的`uintptr_t`类型来处理指针的整数表示。 因此,如果我们要对齐指针,可以这样: #define ALIGN_PTR(ptr, a) (void *)ALIGN((uintptr_t)(ptr), a) 但是,原问题没有特别指明是对齐地址还是对齐大小,所以我们可以分别实现。 不过,通常ALIGN宏用于对齐地址(指针)或对齐一个整数(如大小)。我们这里实现一个通用的整数对齐宏。 示例代码: 假设我们定义: #define ALIGN(x, align) (((x) + (align) - 1) & ~((align) - 1)) 注意:这个宏在align为2的幂时有效。 测试: ALIGN(7, 4) = (7+4-1) & ~(3) = (10) & ...11111100 = 8 ALIGN(8, 4) = (8+4-1) & ~3 = 11 & ...11111100 = 8? 不对,11的二进制是1011,与~3(...11111100)按位与,得到1000,即8,正确。 但是,如果align不是2的幂,比如3,那么~(3-1)= ~2,即...11111101,这样可能不会得到预期的结果。 因此,我们可以在宏中增加一个静态断言(如果编译器支持)来确保align是2的幂。但是,在宏中实现静态断言比较麻烦,通常我们依赖于调用者确保。 在Linux内核中,有一个宏用于检查是否为2的幂: #define is_power_of_2(x) ((x) != 0 && (((x) & ((x) - 1)) == 0)) 但是,在宏中无法直接使用这个检查(除非使用_Bool常量表达式),所以一般我们不在宏内部检查,而是通过注释说明。 因此,我们实现的ALIGN宏如下(用于整数对齐): #define ALIGN(x, align) (((x) + (align) - 1) & ~((align) - 1)) 但是,这个宏在align为0时会有问题,所以调用者必须确保align>0且为2的幂。 另外,我们也可以参考Linux内核中的实现,它使用了一个更通用的方式,并且通过类型转换避免了一些问题。 在Linux内核中(include/linux/align.h): #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (__typeof__(x))(a) - 1) #define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask)) #define ALIGN(x, a) __ALIGN_KERNEL((x), (a)) 这里使用了`__typeof__(x)`来确保掩码的类型与x相同。 因此,我们可以这样写: #define ALIGN(x, a) (((x) + (typeof(x))(a) - 1) & ~((typeof(x))(a) - 1)) 但是,这样写有一个问题:当a是表达式时,typeof(x)可能和a的类型不同,而且typeof是GCC扩展,不是标准C。 在用户空间,如果我们希望可移植,可能避免使用typeof。我们可以要求x和a都是同一种整数类型,或者使用强制类型转换。 另一种常见做法是使用无符号长整型,因为地址对齐通常不会超过unsigned long的范围。 因此,我们可以这样: #define ALIGN(x, a) ((typeof(x))((((unsigned long)(x)) + (a) - 1) & ~((unsigned long)(a) - 1))) 但是,这仍然使用了typeof,并且当x是指针时,转换为unsigned long可能不够(在64位系统上,指针是64位,而unsigned long在Windows 64位上是32位?不,在Linux x86_64上,unsigned long是64位)。为了可移植性,我们可以使用uintptr_t(在用户空间)或内核中的unsigned long(在内核中,指针和unsigned long是相同位数的)。 用户空间实现: 我们可以这样: #include <stdint.h> #define ALIGN(x, align) ( (typeof(x)) ( ( ((uintptr_t)(x)) + (align) - 1) & ~((uintptr_t)(align) - 1) ) ) 但是,如果x是整数(比如size_t),而align也是整数,那么用uintptr_t转换可能没问题,因为uintptr_t足够大(可以容纳指针和整数)。但是,如果x是一个非常大的整数(超过指针的位数),则可能有问题。不过,通常对齐操作都是用于地址或普通整数,而地址的位数和uintptr_t相同。 然而,如果x是浮点数?显然,对齐操作通常只用于整数和指针。所以,我们假设x是整数或指针。 但是,这个宏在x为负数时可能不适用。因为对齐操作通常用于地址(无符号)或大小(无符号)。所以,我们通常处理无符号数。 因此,我们可以限制x必须是非负整数(或指针)。 总结:我们实现一个用户空间的ALIGN宏,用于整数和指针的对齐(向上对齐)。 代码示例: ```c #include <stdint.h> /* 对齐宏:将x向上对齐align的倍数,align必须是2的幂 */ #define ALIGN(x, align) ( (typeof(x)) ( ( ((uintptr_t)(x)) + (align) - 1) & ~((uintptr_t)(align) - 1) ) ) ``` 注意:typeof是GCC扩展,在非GCC编译器上可能不可用。为了可移植,我们可以使用内联函数代替宏,但内联函数不能处理类型泛型(即同时支持整数和指针)。或者,我们可以分别定义整数版本和指针版本。 整数版本: #define ALIGN_INT(x, align) ( ( (x) + (align) - 1) & ~((align) - 1) ) 指针版本: #define ALIGN_PTR(ptr, align) ( (void *)( ( ((uintptr_t)(ptr)) + (align) - 1) & ~((uintptr_t)(align) - 1) ) ) 这样,我们可以根据情况使用。 但是,原问题没有特别指明,我们假设需要同时处理整数和指针。不过,由于整数和指针的转换,我们可以用一个宏处理:当x是指针时,我们转换为uintptr_t,然后再转换回去;当x是整数时,同样处理。但是,如果x是整数,而整数类型比uintptr_t小(比如在64位系统上,int是32位,uintptr_t是64位),那么转换是安全的。如果整数类型比uintptr_t大,那么可能会有问题,但这种情况很少见(因为地址空间通常不会超过机器字长)。 因此,我们使用上述的ALIGN宏(使用typeof和uintptr_t)是一个常见的做法。 如果不想使用typeof,我们可以这样定义两个宏,或者要求用户传递类型。 在Linux内核中,他们使用了一个更通用的方法,并且在内核中已经处理了类型问题(因为内核通常使用GCC编译,所以typeof可用)。 所以,在用户空间,如果我们使用GCC,可以这样定义。如果是其他编译器,可能需要调整。 另外,我们也可以考虑使用C11的_Generic,但这样比较复杂。 鉴于问题要求的是在Linux中实现,而Linux上通常使用GCC或Clang(都支持typeof),所以我们可以使用上述带typeof的宏。 最后,我们给出一个完整的示例程序: ```c #include <stdio.h> #include <stdint.h> // 使用typeof,支持整数和指针类型,对齐align必须是2的幂 #define ALIGN(x, align) ( (typeof(x)) ( ( ((uintptr_t)(x)) + (align) - 1) & ~((uintptr_t)(align) - 1) ) ) int main() { // 测试整数 unsigned int x = 7; unsigned int aligned_x = ALIGN(x, 4); printf("ALIGN(7, 4) = %u\n", aligned_x); // 期望8 // 测试指针 void *ptr = (void*)0x1234; void *aligned_ptr = ALIGN(ptr, 4096); printf("ALIGN(0x1234, 4096) = %p\n", aligned_ptr); // 期望0x2000 return 0; } ``` 注意:0x1234向上对齐到4096(0x1000)的倍数:0x1234除以4096的余数是0x234,所以下一个边界是0x2000(即8192)。 运行结果: ALIGN(7,4)=8 ALIGN(0x1234,4096)=0x2000 但是,这个宏在align不是2的幂时行为未定义,所以调用者必须确保。 另外,我们也可以实现一个向下对齐的宏: ```c #define ALIGN_DOWN(x, align) ( (typeof(x)) ( ((uintptr_t)(x)) & ~((uintptr_t)(align) - 1) ) ) ``` 这样,我们就实现了内存对齐的宏。 总结:在Linux中,我们可以使用上述宏来实现内存对齐,该宏利用了GCC的typeof扩展和C标准的uintptr_t类型。</think>### 在Linux中手动实现内存对齐ALIGN宏 在Linux中,内存对齐ALIGN宏通常用于将地址或大小向上对齐到指定边界(必须是2的幂)。以下是两种常见的实现方式: #### 1. 基础整数实现(适用于无符号整数) ```c #include <stdint.h> #define ALIGN(x, align) (((x) + (align) - 1) & ~((align) - 1)) ``` **原理**: - `(align - 1)`:生成低位掩码(如align=4时得到0b11) - `~(align - 1)`:生成高位掩码(如align=4时得到0b...11111100) - `(x + align - 1)`:确保越过对齐边界时进位 - 按位与操作:清除低位未对齐部分 **示例**: ```c uintptr_t addr = 0x1007; uintptr_t aligned = ALIGN(addr, 8); // 结果:0x1008 ``` #### 2. 通用类型实现(支持指针和整数) ```c #include <stdint.h> #define ALIGN(x, align) ({ \ typeof(x) _x = (x); \ typeof(align) _a = (align); \ (typeof(x))((((uintptr_t)_x) + _a - 1) & ~(_a - 1)); \ }) ``` **特性**: - 使用`typeof`保持原始类型(GCC/Clang扩展) - 通过`uintptr_t`处理指针和整数的统一转换 - 临时变量避免多次求值 #### 3. 向下对齐宏(补充) ```c #define ALIGN_DOWN(x, align) ((x) & ~((align) - 1)) ``` #### 完整示例代码 ```c #include <stdio.h> #include <stdint.h> // 通用对齐宏 #define ALIGN(x, align) ({ \ typeof(x) _x = (x); \ typeof(align) _a = (align); \ (typeof(x))((((uintptr_t)_x) + _a - 1) & ~(_a - 1)); \ }) int main() { // 整数对齐测试 size_t size = 17; size_t aligned_size = ALIGN(size, 16); // 对齐到32 printf("Size: %zu → Aligned: %zu\n", size, aligned_size); // 指针对齐测试 void* ptr = (void*)0x1234ABCD; void* aligned_ptr = ALIGN(ptr, 4096); // 页面对齐 printf("Ptr: %p → Aligned: %p\n", ptr, aligned_ptr); return 0; } ``` #### 关键注意事项 1. **对齐值必须是2的幂**:如4/8/16/4096等,否则宏行为未定义 2. **类型安全**:使用`uintptr_t`确保指针运算合法[^4] 3. **性能**:位运算在编译期优化,无运行时开销 4. **适用场景**: - 内存分配器实现(如`malloc`内部) - 硬件寄存器访问(需要特定对齐) - SIMD指令集优化(AVX/SSE等)[^3] - 结构体填充(配合`__attribute__((aligned))`)[^5] #### 与标准库对比 | 方法 | 优点 | 缺点 | |------------------|--------------------------|--------------------------| | 手动ALIGN宏 | 零开销,灵活嵌入代码 | 需自行验证对齐值有效性 | | `posix_memalign` | 标准API,错误检查完善 | 有函数调用开销[^3] | | `aligned_alloc` | C11标准,类型安全 | 需C11支持,大小需对齐[^3] | > 实际开发中,推荐优先使用C11的`aligned_alloc`或POSIX的`posix_memalign`,仅在性能敏感场景使用手动宏[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值