實驗?zāi)康模?br>通過一個簡單的設(shè)備驅(qū)動的實現(xiàn)過程。學(xué)會Linux中設(shè)備驅(qū)動程序的編寫
實驗內(nèi)容:
設(shè)計和實現(xiàn)一個虛擬命名管道(FIFO)的字符設(shè)備。寫一個模塊化的字符設(shè)備驅(qū)動程序
實驗提示:
一、設(shè)備的功能
設(shè)計和實現(xiàn)一個虛擬命名管道(FIFO)的字符設(shè)備。我們知道,管道是進程間通信的一種
方式:一個進程向管道中寫數(shù)據(jù),另一個進程從管道中讀取數(shù)據(jù),先寫入的數(shù)據(jù)先讀出。我
們的驅(qū)動程序要實現(xiàn)N(N=4)個管道,每個管道對應(yīng)兩個設(shè)備,次設(shè)備號是偶數(shù)的設(shè)備是只
寫設(shè)備,次設(shè)備號是奇數(shù)的是只讀設(shè)備。寫入設(shè)備i(i是偶數(shù))的字符可以從設(shè)備i+1讀出。
這樣,我們一共就需要2N 個次設(shè)備號。
我們的目標是寫一個模塊化的字符設(shè)備驅(qū)動程序。設(shè)備所使用的主設(shè)備號可以從尚未分
配的主設(shè)備號中任選一個,/Documentation/devices.txt 記錄了當(dāng)前版本內(nèi)核的主設(shè)備號分配
情況。如果設(shè)備文件系統(tǒng)(devfs)尚未激活,我們在加載模塊之后,還必須用mknod 命令創(chuàng)
建相應(yīng)的設(shè)備文件節(jié)點。
如果FIFO 的寫入端尚未打開,F(xiàn)IFO 中就不會有數(shù)據(jù)可讀,所以此時試圖從FIFO 中讀
取數(shù)據(jù)的進程應(yīng)該返回一個錯誤碼。如果寫入端已經(jīng)打開,為了保證對臨界區(qū)的互斥訪問,
調(diào)用讀操作的進程必須被阻塞。如果存在被阻塞的讀者,在寫操作完成后(或者關(guān)閉一個寫
設(shè)備時)必須喚醒它。
如果寫入的數(shù)據(jù)太多,超出了緩沖區(qū)中空閑塊的大小,調(diào)用寫操作的進程必須睡眠,以
等待緩沖區(qū)中有新的空閑塊。
二、設(shè)備的實現(xiàn)
1. 數(shù)據(jù)結(jié)構(gòu):
首先,我們要包含一些必要的頭文件、宏和全局變量。
vfifo.c
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif
#define __NO_VERSION__
#include<linux/config.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/malloc.h>
#include<linux/fs.h>
#include<linux/proc_fs.h>
#include<linux/errno.h>
#include<linux/types.h>
#include<linux/fcntl.h>
#include<linux/init.h>
#include<asm/system.h>
#include<asm/uaccess.h>
#ifndef VFIFO_MAJOR
#define VFIFO_MAJOR 241
#endif
#ifndef VFIFO_NR_DEVS
#define VFIFO_NR_DEVS 4
#endif
#ifndef VFIFO_BUFFER
#define VFIFO_BUFFER 4000
#endif
#include<linux/devfs_fs_kernel.h>
devfs_handle_t vfifo_devfs_dir;
struct file_operations vfifo_fops;
int vfifo_major=VFIFO_MAJOR;
int vfifo_nr_devs=VFIFO_NR_DEVS;
int vfifo_buffer=VFIFO_BUFFER;
MODULE_PARM(vfifo_major,"i");
MODULE_PARM(vfifo_nr_devs,"i");
MODULE_PARM(vfifo_buffer,"i");
MODULE_AUTHOR("EBUDDY");
每個實際的FIFO 設(shè)備都對應(yīng)于一個Vfifo_Dev{ }結(jié)構(gòu)體。其中,rdq 是阻塞讀的等待
隊列,wrq 是阻塞寫的等待隊列,base 是所分配緩沖區(qū)的起始地址,buffersize 是緩沖區(qū)的
大小,len表示管道中已有數(shù)據(jù)塊的長度,start 表示當(dāng)前應(yīng)該讀取的緩沖區(qū)位置相對于base
的偏移量,即緩沖區(qū)起始數(shù)據(jù)的偏移量,readers和writers分別表示VFIFO 設(shè)備當(dāng)前的讀者
個數(shù)和寫者個數(shù),sem是用于互斥訪問的信號量,r_handle和w_handle用于保存設(shè)備文件系
統(tǒng)的注冊句柄,r_handle對應(yīng)的是只讀設(shè)備,w_handle對應(yīng)的是同一管道的只寫設(shè)備。具體
的定義如下所示:
vfifo.c
typedef struct Vfifo_Dev{
wait_queue_head rdq,wrq;
char* base;
unsigned int buffersize;
unsigned int len;
unsigned int start;
unsigned int readers,writers;
struct semaphore sem;
devfs_handle_t r_handle,w_handle;
}Vfifo_Dev;
2.設(shè)備操作接口
(1).注冊與注銷
注冊時,我們必須考慮到兩種管理方式(傳統(tǒng)方式與devfs方式)的兼容性。在這里,
我們用條件編譯來解決這個問題。由于許多主設(shè)備號已經(jīng)靜態(tài)地分配給了公用設(shè)備,Linux
提供了動態(tài)分配機制以獲取空閑的主設(shè)備號。傳統(tǒng)方式下,如果調(diào)用devfs_register_chrdev( )
時的major 為零的話,它所調(diào)用的register_chrdev( )函數(shù)就會選擇一個空閑號碼作為返回值
返回。主設(shè)備號總是正的,因此不會和錯誤碼混淆。在devfs方式下,如果devfs_register( )
的flags 參數(shù)值為DEVFS_FL_AUTO_DEVNUM,注冊時就會自動生成設(shè)備號。
動態(tài)分配的缺點是:由于分配的主設(shè)備號不能保證總是一樣的,無法事先創(chuàng)建設(shè)備節(jié)點。
但是這并不是什么問題,因為一旦分配了設(shè)備號,我們就可以從/proc/devices 讀到。為了加
載一個設(shè)備驅(qū)動程序,我們可以用一個簡單的腳本替換對insmod的調(diào)用,它通過/proc/devices
獲得新分配的主設(shè)備號,并創(chuàng)建節(jié)點。加載動態(tài)分配主設(shè)備號驅(qū)動程序的腳本可以利用awk
這類工具從/proc/devices 中獲取信息,并在/dev中創(chuàng)建文件。在我們的實例程序中,為了簡
單起見,仍然使用靜態(tài)分配的主設(shè)備號。
你也許會注意到我們并沒有使用統(tǒng)一的函數(shù)名init_module( )和cleanup_module( ),這是
由于內(nèi)核編程風(fēng)格的變化。自從2.3.13 版的內(nèi)核以來,Linux提供了兩個宏module_init( )和
module_exit( )來顯式地命名模塊的注冊和注銷函數(shù)。通常在源文件的末尾寫上這兩個宏,例
如:
module_init(vfifo_init_module);
module_exit(vfifo_exit_module);
注意,在使用這兩個宏之前必須先包含頭文件<linux/init.h>。這樣做的好處是,內(nèi)核中
的每一個注冊和注銷函數(shù)都有一個唯一的名字,有助于調(diào)試。我們知道驅(qū)動程序既可以設(shè)計
成模塊,又可以靜態(tài)地編譯進內(nèi)核,用了這兩個宏后就能更方便地支持這兩種方式。實際上,
對于模塊來說,它們所做的工作僅僅是把給出的函數(shù)名重新命名為 init_module( )和
cleanup_module( )。當(dāng)然,如果你已使用了init_module( )和cleanup_module( )作為函數(shù)名,
那就沒必要再使用這兩個宏了。
在函數(shù)名之前,我們可以看到一個表示屬性的詞“__init”,加了這個屬性之后,系統(tǒng)
會在初始化完成之后丟棄初始化函數(shù),收回它所占用的內(nèi)存。這樣可以減小內(nèi)核所占用的內(nèi)
存空間。但它只對內(nèi)建的驅(qū)動程序有用,對于模塊則沒有影響。
char vfifoname[8];
static int __init vfifo_init_module(void)
{
int result,i;
SET_MODULE_OWNER(&vfifo_fops);
#ifdef CONFIG_DEVFS_FS
vfifo_devfs_dir=devfs_mk_dir(NULL,"vfifo",NULL);
if(!vfifo_devfs_dir)
return -EBUSY;
#endif
result=devfs_register_chrdev(vfifo_major,"vfifo",&vfifo_fops);
if(result<0){
printk(KERN_WARNING "vfifo: can‘t get major %d",vfifo_major);
return result;
}
if(vfifo_major==0)
vfifo_major=result;
vfifo_devices = kmalloc(vfifo_nr_devs*sizeof(Vfifo_Dev),GFP_KERNEL);
if(!vfifo_devices){
return -ENOMEM;
}
memset(vfifo_devices,0,vfifo_nr_devs*sizeof(Vfifo_Dev));
for(i=0;i<vfifo_nr_devs;i++){
init_waitqueue_head(&vfifo_devices[i].rdq);
init_waitqueue_head(&vfifo_devices[i].wrq);
sema_init(&vfifo_devices[i].sem,1);
#ifdef CONFIG_DEVFS_FS
sprintf(vfifoname,"vfifo%d",2*i);
vfifo_devices[i].w_handle=
devfs_register(vfifo_devfs_dir,vfifoname,
DEVFS_FL_NON,
vfifo_major,2*i,S_IFCHR|S_IRUGO|S_IWUGO,
&vfifo_fops,vfifo_device+i);
sprintf(vfifoname,"vfifo%d",2*i+1);
vfifo_devices[i].r_handle=
devfs_register(vfifo_devfs_dir,vfifoname,
DEVFS_FL_NON,
vfifo_major,2*i+1,S_IFCHR|S_IRUGO|S_IWUGO,
&vfifo_fops,vfifo_device+i);
if(!vfifo_devices[i].r_handle||!vfifo_devices[i].w_handle){
printk(KERN_WARNING "vfifo: can‘t register vfifo device nr %i\n",i);
}
#endif
}
#ifdef VFIFO_DEBUG
create_proc_read_entry("vfifo",0,NULL,vfifo_read_mem,NULL);
#endif
return 0;
注銷的工作相對簡單。需要注意的是在卸載驅(qū)動程序之后要刪除設(shè)備節(jié)點。如果設(shè)備節(jié)
點是在加載時創(chuàng)建的,可以寫一個簡單的腳本在卸載時刪除它們。如果動態(tài)節(jié)點沒有從/dev
中刪除,就可能造成不可預(yù)期的錯誤:系統(tǒng)可能會給另一個設(shè)備分配相同的主設(shè)備號,這樣
在打開設(shè)備時就會出錯。
我們可以看到在函數(shù)名前標有屬性“__exit”,它的作用類似于“__init”,即使內(nèi)建的
驅(qū)動程序忽略它所標記的函數(shù)。同樣的,它對模塊也沒有影響。
vfifo.c
static void __exit vfifo_cleanup_module(void)
{
int i;
devfs_unregister_chrdev(vfifo_major,"vfifo");
#ifdef VFIFO_DEBUG
remove_proc_entry("vfifo",NULL);
#endif
if(vfifo_devices){
for(i=0;i<vfifo_nr_devs;i++){
if(vfifo_devices[i].base)
kfree(vfifo_devices[i].base);
devfs_unregister(vfifo_devices[i].r_handle);
devfs_unregister(vfifo_devices[i].w_handle);
}
kfree(vfifo_devices);
devfs_unregister(vfifo_devfs_dir);
}
}
}
(2). 打開與釋放
打開設(shè)備主要是完成一些初始化工作,以及增加引用計數(shù),防止模塊在設(shè)備關(guān)閉前被注
銷。我們知道內(nèi)核用主設(shè)備號區(qū)分不同類型的設(shè)備,而驅(qū)動程序用次設(shè)備號識別具體的設(shè)備。
利用這一特性,我們可以用不同的方式打開同一個設(shè)備。
vfifo.c
static int vfifo_open(struct inode *inode,struct file *filp)
{
Vfifo_Dev *dev;
int num=MINOR(inode->i_rdev);
/*檢查讀寫權(quán)限是否合法 */
if((flip->f_mode&FMODE_READ)&&!(num%2)||(filp->f_mode&FMODE_WRITE)&&(num%2))
return -EPERM;
if(!filp->private_data){
if(num>=vfifo_nr_devs*2)
return -ENODEV;
dev=&vfifo_nr_devices[num/2];
filp->private_data=dev;
}
else{
dev=filp->private_data;
}
/*獲得互斥訪問的信號量 */
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
/*如果尚未分配緩沖區(qū),則分配并初始化 */
if(!dev->base){
dev->base=kmalloc(vfifo_buffer,GFP_KERNEL);
if(!dev->base){
up(&dev->sem);
return -ENOMEN;
}
dev->buffersize=vfifo_buffer;
dev->len=dev->start=0;
}
if(filp->mode&FMODE_READ)
dev->readers++;
if(filp->mode&FMODE_WRITE)
dev->writers++;
filp->private_data=dev;
MOD_INC_USE_COUNT;
return 0;
}
釋放(或關(guān)閉)設(shè)備就是打開設(shè)備的逆過程。
vfifo.c
static int vfifo_release(struct inode *inode,struct file *filp)
{
Vfifo_Dev *dev=filp->private_data;
/*獲得互斥訪問的信號量 */
down(&dev->sem);
if(filp->f_mode&FMODE_READ)
dev->readers--;
if(filp->f_mode&FMODE_WRITE){
dev->writes--;
wake_up_interruptible(&dev->sem);
}
if((dev->readers+dev->writers==0)&&(dev->len==0)){
kfree(dev->base);
dev->base=NULL;
}
up(&dev->sem);
MOD_DEC_USE_COUNT;
return 0;
}
poll方法.4
使用非阻塞型I/O 的應(yīng)用程序經(jīng)常要用到poll 和select 系統(tǒng)調(diào)用。poll 和select 本質(zhì)上
具有相同的功能:它們都允許一個進程決定它是否能無阻塞地從一個或多個打開的文件中讀
數(shù)據(jù),或者向這些文件中寫數(shù)據(jù)。這兩個系統(tǒng)調(diào)用還可用來實現(xiàn)在無阻塞情況下的不同源輸
入的多路復(fù)用。同樣的功能為什么要由兩個不同的函數(shù)提供呢?這是因為它們幾乎是在同一
時間由兩個不同的團體引入Unix 系統(tǒng)中的:BSD Unix引入了select,System V引入了poll。
在Linux 2.0 版本的內(nèi)核中只支持select,從2.1.23 版本的內(nèi)核開始,系統(tǒng)提供了對兩種調(diào)用
的支持。我們的驅(qū)動程序是基于poll系統(tǒng)調(diào)用,因為poll提供了比select更詳細的支持。
poll的實現(xiàn)可以執(zhí)行poll和select兩種系統(tǒng)調(diào)用,它的原型如下:
unsigned int (*poll) (struct file *,poll_table * )
驅(qū)動程序中的poll主要完成兩個任務(wù):
l。 在一個可能在將來喚醒它的等待隊列中將當(dāng)前進程排隊。通常,這意味著同時在輸
入和輸出隊列中對進程排隊。函數(shù)poll_wait( )就用于這個目的,其工作方式說
select_wait( )非常類似。
2。 構(gòu)造一個位掩碼描述設(shè)備的狀態(tài),并將其返回給調(diào)用者。這個位掩碼描述了能立即
被無阻塞執(zhí)行的操作。
這兩個操作通常是很簡單的,在每個驅(qū)動程序中的實現(xiàn)都非常相似。然而,它們依賴于
一些只有驅(qū)動程序才能提供的信息,因此必須在每個驅(qū)動程序中分別實現(xiàn)。
poll_table 結(jié)構(gòu)是在
這個頭文件。需要提醒的是,你無需了解它的內(nèi)部結(jié)構(gòu),你只要調(diào)用操作該結(jié)構(gòu)的函數(shù)就行
了。當(dāng)然,如果你想了解的話,你可以自己去看源代碼。
poll部分標志位的列表如下:
POLLIN 如果設(shè)備可以被無阻塞地讀,那么該位必須被設(shè)置。
POLLRDNORM 如果“普通”數(shù)據(jù)可以被讀,該位必須被設(shè)置。一個可讀設(shè)備返回
(POLLIN | POLLRDNORM)。
POLLOUT 如果設(shè)備可以被無阻塞地寫,則該位在返回值中被設(shè)置。
POLLWRNORM 該位與POLLOUT,有時甚至的確為同一個數(shù)。一個可寫的設(shè)備返回
(POLLOUT | POLLWRNORM)。
具體的poll實現(xiàn)代碼如下:
vfifo.c
unsigned int vfifo_poll(struct file *filp, poll_table *wait)
{
Vfifo_Dev *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->rdq, wait);
poll_wait(filp, &dev->wrq, wait);
if (dev->len > 0) mask |= POLLIN | POLLRDNORM; /* readable */
if (dev->len != dev->buffersize) mask |= POLLOUT | POLLWRNORM; /* writable */
return mask;
}
三、設(shè)備的安裝
采用下面的命令可以對vfifo.c進行編譯:
#gcc –c vfifo.c –D__KERNEL__ -DMODULE –O2 –g -Wall
如果沒有出錯的話,將會在本目錄下生成一個vfifo.o 文件。
下面的操作必須是以root身份進行的:
先執(zhí)行module的插入操作,
#insmod vfifo.o
如果設(shè)備文件系統(tǒng)已經(jīng)應(yīng)用起來的話,此時在設(shè)備文件系統(tǒng)掛接的目錄(通常是/dev)
下,就可以找到vfifo 文件節(jié)點了。如果沒有應(yīng)用設(shè)備文件系統(tǒng),則需要手工為設(shè)備添加文
件節(jié)點。首先進入dev目錄,再執(zhí)行如下命令:
[root
[root
……
[root
此時就可以對設(shè)備進行讀、寫、ioctl等操作了。
當(dāng)不再需要對設(shè)備進行操作時,可以采用下面的命令卸載module:
[root
四、設(shè)備的使用
設(shè)備安裝好之后就可以使用了。你可以用cp、dd等命令以及輸入/輸出重定向機制來測
試這個驅(qū)動程序。為了更清晰地了解程序是如何運行的,你可以在適當(dāng)?shù)奈恢眉尤雙rintk( ),
通過它來跟蹤程序。另外,你還可以用專門的調(diào)試工具如strace來監(jiān)視程序使用的系統(tǒng)調(diào)用。
例如,你可以這樣來寫vfifo設(shè)備:
#strace ls /dev/vfifo* > /dev/vfifo0
#strace cat /dev/vfifo1
到此為止,我們已經(jīng)完成了對Linux設(shè)備驅(qū)動的分析,并且自己設(shè)計了一個與具
體設(shè)備無關(guān)的特殊設(shè)備的驅(qū)動程序。但還有一些我們并沒有涉及到的內(nèi)容,如ioctl、I/O 端
口等,如有興趣可以自己去深入鉆研。
(驅(qū)動程序開發(fā))