现在的位置: 首页 > 综合 > 正文

LINUX设备驱动学习(十二)—内核数据结构

2013年09月15日 ⁄ 综合 ⁄ 共 5397字 ⁄ 字号 评论关闭

这里按照http://blog.chinaunix.net/u1/34474/showart.php?id=418515的学习路线,开始内核数据结构的学习。

 

将linux 移植到新的体系结构时,开发者遇到的若干问题都与不正确的数据类型有关。坚持使用严格的数据类型和使用 -Wall -Wstrict-prototypes 进行编译可能避免大部分的 bug。

内核数据使用的数据类型主要分为 3 个类型: 标准 C 语言类型确定大小的类型特定内核对象的类型

 

标准C语言类型

 

当需要“一个2字节填充符”或“用一个4字节字串来代表某个东西”,就不能使用标准C语言类型,因为在不同的体系结构,C 语言的数据类型所占的空间大小不同。后面的datasize 程序实验展示了用户空间各种 C 的数据类型在当前平台所占空间的大小。而且有的构架,内核空间和用户空间的C 数据类型所占空间大小也可能不同。kdatasize模块显示了当前模块的内核空间C 数据类型所占空间大小。

尽管概念上地址是指针,但使用一个无符号整型可以更好地实现内存管理; 内核把物理内存看成一个巨型数组, 内存地址就是该数组的索引。 我们可以方便地对指针取值,但直接处理内存地址时,我们几乎从不会以这种方式对他取值。使用一个整数类型避免了这种取值,因此避免了 bug。所以,利用至少在 Linux 目前支持的所有平台上,指针和长整型始终是相同大小的这一事实,内核中内存地址常常是 unsigned long

C99 标准定义了 intptr_t 和 uintptr_t 类型,它们是能够保存指针值的整型变量。但没在 2.6 内核中几乎没使用。

 

 分配确定大小的数据类型

分配确定大小的数据项,多半是用来匹配预定义的二进制结构,或者和用户空间进行交互,或者向结构体中插入“填白”字段来对其数据。

 

当需要知道你定义的数据的大小时,可以使用内核提供的下列数据类型(所有的数据声明在 <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__ 定义的。

 

 

 

接口特定的类型

内核中最常用的数据类型由他们自己的typedef声明,防止移植问题。例如一个进程的标识符pid类型为pid_t。“接口特定”是由某个库定义的一种数据类型,以便为某个特定的数据结构提供接口。

jiffies的计数类型为unsigned long

 

即使没有定义接口特定类型,也应该始终是用和内核其他部分保持一致、适当的数据类型。只要驱动使用了这种“定制”类型的函数,但又不遵照约定,编译器会发出警告,这时使用 -Wall 编译器选项并小心去除所有的警告,就可以确信代码的可移植性了。

_t 类型的主要问题是:打印它们时,常常不容易选择正确的 printk 或 printf 格式。打印接口特定的数据的最好方法是:将其强制转换为可能的最大类型(常常是 long 或 unsigned long ) 并用相应的格式打印。

 

 

其他的移植问题

在编写一个能运行在不同平台上的驱动程序时,一个通用原则是避免使用显示的常量值,要通过预处理的宏使之参数化。

时间间隔问题:在处理时间间隔时不要假定每秒一定有固定数量的jiffies(unsigned long),并不是每个linux平台都已相同的速度在运行,而应该使用HZ(每秒定时器中断的次数)来衡量。例如:为了检测半秒的延时,可以将消逝的时间与HZ/2做比较。s3c2410的HZ值默认为200

 

页大小:使用内存时,要记住内存页的大小为PAGE_SIZE字节,而不是4KB。因为它们在不同平台或者相同平台的实现是不一样的。这一问题设计到PAGE_SIZE和PAGE_SHIFT(得到一个地址所在页的页号)两个宏。这些宏在<asm/page.h>中定义。

若一个驱动需要 16 KB 来暂存数据,一个可移植得解决方法是 get_order:

#include <asm/pages.h>

int order = get_order(16*1024); //参数必须是2的幂

buf = get_free_pages(GFP_KERNEL,order);

 

字节序:不要假设字节序。 代码应该编写成不依赖所操作数据的字节序的方式。

头文件 <asm/byteorder.h> 定义:

#ifdef __ARMEB__
#include <linux/byteorder/big_endian.h>
#else
#include <linux/byteorder/little_endian.h>
#endif

<linux/byteorder/big_endian.h>中定义了__BIG_ENDIAN ,而在<linux/byteorder/little_endian.h>中定义了__LITTLE_ENDIAN,这些依赖处理器的字节序当处理字节序问题时,需要编码一堆类似 #ifdef __LITTTLE_ENDIAN 的条件语句。

但是还有一个更好的方法:Linux 内核有一套宏定义来处理处理器字节序和特定字节序之间的转换。例如:

u32 cpu_to_le32 (u32);
u32 le32_to_cpu (u32);
/*这些宏定义将一个CPU使用的值转换成一个无符号的32位小头数值,无论 CPU 是大端还是小端,也不管是不是32 位处理器。在没有转换工作需要做时,返回未修改的值。*/

/*有很多类似的函数在 <linux/byteorder/big_endian.h> 和 <linux/byteorder/little_endian.h> 中定义*/

 

数据对齐:大部分现代体系架构在每次程序试图访问未对齐的数据时都会产生一个异常,这是数据传输会被异常处理程序处理,因此会带来大的性能损失。

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

这些宏是无类型的,并对各总数据项,不管是 1、2、4或 8 个字节,他们都有效,并且在所有内核版本中都有定义。

 

关于对齐的另一个问题是数据结构的跨平台移植性。同样的数据结构在不同的平台上可能被不同地编译。为了编写可以跨体系移植的数据结构,应当始终强制数据项的自然对齐。自然对齐(natural alignment)指的是:数据项大小的整数倍的地址上存储数据项。 应当使用填充符避免强制自然对齐时编译器移动数据结构的字段,在数据结构中留下空洞。

 

 

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

/*如果在 64-位平台上编译这个结构,若没有 __attribute__ ((packed)), lun 成员可能在前面被添加 2 个或 6 个填充符字节。指针和错误值*/

 

指针和错误值:许多内核接口通过把错误值编码到一个指针值中来返回错误信息。通过一下三个函数来进行处理:

void *ERR_PTR(long errno);    //将错误值转换成指针值

long IS_ERR(const void *ptr); //检查返回的指针值是否是真的错误值

long PTR_ERR(const void *ptr); //如果第二个函数对某个指针返回为真,则可以调用这个函数提取出实际的错误代码。

 

具体可以参见以下分析:


static inline long IS_ERR(const void *ptr)
{
  return IS_ERR_VALUE((unsigned long)ptr);
}

static inline long PTR_ERR(const void *ptr)
{
  return (long)ptr;
}
关于内核空间,我只想说,所有的驱动程序都是运行在内核空间,内核空间虽然很大,但总是有限的.要知道即便是我们这个幅员辽阔的伟大祖国其空间也是有限的,也只有960万平方公里,所以内核空间当然也是一个有限的空间,而在这有限的空间中,其最后一个page是专门保留的,也就是说一般人不可能用到内核空间最后一个page的指针.换句话说,你在写设备驱动程序的过程中,涉及到的任何一个指针,必然有三种情况,一种是有效指针,一种是NULL,空指针,一种是错误指针,或者说无效指针.而所谓的错误指针就是指其已经到达了最后一个page.比如对于32bit的系统来说,内核空间最高地址0xffffffff,那么最后一个page就是指的0xfffff000~0xffffffff(假设4k一个page).这段地址是被保留的,一般人不得越雷池半步,如果你发现你的一个指针指向这个范围中的某个地址,那么恭喜你,你的代码肯定出错了.
而对于Linux内核来说,不管任何体系结构,最多最多,错误号不会超过4095.所有我们可以专门空出一个页面来来对错误记录进行保存。
这里的IS_ERR(),它就是判断kthread_run()返回的指针是否有错,如果指针并不是指向最后一个page,那么没有问题,申请成功了,如果指针指向了最后一个page,那么说明实际上这不是一个有效的指针,这个指针里保存的实际上是一种错误代码.而通常很常用的方法就是先用IS_ERR()来判断是否是错误,然后如果是,那么就调用PTR_ERR()来返回这个错误代码.如下:
 if (IS_ERR(vpu_class)) {
  err = PTR_ERR(vpu_class);
  goto ....;
 }

 

 

 

 

内核中的链表:内核开发者已经建立了一套标准的循环、双向链表结构。当使用这些链表接口时,应当记住列表函数没做加锁。若驱动可能同一个列表并发操作,就必须实现一个锁方案。

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

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

实际代码中使用的链表几乎总是由某个结构类型组成,
每个结构描述链表中的一项. 为使用 Linux 链表,只需嵌入一个 list_head 在构成在这个链表的结构里面。链表头常常是一个独立的 list_head 结构。下图显示了这个简单的 struct list_head 是如何用来维护一个数据结构的列表的.

 

/*链表头必须在使用前初始化,有两种形式: */
/*
一是运行时初始化:*/
struct list_head
todo_list
;

INIT_LIST_HEAD
(&todo_list);


/*
二是编译时初始化:*/
LIST_HEAD
(todo_list);

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_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 个链表.*/

/*list_entry是将一个 list_head
结构指针转换到一个指向包含它的结构体的指针 。看了源码你就会发现,似曾相识。是的,其实在模块的open方法中已经用到了container_of list_entry的变体好有很多,看源码就知道了*/
#define
list_entry
(ptr,
type
, member)

/
 container_of
(ptr,
type
, member)

 

这里自己写的关于list_entry函数分析,可能没保存自动没了,转了一下别人的,懒的写了

 

 

 

 

 

 

 

 

 

 

 

 

抱歉!评论已关闭.