Linux字符設備驅動和雜項設備驅動對比

初學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)
以上這些步驟其實在Linux2.6版本之前,都是直接使用register_chrdev函數一步到位,全部完成的。雖然使用register_chrdev在驅動程序註冊的過程中變得簡單了,但是卻存在很大侷限性,尤其用戶自定性不高。因此目前的Linux內核,這兩種方式都提供,但通常採用的方式,就是我上面寫的那幾個步驟。在設備註冊這一步驟需要注意的是cdev_alloc和cdev_init這兩個函數之間的區別。關於相同點和不同點,我還是直接上它們的代碼進行分析:
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驅動可以滿足要求的話,那麼這可以爲開發人員剩下不少麻煩。

正是因爲雜項設備驅動不需要開發人員考慮普通字符型設備註冊驅動程序時所需要的步驟,例如:設備號申請、添加設備cdev、在/dev下生成字符設備等等,這是因爲雜項設備對這些流程都進行了封裝,只需要使用misc_register函數即可以自動實現普通字符設備註冊所需要的步驟,所以我自己在編寫驅動程序的過程中也頻繁使用到該類型的設備驅動。但是由於之前對於雜項設備驅動和普通字符型設備驅動不是很熟悉,所以導致我在編寫驅動的過程中,也犯了一個非常嚴重的錯誤,並且這個錯誤所導致的原因一直困擾着好久。

下面我將這個錯誤記錄下來:
首先我在編寫驅動的時候,定義了一個全局的結構體,結構體是這樣的:
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.292704] Unable to handle kernel paging request at virtual address aa6fb9a0
[   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


總之每一次導致出錯的地址和函數都不一樣,而且也不好追蹤到底是哪裏出了問題。最後的解決方式當然是換回普通字符設備註冊驅動的方式,首先在驅動的init函數中給chr_dev結構體分配內存,然後將che_dev->dev使用cdev_add函數加入Kernel中,這一問題才得以成功解決。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章