一.设计原理
Linux内核中的设备驱动程序是一组常驻内存的具有特权的共享库,是低级硬件处理例程。每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备.设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序.
Linux支持3种设备:字符设备、块设备和网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作.块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待.
一个典型的驱动程序,大体上可以分为这么几个部分:
1. 注册设备
在系统初启,或者模块加载时候,必须将设备登记到相应的设备数组,并返回设备的主驱动号。
2. 定义功能函数
对于每一个驱动函数来说,都有一些和此设备密切相关的功能函数。拿最常用的块设备或者字符设备来说,都存在着诸如 open()、read()这一类的操作。当系统调用这些调用时,将自动的使用驱动函数中特定的模块。来实现具体的操作。
3. 卸载设备
在不用这个设备时,可以将它卸载,主要是从/proc 中取消这个设备的特殊文件。
二.模块
linux中的大部分驱动程序,是以模块的形式编写的,可以像内核模块一样在需要的时候动态加载,不使用时卸载。
模块的加载方式有两种。首先一种是使用insmod命令手工加载模块。另外一种则是在需要时加载模块,如常用的mount命令。
通过rmmod命令来删除模块。
模块的实现机制:
对于每一个内核模块来说,必定包含两个函数:
int init_module() 这个函数在插入内核时启动,在内核中注册一定的功能函数。
int cleanup_module() 当内核模块卸载时,调用它将模块从内核中清除。
注册设备:register_chrdev(…)在init_module()中调用此函数用来注册设备。
卸载设备:unregister_chrdev(…)
在cleanup_module()中调用此函数用来卸载设备。
在实际应用中,为外部设备编写驱动程序是一件很复杂的工作,需要解决的问题很多,如注册设备、为设备分配端口、响应中断、对设备状态进行控制、防止缓冲区溢出等等,这里既有软件方面的,也有硬件方面的。在设计时没有具体的设备对象,只好将内存中的一段看成设备,并针对它编写驱动程序,因此程序实现的功能非常简单,不必考虑上述问题,设备的操作也只是对内存的读和写。
三.准备过程
由于之前我对设备驱动的编写完全不熟悉,所以最初我尝试编写了一个最简单的字符设备驱动,它只是实现了简单的读写操作。虽然这个驱动程序并没有什么实用价值,但是我通过它对一个驱动程序的编写,特别事字符设备驱动程序有一定的认识。
具体步骤如下:
1.这个设备驱动程序提供给文件系统的接口
struct file_operations {
struct module *owner;
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 dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
};
这是在fs.h头文件中定义的一个接口。我们编写驱动程序,实际上就是要通过系统调用来实现对设备的操作。在最初的简单字符设备驱动程序中,我们只使用了open(),release(),read(),write()四个系统调用。
2.头文件、宏定义和全局变量
一个典型的设备驱动程序一般都包含一个专用头文件,这个头文件中包含一些系统函数的声明、设备寄存器的地址、寄存器状态位和控制位的定义以及用于次设备驱动程序的全局变量的定义。
3.Open()函数
功能:无论一个进程何时试图去打开这个设备都会调用这个函数。
4.Release()函数
功能:当一个进程试图关闭这个设备特殊文件的时候调用这个函数
5.Read()函数
功能:当一个进程已经打开次设备文件以后并且试图去读它的时候调用这个函数
6.Write()函数
功能:当试图将数据写入这个设备文件的时候,这个函数被调用
7.模块的初始化和模块的卸载
主要事init_module()和 cleanup_module()函数的调用
具体源程序如下:
#include<linux/fs.h>
#include<linux/config.h>
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/version.h>
#include<linux/types.h>
#include<linux/errno.h>
#include<asm/segment.h>
#include<asm-i386/uaccess.h>
#define _NO_VERSION_
#define SUCCESS 0
#define DEVICE_NAME "char_dev"
#define BUF_LEN 50
MODULE_LICENSE("GPL");
char kernel_version[]=UTS_RELEASE;
static int Device_Open=0;
static int test_major=0;
static char Message[BUF_LEN]="this is a simple program about device driver.";
static char *Message_Ptr;
static int device_open(struct inode *ind,struct file *fip)
{
printk("<1>device open:%d,%d\n",ind->i_rdev>>8,ind->i_rdev&0xFF);
if(Device_Open)
return -EBUSY;
Device_Open++;
Message_Ptr=Message;
MOD_INC_USE_COUNT;
return SUCCESS;
}
static int device_release(struct inode *ind,struct file *fip)
{
printk("<1>device release:%d,%d.\n",ind->i_rdev>>8,ind->i_rdev&0xFF);
Device_Open--;
MOD_DEC_USE_COUNT;
return 0;
}
static ssize_t device_read(struct file *fip,char *buffer,
size_t length,loff_t *offset)
{
int bytes_read=0;
if(*Message_Ptr==0)
return 0;
printk("<1>The message is \"%s\"\n",Message);
while(length&&*Message_Ptr){
put_user(*(Message_Ptr++),buffer++);
length--;
bytes_read++;
}
return bytes_read;
}
static ssize_t device_write(struct file *fip,const char *buffer,
size_t length,loff_t *offset)
{
int i;
printk("<1>Befor write test,the message is \"%s\"\n",Message);
for(i=0;i<length&&i<BUF_LEN;i++)
get_user(Message[i],buffer+i);
printk("<1>After write test,the message is \"%s\"\n",Message);
Message_Ptr=Message;
return i;
}
struct file_operations fops={
NULL,
NULL,
read:device_read,
write:device_write,
NULL,
NULL,
NULL,
NULL,
open:device_open,
NULL,
release:device_release,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};
int init_module()
{
int result;
result=register_chrdev(0,DEVICE_NAME,&fops);
if(result<0){
printk("<1>%s device failed with %d\n","sorry,registering the chardev",
result);
return result;
}
printk("<1>%s The major device number is %d.\n",
"Registeration is a success.",result);
if(test_major==0) test_major=result;
return 0;
}
void cleanup_module()
{
int ret;
ret=unregister_chrdev(test_major,DEVICE_NAME);
printk("<1>The device %d was unregistered.\n",test_major);
if(ret<0)
printk("<1>Error in unregister_chrdev:%d\n",ret);
return;
}
编译过程如下:
gcc -O2 -D__KERNEL__ -DMODULE -I /usr/src/linux-2.4.20-8/include -o device.o -c device.c生成可加载模块device.o,然后将模块加载到内核中(insmod vprinter.o),运行cat /proc/devices,查得设备vprinter分得的主设备号是254。最后根据主设备号创建设备文件mknod /dev/vprinter c 254 0,至此设备驱动程序完成。写了一个简单的程序进行测试,先向设备中写入数据,然后读出数据,测试通过,写到系统日志里的信息为:
Registeration is a success. The major device number is 254.
device open:254,0
The message is " this is a simple program about device driver."
device release:254,0.
四.程序设计
有了上面的基础,我们开始了一个稍微复杂一些的驱动程序的设计。
该程序模拟一个打印机的等待队列,采用队列的数据结构。当向设备写数据时,将数据分成大小相等块,插入队列中。当从设备读取数据时,按指定数据长度计算所需的块数,在从设备的等待队列读出。
源程序如下所示:
#include<linux/fs.h>
#include<linux/config.h>
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/version.h>
#include<linux/types.h>
#include<linux/slab.h>
#include<linux/errno.h>
#include<linux/mm.h>
#include<asm/segment.h>
#include<asm-i386/uaccess.h>
#include<linux/init.h>
#define _NO_VERSION_
#define SUCCESS 0
#define DEVICE_NAME "vprinter" /*vprinter是设备名,它将出现在/proc/devices中*/
MODULE_LICENSE("GPL");
#define SIZE 5
char kernel_version[]=UTS_RELEASE;
static int Device_Open=0;
/*为了防止不同的进程在同一个时间使用此设备,定义此静态变量跟踪设备的状态*/
static int test_major=0; /*主设备号*/
typedef struct wait_q_node {
char buf[6];
struct wait_q_node *next;
}wait_q_node,*wait_q_ptr; /*等待队列每个节点的结构定义*/
wait_q_ptr f;
typedef struct{
wait_q_ptr head;
wait_q_ptr tail;
}LinkQueue; /*等待队列的头指针和尾指针*/
LinkQueue Q,*Q0;
static int vprinter_open(struct inode *ind,struct file *fip)
{
printk("<1>device open:%d,%d\n",ind->i_rdev>>8,ind->i_rdev&0xFF);
/*printk()是在内核代码中使用的与printf相似的函数。ind->i_rdev>>8是主设备号,ind->i_rdev&0xFF是次设备号*/
if(Device_Open==1)
return -EBUSY; /*设备正在使用中,返回-EBUSY(-1)*/
Device_Open++;
Q0=(LinkQueue *)kmalloc(sizeof(LinkQueue),GFP_KERNEL);
if(Q0==NULL) return(-1);
Q=*Q0;
Q.head=Q.tail=(wait_q_ptr)kmalloc(sizeof(wait_q_node),GFP_KERNEL);
if(Q.head==NULL) return(-1);
Q.head->next=NULL; /*构造一个空的等待队列*/
MOD_INC_USE_COUNT;
/*当这个设备文件被打开的时候,我们必须确认该模块还没有被移走并且增加此模块的用户数目(在移走一个模块的时候会根据这个数字来决定可否移去,如果不是0则表示还有进程正在使用这个模块,不能移走)*/
return SUCCESS; /*打开设备成功*/
}
static int vprinter_release(struct inode *ind,struct file *fip)
{
printk("<1>device release:%d,%d.\n",ind->i_rdev>>8,ind->i_rdev&0xFF);
Device_Open--; /*为了下一个使用这个设备的进程做准备*/
MOD_DEC_USE_COUNT;
/*减少这个模块使用者的数目,否则一旦你打开这个模块以后,你永远都不能释放掉它*/
kfree(Q.head);
kfree(Q0);
return 0;
}
static ssize_t vprinter_read(struct file *fip,
char *buffer,/*把读出的数据放到这个缓冲区*/
size_t length,/*读出数据的长度*/
loff_t *offset)/*文件中的偏移*/
{
int i=0,m,n;
m=length/SIZE;/*读取数据所需的块数*/
n=length%SIZE; /*非整数块时的字节数*/
/*从设备(即等待队列中)读取所需内容到缓冲区中*/
for(;m>0;m--,i+=SIZE,buffer+=i) {
copy_to_user(buffer++,Q.head->next->buf,SIZE);
/*把内核空间里的内容送到缓冲区,这是kernel提供的一个函数,用于向用户传送数据*/
printk("<1>The message is \"%s\"\n",Q.head->next->buf);
f=Q.head->next;
Q.head->next=Q.head->next->next;
kfree(f);
}
if(n) {
copy_to_user(buffer++,Q.head->next->buf,n);
printk("<1>The message is \"%s\"\n",Q.head->next->buf);
f=Q.head->next;
Q.head->next=Q.head->next->next;
kfree(f);
}
return length;
}
static ssize_t vprinter_write(struct file *fip,const char *buffer,
size_t length,loff_t *offset)
{
int i=0,m,n;
m=length/SIZE;
n=length%SIZE;
/*把缓冲区中的内容写到设备上(即添加到等待队列中)*/
for(;m>0;m--,i+=SIZE){
wait_q_ptr q=(wait_q_ptr)kmalloc(sizeof(wait_q_node),GFP_KERNEL);
if(q==NULL) return(-1);
/*把缓冲区里的内容写入内核*/
copy_from_user(q->buf,buffer+i,SIZE);
copy_from_user(&(q->buf[5]),&(buffer[20]),1);
Q.tail->next=q;
Q.tail=q;
}
if(!n){
wait_q_ptr q=(wait_q_ptr)kmalloc(sizeof(wait_q_node),GFP_KERNEL);
if(q==NULL) return(-1);
/*把缓冲区里的内容写入内核*/
copy_from_user(q->buf,buffer+i,n);
copy_from_user(&(q->buf[5]),&(buffer[20]),1);
printk("%s",q->buf);
Q.tail->next=q;
Q.tail=q;
}
if(Q.head!=Q.tail) {
wait_q_ptr p;
p=Q.head->next;
do{
printk("<1>Now the content of the queue is \"%s\"\n",p->buf); /*显示队列内容*/
p=p->next;
}while(p!=NULL);
}
return i;
}
struct file_operations fops={
NULL,
NULL,
read:vprinter_read,
write:vprinter_write,
NULL,
NULL,
NULL,
NULL,
open:vprinter_open,
NULL,
release:vprinter_release,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};
/*设备驱动程序提供给文件系统的接口.它的指针保存在设备表中,
在init_module()中被传递给操作系统.这个结构的每一个成员的名字都对应着
一个系统调用.用户进程利用系统调用在对设备文件进行诸如read/write操作时,
系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数
据结构相应的函数指针,接着把控制权交给该函数.这是linux的设备驱动程序工
作的基本原理.*/
int init_module()
{
int result;
result=register_chrdev(0,DEVICE_NAME,&fops);
if(result<0){
printk("<1>%s device failed with %d\n","sorry,registering the chardev",result);
return result;/*注册失败*/
}
printk("<1>%s The major device number is %d.\n","Registeration is a success.",result);
if(test_major==0) test_major=result;
return 0;
}
/*在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在
这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符
设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是
零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,
参数三用来登记驱动程序实际执行操作的函数的指针。如果登记成功,
返回设备的主设备号,不成功,返回一个负值。*/
void cleanup_module()
{
int ret;
ret=unregister_chrdev(test_major,DEVICE_NAME);/*卸载设备*/
printk("<1>The device %d was unregistered.\n",test_major);
if(ret<0)
printk("<1>Error in unregister_chrdev:%d\n",ret);
return;
}
/*在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备
在系统字符设备表中占有的表项,即从/proc中取消注册的设备特殊文件.
unregister_chrdev需要两个参数,参数一是获得的设备号,参数二是设备文件名.
*/
编译时使用命令gcc -O2 -D__KERNEL__ -DMODULE -I /usr/src/linux-2.4.20-8/include -o vprinter.o -c vprinter.c,生成可加载模块vprinter.o,然后将模块加载到内核中(insmod vprinter.o),运行cat /proc/devices,查得设备vprinter分得的主设备号是254。最后根据主设备号创建设备文件mknod /dev/vprinter c 254 0,至此设备驱动程序完成。写了一个简单的程序进行测试,先向设备中写入数据“This is a vitual pri“,将该数据分成大小相等的块,每块为5Byte,建立一个等待队列。然后将其读出并打印输出,测试通过,写到系统日志里的信息为:
Registeration is a success. The major device number is 254.
device open:254,0
Now the content of the queue is "This "
Now the content of the queue is "is a "
Now the content of the queue is "vitua"
Now the content of the queue is "l pri"
Now the content of the queue is ""
The message is "This "
The message is "is a "
The message is "vitua"
The message is "l pri"
device release:254,0.
测试程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>
#include <fcntl.h>
#include <linux/fs.h>
main()
{
int testdev=0;
int length;
char buf[21]="This is a vitual pri";
testdev = open("/dev/vprinter",O_RDWR);
printf("%d\n",testdev);
if(testdev == -1)
{
printf("Cann't open the device!\n");
exit(0);
}
write(testdev,buf,20);
read(testdev,buf,20);
close(testdev);
return testdev;
}
http://blog.chinaunix.net/space.php?uid=254237&do=blog&id=2458604