编程小回忆四

在我们进入更高级主题之前, 我们需要停下来快速关注一下可移植性问题. 现
代版本的 Linux 内核是高度可移植的, 它正运行在很多不同体系上. 由于
Linux 内核的多平台特性, 打算做认真使用的驱动应当也是可移植的.
但是内核代码的一个核心问题是不但能够存取已知长度的数据项(例如, 文件系
统数据结构或者设备单板上的寄存器), 而且可以使用不同处理器的能力(32-位
和 64-位 体系, 并且也可能是 16 位).
内核开发者在移植 x86 代码到新体系时遇到的几个问题与不正确的数据类型相
关. 坚持严格的数据类型和使用 -Wall -Wstrict-prototypes 进行编译可能避
免大部分的 bug.
内核数据使用的数据类型分为 3 个主要类型: 标准 C 类型例如 int, 明确大
小的类型例如 u32, 以及用作特定内核对象的类型, 例如 pid_t. 我们将看到
这 3 个类型种类应当什么时候以及应当如何使用. 本章的最后的节谈论一些其
他的典型问题, 你在移植 x86 的驱动到其他平台时可能遇到的问题, 并且介绍
近期内核头文件输出的链表的常用支持.
如果你遵照我们提供的指引, 你的驱动应当编译和运行在你无法测试的平台上.
11.1. 标准 C 类型的使用
尽管大部分程序员习惯自由使用标准类型, 如 int 和 long, 编写设备驱动需
要一些小心来避免类型冲突和模糊的 bug.
这个问题是你不能使用标准类型, 当你需要"一个 2-字节 填充者"或者"一个东
西来代表一个 4-字节 字串", 因为正常的 C 数据类型在所有体系上不是相同
大小. 为展示各种 C 类型的数据大小, datasize 程序已包含在例子文件
misc-progs 目录中, 由 O' Reilly's FTP 站点提供. 这是一个程序的样例运行,
在一个 i386 系统上(显示的最后 4 个类型在下一章介绍):
morgana% misc-progs/datasize
arch Size: char short int long ptr long-long u8 u16 u32 u64

i686     1    2     4   4    4   8         1  2   4   8
这个程序可以用来显示长整型和指针在 64-位 平台上的不同大小, 如同在不同
Linux 计算机上运行程序所演示的:

查看更多精彩图片

注意有趣的是 SPARC 64 体系在一个 32-位 用户空间运行, 因此那里指针是
32 位宽, 尽管它们在内核空间是 64 位宽. 这可用加载 kdatasize 模块(在例
子文件的 misc-modules 目录里)来验证. 这个模块在加载时使用 printk 来报
告大小信息, 并且返回一个错误( 因此没有必要卸载它 ):

kernel: arch Size: char short int long ptr long-long u8 u16 u32 u64
kernel: sparc64    1    2     4   8    8   8         1  2   4   8

尽管在混合不同数据类型时你必须小心, 有时有很好的理由这样做. 一种情况
是因为内存存取, 与内核相关时是特殊的. 概念上, 尽管地址是指针, 内存管
理常常使用一个无符号的整数类型更好地完成; 内核对待物理内存如同一个大
数组, 并且内存地址只是一个数组索引. 进一步地, 一个指针容易解引用; 当
直接处理内存存取时, 你几乎从不想以这种方式解引用. 使用一个整数类型避
免了这种解引用, 因此避免了 bug. 因此, 内核中通常的内存地址常常是
unsigned long, 利用了指针和长整型一直是相同大小的这个事实, 至少在
Linux 目前支持的所有平台上.
因为其所值的原因, C99 标准定义了 intptr_t 和 uintptr_t 类型给一个可以
持有一个指针值的整型变量. 但是, 这些类型几乎没在 2.6 内核中使用.
11.2. 安排一个明确大小给数据项
有时内核代码需要一个特定大小的数据项, 也许要匹配预定义的二进制结构,[39]
 
来和用户空间通讯, 或者来用插入"填充"字段来对齐结构中的数据( 但是关于
对齐问题的信息参考 "数据对齐" 一节 ).
内核提供了下列数据类型来使用, 无论你什么时候需要知道你的数据的大小.
所有的数据声明在 <asm/types.h>, 它又被 <linux/types.h> 包含. u8; /* unsigned byte (8 bits) */
u16; /* unsigned word (16 bits) */
u32; /* unsigned 32-bit value */
u64; /* unsigned 64-bit value */
存在对应的有符号类型, 但是很少需要; 如果你需要它们, 只要在名子里用 s
代替 u. 
如果一个用户空间程序需要使用这些类型, 可用使用一个双下划线前缀在名子
上: __u8 和其它独立于 __KERNEL__ 定义的类型. 例如, 如果, 一个驱动需要
与用户空间中运行的程序交换二进制结构, 通过 ioctl, 头文件应当在结构中
声明 32-位 成员为 __u32.
重要的是记住这些类型是 Linux 特定的, 并且使用它们妨碍了移植软件到其他
的 Unix 口味上. 使用近期编译器的系统支持 C99-标准 类型, 例如 uint8_t
和 uint32_t; 如果考虑到移植性, 使用这些类型比 Linux-特定的变体要好.
你可能也注意到有时内核使用传统的类型, 例如 unsigned int, 给那些维数与
体系无关的项. 这是为后向兼容而做的. 当 u32 和它的类似物在版本 1.1.67
引入时, 开发者不能改变存在的数据结构为新的类型, 因为编译器发出一个警
告当在结构成员和安排给它的值之间有一个类型不匹配时.. Linus 不希望他写
给自己使用的操作系统称为多平台的; 结果是, 老的结构有时被松散的键入.
事实上, 编译器指示类型不一致, 甚至在 2 个类型只是同一个对象的不同名子,
例如在 PC 上 unsigned long 和 u32.

11.3. 接口特定的类型
内核中一些通常使用的数据类型有它们自己的 typedef 语句, 因此阻止了任何
移植性问题. 例如, 一个进程标识符 ( pid ) 常常是 pid_t 而不是 int. 使用
pid_t 屏蔽了任何在实际数据类型上的不同. 我们使用接口特定的表达式来指
一个类型, 由一个库定义的, 以便于提供一个接口给一个特定的数据结构.
注意, 在近期, 已经相对少定义新的接口特定类型. 使用 typedef 语句已经有
许多内核开发者不喜欢, 它们宁愿看到代码中直接使用的真实类型信息, 不是
藏在一个用户定义的类型后面. 很多老的接口特定的类型在内核中保留, 但是,
并且它们不会很快消失. 甚至当没有定义接口特定的类型, 以和内核其他部分保持一致的方式使用正确
的数据类型是一直重要的. 一个嘀哒计数, 例如, 一直是 unsigned long, 独
立于它实际的大小, 因此 unsigned long 类型应当在使用 jiffy 时一直使用.
本节我们集中于 _t 类型的使用.
很多 _t 类型在 <linux/types.h> 中定义, 但是列出来是很少有用. 当你需要
一个特定类型, 你会在你需要调用的函数的原型中发现它, 或者在你使用的数
据结构中.
无论何时你的驱动使用需要这样"定制"类型的函数并且你不遵照惯例, 编译器
发出一个警告; 如果你使用 -Wall 编译器标志并且小心去除所有的警告, 你能
有信心你的代码是可移植的.
_t 数据项的主要问题是当你需要打印它们时, 常常不容易选择正确的 printk
或 printf 格式, 你在一个体系上出现的警告会在另一个上重新出现. 例如,
你如何打印一个 size_t, 它在一些平台上是 unsigned long 而在其他某个上
面是 unsigned int?
无论何时你需要打印某个接口特定的数据, 最好的方法是转换它的值为最大的
可能类型(常常是 long 或者 unsigned long ) 并且接着打印它通过对应的格式.
这种调整不会产生错误或者警告, 因为格式匹配类型, 并且你不会丢失数据位,
因为这个转换或者是一个空操作或者是数据项向更大数据类型的扩展.
实际上, 我们在谈论的数据项不会常常要打印的, 因此这个问题只适用于调试
信息. 常常, 代码只需要存储和比较接口特定的类型, 加上传递它们作为给库
或者内核函数的参数.
尽管 _t 类型是大部分情况的正确解决方法, 有时正确的类型不存取. 这发生
在某些还未被清理的老接口.
我们在内核头文件中发现的一个模糊之处是用在 I/O 函数的数据类型, 它松散
地定义( 看第 9 章"平台相关性" 一节 ). 松散的类型在那里主要是因为历史
原因, 但是在写代码时它可能产生问题. 例如, 交换给函数如 outb 的参数可
能会有麻烦; 如果有一个 port_t 类型, 编译器会发现这个类型.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值