linux kernel 学习手记4 内核中的数据类型

 11  内核中的数据类型

坚持严格的数据类型和使用 -Wall -Wstrict-prototypes 进行编译可能避免大部分的 bug.

内核数据使用的数据类型分为 3 个主要类型:

1.       标准 C 类型例如 int,

2.       明确大小的类型例如 u32,

3.       以及用作特定内核对象的类型, 例如 pid_t.

内核中通常的内存地址常常是 unsigned long, 利用了指针和长整型一直是相同大小的这个事实

所有的数据声明在 <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 */

如果一个用户空间程序需要使用这些类型, 可用使用一个双下划线前缀在名子上: __u8 和其它独立于 __KERNEL__ 定义的类型. 例如, 如果, 一个驱动需要与用户空间中运行的程序交换二进制结构, 通过 ioctl, 头文件应当在结构中声明 32-位 成员为 __u32.

重要的是记住这些类型是 Linux 特定的, 并且使用它们妨碍了移植软件到其他的 Unix 口味上. 使用近期编译器的系统支持 C99-标准 类型, 例如 uint8_t uint32_t; 如果考虑到移植性, 使用这些类型比 Linux-特定的变体要好.

接口特定的类型

_t 类型在 <linux/types.h> 中定义,很多通过 typedef定义来隐藏实际数据类型,实现移殖性,但是这种用法不受开发者欢迎。

但是如果驱动程序使用了这些订制的类型的函数,但是有不遵守约定时候,编译器就会产生警告,可以使用-Wall编译选项来细心消除所有的警告确保可移植性。

printfprintk中使用了这些结构时候,最好先做一次类型强制转换,消除警告。

还有一条通用的原则就是避免显示使用常量,代码通过宏定义来避免这个问题,实现可移植性性和可读性。

时间间隔

使用宏HZ常见是msec毫秒对应的jiffies数目总是msec*HZ/1000

页大小

PAGE_SIZEPAGE_SHIFT是为了得到一个地址所在页的页号需要对该地址右移的位数。

如果一个驱动需要 16 KB 来暂存数据, 它不应当指定一个 2 的指数 给 get_free_pages. 你需要一个可移植解决方法. 这样的解决方法, 幸运的是, 已经由内核开发者写好并且称为 get_order:

#include <asm/page.h>
int order = get_order(16*1024);
buf = get_free_pages(GFP_KERNEL, order);

记住, get_order 的参数必须是 2 的幂.

字节序

包含文件 <asm/byteorder.h> 定义了或者 __BIG_ENDIAN 或者 __LITTLE_ENDIAN, 依赖处理器的字节序. 当处理字节序问题时, 你可能编码一堆 #ifdef __LITTTLE_ENDIAN 条件语句, 但是有一个更好的方法. Linux 内核定义了一套宏定义来处理之间的转换, 在处理器字节序和你需要以特定字节序存储和加载的数据之间. 例如:

u32 cpu_to_le32 (u32);
u32 le32_to_cpu (u32);

此类的函数有十几个个在<linux/byteorder/big_endian.h> <linux/byteorder/little_endian.h> 中见到完整列表.

数据对齐

果你需要存取不对齐的数据, 你应当使用下列宏:

#include <asm/unaligned.h>
get_unaligned(ptr);
put_unaligned(val, ptr);

应当一直强制自然的数据项对齐, 加上对一个特定对齐方式的标准化. 自然对齐意味着存储数据项在是它的大小的整数倍的地址上(例如, 8-byte 项在 8 的整数倍的地址上). 为强制自然对齐在阻止编译器以不希望的方式安排成员量的时候, 你应当使用填充者成员来避免在数据结构中留下空洞.

要知道编译器可能自己悄悄地插入填充到结构中来保证每个成员是对齐的, 为了目标处理器的良好性能. 如果你定义一个结构打算来匹配一个设备期望的结构, 这个自动的填充可能妨碍你的企图. 解决这个问题的方法是告诉编译器这个结构必须是"紧凑的", 不能增加填充者. __attribute__ ((packed))如:内核头文件 <linux/edd.h> 定义几个与 x86 BIOS 接口的数据结构, 并且它包含下列的定义:

struct
{
        u16 id;
        u64 lun;
        u16 reserved1;
        u32 reserved2;
}
__attribute__ ((packed)) scsi;

指针和错误值

为帮助创建和使用这类接口, 一小部分函数已可用( <linux/err.h>).

一个返回指针类型的函数可以返回一个错误值, 使用:

void *ERR_PTR(long error);

这里, error 是常见的负值错误码. 调用者可用使用 IS_ERR 来测试是否一个返回的指针是不是一个错误码:

long IS_ERR(const void *ptr); 

如果你需要实际的错误码, 它可能被抽取到, 使用:

long PTR_ERR(const void *ptr); 

你应当只对 IS_ERR 返回一个真值的值使用 PTR_ERR; 任何其他的值是一个有效的指针.

链表

为使用列表机制, 你的驱动必须包含文件 <linux/list.h>. 这个文件定义了一个简单的类型 list_head 结构:

struct list_head { struct list_head *next, *prev; }; 

为在你的代码中使用 Linux 列表, 你只需要嵌入一个 list_head 在构成这个链表的结构里面.

链表头必须在使用前用 INIT_LIST_HEAD 宏来初始化. 
struct list_head todo_list; 
INIT_LIST_HEAD(&todo_list); 
还可以直接调用这个宏在编译时候初始化
LIST_HEAD(todo_list); 

 

几个使用链表的函数定义在 <linux/list.h>:

list_add(struct list_head *new, struct list_head *head);

在紧接着链表 head 后面增加新入口项 -- 正常地在链表的开头. 因此, 它可用来构建堆栈. 但是, 注意, head 不需要是链表名义上的头; 如果你传递一个 list_head 结构, 它在链表某处的中间, 新的项紧靠在它后面. 因为 Linux 链表是环形的, 链表的头通常和任何其他的项没有区别.

list_add_tail(struct list_head *new, struct list_head *head);

刚好在给定链表头前面增加一个新入口项 -- 在链表的尾部, 换句话说. list_add_tail 能够, 因此, 用来构建先入先出队列.

list_del(struct list_head *entry);

list_del_init(struct list_head *entry);

给定的项从队列中去除. 如果入口项可能注册在另外的链表中, 你应当使用 list_del_init, 它重新初始化这个链表指针.

list_move(struct list_head *entry, struct list_head *head);

list_move_tail(struct list_head *entry, struct list_head *head);

给定的入口项从它当前的链表里去除并且增加到 head 的开始. 为安放入口项在新链表的末尾, 使用 list_move_tail 代替.

list_empty(struct list_head *head);

如果给定链表是空, 返回一个非零值.

list_splice(struct list_head *list, struct list_head *head);

list 紧接在 head 之后来连接 2 个链表.

ist_head 结构对于实现一个相似结构的链表是好的, 但是调用程序常常感兴趣更大的结构, 它组成链表作为一个整体. 一个宏定义, list_entry, 映射一个 list_head 结构指针到一个指向包含它的结构的指针. 它如下被调用:

list_entry(struct list_head *ptr, type_of_struct, field_name); 

这里 ptr 是一个指向使用的 struct list_head 的指针, type_of_struct 是包含 ptr 的结构的类型, field_name 是结构中列表成员的名子. 在我们之前的 todo_struct 结构中, 链表成员称为简单列表. 因此, 我们应当转变一个列表入口项为它的包含结构, 使用这样一行:

struct todo_struct *todo_ptr = list_entry(listptr, struct todo_struct, list); 

 

 

struct todo_struct
{
        struct list_head list;
        int priority; /* driver specific */
        /* ... add other driver-specific fields */
};
struct todo_struct *todo_ptr = list_entry(listptr, struct todo_struct, list); 

 

 

为一个通用的规则, 最好使用一个预定义的宏来创建循环, 它遍历链表. 前一个循环, 例如, 可这样编码:

void todo_add_entry(struct todo_struct *new)
{
        struct list_head *ptr;
        struct todo_struct *entry;

  
  
   
    
  
  
        list_for_each(ptr, &todo_list)
        {
entry = list_entry(ptr, struct todo_struct, list);
                if (entry->priority < new->priority) {

  
  
   
    
  
  
                        list_add_tail(&new->list, ptr);
                        return;
                }
        }
        list_add_tail(&new->list, &todo_struct)

  
  
   
    
  
  
}

list_for_each(struct list_head *cursor, struct list_head *list)

这个宏创建一个 for 循环, 执行一次, cursor 指向链表中的每个连续的入口项. 小心改变列表在遍历它时.

list_for_each_prev(struct list_head *cursor, struct list_head *list)

这个版本后向遍历链表.

list_for_each_safe(struct list_head *cursor, struct list_head *next, struct list_head *list)

如果你的循环可能删除列表中的项, 使用这个版本. 它简单的存储列表 next 中下一个项, 在循环的开始, 因此如果 cursor 指向的入口项被删除, 它不会被搞乱.

list_for_each_entry(type *cursor, struct list_head *list, member)

list_for_each_entry_safe(type *cursor, type *next, struct list_head *list, member)

这些宏定义减轻了对一个包含给定结构类型的列表的处理. 这里, cursor 是一个指向包含数据类型的指针, member 是包含结构中 list_head 结构的名子. 有了这些宏, 没有必要安放 list_entry 调用在循环里了.

如果你查看 <linux/list.h> 里面, 你看到有一些附加的声明. hlist 类型是一个有一个单独的, 单指针列表头类型的双向链表; 它常用作创建哈希表和类型结构.

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值