编写具有良好移植性Linux C代码的若干技巧

本文详述了在Linux内核开发中提升代码可移植性的关键技巧,包括避免使用显式常量,正确处理滴答值、页大小、字节序,以及数据对齐等问题。同时,介绍了如何利用宏定义和内建函数来解决这些问题,确保代码在不同体系结构上的兼容性。此外,还提及了错误处理方法和严格的数据类型使用,为编写高质量、可移植的Linux应用程序提供指导。
摘要由CSDN通过智能技术生成

摘要:本文主要讲述了linux内核开发过程中,需要注意的有关提高代码可移植性的若干技巧,同样对编写Linux应用程序也有较好的参考意义。干货满满,请君一阅。

1. 避免使用显式常量

  • 这点应该很好理解,如果你的代码中充斥着各种硬件相关的或者体系特点的常量值,如果将来改起来将是非常麻烦的。
  • 解决办法:通过宏定义(预处理宏),并在头文件中予以说明。

2. 不要假定系统每秒有多少个滴答值

  • 很多人,特别是在i386体系下编程的同学,习惯默认1秒是1000个滴答,但事实上并不是每隔平台都是按照这个速度运行的。
  • 解决办法:任意时候,涉及到使用滴答数来计算时间间隔时,使用HZ(每秒的定时器中断数)。例如半秒就是HZ/2个滴答数,一毫秒就是HZ/1000个滴答数。

3.不要假定页大小是4K

  • 不同平台的页大小是不同的,它从4KB到64KB。

  • 解决办法

    1. 使用宏定义PAGE_SIZEPAGE_SHIFT(<asm/page.h>)。后者通常用于将一个地址右移位PAGE_SHIFT位后,得到该地址所在的页号。当页大小为4KB时,该值取12。

    2. 如果你在用户空间编程,可以使用size_t getpagesize(void);(<unistd.h>)库函数来获取当前系统(不是实际某硬件的页大小)的页大小(字节数),如果页大小为4KB,则函数返回4096。

    3. 使用内核函数int get_order(unsigned long)(<asm/page.h>),它一般和get_free_page()函数搭配使用,用来计算分配相应字节内存时需要的order数:

      #include <asm/page.h>
      char *buff;
      int order;
      
      order = get_order (8*1024);			//要分配的字节数必须是2的幂!
      buff = get_free_pages (GFP_KERNEL, order);	//若在用户空间使用需要在函数前加双下划线
      

4. 不要假定字节序

  • 不同的体系结构其存储字节的顺序有大端和小端的区别。

  • 解决办法

    1. 宏定义:__BIG_ENDIAN__LITTLE_ENDIAN(<asm/byteorder.h>),任意系统会根据自己的字节序定义相应的宏变量。当处理字节序问题时,你可以采取如下编码结构:

      #ifdef __BIG_ENDIAN
      	...
      #else
          ...
      #endif
      
    2. 前面的方法虽然可以解决问题,但带来了代码冗余。所以更好的办法是利用linux内核提拱的一套宏定义来处理字节序的转换:

      #include <linux/byteorder/big_endian.h>
      #include <linux/byteorder/little_endian.h>
      /* the "32" can be replaced by 64 or 16,they return the converted value */
      u32 cpu_to_be32(u32);
      u32 cpu_to_le32(u32);
      
      u32 be32_to_cpu(u32);
      u32 le32_cpu_to(u32);
      
      /* 他们的指针变体:将指针所指字的字节序进行相应转换,并返回转换的值 */
      u32 cpu_to_be32p(const u32*);
      u32 cpu_to_le32p(const u32*);
      
      u32 be32_to_cpup(const u32*);
      u32 le32_cpu_top(const u32*);
      
      /* 他们的另一种指针变体:将指针所指字的字节序直接进行相应的转换 */
      void cpu_to_be32s(u32 *);
      void cpu_to_le32s(u32 *);
      
      void be32_to_cpus(u32 *);
      void le32_cpu_tos(u32 *);
      

5. 关于数据对齐

  • 为了保持可移植性,强烈建议存取数据时,其地址要和数据大小对齐(即若数据类型是8字节大小,则其地址应该是8字节对齐)!!!但如果确实需要存取不对齐的数据时怎么办?

  • 解决办法

    1. 使用内核提供的宏定义函数:

      #include <asm/unaligned.h>
      /* 这些宏是无类型的(可存取1、2、4或8字节长的数据类型) */
      get_unaligned(ptr);
      put_unaligned(val, ptr);
      
      /*使用举例:*/
      
      /* 移植性差的代码 */
      char data[10]= {1,2,3,4,5,6,7,8,9,0};	
      unsigned int d[4];
      d[0] = *(unsigned int *)&data[0];	//data数组不一定是4字节对齐的
      
      /* 移植性好的代码 */
      char data[10]= {1,2,3,4,5,6,7,8,9,0};
      unsigned int val, *val_ptr;
      val_ptr = &data[0]
      val = get_unaligned(val_ptr);
      
  • 我们知道,编译器为了目标处理器获得更好的性能,它会悄悄的强制使数据项地址和其大小对齐,但这里有两点需要特别注意:

    1. 并不是所有平台将64位(8字节)数据对其在8字节大小的边界地址上。据我所知至少有i386、i686、armv4l这3种平台的64位数据是对齐在4字节大小的边界地址上。为了可移植性,你需要填充一些额外的数据类型来强制对齐。

    2. 但如果你就是想得到一个紧凑的数据项结构时,如何避开编译器这种自动填充额外数据项呢?

      • 利用声明数据属性
      struct 
      {
          u16 var1;
          u64 var2;	//此处前面可能被编译器自动插入2字节(64位数据满足32位对齐)或6字节(64位数据满足64位对齐)的额外数据项
          u16 var3;
          u32 var4;
      }__attribute__((packed)) scsi;		//添加了该属性申明后,就不会额外插入数据项了
      

6. 指针和错误值

  • 很多时候,我们看到某些返回值为指针的函数,在执行时遇到错误的时候会默认返回NULL指针以表示错误。但调用者并不能通过该NULL得到任何关于遇到何种错误的有用信息。那有没有办法通过返回不同的指针来指示错误类型,而又不和正常返回的指针冲突呢?

  • 解决办法:内核在<linux/err.h>提供了函数,使得可以将错误编码通过指针返回给调用者:

    void *ERR_PTR(long error);	//error是常见的负值错误码
    
    long IS_ERR(const void *ptr);	//判断ERR_PTR()函数返回的指针是否含有负值错误编码
    
    long PTR_ERR(const void *ptr);	//取出ERR_PTR()函数返回的指针包含的负值错误编码
    

7. 使用严格的数据类型进行编译

  • 在代码进行编译时选择如下选项:-Wall -Wstrict-prototypes,如此可避免大部分的bug。
  • 或者,更严格一点,你可以将任何编译器警告作为错误来对待,此时可添选项:-Werror
  • 内核代码主要使用三种类型的数据:基本数据类型(char、short、int、long、long long等)、明确大小的Linux数据类型(u8、u16、 u32、 u64及其有符号类型变种s8、s16、s32、s64)和特定内核对象的类型(pid_t、size_t等)。
    • 对于基本数据类型:你要知道不同体系结构其大小是不一样的。但一般来说char、short、int、long long大小是固定的,分别是1、2、4、8个字节大小。但long和指针类型会随平台各异(aipha、ia64、x86_64平台上是8字节,其它平台是4字节),但他们二者的大小永远是相等的,所以在内存管理方面,经常使用一个unsigned long来代替指针类型。
    • 对于与明确大小的数据项:其在头文件<linux/types.h>或<asm/types.h>中定义。且如果用户空间需要使用它们,可以在名字前添加双下划线引用(类似__u8)。这些类型是linux特定的,为了更好的移植性,建议使用C99标准类型(uint8_t、uint16_t、uint32_t等)进行代替。
    • 对于接口特定的数据项:其在头文件<linux/types.h>中定义,其实用原则是与内核其它部分保持一致。例如,时钟滴答数jiffy一直是用unsigned long类型表示的,那你就不要用typedef语句重新定义一个新的类型;又如pid_t类型,在某些系统中是int,而在另一些系统中又是long,所以,在定义进程号变量时,你就用pid_t,而不是int或是long。另一个需要关注的是,当你需要使用printf或是printk打印这些特定类型时,如何选用打印格式的问题。在此,建议将它们转化为它所有可能类型中的值为最大的一种(通常为long或者是unsigned long)。例如pid_t可以扩展为unsigned long。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值