Linux字元驅動開發學習總結
阿新 • • 發佈:2019-01-25
linux驅動編寫(虛擬字元裝置編寫)
昨天我們說了一些簡單模組編寫方法,但是終歸沒有涉及到裝置的編寫內容,今天我們就可以瞭解一下相關方面的內容,並且用一個例項來說明在linux上面裝置是如何編寫的。
因為我們是在pc linux上學習驅動的,因此暫時沒有真實的外接裝置可以使用,但是這絲毫不影響我們學習的熱情。通過定時器、程序,我們可以模擬出真實裝置的各種需求,所以對於系統來說,它是無所謂真裝置、假裝置的,基本的處理流程對它來說都是一樣的。只要大家一步一步做下去,肯定可以瞭解linux驅動裝置的開發工程的。
下面,為了說明問題,我們可以編寫一段簡單的char裝置驅動程式碼,檔名為char.c,
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static struct cdev chr_dev;
static dev_t ndev;
static int chr_open(struct inode* nd, struct file* filp)
{
int major ;
int minor;
major = MAJOR(nd->i_rdev);
minor = MINOR(nd->i_rdev);
printk("chr_open, major = %d, minor = %d\n", major, minor);
return 0;
}
static ssize_t chr_read(struct file* filp, char __user* u, size_t sz, loff_t* off)
{
printk("chr_read process!\n");
return 0;
}
struct file_operations chr_ops = {
.owner = THIS_MODULE,
.open = chr_open,
.read = chr_read
};
static int demo_init(void)
{
int ret;
cdev_init(&chr_dev, &chr_ops);
ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
if(ret < 0 )
{
return ret;
}
printk("demo_init(): major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev));
ret = cdev_add(&chr_dev, ndev, 1);
if(ret < 0)
{
return ret;
}
return 0;
}
static void demo_exit(void)
{
printk("demo_exit process!\n");
cdev_del(&chr_dev);
unregister_chrdev_region(ndev, 1);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("
MODULE_DESCRIPTION("A simple device example!");
在module_init中的函式是模組載入時處理的函式,而模組解除安裝的函式則是在module_exit中。每一個裝置都要對應一個基本的裝置資料,當然為了使得這個設備註冊在整個系統當中,我們還需要分配一個裝置節點,alloc_chrdev_region就完成這樣一個功能。等到cdev_add的時候,整個設備註冊的過程就全部完成了,就是這麼簡單。當然為了編寫這個檔案,我們還需要編寫一個Makefile檔案,
[cpp] view plain copy
ifneq ($(KERNELRELEASE),)
obj-m := char.o
else
PWD := $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions modules.* Module.*
endif
這個Makefile檔案和我們之前編寫的makefile基本上沒有區別,唯一的區別就是檔名稱改成了char.o,僅此而已。為了編寫模組,我們直接輸入make即可。這時候,char.ko檔案就可以生成了。然後,模組需要被註冊在系統當中,insmod char.ko是少不了的。如果此時,我們還不確信是否模組已經加入到系統當中,完全可以通過輸入lsmod | grep char進行查詢和驗證。為了建立裝置節點,我們需要知道裝置為我們建立的major、minor數值是多少,所以dmesg | tail 查詢一下數值。在我hp的機器上,這兩個數值分別是249和0,所以下面可以利用它們直接建立裝置節點了,輸入mknod /dev/chr_dev c 249 0即可,此時可以輸入ls /dev/chr_dev驗證一下。那麼,按照這種方法,真的可以訪問這個虛擬裝置了嗎,我們可以編寫一段簡單的程式碼驗證一下,
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define CHAR_DEV_NAME "/dev/chr_dev"
int main()
{
int ret;
int fd;
char buf[32];
fd = open(CHAR_DEV_NAME, O_RDONLY | O_NDELAY);
if(fd < 0)
{
printf("open failed!\n");
return -1;
}
read(fd, buf, 32);
close(fd);
return 0;
}
程式碼的內容非常簡單,就是利用CHAR_DEV_NAME直接開啟裝置,讀寫裝置。當然。首先還是需要對這個檔案進行編譯,檔名為test.c,輸入gcc test.c -o test,其次就是執行這個檔案,直接輸入./test即可。如果沒有問題的話,那麼說明我們的程式碼是ok的,但是我們還是沒有看到任何內容。沒關係,我們還是通過dmesg這個命令檢視核心中是否存在相關的列印內容,直接輸入dmesg | tail即可。此時如果沒有意外的話,我們就可以看到之前在chr_open和chr_read中留下的printk列印,這說明我們的程式碼完全是ok的。
上面的程式碼只是一段小例子,真實的內容要比這複雜一下。不過既然我們都已經入門了,那麼後面的內容其實也沒有什麼好怕的了。最後有兩個事情補充一下:(1)如果大家在建立節點後想刪除裝置節點,直接rm -rf /dev/chr_dev即可;(2)上面這段程式碼的原型來自於《深入linux裝置驅動程式核心機制》這本書,稍作修改,如果大家對核心機制的內容感興趣,可以參考這本書的內容。
========
linux驅動編寫(字元裝置編寫框架)
上次我們編寫了一個簡單的字元裝置,但是涉及的內容比較少,只有open和read兩個函式。今天,我們打算在此基礎上擴充一下內容。基本的思路是這樣的:(1)編寫字元裝置下需要處理的各個函式,包括open、release、read、write、ioctl、lseek函式;(2)編寫一個使用者側的程式來驗證我們編寫的驅動函式是否正確。當然,我們編寫的程式碼部分參考了宋寶華先生的《linux裝置驅動開發詳解》一書,在此說明一下。
在開始今天的內容之前,其實有一些題外話可以和大家分享一下。自從工作以來,我個人一直都有一個觀點。那就怎麼樣利用簡單的程式碼來說明開發中的問題,或者是解釋軟體中的原理,這是一個很高的學問。有些道理看上去雲裡霧裡說不清楚,其實都可以通過編寫程式碼來驗證的。os可以、cpu可以、cache可以、編譯器可以、網路協議也可以,很多很多的內容完全可以通過幾行程式碼就可以表達得非常清楚,但是事實上我們並沒有這麼做。我想原因無非是這麼幾條,一來授業者對相關知識的學習也是停留在概念上而已,二來我們的學習過於死板和教條、太關注知識、不求實踐,三就是學習者自身缺少思考的能力、缺少自我反省的能力、對很多東西不求甚解。對於簡單的linux裝置,我們完全可以通過這幾行程式碼說清楚問題,免得大家還要苦苦追尋,百思而不得入門。
好了,說了這麼多,我們看看現在的驅動程式碼是怎麼修改的把。
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define CHRMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
static int chr_major;
struct chr_dev
{
struct cdev cdev;
unsigned char mem[CHRMEM_SIZE];
};
struct chr_dev* char_devp;
int chr_open(struct inode* inode, struct file* filp)
{
filp->private_data = char_devp;
return 0;
}
int chr_release(struct inode* inode, struct file* filp)
{
return 0;
}
static int chr_ioctl(struct inode* inode, struct file* filp, unsigned int cmd, unsigned long arg)
{
struct chr_dev* dev = filp->private_data;
switch(cmd)
{
case MEM_CLEAR:
memset(dev->mem, 0, CHRMEM_SIZE);
break;
default:
return -EINVAL;
}
return 0;
}
static ssize_t chr_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chr_dev* dev = filp->private_data;
if(p >= CHRMEM_SIZE)
{
return 0;
}
if(count > CHRMEM_SIZE - p)
{
return 0;
}
if(copy_to_user(buf, (void*)(dev->mem + p), count))
{
return -EINVAL;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
static ssize_t chr_write(struct file* filp, const char __user* buf, ssize_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chr_dev* dev = filp->private_data;
if(p >= CHRMEM_SIZE)
{
return 0;
}
if(count > CHRMEM_SIZE - p)
{
count = CHRMEM_SIZE - p;
}
if(copy_from_user(dev->mem + p, buf, count))
{
ret = -EINVAL;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
static loff_t chr_llseek(struct file* filp, loff_t offset, int orig)
{
loff_t ret = 0;
/* orig can be SEEK_SET, SEEK_CUR, SEEK_END */
switch(orig)
{
case 0:
if(offset < 0)
{
ret = -EINVAL;
break;
}
if((unsigned int) offset > CHRMEM_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int) offset;
ret = filp->f_pos;
break;
case 1:
if((filp->f_pos + offset) > CHRMEM_SIZE)
{
ret = -EINVAL;
break;
}
if((filp->f_pos + offset) < 0)
{
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;
}
static const struct file_operations chr_ops =
{
.owner = THIS_MODULE,
.llseek = chr_llseek,
.read = chr_read,
.write = chr_write,
.ioctl = chr_ioctl,
.open = chr_open,
.release = chr_release
};
static void chr_setup_cdev(struct chr_dev* dev, int index)
{
int err;
int devno = MKDEV(chr_major, index);
cdev_init(&dev->cdev, &chr_ops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if(err)
{
printk(KERN_NOTICE "Error happend!\n");
}
}
int chr_init(void)
{
int result;
dev_t ndev;
result = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
if(result < 0 )
{
return result;
}
printk("chr_init(): major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev));
chr_major = MAJOR(ndev);
char_devp = kmalloc(sizeof(struct chr_dev), GFP_KERNEL);
if(!char_devp)
{
result = -ENOMEM;
goto final;
}
memset(char_devp, 0, sizeof(struct chr_dev));
chr_setup_cdev(char_devp, 0);
return 0;
final:
unregister_chrdev_region(ndev, 1);
return 0;
}
void chr_exit()
{
cdev_del(&char_devp->cdev);
kfree(char_devp);
unregister_chrdev_region(MKDEV(chr_major, 0), 1);
}
module_init(chr_init);
module_exit(chr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("feixiaoxing!163.com");
MODULE_DESCRIPTION("A simple device example!");
不可否認,我們的程式碼出現了更多的內容,但是基本框架還是一致的。要是說區別,無非就是我們在原來的基礎上添加了新的處理函式而已。說起來,我們對於裝置的主要操作也就是這麼幾種,大家如果對此的概念已經非常成熟了,那麼後面的學習就會輕鬆很多。當然和之前的驅動一樣,我們也需要make & insmod char.ko & mknod /dev/chr_dev c 249 0。接下來,為了驗證上述的內容是否正確,編寫一段簡單的測試程式碼是必不可少的。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define MEM_CLEAR 0x01
#define CHAR_DEV_NAME "/dev/chr_dev"
int main()
{
int ret;
int fd;
int index;
char buf[32];
/* open device */
fd = open(CHAR_DEV_NAME, O_RDWR | O_NONBLOCK);
if(fd < 0)
{
printf("open failed!\n");
return -1;
}
/* set buffer data, which will be stored into device */
for(index = 0; index < 32; index ++)
{
buf[index] = index;
}
/* write data */
write(fd, buf, 32);
memset(buf, 0, 32);
/* read data */
lseek(fd, 0, SEEK_SET);
read(fd, buf, 32);
for(index = 0; index < 32; index ++)
{
printf("data[%d] = %d\n", index, buf[index]);
}
/* reset all data to zero, read it and check whether it is ok */
ioctl(fd, MEM_CLEAR, NULL);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 32);
for(index = 0; index < 32; index ++)
{
printf("data[%d] = %d\n", index, buf[index]);
}
close(fd);
return 0;
}
細心的朋友可能發現了,我們在使用者側程式碼中使用了很多的處理函式,基本上從open、release、read、write、lseek、ioctl全部包括了。測試程式碼處理的流程也非常簡單,首先開啟裝置,接著寫資料,後面就是讀取資料,最後利用ioctl清除資料,程式返回。因為程式碼中包含了註釋的內容,在此我們就不過多贅述了。大家慢慢看程式碼,應該都會了解和明白的。注意,使用者測的程式碼也要在sudo模式下執行。
========
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" );
到這裡,這個字元裝置驅動已經完成,接下來就是編譯它。
======================================================================================================
編譯程式碼
======================================================================================================
這個是我寫的makefile檔案,在我臺機上我沒把這個模組加入到核心原始碼的字元裝置目錄下,而是放在了使用者目錄下面。但這個makefile檔案對以上兩種情況都支援:
#wjb add 2011-10-21
ifneq ($(KERNELRELEASE), )
obj-m := hellow.o
else
KERNELDIR =/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
.PHONY: modules modules_install clean
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
======================================================================================================
模組載入&裝置檔案節點構造:
======================================================================================================
1. 編譯成功後,會得到hellow.ko, 這時你就可以通過insmod命令載入模組
# insmod hellow.ko
這時你的日誌控制檯中會出現hello_init中的列印資訊,如果你使用lsmod列出當前已載入模組,會發現hellow模組赫然在目:
[email protected]:~/share/hellow# ls
a.out hellow.ko hellow.o Makefile Module.symvers
hellow.c hellow.mod.c main.c Makefile~
hellow.c~ hellow.mod.o main.c~ modules.order
[email protected]:~/share/hellow# insmod hellow.ko
[email protected]:~/share/hellow# dmesg | tail
[ 3711.851658] hello init. major:251, minor:0
2.要想使用驅動,你需要在/dev 目錄下建立裝置檔案節點,語法是
mknod [options]name {bc} major minor
這裡需要知道裝置的主、次裝置號,何以知之?使用cat /proc/devices | grep hello 你就會得到其主裝置號
比如我這裡得知hellow的主裝置號為251
那麼就用下面的命令:
[email protected]:~/share/hellow# mknod /dev/hellow c 251 0
c表示字元裝置,這樣就可以通過該裝置檔案操作裝置了。
======================================================================================================
測試程式:
======================================================================================================
現在就可以通過系統呼叫操作裝置了,我寫了一個測試程式來呼叫:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
int fd, ret;
char *buf = " Hello world !";
char temp[10] = "0";
fd = open ( "/dev/hellow" , O_RDWR);
if ( fd == -1 )
{
perror("open");
exit(0);
}
ret = write( fd, buf,strlen(buf));
if ( ret == -1 )
{
perror("write");
exit(0);
}
ret = read ( fd ,temp, strlen(temp) );
if ( ret == -1)
{
perror("read");
exit(0);
}
close(fd);
return 0;
}
編譯之:
[email protected]:~/share/hellow# gcc main.c
生成的目標檔案為a.out
執行之:
[email protected]:~/share/hellow# ./[email protected]:~/share/hellow# dmesg | tail
結果:
[ 4082.930492] Hello device open!
[ 4082.930520] hello: user has written 14 bytes to me: Hello world !
[ 4082.930524] hello: user read 1 bytes from me. A
[ 4082.930829] Hello device close!
當然,如果你想移除這個字元裝置,可以輸入如下命令:
[email protected]:~/share/hellow# rmmod hellow
[email protected]:~/share/hellow# dmesg | tail
結果顯示如下資訊,說明已經移除:
[ 4344.602407] hello exit. major:251,minor 0
========
linux裝置驅動程式之簡單字元裝置驅動
一、linux系統將裝置分為3類:字元裝置、塊裝置、網路裝置。使用驅動程式:
1、字元裝置:是指只能一個位元組一個位元組讀寫的裝置,不能隨機讀取裝置記憶體中的某一資料,讀取資料需要按照先後資料。字元裝置是面向流的裝置,常見的字元裝置有滑鼠、鍵盤、串列埠、控制檯和LED裝置等。
2、塊裝置:是指可以從裝置的任意位置讀取一定長度資料的裝置。塊裝置包括硬碟、磁碟、U盤和SD卡等。
每一個字元裝置或塊裝置都在/dev目錄下對應一個裝置檔案。linux使用者程式通過裝置檔案(或稱裝置節點)來使用驅動程式操作字元裝置和塊裝置。
二、字元裝置驅動程式基礎:
1、主裝置號和次裝置號(二者一起為裝置號):
一個字元裝置或塊裝置都有一個主裝置號和一個次裝置號。主裝置號用來標識與裝置檔案相連的驅動程式,用來反映裝置型別。次裝置號被驅動程式用來辨別操作的是哪個裝置,用來區分同類型的裝置。
linux核心中,裝置號用dev_t來描述,2.6.28中定義如下:
typedef u_long dev_t;
在32位機中是4個位元組,高12位表示主裝置號,低12位表示次裝置號。
可以使用下列巨集從dev_t中獲得主次裝置號: 也可以使用下列巨集通過主次裝置號生成dev_t:
MAJOR(dev_t dev); MKDEV(int major,int minor);
MINOR(dev_t dev);
//巨集定義:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2、分配裝置號(兩種方法):
(1)靜態申請:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
(2)動態分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
登出裝置號:
void unregister_chrdev_region(dev_t from, unsigned count);
建立裝置檔案:
利用cat /proc/devices檢視申請到的裝置名,裝置號。
(1)使用mknod手工建立:mknod filename type major minor
(2)自動建立;
利用udev(mdev)來實現裝置檔案的自動建立,首先應保證支援udev(mdev),由busybox配置。在驅動初始化程式碼裡呼叫class_create為該裝置建立一個class,再為每個裝置呼叫device_create建立對應的裝置。
3、字元裝置驅動程式重要的資料結構:
(1)struct file:代表一個開啟的檔案描述符,系統中每一個開啟的檔案在核心中都有一個關聯的struct file。它由核心在open時建立,並傳遞給在檔案上操作的任何函式,直到最後關閉。當檔案的所有例項都關閉之後,核心釋放這個資料結構。
//重要成員:
const struct file_operations *f_op; //該操作是定義檔案關聯的操作的。核心在執行open時對這個指標賦值。
off_t f_pos; //該檔案讀寫位置。
void *private_data;//該成員是系統呼叫時儲存狀態資訊非常有用的資源。
(2)struct inode:用來記錄檔案的物理資訊。它和代表開啟的file結構是不同的。一個檔案可以對應多個file結構,但只有一個inode結構。inode一般作為file_operations結構中函式的引數傳遞過來。
inode譯成中文就是索引節點。每個儲存裝置或儲存裝置的分割槽(儲存裝置是硬碟、軟盤、U盤 ... ... )被格式化為檔案系統後,應該有兩部份,一部份是inode,另一部份是Block,Block是用來儲存資料用的。而inode呢,就是用來儲存這些資料的資訊,這些資訊包括檔案大小、屬主、歸屬的使用者組、讀寫許可權等。inode為每個檔案進行資訊索引,所以就有了inode的數值。作業系統根據指令,能通過inode值最快的找到相對應的檔案。
dev_t i_rdev; //對錶示裝置檔案的inode結構,該欄位包含了真正的裝置編號。
struct cdev *i_cdev; //是表示字元裝置的核心的內部結構。當inode指向一個字元裝置檔案時,該欄位包含了指向struct cdev結構的指標。
//我們也可以使用下邊兩個巨集從inode中獲得主裝置號和此裝置號:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
(3)struct file_operations
本部分來源於:http://blog.chinaunix.net/space.php?uid=20729583&do=blog&id=1884550
struct file_operations ***_ops={
.owner = THIS_MODULE,
.llseek = ***_llseek,
.read = ***_read,
.write = ***_write,
.ioctl = ***_ioctl,
.open = ***_open,
.release = ***_release,
。。。 。。。
};
struct module *owner;
/*第一個 file_operations 成員根本不是一個操作; 它是一個指向擁有這個結構的模組的指標.
這個成員用來在它的操作還在被使用時阻止模組被解除安裝. 幾乎所有時間中, 它被簡單初始化為
THIS_MODULE, 一個在 <linux/module.h> 中定義的巨集.這個巨集比較複雜,在進行簡單學習操作的時候,一般初始化為THIS_MODULE。*/
loff_t (*llseek) (struct file * filp , loff_t p, int orig);
/*(指標引數filp為進行讀取資訊的目標檔案結構體指標;引數 p 為檔案定位的目標偏移量;引數orig為對檔案定位
的起始地址,這個值可以為檔案開頭(SEEK_SET,0,當前位置(SEEK_CUR,1),檔案末尾(SEEK_END,2))
llseek 方法用作改變檔案中的當前讀/寫位置, 並且新位置作為(正的)返回值.
loff_t 引數是一個"long offset", 並且就算在 32位平臺上也至少 64 位寬. 錯誤由一個負返回值指示.
如果這個函式指標是 NULL, seek 呼叫會以潛在地無法預知的方式修改 file 結構中的位置計數器( 在"file 結構" 一節中描述).*/
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
/*(指標引數 filp 為進行讀取資訊的目標檔案,指標引數buffer 為對應放置資訊的緩衝區(即使用者空間記憶體地址),
引數size為要讀取的資訊長度,引數 p 為讀的位置相對於檔案開頭的偏移,在讀取資訊後,這個指標一般都會移動,移動的值為要讀取資訊的長度值)
這個函式用來從裝置中獲取資料. 在這個位置的一個空指標導致 read 系統呼叫以 -EINVAL("Invalid argument") 失敗.
一個非負返回值代表了成功讀取的位元組數( 返回值是一個 "signed size" 型別, 常常是目標平臺本地的整數型別).*/
ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p);
/*可以看出,這個函式的第一、三個引數和本結構體中的read()函式的第一、三個引數是不同 的,
非同步讀寫的第三個引數直接傳遞值,而同步讀寫的第三個引數傳遞的是指標,因為AIO從來不需要改變檔案的位置。
非同步讀寫的第一個引數為指向kiocb結構體的指標,而同步讀寫的第一引數為指向file結構體的指標,每一個I/O請求都對應一個kiocb結構體);
初始化一個非同步讀 -- 可能在函式返回前不結束的讀操作.如果這個方法是 NULL, 所有的操作會由 read 代替進行(同步地).
(有關linux非同步I/O,可以參考有關的資料,《linux裝置驅動開發詳解》中給出了詳細的解答)*/
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
/*(引數filp為目標檔案結構體指標,buffer為要寫入檔案的資訊緩衝區,count為要寫入資訊的長度,
ppos為當前的偏移位置,這個值通常是用來判斷寫檔案是否越界)
傳送資料給裝置. 如果 NULL, -EINVAL 返回給呼叫 write 系統呼叫的程式. 如果非負, 返回值代表成功寫的位元組數.
(注:這個操作和上面的對檔案進行讀的操作均為阻塞操作)*/
ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos);
/*初始化裝置上的一個非同步寫.引數型別同aio_read()函式;*/
int (*readdir) (struct file * filp, void *, filldir_t);
/*對於裝置檔案這個成員應當為 NULL; 它用來讀取目錄, 並且僅對檔案系統有用.*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*(這是一個裝置驅動中的輪詢函式,第一個引數為file結構指標,第二個為輪詢表指標)
這個函式返回裝置資源的可獲取狀態,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等巨集的位“或”結果。
每個巨集都表明裝置的一種狀態,如:POLLIN(定義為0x0001)意味著裝置可以無阻塞的讀,POLLOUT(定義為0x0004)意味著裝置可以無阻塞的寫。
(poll 方法是 3 個系統呼叫的後端: poll, epoll, 和 select, 都用作查詢對一個或多個檔案描述符的讀或寫是否會阻塞.
poll 方法應當返回一個位掩碼指示是否非阻塞的讀或寫是可能的, 並且, 可能地, 提供給核心資訊用來使呼叫程序睡眠直到 I/O 變為可能.
如果一個驅動的 poll 方法為 NULL, 裝置假定為不阻塞地可讀可寫.
(這裡通常將裝置看作一個檔案進行相關的操作,而輪詢操作的取值直接關係到裝置的響應情況,可以是阻塞操作結果,同時也可以是非阻塞操作結果)*/
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
/*(inode 和 filp 指標是對應應用程式傳遞的檔案描述符 fd 的值, 和傳遞給 open 方法的相同引數.
cmd 引數從使用者那裡不改變地傳下來, 並且可選的引數 arg 引數以一個 unsigned long 的形式傳遞, 不管它是否由使用者給定為一個整數或一個指標.
如果呼叫程式不傳遞第 3 個引數, 被驅動操作收到的 arg 值是無定義的.
因為型別檢查在這個額外引數上被關閉, 編譯器不能警告你如果一個無效的引數被傳遞給 ioctl, 並且任何關聯的錯誤將難以查詢.)
ioctl 系統呼叫提供了發出裝置特定命令的方法(例如格式化軟盤的一個磁軌, 這不是讀也不是寫). 另外, 幾個 ioctl 命令被核心識別而不必引用 fops 表.
如果裝置不提供 ioctl 方法, 對於任何未事先定義的請求(-ENOTTY, "裝置無這樣的 ioctl"), 系統呼叫返回一個錯誤.*/
int (*mmap) (struct file *, struct vm_area_struct *);
/*mmap 用來請求將裝置記憶體對映到程序的地址空間. 如果這個方法是 NULL, mmap 系統呼叫返回 -ENODEV.
(如果想對這個函式有個徹底的瞭解,那麼請看有關“程序地址空間”介紹的書籍)*/
int (*open) (struct inode * inode , struct file * filp ) ;
/*(inode 為檔案節點,這個節點只有一個,無論使用者開啟多少個檔案,都只是對應著一個inode結構;
但是filp就不同,只要開啟一個檔案,就對應著一個file結構體,file結構體通常用來追蹤檔案在執行時的狀態資訊)
儘管這常常是對裝置檔案進行的第一個操作, 不要求驅動宣告一個對應的方法. 如果這個項是 NULL, 裝置開啟一直成功, 但是你的驅動不會得到通知.
與open()函式對應的是release()函式。*/
int (*flush) (struct file *);
/*flush 操作在程序關閉它的裝置檔案描述符的拷貝時呼叫; 它應當執行(並且等待)裝置的任何未完成的操作.
這個必須不要和使用者查詢請求的 fsync 操作混淆了. 當前, flush 在很少驅動中使用;
SCSI 磁帶驅動使用它, 例如, 為確保所有寫的資料在裝置關閉前寫到磁帶上. 如果 flush 為 NULL, 核心簡單地忽略使用者應用程式的請求.*/
int (*release) (struct inode *, struct file *);
/*release ()函式當最後一個開啟裝置的使用者程序執行close()系統呼叫的時候,核心將呼叫驅動程式release()函式:
void release(struct inode inode,struct file *file),release函式的主要任務是清理未結束的輸入輸出操作,釋放資源,使用者自定義排他標誌的復位等。
在檔案結構被釋放時引用這個操作. 如同 open, release 可以為 NULL.*/
int(*synch)(struct file *,struct dentry *,int datasync);
//重新整理待處理的資料,允許程序把所有的髒緩衝區重新整理到磁碟。
int (*aio_fsync)(struct kiocb *, int);
/*這是 fsync 方法的非同步版本.所謂的fsync方法是一個系統呼叫函式。系統呼叫fsync
把檔案所指定的檔案的所有髒緩衝區寫到磁碟中(如果需要,還包括存有索引節點的緩衝區)。
相應的服務例程獲得檔案物件的地址,並隨後呼叫fsync方法。通常這個方法以呼叫函式__writeback_single_inode()結束,
這個函式把與被選中的索引節點相關的髒頁和索引節點本身都寫回磁碟。*/
int (*fasync) (int, struct file *, int);
//這個函式是系統支援非同步通知的裝置驅動,下面是這個函式的模板:
static int ***_fasync(int fd,struct file *filp,int mode)
{
struct ***_dev * dev=filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);//第四個引數為 fasync_struct結構體指標的指標。
//這個函式是用來處理FASYNC標誌的函式。(FASYNC:表示相容BSD的fcntl同步操作)當這個標誌改變時,驅動程式中的fasync()函式將得到執行。
}
/*此操作用來通知裝置它的 FASYNC 標誌的改變. 非同步通知是一個高階的主題, 在第 6 章中描述.
這個成員可以是NULL 如果驅動不支援非同步通知.*/
int (*lock) (struct file *, int, struct file_lock *);
//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 *);
/*這些方法實現發散/匯聚讀和寫操作. 應用程式偶爾需要做一個包含多個記憶體區的單個讀或寫操作;
這些系統呼叫允許它們這樣做而不必對資料進行額外拷貝. 如果這些函式指標為 NULL, read 和 write 方法被呼叫( 可能多於一次 ).*/
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
/*這個方法實現 sendfile 系統呼叫的讀, 使用最少的拷貝從一個檔案描述符搬移資料到另一個.
例如, 它被一個需要傳送檔案內容到一個網路連線的 web 伺服器使用. 裝置驅動常常使 sendfile 為 NULL.*/
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/*sendpage 是 sendfile 的另一半; 它由核心呼叫來發送資料, 一次一頁, 到對應的檔案. 裝置驅動實際上不實現 sendpage.*/
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/*這個方法的目的是在程序的地址空間找一個合適的位置來對映在底層裝置上的記憶體段中.
這個任務通常由記憶體管理程式碼進行; 這個方法存在為了使驅動能強制特殊裝置可能有的任何的對齊請求. 大部分驅動可以置這個方法為 NULL.[10]*/
int (*check_flags)(int)
//這個方法允許模組檢查傳遞給 fnctl(F_SETFL...) 呼叫的標誌.
int (*dir_notify)(struct file *, unsigned long);
//這個方法在應用程式使用 fcntl 來請求目錄改變通知時呼叫. 只對檔案系統有用; 驅動不需要實現 dir_notify.
三、字元裝置驅動程式設計:
1.設備註冊:
在linux2.6核心中,字元裝置使用struct cdev來描述;
struct cdev
{
struct kobject kobj;//內嵌的kobject物件
struct module *owner;//所屬模組
struct file_operations *ops;//檔案操作結構體
struct list_head list;
dev_t dev;//裝置號,長度為32位,其中高12為主裝置號,低20位為此裝置號
unsigned int count;
};
字元裝置的註冊分為三個步驟:
(1)分配cdev: struct cdev *cdev_alloc(void);
(2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops);
(3)新增cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count)
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
2.裝置操作的實現:file_operations函式集的實現(要明確某個函式什麼時候被呼叫?呼叫來做什麼操作?)
特別注意:驅動程式應用程式的資料交換:
驅動程式和應用程式的資料交換是非常重要的。file_operations中的read()和write()函式,就是用來在驅動程式和應用程式間交換資料的。通過資料交換,驅動程式和應用程式可以彼此瞭解對方的情況。但是驅動程式和應用程式屬於不同的地址空間。驅動程式不能直接訪問應用程式的地址空間;同樣應用程式也不能直接訪問驅動程式的地址空間,否則會破壞彼此空間中的資料,從而造成系統崩潰,或者資料損壞。安全的方法是使用核心提供的專用函式,完成資料在應用程式空間和驅動程式空間的交換。這些函式對使用者程式傳過來的指標進行了嚴格的檢查和必要的轉換,從而保證使用者程式與驅動程式交換資料的安全性。這些函式有:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
put_user(local,user);
get_user(local,user);
3.設備註銷:void cdev_del(struct cdev *p);
四、字元裝置驅動小結:
字元裝置是3大類裝置(字元裝置、塊裝置、網路裝置)中較簡單的一類裝置,其驅動程式中完成的主要工作是初始化、新增和刪除cdev結構體,申請和釋放裝置號,以及填充file_operation結構體中操作函式,並實現file_operations結構體中的read()、write()、ioctl()等重要函式。如圖所示為cdev結構體、file_operations和使用者空間呼叫驅動的關係。
五:字元裝置驅動程式分析:
(1)memdev.h
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 251 /*預設的mem的主裝置號*/
#endif
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*裝置數*/
#endif
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
/*mem裝置描述結構體*/
struct mem_dev
{
char *data;
unsigned long size;
};
#endif /* _MEMDEV_H_ */
(2)memdev.c
static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*裝置結構體指標*/
struct cdev cdev;
/*檔案開啟函式*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*獲取次裝置號*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*將裝置描述結構指標賦值給檔案私有資料指標*/
filp->private_data = dev;
return 0;
}
/*檔案釋放函式*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*讀函式*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; /*記錄檔案指標偏移位置*/
unsigned int count = size; /*記錄需要讀取的位元組數*/
int ret = 0; /*返回值*/
struct mem_dev *dev = filp->private_data; /*獲得裝置結構體指標*/
/*判斷讀位置是否有效*/
if (p >= MEMDEV_SIZE) /*要讀取的偏移大於裝置的記憶體空間*/
return 0;
if (count > MEMDEV_SIZE - p) /*要讀取的位元組大於裝置的記憶體空間*/
count = MEMDEV_SIZE - p;
/*讀資料到使用者空間:核心空間->使用者空間交換資料*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
return ret;
}
/*寫函式*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*獲得裝置結構體指標*/
/*分析和獲取有效的寫長度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p) /*要寫入的位元組大於裝置的記憶體空間*/
count = MEMDEV_SIZE - p;
/*從使用者空間寫入資料*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count; /*增加偏移位置*/
ret = count; /*返回實際的寫入位元組數*/
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}
/* seek檔案定位函式 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */ /*相對檔案開始位置偏移*/
newpos = offset; /*更新檔案指標位置*/
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*檔案操作結構體*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*裝置驅動模組載入函式*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 申請裝置號,當xxx_major不為0時,表示靜態指定;當為0時,表示動態申請*/
/* 靜態申請裝置號*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 動態分配裝置號 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno); /*獲得申請的主裝置號*/
}
if (result < 0)
return result;
/*初始化cdev結構,並傳遞file_operations結構指標*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE; /*指定所屬模組*/
cdev.ops = &mem_fops;
/* 註冊字元裝置 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 為裝置描述結構分配記憶體*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申請失敗*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*為裝置分配記憶體*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模組解除安裝函式*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*登出裝置*/
kfree(mem_devp); /*釋放裝置結構體記憶體*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*釋放裝置號*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
(3)應用程式(測試檔案):app-mem.c
#include <stdio.h>
int main()
{
FILE *fp0 = NULL;
char Buf[4096];
/*初始化Buf*/
strcpy(Buf,"Mem is char dev!");
printf("BUF: %s\n",Buf);
/*開啟裝置檔案*/
fp0 = fopen("/dev/memdev0","r+");
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}
/*寫入裝置*/
fwrite(Buf, sizeof(Buf), 1, fp0);
/*重新定位檔案位置(思考沒有該指令,會有何後果)*/
fseek(fp0,0,SEEK_SET);
/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("BUF: %s\n",Buf);
/*讀出裝置*/
fread(Buf, sizeof(Buf), 1, fp0);
/*檢測結果*/
printf("BUF: %s\n",Buf);
return 0;
}
測試步驟:
1)cat /proc/devices看看有哪些編號已經被使用,我們選一個沒有使用的XXX。
2)insmod memdev.ko
3)通過"mknod /dev/memdev0 c XXX 0"命令建立"/dev/memdev0"裝置節點。
4)交叉編譯app-mem.c檔案,下載並執行:
#./app-mem,顯示:
Mem is char dev!
========
Linux 字元驅動開發心得
Linux字元驅動框架相比初學還是比較難記的,在學了一陣子字元驅動的開發後對於框架的搭建總結出了幾個字 。
對於框架來講主要要完成兩步。
申請裝置號,註冊字元驅動
其關鍵程式碼就兩句
~
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);//動態申請裝置號
int cdev_add(struct cdev *, dev_t, unsigned); //註冊字元驅動
~
執行完次就可以將我們的驅動程式載入到核心裡了
首先我們搭建主程式,字元驅動的名字就叫做"main"
首先先寫下將要用到的標頭檔案,以及一個巨集定義,指明瞭我們驅動的名稱,當然名稱可以任意這裡就取"main" 作為名字
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/coda.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define MUDULE_NAME "main"
驅動由於需要載入到核心裡,所以我們需要宣告一下我們驅動所遵循的協議,如果沒有申明,那麼載入核心的時候系統會提示一段資訊。我們按照核心的風格來,就加一個GPL協議吧
MODULE_LICENSE("GPL");
我們要想將我們的驅動註冊到核心裡