歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Linux字符設備驅動入門

Linux字符設備驅動入門

日期:2017/3/1 10:57:22   编辑:Linux編程

先亮一下裝備:
平台:VMware 7.0 + Linux Ubuntu 3.0.0-12-generic

編譯器:gcc

參考資料:LDD 3

功能:實現簡單的字符操作(從用戶空間向內核空間寫入一串字符;從內核空間讀一個字符到內核空間)
眾所周知,字符設備是linux下最基本,也是最常用到的設備,它是學習Linux驅動入門最好的選擇,計算機的東西很多都是相通的,掌握了其中一塊,其他就可以觸類旁通了。在寫驅動前,必須先搞清楚字符設備的框架大概是怎樣的,弄清楚了流程,才開始動手,不要一開始就動手寫代碼!
這裡所說的框架是參考LLD3上介紹的,內核是基於Linux 2.6,3.0以上的有些地方會不一樣(主要是file_operations中的ioctl修改了),但基本上適用,因為我就是在3.0的內核上實現的!字符設備驅動的初始化流程大概如下所示:

定義相關的設備文件結構體(如file_operation()中的相關成員函數的定義)->向內核申請主設備號(建議采用動態方式) ->申請成功後,調用MAJOR()獲取主設備號 ->初始化cdev的結構體,調用cdev_init() ->調用cdev_add(),注冊cdev到kernel ->注冊設備模塊:module_init()、module_exit()。

======================================================================================================

編寫代碼

======================================================================================================

首先定義兩個全局變量(主設備號和字符設備hellow):

static int hello_major = 0; /* major device number */

static struct cdev hellow; /* hello device structure */

然後來看看file_operations(),它的定義可以在../include/linux/fs.h下找到,這裡只用到了其中的幾個成員函數:

/* file operations for hello device */
static struct file_operations hello_ops = {
.owner = THIS_MODULE, /*owner為所有者字段,防止在使用時模塊被卸載。一邊都設為THIS_MODULE*/
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,

};


不同於windows驅動程序,Linux設備驅動程序在與硬件設備之間建立了標准的抽象接口。通過這個接口,用戶可以像處理普通文件一樣,通過open,close,read,write等系統調用對設備進行操作,如此一來也大大簡化了linux驅動程序的開發。通過file_operations這個結構體(實際上是一個函數指針的集合),把驅動的操作和設備號聯系起來,程序員所要做的工作只是通過file_operations掛接自己的系統調用函數。


接下來就是實現open,close,read,write操作了,這個驅動什麼都沒干,所以很好理解,用戶請求read系統調用時,這個虛擬設備反回相應長度的“A”字符串,用戶write時,將內容顯示到日志中。這裡要注意的是,內核空間中不能使用用戶態的malloc,而是使用kmalloc/kfree。而且,用戶read/write提供的buf地址也是用戶態的,內核自然不能直接訪問,需要通過copy_to_user/copy_from_user 進行數據拷貝,具體如下:

/* Open the device */
static int hello_open( struct inode *inode, struct file *filp ){
printk( KERN_NOTICE"Hello device open!\n" );
return 0;
}


/* Close hello_device */
static int hello_release( struct inode *inode, struct file *filp ){
printk( KERN_NOTICE"Hello device close!\n" );
return 0;
}


/* user read from hello device*/
ssize_t hello_read( struct file *flip, char __user *buf, size_t count,loff_t
*f_pos){
ssize_t retval = 0;
char *bank;
bank = kmalloc(count+1, GFP_KERNEL );
if( bank == NULL )
return -1;
memset( bank, 'A',count );
if( copy_to_user( buf, bank, count ) ){
retval = -EFAULT;
goto out;
}
retval += count;
*(bank+count)=0;
printk( KERN_NOTICE"hello: user read %d bytes from me. %s\n",count,bank );
out:
kfree(bank);
return retval;
}

/* write to hello device */
ssize_t hello_write( struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos ){
ssize_t retval = 0;
char *bank = kmalloc( count ,GFP_KERNEL );
if( bank == NULL )
return retval;
if( copy_from_user(bank, buf, count ) ){
retval = -EFAULT;
printk( KERN_NOTICE"hello: write error\n" );
goto out;
}
retval += count;
printk( KERN_NOTICE"hello: user has written %d bytes to me: %s\n",count,
bank );
out:
kfree(bank );
return retval;
}

你可能會注意到open和release函數頭中的file和inode結構體,inode是內核內部文件的表示,當其指向一個字符設備時,其中的i_cdev成員既包含了指向cdev結構的指針。而file表示打開的文件描述符,對一個文件,若打開多次,則會有多個file結構,但只有一個inode與之對應。

因為驅動工作在內核空間,不能使用用戶空間的libc函數,所以程序中打印語句為內核提供的printk,而非printf,KERN_NOTICE宏其實標記的是日志級別(共有八個)不同級別的消息會記錄到不同的地方。如果你運行本模塊,可能會發現printk語句並沒有輸出到控制台,這是正常的,控制台只顯示一定級別的消息。當日志級別小於console_loglevel時,消息才能顯示出來。你可以通過dmsg命令看到這些信息,也可以通過修改日志級別使之輸出到你的虛擬終端。

作好以上准備工作後,接下來就可以開始進行向內核申請主設備號了。設備號是干什麼吃的?據LDD記載,對字符設備的訪問是通過文件系統內的設備名稱進行的。那些被稱為特殊文件、設備文件的節點,通常位於/dev目錄,如果ls -l 查看該目錄,第一列中帶有c標志的即為字符設備,有b標志的為塊設備。而第5、6列所示的兩個數字分別為設備的主、次設備號。通常,主設備號標識設備所用的驅動程序(現在大多設備仍然采用“一個主設備號對應一個驅動程序”的規則),次設備號用於確定設備,比如你有兩塊網卡,使用同一驅動,主設備號相同,那麼他們將由次設備號區分。
/* Module housekeeping */
static int hello_init(void){
int result;
dev_t dev = MKDEV( hello_major, 0 );/*to transfer major as dev_t type*/

/* alloc the major device number dynamicly */
result = alloc_chrdev_region(&dev, 0 ,1, "hello" );
if( result < 0 ){
printk( KERN_NOTICE"Hello: unable to get major %d\n",hello_major );
return result;
}
hello_major = MAJOR(dev);
/* set up devices, in this case, there is only one device */
printk( KERN_NOTICE"hello init. major:%d, minor:%d\n",hello_major,0 );
//printk( KERN_ALERT"hello init: %d, %d\n",hello_major,0 );
hello_setup_cdev(&hellow, 0 , &hello_ops );

return 0;

}

/* Exit routine */
static void hello_exit(void){
/* remove the cdev from kernel */
cdev_del(&hellow );
/* release the device numble alloced earlier */
unregister_chrdev_region( MKDEV( hello_major, 0 ), 1 );
printk( KERN_NOTICE"hello exit. major:%d,minor %d\n",hello_major,0 );
}

這裡主設備號的分配由alloc_chrdev_region(第一個參數為dev_t 指針,用來存放設備編號,第二個參數為要使用的第一個次設備號,通常為0,第三個參數為請求的連續設備編號個數)動態分配,當然也可以靜態指定一個未被使用的主設備號,相應函數為register_chrdev_region,但不推薦這樣做。在模塊被卸載時(hello_exit),通過unregister_chrdev_region釋放設備號。MKDEV宏將給出的主、次設備號轉換成dev_t類型,MAJOR,MINOR分別從dev_t中析取主次設備號。

這裡幾個函數的原型為:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

void unregister_chrdev_region(dev_t first, unsigned int count);


然後進入hello_setup_cdev函數,對設備進行初始化這裡cdev結構體是內核內部使用來表示字符設備的。在內核調用設備操作之前,必須分配並注冊一個或多個這樣的結構。為了方便,沒有動態使用cdev_alloc函數分配空間,而是定義了一個全局靜態cdev變量。通常你可以將你的cdev嵌入到自定義的結構體中(這個驅動很naive,沒有這麼做),通過cdev_init 函數初始化。最後調用cdev_add(),注冊cdev到內核。

/* set up the cdev stucture for a device */
static void hello_setup_cdev( struct cdev *dev, int minor, struct
file_operations *fops ){
int err;
int devno = MKDEV( hello_major, minor );
/* initialize the cdev struct */
cdev_init( dev,fops );
dev->owner = THIS_MODULE;
err = cdev_add( dev, devno, 1 ); /* register the cdev in the kernel */
if( err )
printk( KERN_NOTICE"Error %d adding hello%d\n",err ,minor );
}


最後module_init( hello_init ); module_exit( hello_exit );指定了模塊初始化和關閉函數。MODULE_LICENSE( "Dual BSD/GPL" ); 指定模塊使用的許可證能被內核識別的許可證有GPL、GPL v2、 Dual BSD/GPL、 Dual MPL/GPL、Proprietary(專有)等,如果模塊沒有顯式標記許可證,則會被認定為“專有”,內核加載這樣的模塊會被“污染”。


/* register the init and exit routine of the module */
module_init( hello_init );
module_exit( hello_exit );

MODULE_AUTHOR( "jabenwang" );
MODULE_LICENSE( "Dual BSD/GPL" );

到這裡,這個字符設備驅動已經完成,接下來就是編譯它。

Copyright © Linux教程網 All Rights Reserved