因爲在Ubuntu環境下寫的文章和做的實驗,沒有安裝linux下比較好用的截圖工具,所以沒有附帶太多截屏,還望海涵,不過該描述的都到位了。
曾經還一直處於應用程序開發的我,以爲驅動開發者是那麼的厲害,以爲只有牛人才能走到這一步,隨着知識的積累,發現並非如此,驅動開發並不像想象中那麼特別,俗話說術業有專攻,開發者只是使用的工具不同,且從事的領域不同,產品不同罷了。只要能作出好的產品,你就是一個”牛人”。
從這裏開始進行系統化的驅動學習,主線是《Linux設備驅動開發詳解》,之前大致看過這本書,起初感覺有些晦澀,但看了兩本內核的書籍以後,重新回來讀起來就比較順流了,一口氣讀了好幾章(主要是前幾章是知識介紹性文章)。所以這裏順便推薦兩本內核的書籍:
《Linux內核設計與實現》,我看的是第二版,寫的非常棒,簡單易懂,要在介紹內核原理與實現機制,廣度到了,深度不夠,所以最好還得配合下邊這本書一塊兒看。
《UnderstandingLinux kernel》——深入理解linux內核,這本書的第三版是基於2.6內核的,06年出版。我看的是英文原版的,所以看得速度比較滿,不過正好和《Linux設備驅動開發詳解》對接上了,剛看過《Understanding Linux kernel》中的同步異步,應該是第五章那裏,然後《Linux設備驅動開發詳解》就在第七章也講到了,這樣,可能那麼多機制:鎖,讀寫鎖,順序鎖,信號量,讀寫信號量……一下出來這麼多東西的話有些接受不了,但如果你之前看了《Linux內核設計與實現》後,最起碼不會感覺恐慌,其實學習新的知識就是這樣,一回生兩回熟,再難理解的東西,功夫到了,也就理解了。
好的,開始第一個驅動程序的學習,實例來自 Linux設備驅動開發詳解,這裏是創建了一個虛擬的字符設備globalmem,也就是一片內核空間的內存區域,來實現內核空間和用戶空間的信息傳遞。(確實,我舉不出來比這更好的例子來作第一個例子了,不過請相信,哪怕就是這個例子也是我一個字母一個字母敲出來的,並未直接取材自隋書的源碼,主要是看我的註釋,和我遇到的問題以及解決它的全過程,成功者找方法,失敗者找藉口!呵呵)
不廢話,上代碼,別心急,看註釋。。。
下邊是驅動的源碼,附上了詳盡的註釋:
globalmem.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/errno.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 GLOBALMEM_SIZE 0x1000 /*4k的空間*/
#define MEM_CLEAR 0x1 /*清空全局內存*/
#define GLOBALMEM_MAJOR 250 /*預設的主設備號*/
static int globalmem_major = GLOBALMEM_MAJOR;
/*用面向對象思想對cdev重新封裝,以便於方便我們的操作*/
struct globalmem_dev{
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
struct globalmem_dev *globalmem_devp; /*聲明一個全局的設備結構體*/
/*用來註冊到file_operations結構中open */
int globalmem_open(struct inode *inode,struct file *filp)
{
filp->private_data = globalmem_devp; /*當有多個同類設備時,用私有變量訪問很有必要也很方便*/
return 0;
}
/*用來註冊到file_operations結構中release */
int globalmem_release(struct inode *inode,struct file *filp)
{
return 0;
}
/*用來註冊到file_operations結構中ioctl */
static int globalmem_ioctl(struct inode *inodep,struct file *filp,unsigned int cmd,unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data; /*從私有數據獲取設備結構體指針*/
switch (cmd){
case MEM_CLEAR:
memset(dev->mem,0,GLOBALMEM_SIZE);
printk(KERN_INFO"globalmem is set to zero\n");
break;
default:
return -EINVAL;
}
return 0;
}
/*用來註冊到file_operations結構中read */
static ssize_t globalmem_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 globalmem_dev *dev = filp->private_data; /*從私有變量獲取到設備結構體指針*/
/*分析和獲取有效的讀長度,就是看給的要讀取的長度是否合法*/
if(p>=GLOBALMEM_SIZE)
return 0;
if(count>GLOBALMEM_SIZE-p) /*如果要讀取的量比剩餘的還多,只給它可讀到的量*/
count=GLOBALMEM_SIZE-p;
/*一切就緒後就開始往用戶空間讀了,這裏的讀指的是用戶空間的讀,就是說從內核拷貝到用戶空間,
*因爲內核是萬能的有着所有的權限,主動權在於它,並不是說用戶想讀就可以讀的,因爲該空間是在內核狀態下分配出來的
*/
if(copy_to_user(buf,(void*)(dev->mem+p),count)) /*從mem的偏移量p處拷貝count個數據到buf所指區*/
ret = -EFAULT; /*拷貝失敗,返回-EFAULT*/
else{ /*拷貝成功,返回重新拷貝的數據量並重新計算的偏移量*/
*ppos+=count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p);
}
return ret;
}
/*用來註冊到file_operations結構中write */
static ssize_t globalmem_write(struct file *filp,char __user *buf,size_t size,loff_t *ppos)
{
unsigned long p = *ppos; /*獲取到當前全局內存的*/
unsigned int count = size; /*獲取到要讀取數據的大小*/
int ret = 0; /*用來記錄返回值*/
struct globalmem_dev *dev = filp->private_data; /*從私有變量獲取到設備結構體指針*/
/*分析和獲取有效的寫長度,就是看給的要寫的長度是否合法*/
if(p>=GLOBALMEM_SIZE)
return 0;
if(count>GLOBALMEM_SIZE-p) /*如果要讀取的量比剩餘的還多,只給它可讀到的量*/
count=GLOBALMEM_SIZE-p;
/*一切就緒後就開始往共享空間裏寫了,這裏的寫指的就是說從用戶空間拷貝到內核,
*因爲內核是萬能的有着所有的權限,主動權在於它,並不是說用戶想寫就可以寫的,因爲該空間是在內核狀態下分配出來的
*/
if(copy_from_user(dev->mem+p,buf,count)) /*往mem的偏移量p處拷貝count個數據(從buf所指區)*/
ret = -EFAULT; /*拷貝失敗,返回-EFAULT*/
else{ /*拷貝成功,返回重新拷貝的數據量並重新計算的偏移量*/
*ppos+=count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p);
}
return ret;
}
/*seek文件定位函數*/
static loff_t globalmem_llseek(struct file *filp,loff_t offset,int orig)
{
loff_t ret = 0;
switch (orig){
case 0: /*相對於文件開始位置*/
if(offset<0){
ret = -EINVAL;
break;
}
if((unsigned int)offset>GLOBALMEM_SIZE){
ret = -EINVAL;
break;
}
filp->f_pos=(unsigned int)offset;
ret=filp->f_pos;
break;
case 1: /*相對於文件當前位置*/
if((filp->f_pos+offset)>GLOBALMEM_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 globalmem_fops={
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
/*對設備進行初始化的函數*/
static void globalmem_setup_cdev(struct globalmem_dev *dev,int index)
{
int err,devno = MKDEV(globalmem_major,index); /*MKDEV宏來生成設備號主設備號佔12位,從設備號佔20位*/
cdev_init(&dev->cdev,&globalmem_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev,devno,1);
if(err)
printk(KERN_NOTICE "ERROR %d adding globalmem %d",err,index);
}
/**************************模塊相關函數******************************/
/*爲了便於開發和派錯,建議上來先定義這些模塊相關的初始化和卸載函數,你完全可以
*只做一個空的函數實現,意在每寫一個功能函數就編譯(make)一次,這樣會很有利於開發的
*/
/*驅動加載函數*/
int globalmem_init(void)
{
int result;
dev_t devno =MKDEV(globalmem_major,0);
/*申請設備號*/
if(globalmem_major)
result = register_chrdev_region(devno,1,"globalmem");
else{ /*動態申請*/
result = alloc_chrdev_region(&devno,0,1,"globalmem");
globalmem_major = MAJOR(devno);
}
if(result<0)
return result;
/*動態申請設備結構體用到的全局共享內存*/
globalmem_devp = kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
if(!globalmem_devp){
result = -ENOMEM;
goto fail_malloc;
}
memset(globalmem_devp,0,sizeof(struct globalmem_dev));
globalmem_setup_cdev(globalmem_devp,0);
return 0;
fail_malloc: /*分配失敗後要還原現場,把已經註冊的設備給釋放掉*/
unregister_chrdev_region(devno,1); /*釋放設備號*/
return result;
}
/*驅動卸載函數*/
void globalmem_exit(void)
{
cdev_del(&globalmem_devp->cdev); /*註銷掉設備*/
kfree(globalmem_devp); /*釋放掉分配的內存,好借好還再借不難*/
unregister_chrdev_region(MKDEV(globalmem_major,0),1); /*釋放設備號*/
}
MODULE_AUTHOR("Jun < [email protected] >");
MODULE_LICENSE("DUAL BSD/GPL");
module_init(globalmem_init);
module_exit(globalmem_exit);
Makefile的寫法和前面一篇文章的helloworld的模塊的Makefile寫法一致,換個模塊的名字而已,這裏是:
obj-m += globalmem.o
# XXX.o對應於你的XXX.c同時也是你的模塊名稱
all:
make -C /usr/src/linux-headers-2.6.32-27-generic M=$(shell pwd) modules
# 這裏通過uname -r命令獲取系統信息,同時拼裝出內核源碼樹的路徑;
# pwd獲取當前文件夾,這就要求着在你進行make的時候要在源碼目錄下。
clean:
make -C /usr/src/linux-headers-2.6.32-27-generic M=$(shell pwd) clean
# 原理同上
準備好後開始進行編譯,就是make一下就OK ;
make時又遇到了這樣的問題:
make:Nothing to be done for 'all'.
Makeclearn 時也會出現:
make:Nothing to be done for 'clearn'.
網上說的都是一編譯好了,只是沒有修改源文件,所以沒有進行再編譯(我知道這也是make存在的理由,它的一方面功能就是這樣的,避免重複編譯未修改的文件),但顯示是它確實不存在這個問題,我小糾結了一下,並且也沒有其他任何編譯時的報錯提示。爲了做測試,我又試着編譯之前的helloworld的模塊,終於報錯了,哈哈哈。問題在於,make中的make -C /usr/src/linux-headers-$(shell uname -r) M=$(shell pwd)modules ,該命令是動態的通過shell命令的uname來獲取當前內核源碼路徑,並進行連接編譯驅動的。而我下載並構建的內核是:2.6.32-30-generic,而此時系統裝載的是(也就是uname -r命令得到的內核版本):2.6.30-27-generic,而該版本的內核源碼樹我已經手動刪除,所以我的源碼路徑和通過uname -r組裝出來的是不一致的,所以我把Makefile中的命令手動修改成了:
make -C /usr/src/linux-headers-2.6.32-30-generic M=$(shell pwd) modules ,直接指定路徑(這樣的可維護性下降了,不過我們的工程太小,可以忽略這個問題)。OK ,try again,make 通過了,目錄下編譯出來了那羣你夢寐以求想看到的文件們,他們這時顯得是如此的可愛。
這裏還要說的是,模塊只是一種把自己的代碼動態加載到內核的手段,這也就說明了爲什麼我的驅動模塊的Makefile文件爲何和helloworld模塊的Makefile文件是一模一樣的(確實,模塊的名字是不一樣的,你知道我是什麼意思),所以對於驅動來說,模塊就是一條小船,它把代碼運載到了kernel所在的海域,作爲一個載體把驅動的代碼帶到了內核空間,從而使得你在用戶空間調用某些系統操作時,OS可以找到對應的代碼來完成你的請求。
好的,編譯好了,就該裝載並測試了。
$ insmod globalmem.ko #“Ubuntu下必要時記着加sudo,因爲我已經開啓了ubuntu的su”
呵呵,又來錯誤了。看來又要成長了,要善待你碰到的沒一個失敗,畢竟她是成功他媽。
裝載時報錯了:
insmod:error inserting 'globalmem.ko': -1 Invalid module format
還是因爲我係統現裝載的內核和對模塊進行編譯的內核版本不一致造成的,所以,我得把在構建的內核源碼樹中版本較新的內核安裝到系統中去才行。這裏安裝新內核的方法,可以借鑑這篇博文(哈哈,我越來越愛西郵的學生了,如果你想考研或者招聘,西郵出來的做linux的都是非常棒的。額,作廣告了,呵呵):http://edsionte.com/techblog/archives/3289/comment-page-1#comment-2350
#追加說明
三樓的評論點醒了我,這是一個程序編譯的問題;這裏應該是系統自帶的防範問題,它會檢測你的模塊記載版本號的字符串和當前正在運行的內核模塊的不一致,理論上是沒問題的。
Invalid module format,那很有可能是以下原因引起的:
- 所用內核源碼版本號與目前使用的內核不同;
- 編譯目標不同,比如編譯的是i686,裝好的是i386;
- 使用編譯器版本不同;
- 目前使用的內核不是自己編譯出來的。
- 這裏推薦看看這篇文章應該就明白了:第一個驅動helloworld module加載insmod “Invalid module format ”問題解決
一切就緒,有裝載過對應版本的內核後,重啓一下,進入對應的內核版本中去。重新insert our module ,try again。
$ insmod globalmem.ko
welldone,we've already made it.好的,接下來在用戶空間測試一下。
驅動是針對設備而言的(雖然這裏的設備並非是實實在在的,只是一片內存),而linux下的設備又都是抽象成文件來看待的(畢竟unix最早是從一個文件系統演變過來的,也就是這一壯舉,使得驅動的開發容易多了,統一的抽象帶來了非常大的方便)。所以我們要把這個設備文件創建出來。
$ mknod /dev/globalmem c 250 0 #創建一個主設備號爲250次設備號是0的字符設備文件globalmem到/dev下。
然後就可以開始測試了:
$ echo“Hello jun” > /dev/globalmem #寫“hello jun”到設備
提示:
bash:/dev/globalmem: Permission denied
權限不夠,ls -l 發現:
crw-r--r--1 root root 250, 0 Oct 20 15:47 /dev/globalmem
你要麼用sudo來echo,要麼把文件權限該一下,這個自由留給你了。呵呵
$ cat /dev/globalmem #顯示設備文件的內容
顯示如下:
root@jun-desktop:/home/jun/driver/ch6#cat /dev/globalmem
hellojun
好的,大功告成!!!
後記說明:
1、做驅動的話,建議可以在原生的linux環境下,其實也挺方便的。
2、推薦一個c/c++的IDE吧——Code:Blocks ,挺好用的集成開發環境,只是第一次用它就喜歡上了,不過和SCIM輸入法稍有衝突,註釋的時候要輸入中文,如果中文文字刪減時,會出現打不上中文的情況,要調成英文狀態再調回中文狀態才能繼續。
另外,它帶有語句聯想功能,當然是只支持用戶空間c函數庫德聯想,不支持kernel中的函數或結構。個人感覺要比Vi用着來的有效率些,呵呵
3、 對於echo 是“!”的問題。截屏中可以看到,要輸出!還要進行轉義字符轉義。
#追加解釋:這裏主要是因爲!是shell下的命令," "雙引號中的shell命令會被解析,這裏可用\!進行轉義,或者使用' '單引號,單引號會不進行解析……shell編程沒基礎,還得加強啊!
4、從文章可以看出,作者我確實夠笨,每次記錄都會碰見如此多的看似比較弱智的問題,好在找到了解決辦法,詳盡寫實的記錄比較符合本人博客的特色。
5、編寫和編譯該驅動的環境並非前邊文章介紹的虛擬機環境,是本機硬盤上的Ubuntu10.04,在我機器上有些時日了,忘了內核版本才凸顯了上邊遇到的很多問題,unlikely(你的開發過程中不會遇到類似的裝載內核和編譯內核不協調的情況);不過你可能會在其他地方遇到,遇到是千萬別說沒見過而束手無策。
6、提前把第一個驅動實驗的記錄過程發blog了,驅動相關的知識還沒有內容介紹,可能和前邊知識不太銜接,得花謝時間儘快補上。不過希望讀者和我同步一起不斷積累內核體系的相關知識,非常方便理解的。
。
。