初學Linux驅動程序的時候,可能對什麼是字符設備驅動(char device)和雜項設備驅動(misc device)並不是很瞭解,更談不上如何區分了。我自己當初在學習Linux字符設備驅動的時候,也並沒有特地去了解其兩者之間的區別,尤其是在兩種驅動設備註冊的時候,沒有意識到其不同之處,導致後來在項目中出現了很嚴重的問題,但卻遲遲到找不到解決方案。所以今天就趁這個機會,好好分析一下兩者之間的區別,以便有出現在我類似問題的朋友,以後不會再犯同樣的錯誤。
普通字符型設備
對於字符型設備,可能學習過Linux設備驅動的,都應該是比較瞭解得,並且我們在項目中大多要自己編寫的驅動設備也都是字符類型的。在普通的字符型設備驅動註冊的過程中,需要經過以下這幾個步驟:
- 申請設備號
- 動態申請設備號(alloc_chrdev_region)
- 靜態申請設備號(register_chrdev_region)
- 設備註冊
- 爲cdev分配空間(cdev_alloc)
- 初始化cdev(cdev_init)
- 將cdev添加進Kernel(cdev_add)
- 生成設備節點
- 創建class(class_create)
- 通過class,創建設備節點(device_create)
struct cdev *cdev_alloc(void) //無參數
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); //爲cdev申請內存
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic); //這兩步是初始化cdev,如將cdev加入鏈表中
}
return p;
}
由上面的代碼可以知道,cdev_alloc函數主要做了兩件事情,第一件事在內核中,爲cdev申請內存(這個是每一個驅動設備文件描述結構體需要做的事情,申請完cdev的內存之後,其cdev的地址將存放在inode結構中的i_cdev成員中)。第二件事情就是,初始化cdev,將其放入相應的鏈表中。最後返回申請到的cdev結構體的地址。下面我們再來看一下cdev_init函數:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev); //將cdev結構體裏面的內容清零
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default); //上面這兩步,與cdev_alloc一樣,初始化cdev
cdev->ops = fops; //將驅動程序的file_operation地址賦值給cdev的ops指針,這樣cdev就真正具有了操作驅動的作用了。
}
由上面的代碼,我們可以看到cdev_init函數並沒有申請cdev的內存空間,所以要使用cdev之前,應該自己爲自己驅動的cdev變量申請內存空間,並用變量將其引用。例如:struct cdev *p
= kzalloc(sizeof(struct cdev), GFP_KERNEL);然後將p傳遞給cdev_init。同時我們還看到,在cdev_init中,還包含了將file_operation的地址指向cdev的ops指針,這也是非常關鍵的一步,以後只要通過inode知道i_cdev了,那麼也就知道file_operation了,從而就知道如何操作該驅動了。雜項設備驅動,是對字符設備的一種封裝,是一種特殊的字符型設備驅動,也是在Linux嵌入式設備中使用的比較多的一種驅動。之所以很大一部分驅動使用的是雜項設備驅動,主要有以下幾個方面的原因(由知乎網友整理):
使用普通字符設備,不管該驅動的主設備號是靜態還是動態分配,都會消耗一個主設備號,這太浪費了。而且如果你的這個驅動最終會提交到內核主線版本上的話,需要申請一個專門的主設備號,這也麻煩。
如果使用misc驅動的話就好多了。因爲內核中已經爲misc驅動分配了一個主設備號。當系統中擁有多個misc設備驅動時,那麼它們的主設備號相同,而用子設備號來區分它們。
第二,使用簡單:
有時候驅動開發人員需要開發一個功能較簡單的字符設備驅動,導出接口讓用戶空間程序方便地控制硬件,只需要使用misc子系統提供的接口即可快速地創建一個misc設備驅動。
當使用普通的字符設備驅動時,如果開發人員需要導出操作接口給用戶空間的話,需要自己去註冊字符驅動,並創建字符設備class以自動在/dev下生成設備節點,相對麻煩一點。而misc驅動則無需考慮這些,基本上只需要把一些基本信息通過struct miscdevice交給misc_register()去處理即可。
本質上misc驅動也是一個字符設備驅動,可能相對特殊一點而已。在drivers/char/misc.c的misc驅動初始化函數misc_init()中實際上使用了MISC_MAJOR(主設備號爲10)並調用register_chrdev()去註冊了一個字符設備驅動。同時也創建了一個misc_class,使得最後可自動在/dev下自動生成一個主設備號爲10的字符設備。總的來講,如果使用misc驅動可以滿足要求的話,那麼這可以爲開發人員剩下不少麻煩。
struct chr_dev
{
char trig_flag1,trig_flag2; //發送數據標誌位,
char rise_flag1,rise_flag2;
struct cir_buf rx_buf;
wait_queue_head_t rd_waitq;
struct cdev dev;
struct timer_list s_timer;
struct timeval tv1; //用來獲取當前時間
struct tasklet_struct my_tasklet;
int h_rise_time1;
int l_rise_time1;
int h_fall_time1;
int l_fall_time1;
int distance1;
char irqflag;
};
在這個chr_dev的結構體中,我也定義了cdev結構體,也就是說,按照我這樣的編寫,應該是採用普通字符設備驅動的註冊方式進行註冊。因爲對於雜項設備驅動程序,是不需要爲驅動設備程序定義cdev結構體的。因爲只要使用到misc_register函數,對於cdev這些結構體的定義和內存分配,全部自己自動執行了。但是可以看到我們這裏的chr_dev結構體裏面不光包含有cdev,還有許多其他的變量和結構體,因此使用雜項設備驅動程序,自然是不行的。但是由於之前不瞭解雜項設備的內在原理,所以我在定義了chr_dev的這個全局結構體的情況下使用了雜項設備註冊方式misc_register註冊。並且最重要的是我並沒有對chr_dev進行初始化,或者爲其分配內存,直接在open等函數裏面使用如下的機制:static int Radar_open(struct inode *inode, struct file *file)
{
//獲取設備結構體的地址
pdev= container_of(inode->i_cdev, struct chr_dev, dev);
file->private_data=pdev;
}
我使用這個機制的本意是通過inode結構體中的i_cdev地址獲取到chr_dev結構體的地址,但此時的i_cdev與chr_dev結構體中的cdev半點關係都沒有,並且我也沒有爲chr_dev分配內存。所以這裏得到的pdev結構體的地址必然會有問題,要麼就佔用了其它變量的地址,要麼就可能會有地址不存在的問題,從而引發如下錯誤:[ 71.824461] Unable to handle kernel paging request at virtual address 64bb7ed0
[ 71.830217] pgd = c0004000
[ 71.832908] [64bb7ed0] *pgd=00000000
[ 71.836469] Internal error: Oops: 5 [#1] PREEMPT SMP
[ 71.841414] Modules linked in:
[ 71.844454] CPU: 0 Not tainted (3.0.15 #4)
[ 71.848887] PC is at __queue_work+0x8c/0x3d4
[ 71.853134] LR is at __queue_work+0x70/0x3d4
[ 71.857387] pc : [<c009a8ec>] lr : [<c009a8d0>] psr: 600000d3
[ 71.857391] sp : d6301c68 ip : d632d584 fp : d6301ca4
[ 71.868843] r10: ffff4dd0 r9 : 00000014 r8 : 00000001
[ 71.874052] r7 : d6289ea0 r6 : d632d580 r5 : 200000d3 r4 : c09ac080
[ 71.880562] r3 : 92dd8f00 r2 : e92dd8f0 r1 : e92dd8f4 r0 : c0041b00
[ 71.887072] Flags: nZCv IRQs off FIQs off Mode SVC_32 ISA ARM Segment kernel
[ 71.894537] Control: 10c5387d Table: 548a804a DAC: 00000015
[ 77.298503] pgd = d3ddc000
[ 77.301194] [aa6fb9a0] *pgd=00000000
[ 77.304761] Internal error: Oops: 805 [#1] PREEMPT SMP
[ 77.309870] Modules linked in:
[ 77.312921] CPU: 0 Not tainted (3.0.15 #24)
[ 77.317458] PC is at T.389+0x54/0xbc
[ 77.320999] LR is at s3c_gpiolib_set+0x54/0x58
[ 77.325427] pc : [<c025eae4>] lr : [<c0067434>] psr: 600001d3
[ 77.325448] sp : d3f9bb00 ip : d3f9a018 fp : d3f9bb24
[ 77.336872] r10: d4471e80 r9 : 00001414 r8 : 00000000
[ 77.342081] r7 : 00000000 r6 : 00000001 r5 : 0000000f r4 : aa6fb9a0
[ 77.348591] r3 : 00000000 r2 : 40010002 r1 : 40010003 r0 : 0000000c
[ 77.355107] Flags: nZCv IRQs off FIQs off Mode SVC_32 ISA ARM Segment user
[ 77.362393] Control: 10c5387d Table: 5494c04a DAC: 00000015