Linux下PCI设备驱动开发详解 下载本文

一、设备驱动程序概述

自Linux在中国发展以来,得到了许多公司的青睐。在国内的玩家也越来越多了,但目前还是停留在玩的水平上,很少有玩家对Linux的系统 进行研究。因为它的开放,我们可以随时拿来“把玩”。这也是Linux一个无可比拟的优势,这样我们可以修改后再加入到里面。但很少有专门的书籍讲到 Linux驱动程序的开发,像上海这样的大城市也很少有讲Linux驱动开发的资料,唉,谁让这个是人家的东西呢,我们还是得跟着人家跑。

我现在讲的这些驱动开发的细节,并不特定哪个版本的内核,这只是大体的思路与步骤。因为大家都知道Linux 2.6.x 与Linux 2.4.x是有不少改动的。所以,具体的大家可以去参考Linux Device Driver 2.4 和Linux Device Driver 2.6这几本书。这是我们学习开发驱动必不可少的东西。好了,下面就开始学习吧。

根据设备的行为,我们可以把设备分为字符设备和块设备,还有网络设备。字符设备是以字节为单位进行顺序读写,数据缓冲系统对它们的访问不提 供缓存。而块设备则是允许随机访问与读写,每次读写的数据量都是数据块长度的整数倍,并且访问还会经过缓冲区缓存系统才能实现。与Unix版本不同的是: Linux的内核允许不是数据块长度整数倍的数据量被读取,用官方的语言就是:但这种不同只是纯粹学术方面的东西。 大多数设备驱动程序都要通过文件系统来进行访问的,但网络设备是不同的。 /dev子目录里都是关于设备的特殊文件,但看起来它们与普通的目录没有什么两样。 如下: $ ls -l /dev ...

brw-rw--- 1 root disk 22, 1 May 5 1998 hdc1 crw-rw--- 1 root daemon 6 0 May 5 1998 lp0

与普通文件有所不同是开头的“C” 和“B”, 即char 和 block的意思,即字符设备和块设备。再后面的“22,1” 和“6,0”即设备的主设备号和次设备号,主设备号表明它是哪一种设备,这与你在Windows里添加硬件时看到的那些是一个意思。次设备号表明是哪一个 具体的设备。内核对这个设备名称不怎么关心,它只关心它的类型和主设备号。 这里提一下如何创建这些特殊文件: mknod name type major minor ,但你必须是root用户。

下面讲一下用户空间与内核空间:

Linux运行在两种模式下,一种是内核模式,也称为超级用户模式;别一种是用户模式。 Intel的X86(X>3),把自己的执行模式命名为Ring(环)0、1、2、3, 第0环的优先级是最高的。在Linux里,第0环代表内核模式,第3环代表用户执行模式,其余两环是没有使用。所以,如果你有用户模式下访问硬件与I/O 是不可能的。 内核模块与内核进行链接,在使用的它们自己的向外提供的函数方面也是有限制的。一个做为内核模块编写出来的设备驱动程序的运行并不是普通意义上的运行,模 块中的符号是在它被加载到内核里去的时候得到解析的。 编写内核的也是注意原则的:

(1) 不要使用浮点运算。内核在切换处理器执行模式时不保存它的FP状态,如果你要使

用的话,你就要自己保存FP状态。 但通常也没有什么理由要用浮点数的。

(2) 在驱动程序里不要进行繁忙的等待;用户空间里的一个应用程序永远也不可能完全独占CPU;但内核里的一个用时1秒的循环看上去也会把系统挂起很长的时间,而且期间什么也不能做的;

(3) 要谦虚,不要自以为是,在内核里增加一条打印语句都会把程序搞坏。 功能取舍

原则就是:只要能在用户空间里编程实现的东西,就绝不要放到内核空间里! 代码有错时,用户空间的错误会输出内存映象,而内核空间里的错误则可能会完全挂起。 建立模块

gcc -D__KERNEL__ -D__SMP__ -DMODULE -DMODVERSIONS -I/usr/src/linux/include -Wall -O2 -o module.o module.c

__KERNEL__: 内核本身用的东西,没有多少好讲的 __SMP__: (Symmetric Multi Processor) 对称多处理器专用品; MODULE: 告诉系统编译成内核模块;

MODVERSIONS:检查内核与模块之间的不兼容性,必须包含这个#include 如下解释,O2是告诉gcc编译期间时行几遍优化, -I是Include路径,如:-I/usr/src/linux, 但如这样的话,#include 实际是指:/usr/src/linux/include/linux/module.h这个文件。 还有注意的就是:名字的空间,在做内核时开发时,一不要把全局性的内核名字空间弄乱。 在导出函数的名字之前加上驱动程序的名字是一个避免出现名称冲突现象的好方法,另一个好方法就是只导出将会被其它驱动程序用到的函数和变量。 把全局变量和函数声明为静态变量和静态函数也可以,但也会有些副作用。其它变量和函数的导出要明确地EXPORT_SYMBOL宏命令来进行,它会把它们 添加到内核的全局符号表里去。一般说来,只有在准备把驱动程序分为几个模块或者准备暴露驱动程序的内部细节以做它用的情况下才需要考虑这一问题。 但还是要把全局名字空间的“污染”降低到最小程度。有关赛马场命令的语法定义如下: EXPORT_SYMBOL(name) 导出代表变量或者函数的符号name;

EXPORT_SYMBOL_NOVERS(name) 导出代表变量或者函数的符号name,但不加上模块版本检查后缀

——即使定义了也不加 EXPORT_NO_SYMBOLS 不导出任何符号 这些是定义在#include文件中。

数据类型

Linux定义了一些标准类型,它们可以在各种平台上的尺寸长度都是一致的如: __u8, ...... __u64 字符到64位长度之间带正负号和不带正负号的变量 __s8, ...... __s64

还有如下的类型:

ssize_t schar_read(...,size_t count, loff_t *offset)

这些数据类型都定义在 linux/types.h 和 asm/posix_types.h

下面,我们温习一个例子来结束今天的东西: #include

#if defined(CONFIG_SMP) #define __SMP__ #endif

#if defined(CONFIG_MODVERSIONS) #define MODVERSIONS #include #endif

#include int init_module(void) {

printk(KERN_DEBUG \ return 0; }

void cleanup_module(void) {

printk(KERN_DEBUG \ }

Printk语句中的KERN_DEBUG的作用就是设置被打印消息的优先级,优先级定义在linux/kernel.h这个文件中。用命令: gcc -D__KERNEL__ -I/usr/src/linux/include -DMODULE -Wall -O2 -c hello.c -o hello.o编译,用如下命令挂载: insmod hello.o dmesg | tail -n1 rmmod hello.o lsmod

这里主要讲一下字符设备,字符设备

字符设备必须向内核注册自己,让内核知道“它”能做什么;向内核提供一些必要的信息使它能够在有应用程序希望与这个设备互动时用正确的函数进行处理,而register_chrdev就是这干这个活的,如下:

int register_chrdev(unsigned int major, const char *name,struct file_operations *fops)

它在失败进返回一个负值,成功就返回一个非负值,即它返回的major的值,如果这个函数把major设置为0,内核将给这个设备动动态分配一个主编号。 这个使用不是困难的,但如果你在上次加载过侬这个模块后主编号又发生了变化,你这次就不得不创建一个正确的特殊文件才能对设备进行访问。但主编号42和 120-127是为本地设备预留的,成品模块是不使用这几个号码的。可以参考Documentation/device.txt文件。

第二个参数是name,只有一个用途就是在/proc/devicess进行注册。即抽象出来的与设备进行互动的参数;最后一个参数是最 有意义的,它定义了设备与外界互动的方式方法;特别是规定哪些功能由它自己来负责完成,又有哪些些功能需要由内核的缺省函数来完成。 可以参考 linux/fs.h,由一系列的指针来完成。 文件操作

对设备的访问需要文件系统的特殊文件,这样的话,设备驱动程序需要注册一组文件操作,这些文件操作定义了设备提供的特定功能。下面是所有的最新的构架定义,但很少设备需要定义所有的函数。

struct file_operations{

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *,char *, size_t, loff_t *); ssize_t (*write) (struct file *,const char *, size_t, loff_t *); int (*readdir) (struct file *,void * , filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int , unsigned long); int (*mmap) (struct file *,struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *);

int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct entry *); int (*fasync) (int , struct file * , int); int (*check_media_change) (Kdev_t dev); int (*revalidate) (Kdev_t dev);

int (*lock) (struct file *, int , struct file_lock*); }

解释一下:

llseek: llseek对应着用户空间里的lseek,它的作用是改变文件结构中的操作位置,说的明确一些就是修改file->f_pos;成功时,它返回一个新位置,失败时返回一个负数值;

read: read是从应用程序的角度看的说法,所以read实际上就是把数据写到用户空间,如果返回一个正数,就是实际读到的字节数;负数返回值表示错误;

write:write的作用是向设备馈送数据,返回值方面与read相同;

readdir:readdir只有文件系统才能使用,它的作用是将读取某个子目录里的内容;

poll: poll允许应用程序响应来自设备的给定事件。它在BSD UNIX里的对应函数是select, 但Linux不推荐使用select,所以 应该用poll代替它;

ioctl: 它的含义是I/O控制,它允许应许程序通过ioctl系统调用控制设备的行为或者从设备取得数据;

mmap:mmap实现了设备地址空间到用户空间的地址的映射。它可以用来提供对设备驱动程序的内部缓冲区或者外设内存空间进行直接访问的功能

Open:Open是应用程序打开设备时将要调用的文件操作。它是唯一一个对字符设备与块设备都有缺省实现的函数。因此,如果你不需要或不想知道设备会在何时被打开,就可以不对这个文件操作进行定义

flush:flush的作用是把缓冲区数据“冲”出去。由于字符设备不使用缓冲区,所以这个条目只对块设备有意义

release:release是在设备关闭时将被调用的文件操作

fsync: fsync的作用是同步内存中与磁盘上的数据状态,把输出缓冲区里尚未写到磁盘上去的数据写出去。它在结束操作之前是不应该返回的。这个条目也只有与块设备有关;

fasync:fasync将在应用程序通过fcntl改变设备行为时调用;

check_media_change: check_media_change检查自从上次访问之后介质是否经过了更换。因此它只对处理可更换介质(比如:CD-ROM和软盘)的块设备有意义;

revalidate:revalidate和check_media_change是密切相关的。如果检测出更换了盘算,就需要调用revalidate来更新设备内部的信息。revalidate也是只对可更换介质的块设备有意义;

lock:lock使用户可以锁定一个文件。它也是只对文件系统有意义

任何设备都不太可能用到上一节所描述的那些方法。你只需要定义那些你用到的函数,不用的全部置NULL,当字符设备注册时,设备的 file_operation结构和设备的名字将添加到一个全局性的chrdevs数组里去,这个数组是由一些device_struct结构组成的,数 组的下标就是设备的主编号,这个数组被字符设备切换表。device_struct结构的定义如下所示:

struct device_struct{

const char *name; struct file_operations *fops; },

这样,通过查看chrdev[YOUR_MAJOR]->fops,内核就知道如何与设备进行交谈以及设备都支持那些入口点;

下面就写一个例子: // forward declarations for _fops

static ssize_t schar_read(struct file *file, char *buf, size_t count, loff_t *offset);

static ssize_t schar_write(struct file *file, const *buf, size_t count, loff_t *offset);

static unsigned int schar_poll (struct file *file, poll_table *wait)

static int schar_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);

static int schar_mmap(struct file *file, struct vm_area_area_struct *vma);

static int schar_open(struct inode *inode, struct file *file); static int schar_release(struct inode *inode, struct file *file);

static struct file_operations schar_fops = {

NULL, //llseek

schar_read, schar_write, NULL, //readdir schar_poll, schar_ioctl, NULL, //mmap schar_open, NULL, //flush schar_release, NULL, //fsync NULL, //fasync NULL, //lock }

这段定义,可以个人风格,放在相应的地方。 下面定义一些简便方式: #define DEBUG

#ifndef DEBUG

#define MSG(string, args...) printk(KERN_DEBUG \ #else

#define MSG(string, args) #endif //定义主设备号

#define SCHAR_MAJOR 42

//程序的注册定义入口点 int init_module(void) { int res;

if(schar_name ==NULL) schar_name= \

//register device with kernel

res = register_chrdev(SCHAR_MAJOR, schar_name, &schar_fops);

if(res){

MSG(\ return res; } }

//若注册没有错误,你就会在/proc/devices的字符设备里能看到这个设备名称了

下面稍微讲一下,模块的使用计数

内核需要记录加载到系统里的每一个模块的使用情况,如果不是这样的话,这统无法知道什么卸载一个模块是安全的,如果你正在往硬盘中写数据,突然去掉硬盘的驱动程序的话,什么后果会发生呢? 我想你会知道它的厉害的!!!

修改模块需要用到两个宏命令,MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT,一个加一,一个是减一;维护模块使用 计数器是由程序员来完成的,即要保证模块不会被意外加载,也不要让模块在使用过程中被卸载, 用MOD_IN_USE宏命令可以救出模块当前的使用计数, 但这也没有什么是必要的,因为内核在尝试卸载一个模块时,系统都会对这个数字进行检查的。

OPEN和RELEASE:设备的打开与关闭

现在模块已经被加载,它会在系统上等等有人进程来打开与之关联的设备。当设备被一个进程打开的时候,schar_open就会被调用。模块的使用计数就是在这里得到增加的,如下代码: static int schar_open(struct inode *inode, struct file *file) {

//increment usage count MOD_INC_USE_COUNT;

//这样可以保证模块不被意外卸载,会返回一个“-EBUSY”错误。

//传递给schar_open的file参数是内核对返回给应用程序的文件描述符的一个内部描述。file结构里有关设备打开模式的信息,如下面代码,测试是否以读方式打开 if(file->f_mode & FMODE_READ){ MSG(*open for reading\\n\ ...

file结构里还有一个读操作发生位置等其他信息。下面给出的是该结构与我们这个模块有关的项

struct file{ ....

mode_t f_mode; loff_t f_pos; unsigned int f_flags;

... };

f_mode是表示的打开模式,可是读、写或者是读写。f_flags提供了更多的信息,其中有一些是与设备模块有关的,比如 O_NONBLOCK或O_NDELAY等。 这些标志是从用户空间应用程序传递给OPEN调用的,它们控制着设备在读和写方面的操作行为。所有有关资料 都可以在ASM/FCNTL.H文件里查到,f_pos是将要进行读写操作的下一个位置。

schar_open函数,下一步是设置定时器,同时schar_open最后会把打开的主、辅编号打印出来, 这两个编号是从传递给它的 inode那里提取出来的。inode它标识出磁盘或者内在中的文件,里面有属主、访问时间、长度、文件系统类型等许多有用信息。stat系统调用可以把 inode的信息提取出来。如果对一个文件执行stat命令,就可以标致诸如这个inode提供什么数据之类的许多线索(stat命令是stat系统调用 的内核外对应事物)。详细资料可以查阅linux/fs.h文件——文件注意文件系统的类型是如何在一个union类型的数据结构u里被定义的。Linux支持许多种文件系统!!!

schar_release除了将使用计数减1之外没有其它事情可做。也没有什么事情需要它做,这是因为schar设备在每次被打开时即不保有通过malloc分配到内在,也没有需要刷新的其他状态信息。

static int schar_release (struct inode *inode, struct file *file) {

MOD_DEC_USE_COUNT; MSG(\ return; }

文件操作read:从设备读出数据

schar_read函数从设备读出数据(实际上它是从内部数据队列里读到的),然后把数据传送到用户空间里去。schar用一个全局变量对数据队列 进行记录;当有数据可用的时候,变量schar_pool里的保存的是设备可以供应的数据字节数。这就是说如果在调用函数schar_read的时候变量 schar_pool是零或者一个负数,读进程就必须转入非活跃状态直到有数据可读为止。

static ssize_t schar_read(struct file *file, char *buf, size_t count, loff_t *offset) {

//if less date than requested is here, put process ot sleep while (count> schar_pool){

MSG(\

// go to sleep, but wake up on signals interruptible_sleep_on(&schar_wq);

if(signal_pending(current)){

MSG(\ //tell vfs about the signal retun -EINTR; } }

//copy the data from our buffer if(copy_to_user(buf,schar_buffer,count); return _EFAULT;

schar_pool -= count; schar.data read += count;

MSG(\

//return data written file->f_pos+= count; return count; }

current任务

在昨天schar_read函数里的好几个地方,我们都用到了current宏定义,这个宏定义代表当前运行中的进程,是一个任务结构的数据项。 也就是说,我们在schar_read里处理的那个叫做bcurrent的东西代表的就是正在读设备操作的当前进程。 任务结构有许多元素——它完整的清单可以在linux/sched.h头里看到,下面我只列出与这个例子有关的项: struct task_struct{ ....

int sigpending; .... pid_t pid; ....

sigset_t signal, blocked; ....... }

正如看到的,系统里的所有的任务其实是链接在一个双向列表里的。state给出的是进程的当前状

态,即它是正在运行、被停止、还是正在被切换。pid是该 进程的程序标识码。signal保存着与发送给该进程的一切信号有关的信息,而blocked是进程本身决定屏蔽的信号掩码。最后,sigpending 保存着与是否有一个非阻塞信号发送给了该里程有关的信息,signal_pending函数检查的就是这个变量。因此,如果signal_pending 返回的真值,我们就通知VFS让它重新开始数据传输过程。 等待队列

没有数据可读的时候,我们利用等待队列把current任务设置为休眠状态,当有新的数据拷贝到schar设备时,我们再它唤醒。这就解放了系 统,使它们可以去运行其它的进程。操作系统的进程调度器在我们唤醒任务之前是不会去考虑它的,而我们只有在使它休眠的条件得到了满足的前提下才会去唤醒 它。这使内核代码对那些访问自己的用户空间应用程序拥有了极大的控制权。 schar_read就使用了这个技术,下面看一下等待队列的数据结构定义:

struct wait_queue{ struct task_struct *task; struct wait_queue *next; }

这个结构比较明显,就不多讲了,task元素把被“催眠”的进程的有关信息保存在一个任务结构中, 而next是指向一等待队列中下一个条目的指针。如何让进程休眠呢,这就需要下面两条命令。

void interruptible_sleep_on(struct wait_queue **P)

long interruptilble_sleep_on_timeout(struct wait_queue **p, long timeout)

这两个宏命令的作用是把进程“催眠”为某个状态,但它们允许被信号唤醒。_timeout变体在内部调用了schedule_timeout,使作为其调用参数的等待队列能够让进程在时间到了的时候自动苏醒。这需要以后讲到的如何设定倒计时时间的。 void sleep_on(struct wait_queue **P)

long sleep_on_timeout(struct wait_queue **p, long timeout)

这两个宏定义的语法与上面的两个函数是完全一样的,只不过,进程的休眠状态被设置成为TASK_INTERRUPTIBLE. 可以在kernel/sched.c里查到它们 唤醒进程也是比较简单的,也不外乎下面两种方式: wake_up_interruptible(struct wait_queue **p) wake_up(struct wait_queue **P)

这两个由_wake_up延伸两个宏定义。前一个只能唤醒可中断休眠进程,而后一个可以唤醒两种休眠进程。但明确地被停止了执行(比如发送一个SIGSTOP信号)的进程不会被唤醒。 当schar设备里没有数据可提供时,它会把读数据进程催眠在自己的等待队列里。而当有一个写数据进程提供了足够多的数据,从而能够满足进程请求时,休眠进程将被 一个定时器唤醒。

interruptible_sleep_on(&schar_wq); if(signal_pending(current)) return -EINTR;

这个结构在内核的许多地方都可以看到。我们把current进程催眠了,但允许它在信号的触发下苏醒过来。在 interruptible_sleep_on成功之后,进程或者因为wake_up_interruptible调用而被唤醒,或者因为接收到一个信号 而苏醒。如果后一种情况,signal_pending先要返回“1”,我们就会以利用返回一个“-EINTR”错误的办法来激发 一个中断调用,VFS会根据这个中断重新启动读数据进程。如果我们使用sleep_on函数简单地催眠了进程,进程就会进入不可中断的休眠以等待数据;在 这种情况下,即使是一个SIGKILL信号了清能消除它。

就像我们前面介绍的那样,在定时器处理器和schar_write两个地方分别唤醒相应的读数据进程。

wake_up_interruptible(&schar_wq);

这个调用会把休眠在队列中的所有读数据进程都唤醒,这样做是否合理要看具体情况而定。 但这里我们只能满足一个读进程,所以我们是否应该唤醒所有的读进程让它们竞争数据呢? Schar设备可以通过设备的每次打开分别建立一个等待队列的办法来解决空上问题。这样做并不涉及什么新概念,只是让设备可以拥有自己的数据。 文件操作write:向设备写入数据

相对而言,schar_write就简单多了,它给schar_pool增加count个字节,再修改file->f_pos的值以反映出有 多少数据被写到设备(实际上是读 到设备的内部缓冲区里)。之所以说它比读 操作简单的多就是因为schar不需要对写到设备里来的这些数据进行处理,它只要照单全收就可以了。 除此之外,那就是唤醒读数据的进程,如下面代码:

static ssize_t schar_write(struct file *file, const char *buf) {

schar_pool +=count; schar_data_written +=count;

//if we were really writing- modify the file position to reflect the amoun of data written file->f_position += count;

if(copy_from_user(schar_buffer, buf,count)) return -EFAULT;

//wake up reading process, if any if(schar_pool>0) {

wake_up_interruptible(&schar_wq);

wake_up_interruptible(&schar_poll_read); }

MSG(\

//return the data written retun count; }

非阻塞性读操作

提供数据服务的驱动程序必须区分阻塞和非阻塞两种打开方式。如果没有足够的数据满足一个请求,我们通常的做法就是使进程进入休眠状态,而一旦有了足 够数据,就再把它唤醒。但如果设备是以非阻塞方式打开 的,我们就不能像刚才那样做了; 我们必须尽可能多的供应数据,而进程在没有数据可读的时候会立刻返回而不是进入休眠状态。 给schar_read函数加上下面阴影部分的代码就实现了非阻塞读操作了;

static ssize_t schar_read(struct file *file, char *buf, size-t count, loft_t *offset) ...

while(count> schar_pool){

//if the device is opened non blocking satisfy what we can of the request and don't put the process to sleep.

if (file->f_flags & O_NONBLOCK){ if(schar_pool>0){

file->f_pos += schar_pool;

if(copy_to_user(buf,schar_buffer,schar_pool)) return -EFAULT;

count = schar_length; schar_pool = 0; return count; }else{

return -EAGAIN; }

MSG(\%u to sleep\\n\ //go to sleep, but wake up on signals interruptible_sleep_on(&schar_wq); if(signal_pending(current)){

MSG(\ //tell vfs about the signal return -EINTR; } } } ... }

schar_read检查f_flag以确定设备当前是以什么方式打开的。如果应用程序请求的数据比我们数据池里的多,就先把里面有的返回给应用程序,等数据池空了时再返回一个“-EAGAIN”错误。 这就暗示读数据进程应该过一会再来试试自己的请求。

查找操作

Schar设备没有自己的查找函数,如果你在register_chrdev函数操作时llseek注册为NULL,那么就表明查找操作将调用内核里的缺 省实现来完成。 内核版本的查找操作可以在fs/read_write.c文件里找到,它的名字是default_llseek,这个调用提供了SEEK_SET、 SEEK_CUR和 SEEK_END三项功能。 这些与llseek用在一起的宏定义的作用是修改file->f_pos的值,即当前文件流里将被读取的下一个字节的位置。如果你想自己定义查找函 数,就必须编写相应的代码。 下面是一个典型的用法示范:

loff_t own_llseek(struct file *file, loff_t offset, int mode) {

switch(mode){

case 0: //seek_set file->f_pos = offset; return file->f_pos; case 1: //seek_SUR file->f_pos += offset; return file->f_pos; case 2: //seek_END return -EINVAL; default:

return -EINVAL; //cannot happen

如果设备没有查找操作在概念的话,就必须定义llseek来阻止查找,可以简单的返回一个\它的意思“查找操作非法”。

loff_t own_llseek(struct file *file, loff_t offset, int mode) {

//illegal seek return -ESPIPE; }

文件操作ioctl:I/O控制

有时候,也需要读取一个运行中驱动程序的参数是有用的,有些驱动程序它们在使用中没有间歇,也不可能把它们从系统里拿下来,重新配置,编译和运行它也就无从谈起。 而ioctl就是驱动程序里让我们能够在它运行时设置或者检索其有关设置情况的入口点。

内核里每个设备都有对应有一个独一无二(基本如此,但也两块硬盘这样的例外)的ioctl基地址和一级命令。举个例子,SCSI主控制器的 ioctl基地址是0X22,并且整个子范围0x00~0xff也都分配给了它。因为大多数设备并不需要支持多达256个命令,所以只用到了子的一小部 分。 16位的基地址构成ioctl命令的上半部分,而16位的设备命令就构成了ioctl命令的下半部分,因此,SCSI主控制器的第一个命令就是 0X2200.ioctl基地址都写在Documentation/ioctl-number.txt文档里,你可以自己仔细查看,查找一个没有使用的基地址。 如果你感兴趣的话,可以在asm/loctl.h文件里查找。

Linux可以区分四种类型的ioctl调用,它们是:直接命令、读、写、读和写。这就是由模块里标识符的写法定义。如下:

_IO(base, command) 定义了中选中命令。 在发出ioctl调用时没有需要传递进出应用程序的数据。

一个_IO类的ioctl调用将返回一个正整数(也就是说它不会被 解释为一个错误)

_IOR(base, command, size) 一个从应用程序角度来看的读操作ioctl命令。size是需要传回给应用程序的参

数的长度

_IOW(base,command,size) 一个应用程序角度看的写操作的ioctl命令。size是从应用程序传来的参数的长

_IOWR(base, command,size) 一个读写操作的ioctl命令。size是来回传递的参数的长度

此外,还有几个用来对待发送命令的合法性进行检查的宏定义,内核方面的编码机制把这个数据

域划分为方向,长度和命令几个部分。这些信息如下:

_IOC_DIR(command) command命令的方向。根据上面介绍的命令类型,这个方向可以是 _IOC_NONE,_IOC_WRITE or _IOC_READ。对_IOWR类的命令,这个方向值

是_IOC_WRITE与_IOC_READ的逻辑或者结果 _IOC_TYPE(command) 这个数据项的ioctl基地址部分,schar的是0xbb _IOC_NR(command) 这个数据项的设备命令部分 _IOC_SIZE(command) 参数的长度--如果有参数的话

ioctl函数本身是在模块注册时提供给内核的file_operations结构里的定义。我们把schar设备支持的ioctl命令定义在schar.h文件里 #define SCHAR_IOCTL_BASE 0xbb

#define SCHAR_TOGGLE_DEBUG _IO(SCHAR_IOCTL_BASE,0)

#define SCHAR_GET_POOL _IOR(SCHAR_IOCTL_BASE,1 , unsigned long) #define SCHAR_EX_TIMER_DELAY _IOWR(SCHAR_IOCTL_BASE,1,unsigned long)

选择基地址远离一切有用的设备,这样的话,就不会引起冲突。 在Schar里我们使用了三种类型的命令。SCHAR_TOGGLE_DEBUG切换打印或不打印调试信息, 因为它是一个_IO类型的ioctl命令,所以不带参数。SCHAR_GET_POOL返回数据池里现有数据的字节长度, SCHAR_EX_TIMER_DELAY设置定时器的每次延时的“jiffies”(内核时基)个数并返回原来的设置值。“jiffies”是内核用来 测量时间的一个基本单位。

schar_ioctl函数几乎完全由一个switch语句构成,设备支持的ioctl命令就在这个语句里得到处理。如下:

static int schar_ioctl(struct inode *inode, struct file *file, unsigned int cmd , unsigned long arg) {

//make sure that the command is really one of schar's if(_IOC_TYPE(cmd) !=SCHAR_IOCTL_BASE) return -ENOTTY;

switch(cmd)

case SCHAR_TOGGLE_DEBUG:{ schar_debug = !schar_debug; return 0; }

case SCHAR_GET_POOL:{

if(put_user(schar_pool, (unsigned long *) arg)) return -EFAULT; break;

case SCHAR_EX_TIMER_DELAY:{ unsigned long tmp = schar_timer_delay; if(!capable(CAP_SYS_ADMIN)) return -EACCES

if(get_user(schar_timer_delay, (unsigned long*) arg)) return -EFAULT;

if(put_user(tmp, (unsigned long*) arg)) return -EFAULT; break;}

default:{

MSG(\ return -ENOTTY; } }

//to keep gcc happy return 0; }

检查用户权限

只要用户有打开设备的权限,他就可以对该设备进行ioctl调用,但并不是每个用户都能使用全部命令,需要根据将要执行的具体操作来定。 SCHAR_EX_TIMER_DELAY命令需要检查打开设备的人是不是超级用户,因为如果倒计时的时间值设置得过低会使定时器很快就会结束计时,因而 使机器没有足够的时间来完成任何有意义的工作。linux定义了许多与用户个人权限有关的设置项目,最适合本例的是CAP_SYS_ADMIN。各种 CAP_类定义都可以在linux/capability.h里找到,里面还有对各种允许执行的操作的解释。 如下:

int capable(int cap);

capable的作用是检查用户的权限,如果用户拥有与参数cap对应的能力,它将返回“1”,否则返回“0”,这个用法并不仅仅局限于ioctl,它们在内核里使用的相当频繁。 文件操作poll:设备对进程的调度

文件操作poll指的是设备对应用程序进程的调度功能 , 它提供了一种让应用程序进程休眠的在设备上等待特定事件发生机制。请不要把它与对设备的反复检查混为一谈,这两者完全是两回事。使用poll系统调用是避 免繁忙循环却又能够 等待事件发生的有效手段。schar设备在一定程序 上实现了进程调度功能,正好适用我们的示例。poll功能的实现是很简单的。因为对schar设备的写操作总是成功的, 所以我们只需要检查读操作时的情况。这里引入了一个schar_poll_read等待队列。如果我们的数据池是空的,读数据进程就将休眠直到有足够的数 据可供读取为止。

static unsigned int schar_poll(struct file *file, poll_table *wait) {

unsigned int mask = 0;

poll_wait(file,&schar_poll_read,wait);

//if the pool contains data, a read will succeed if(schar_pool>0)

mask |= POLLIN| POLLRDNORM;

//a write always succeeds

mask |= POLLOUT | POLLWRNORM;

return mask; }

这就是schar设备里进程调度的全部工作!如果Schar设备的数据池长度还有一个上限,也很容易再给它加上一个schar_poll_write等待队列——只要对POLLOUT做类似的检查就可以做到。这在头文件里看到asm/poll.h里包含着名种可能的poll掩码,我们把其中一些标准的掩码如下所示:

POLLIN 设备可以非阻塞地向后续的读操作提供数据 POLLRDNOM

POLLOUT 设备可以非阻塞地从后续的写操作接受数据 POLLWRNORM

POLLERR 出现一个错误

曾经碰到过让你迷惑不解、类似于int * (* (*fp1) (int) ) [10];这样的变量声明吗?本 文将由易到难,一步一步教会你如何理解这种复杂的

C/C++声明:我们将从每天都能碰到的较简单的声明入手,然后逐步加入const修饰符和 typedef,还有函数指针,最后介绍一个能够让你准确地

理解任何C/C++声明的“右左法则”。需要强调一下的是,复杂的C/C++声明并不是好的编程 风格;我这里仅仅是教你如何去理解这些声明。注

意:为了保证能够在同一行上显示代码和相关注释,本文最好在至少1024x768分辨率的显示 器上阅读。 基础

让我们从一个非常简单的例子开始,如下: int n;

这个应该被理解为“declare n as an int”(n是一个int型的变量)。

接下去来看一下指针变量,如下: int *p;

这个应该被理解为“declare p as an int *”(p是一个int *型的变量),或者说p是一个 指向一个int型变量的指针。我想在这里展开讨论一

下:我觉得在声明一个指针(或引用)类型的变量时,最好将*(或&)写在紧靠变量之前, 而不是紧跟基本类型之后。这样可以避免一些理解

上的误区,比如:

int* p,q;

第一眼看去,好像是p和q都是int*类型的,但事实上,只有p是一个指针,而q是一个最简单 的int型变量。

我们还是继续我们前面的话题,再来看一个指针的指针的例子:

char **argv;

理论上,对于指针的级数没有限制,你可以定义一个浮点类型变量的指针的指针的指针的指 针...

再来看如下的声明:

int RollNum[30][4]; int (*p)[4]=RollNum; int *q[5];

这里,p被声明为一个指向一个4元素(int类型)数组的指针,而q被声明为一个包含5个元 素(int类型的指针)的数组。

另外,我们还可以在同一个声明中混合实用*和&,如下:

int **p1; // p1 is a pointer to a pointer to an int. int *&p2; // p2 is a reference to a pointer to an int. int &*p3; // ERROR: Pointer to a reference is illegal. int &&p4; // ERROR: Reference to a reference is illegal.

注:p1是一个int类型的指针的指针;p2是一个int类型的指针的引用;p3是一个int类型引 用的指针(不合法!);p4是一个int类型引用的引

用(不合法!)。

const修饰符

当你想阻止一个变量被改变,可能会用到const关键字。在你给一个变量加上const修饰符的 同时,通常需要对它进行初始化,因为以后的任何

时候你将没有机会再去改变它。例如:

const int n=5;

int const m=10;

上述两个变量n和m其实是同一种类型的--都是const int(整形恒量)。因为C++标准规定, const关键字放在类型或变量名之前等价的。我个人

更喜欢第一种声明方式,因为它更突出了const修饰符的作用。

当const与指针一起使用时,容易让人感到迷惑。例如,我们来看一下下面的p和q的声明:

const int *p; int const *q;

他们当中哪一个代表const int类型的指针(const直接修饰int),哪一个代表int类型的 const指针(const直接修饰指针)?实际上,p和q都

被声明为const int类型的指针。而int类型的const指针应该这样声明:

int * const r= &n; // n has been declared as an int

这里,p和q都是指向const int类型的指针,也就是说,你在以后的程序里不能改变*p的 值。而r是一个const指针,它在声明的时候被初始化指

向变量n(即r=&n;)之后,r的值将不再允许被改变(但*r的值可以改变)。

组合上述两种const修饰的情况,我们来声明一个指向const int类型的const指针,如下:

const int * const p=&n // n has been declared as const int //------------------------------------------------------------------------

下面给出的一些关于const的声明,将帮助你彻底理清const的用法。不过请注意,下面的一 些声明是不能被编译通过的,因为他们需要在声明

的同时进行初始化。为了简洁起见,我忽略了初始化部分;因为加入初始化代码的话,下面 每个声明都将增加两行代码。

char ** p1; // pointer to pointer to char const char **p2; // pointer to pointer to const char

char * const * p3; // pointer to const pointer to char const char * const * p4; // pointer to const pointer to const char char ** const p5; // const pointer to pointer to char const char ** const p6; // const pointer to pointer to const char char * const * const p7; // const pointer to const pointer to char const char * const * const p8; // const pointer to const pointer to const char 注:

p1是指向char类型的指针的指针; p2是指向const char类型的指针的指针; p3是指向char类型的const指针; p4是指向const char类型的const指针; p5是指向char类型的指针的const指针; p6是指向const char类型的指针的const指针; p7是指向char类型const指针的const指针; p8是指向const char类型的const指针的const指针。 //------------------------------------------------------------------------ typedef的妙用

typedef给你一种方式来克服“*只适合于变量而不适合于类型”的弊端。你可以如下使用 typedef:

typedef char * PCHAR; PCHAR p,q;

这里的p和q都被声明为指针。(如果不使用typedef,q将被声明为一个char变量,这跟我们 的第一眼感觉不太一致!)下面有一些使用typedef

的声明,并且给出了解释:

typedef char * a; // a is a pointer to a char

typedef a b(); // b is a function that returns // a pointer to a char

typedef b *c; // c is a pointer to a function // that returns a pointer to a char

typedef c d(); // d is a function returning // a pointer to a function // that returns a pointer to a char

typedef d *e; // e is a pointer to a function // returning a pointer to a // function that returns a // pointer to a char

e var[10]; // var is an array of 10 pointers to // functions returning pointers to // functions returning pointers to chars.

typedef经常用在一个结构声明之前,如下。这样,当创建结构变量的时候,允许你不使用 关键字struct(在C中,创建结构变量时要求使用str

uct关键字,如struct tagPOINT a;而在C++中,struct可以忽略,如tagPOINT b)。

typedef struct tagPOINT { int x; int y; }POINT;

POINT p; /* Valid C code */

//------------------------------------------------------------------------ 函数指针

函数指针可能是最容易引起理解上的困惑的声明。函数指针在DOS时代写TSR程序时用得最 多;在Win32和X-Windows时代,他们被用在需要回调

函数的场合。当然,还有其它很多地方需要用到函数指针:虚函数表,STL中的一些模板, Win NT/2K/XP系统服务等。让我们来看一个函数指针

的简单例子:

int (*p)(char);

这里p被声明为一个函数指针,这个函数带一个char类型的参数,并且有一个int类型的返回 值。另外,带有两个float类型参数、返回值是char

类型的指针的指针的函数指针可以声明如下:

char ** (*p)(float, float);

那么,带两个char类型的const指针参数、无返回值的函数指针又该如何声明呢?参考如 下:

void * (*a[5])(char * const, char * const);

“右左法则”[重要!!!]

The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you

encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it.

Continue till the whole declaration has been parsed.

这是一个简单的法则,但能让你准确理解所有的声明。这个法则运用如下:从最内部的括号 开始阅读声明,向右看,然后向左看。当你碰到一

个括号时就调转阅读的方向。括号内的所有内容都分析完毕就跳出括号的范围。这样继续, 直到整个声明都被分析完毕。

对上述“右左法则”做一个小小的修正:当你第一次开始阅读声明的时候,你必须从变量名 开始,而不是从最内部的括号。

下面结合例子来演示一下“右左法则”的使用。

int * (* (*fp1) (int) ) [10];

阅读步骤:

1. 从变量名开始 -------------------------------------------- fp1

2. 往右看,什么也没有,碰到了),因此往左看,碰到一个* ------ 一个指针 3. 跳出括号,碰到了(int) ----------------------------------- 一个带一个int参数的 函数

4. 向左看,发现一个* --------------------------------------- (函数)返回一个指 针

5. 跳出括号,向右看,碰到[10] ------------------------------ 一个10元素的数组 6. 向左看,发现一个* --------------------------------------- 指针 7. 向左看,发现int ----------------------------------------- int类型

总结:fp1被声明成为一个函数的指针,该函数返回指向指针数组的指针.

再来看一个例子:

int *( *( *arr[5])())();

阅读步骤:

1. 从变量名开始 -------------------------------------------- arr

2. 往右看,发现是一个数组 ---------------------------------- 一个5元素的数组 3. 向左看,发现一个* --------------------------------------- 指针

4. 跳出括号,向右看,发现() -------------------------------- 不带参数的函数 5. 向左看,碰到* ------------------------------------------- (函数)返回一个指 针

6. 跳出括号,向右发现() ------------------------------------ 不带参数的函数 7. 向左,发现* --------------------------------------------- (函数)返回一个指 针

8. 继续向左,发现int --------------------------------------- int类型

总结:??

还有更多的例子:

float ( * ( *b()) [] )(); // b is a function that returns a // pointer to an array of pointers // to functions returning floats.

void * ( *c) ( char, int (*)()); // c is a pointer to a function that takes

// two parameters: // a char and a pointer to a // function that takes no // parameters and returns // an int

// and returns a pointer to void.

void ** (*d) (int &,

char **(*)(char *, char **)); // d is a pointer to a function that takes // two parameters:

// a reference to an int and a pointer // to a function that takes two parameters: // a pointer to a char and a pointer // to a pointer to a char

// and returns a pointer to a pointer // to a char

// and returns a pointer to a pointer to void

float ( * ( * e[10])

(int &) ) [5]; // e is an array of 10 pointers to // functions that take a single // reference to an int as an argument // and return pointers to // an array of 5 floats.