前言
Linux是Unix操作系统的变体。Linux下写驱动的原理和思路和其他Unix系统完全类似,但是dos或window环境下的驱动就大不相同了。在Linux环境下设计驱动程序,
思路简单,操作方便,功能强大,但配套功能很少,只能依靠内核中的功能。一些常用的操作需要自己写,调试不方便。在过去的几周里,我为我的实验室开发的一个多媒体卡编写了一个驱动程序,获得了一些经验。
我想和Linux迷们分享一下。有不对的地方请指正。
The following text mainly comes from Johnson Kahr's writing linux device drivers, Brennan's inline assembly guide,
Linux A-Z,还有清华BBS上一些关于设备驱动的资料。这些材料有些是过时的,有些是错误的。我根据自己的测试结果修改了它们。
First, the concept of Linux device driver
系统调用是操作系统内核与应用程序的接口,设备驱动是操作系统内核与机器硬件的接口。设备驱动程序为应用程序屏蔽了硬件的细节,因此硬件设备对于应用程序来说只是一个设备文件。
应用程序可以像普通文件一样操作硬件设备。设备驱动程序是内核的一部分,它执行以下功能:
1.初始化并释放设备。
2.将数据从内核传输到硬件,并从硬件读取数据。
3.将应用程序传输的数据读取到设备文件中,并将应用程序请求的数据发送回来。
4.检测和处理设备中的错误。
linux操作系统下的设备文件主要有两种类型,一种是字符设备,另一种是块设备。字符设备和块设备的主要区别在于,当向字符设备发出读/写请求时,实际的硬件I/O通常会立即发生,而块设备则不会。
它使用一块系统内存作为缓冲区。当用户进程对设备的请求能够满足用户的要求时,它返回所请求的数据。如果不能,它就调用请求函数进行实际的I/O操作。块设备主要是为磁盘等慢速设备设计的。
以免花费太多CPU时间等待。
如前所述,用户进程通过设备文件处理实际的硬件。每个设备文件都有其文件属性(c/b ),表明它是否是字符设备。此外,每个文件有两个设备号,第一个是主设备号,用于标识驱动程序,第二个是从设备号。
识别使用相同设备驱动程序的不同硬件设备。例如,如果有两张软盘,它们可以通过从属设备号来区分。设备文件的主设备号必须与设备驱动程序注册时申请的主设备号一致,否则用户进程将无法访问驱动程序。
最后必须提到的是,当用户进程调用驱动时,系统进入核心状态,不再是抢占式调度。也就是说,在你驱动的子功能返回之前,系统无法做其他工作。如果你的驱动陷入无限循环,
可惜你得重启机器,然后会有很长的fsck。//呵呵
当读/写时,它首先查看缓冲区的内容。如果缓冲器中的数据
如何在Linux操作系统下编写设备驱动程序
二、案例分析
让我们写一个简单的字符设备驱动程序。虽然它什么都不做,但是它能理解Linux设备驱动的工作原理。将下面的C代码输入机器,你将得到一个真正的设备驱动程序。
不过我的内核是2.0.34,在内核的较低版本上可能会有问题,这个我还没测试过。//西西
#define __NO_VERSION__
#include linux/modules.h
#include linux/version.h
char kernel_version []=UTS_RELEASE;
这一段定义了一些版本信息,不是很有用,但也是必不可少的。Johnsonm说所有的驱动一开始就应该包含linux/config.h,但我不这么认为。
由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如open,read,write,close…, 注意,不是fopen, fread,
但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:
struct file_operations {
int (*seek) (struct inode * ,struct file *, off_t ,int);
int (*read) (struct inode * ,struct file *, char ,int);
int (*write) (struct inode * ,struct file *, off_t ,int);
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
int (*select) (struct inode * ,struct file *, int ,select_table *);
int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
int (*open) (struct inode * ,struct file *);
int (*release) (struct inode * ,struct file *);
int (*fsync) (struct inode * ,struct file *);
int (*fasync) (struct inode * ,struct file *,int);
int (*check_media_change) (struct inode * ,struct file *);
int (*revalidate) (dev_t dev);
}
这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,
然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,
并填充file_operations的各个域。
相当简单,不是吗?
下面就开始写子程序。
#include linux/types.h
#include linux/fs.h
#include linux/mm.h
#include linux/errno.h
#include asm/segment.h
unsigned int test_major=0;
static int read_test(struct inode *node,struct file *file,
char *buf,int count)
{
int left;
if (verify_area(VERIFY_WRITE,buf,count)==-EFAULT )
return -EFAULT;
for(left=count ; left 0 ; left--)
{
__put_user(1,buf,1);
buf++;
}
return count;
}
这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1.buf 是read调用的一个参数。它是用户进程空间的一个地址。
但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考。
在向用户空间拷贝数据之前,必须验证buf是否可用。
这就用到函数verify_area.
static int write_tibet(struct inode *inode,struct file *file,
const char *buf,int count)
{
return count;
}
static int open_tibet(struct inode *inode,struct file *file )
{
MOD_INC_USE_COUNT;
return 0;
}
static void release_tibet(struct inode *inode,struct file *file )
{
MOD_DEC_USE_COUNT;
}
这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
struct file_operations test_fops={
NULL,
read_test,
write_test,
NULL, /* test_readdir */
NULL,
NULL, /* test_ioctl */
NULL, /* test_mmap */
open_test,
release_test, NULL, /* test_fsync */
NULL, /* test_fasync */
/* nothing more, fill with NULLs */
};
设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌