1. 程式人生 > >LINUX設備驅動程序筆記(三)字符設備驅動程序

LINUX設備驅動程序筆記(三)字符設備驅動程序

準備 p s con 文件系統 write post container form nod

<一>.主設備號和次設備號
對字符設備的訪問時通過文件系統內的設備名稱進行的。那些設備名稱簡單稱之為文件系統樹的節點,它們通常位於/dev文件夾。

字符設備驅動程序的設備文件可通過ls -l命令輸出的第一列中的‘c‘來識別。

塊設備相同位於/dev下,由字符‘b‘標識
crw-rw---- 1 root root 253, 0 2013-09-11 20:33 usbmon0
crw-rw---- 1 root root 253, 1 2013-09-11 20:33 usbmon1
crw-rw---- 1 root root 253, 2 2013-09-11 20:33 usbmon2
brw-rw---- 1 root disk 8, 0 2013-09-11 20:33 sda
brw-rw---- 1 root disk 8, 1 2013-09-11 20:34 sda1
brw-rw---- 1 root disk 8, 2 2013-09-11 20:33 sda2
主設備號標識設備相應的驅動程序,現代的Linux內核同意多個驅動程序共享主設備號,但大多數設備仍然依照"一個主設備相應一個驅動程序"的原則組織。


次設備號由內核使用,用於正確確定設備文件所指的設備。能夠通過次設備號獲得一個指向內核設備的直接指針,也可將次設備號當做設備本地數組的索引。無論用哪種方式。處理知道次設備號用來指向驅動程序所實現的設備之外,內核本身基本不關心關於次設備號的不論什麽其它信息。
1.設備標號的內部表達
在內核中,dev_t類型(在<linux/types.h>中定義)用來保存設備編號----包含主設備號和次設備號。要獲得dev_t的主設備號和次設備號。要使用<linux/kdev_t.h>中定義的宏MAJOR/MINOR:MAJOR(dev_t dev); MINOR(dev_t dev);相反,假設須要將主設備號與次設備號轉換為dev_t類型。則使用MKDEV(int major, int minor);
2.分配和釋放設備編號
在建立一個字符設備前,首先要做的就是獲得一個或者多個設備編號。

完畢該工作的必要函數在<linux/fs.h>中定義:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
a.first是要分配的設備編號範圍的起始值。first的此設備號常常被置為0。但對該函數來講並非必須的。
b.count是所請求的連續設備編號的個數。
c.name是和該編號範圍關聯的設備名稱,它將出如今/proc/devices和sysfs中。
d.register_chrdev_region的返回值在分配成功時為0,在錯誤情況下,將返回一個負的錯誤碼。而且不能使用所請求的編號區域。


假設不知道設備將要使用哪些主設備號。就要使用alloc_chrdev_region動態分配設備編號,

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
a.dev是僅用於輸出的參數。在成功調用後將保存已分配範圍的第一個編號。
b.firstminor應該是要使用的被請求的第一個次設備號,它一般是0。
c.count和name參數與register_chrdev_region函數式一樣的。
不論採用哪種方法分配設備編號。都應該在不再使用它們時釋放這些設備編號,設備編號的釋放須要使用以下的函數void unregister_chrdev_region(dev_t first, unsigned int count);通常我們在清除函數中調用nregister_chrdev_region函數。
在用戶空間程序能夠訪問上述設備編號之前。驅動程序須要將設備編號和內部函數連接起來。這些內部函數用來實現設備的操作。
3.動態分配主設備號一部分主設備號已經靜態地分配給了大部分常見設備。

在內核源代碼樹的Documentation/devicex.txt文件裏能夠找到這些設備的清單。對於一個驅動程序。講義不要隨便選擇一個當前未使用的設備號作為主設備號,而應該使用動態分配機制獲取主設備號。
動態分配的缺點是:因為分配的主設備號不能保證始終一致,所以無法預先創建設備節點。為了載入一個使用動態主設備號的設備驅動程序,對insmod的調用可替換為一個簡單的腳本,該腳本在調用insmod之後讀取/proc/devices以獲得新分配的主設備號,然後創建相應的設備文件。分配主設備號的最佳方式是:默認採用動態分配。同一時候保留在載入甚至是編譯時指定主設備號的余地。


<二> 一些重要的數據結構
大部分主要的驅動程序操作涉及到三個重要的內核數據結構,各自是file_operations、file和inode。


1.文件操作
file_operations結構用來建立連接設備編號和驅動程序操作。該結構定義在<linux/fs.h>中。當中包括了一組函數指針。每一個打開的文件(後面提到的file)和一組函數關聯。驅動程序的操作主要用來實現系統調用,命名為open、read等,能夠覺得文件時一個“對象”。而操作它的函數式“方法”。file_operations結構或者指向這類結構的指針稱為fops。這個結構中的每一個字段都必須指向驅動程序中實現特定操作的函數。對於不支持的操作,相應的字段可置為NULL值。對於各個函數而言,假設相應字段被賦為NULL指針,那麽內核的詳細處理行為不盡同樣。
在file_operations裏。有很多參數包括有__user字符串,它事實上是一種形式的文檔而已,表明指針式一個用戶空間地址。因此,不能被直接引用。
struct module *owner:指向“擁有”該結構的模塊的指針。內核使用這個字段以避免在模塊的操作正在被使用時卸載該模塊。該成員被初始化為THIS_MODULE,它是定義在<linux/module.h>中
int (*open)(struct inode *, struct file *):這是對設備文件的第一個操作,然而卻不要求驅動程序一定要聲明一個相
應的方法。

假設這個入口為NULL,設備的打開操作永遠成功,但系統不會通知驅動程序。


int (*release)(struct inode *, struct file *):當file結構被釋放時。將調用這個操作。與open相仿,也可將release設
置為NULL,release並非在進程每次調用close時都會被調用。僅僅要file結構被共享,release就會等到全部的副本都關閉之後才會得到調用。

假設須要關閉隨意一個副本時刷新那些待處理的數據。則應事先flush方法。
int (*flush)(struct file *):對flush操作的調用發生在進程關閉設備文件描寫敘述符副本的時候,它應該運行設備上尚未完結的操作。

假設flush被置為NULL,內核將簡單忽略用戶程序程序的請求。


unsigned int (*poll)(struct file *, struct poll_table_struct *):poll方法是poll/epoll和select這三個系統調用的後端實現。poll方法應該返回一個位掩碼,用來指出非堵塞的讀取或寫入是否可能,而且也會向內核提供調用進程置於休眠狀態直到I/O變為可能時的信息。假設驅動程序將poll方法定義為NULL。則設備會被覺得既可讀也可寫。而且不會被堵塞。
ssize_t (*read)(struct file *, char __user *, size_t, lofft_t *):用來從設備中讀取數據。該函數指針被賦予NULL時,將導致read系統調用出錯並返回-EINVAL。函數返回非負值表示成功讀取的字節數。


ssize_t (*write)(struct file *, const char __user *, size_t, loff_t):向設備發送數據,假設沒有這個函數。write
系統調用會向程序返回一個-EINVAL,假設返回值非負。則表示成功寫入的字節數。


2.file結構
在<linux/fs.h>中定義的struct file是設備驅動程序所使用的第二個重要的數據結構。

註意,file結構與用戶空間程序中的FILE沒有不論什麽關聯。

FILE在C庫中定義且不會出如今內核代碼中。而struct file是一個內核結構。不會出如今用戶程序中。
file結構代表一個打開的文件。它由內核在open時創建。並傳遞給在該文件上進行操作的全部函數,知道最後的close函數。在文件的全部實例都被關閉之後。內核會釋放這個數據結構。struct file中最重要的成員羅列例如以下:
mode_t f_mode:文件模式,它通過FMODE_READ和FMODE_WRITE位來標識文件是否可讀或可寫,因為內核在調用驅動程序的read和write前已經檢查了訪問權限。所以不必為這兩個方法檢查權限。在沒有獲得相應訪問權限而打開文件的情況下,對文件的讀寫操作將被內核拒絕,驅動程序無需為此而作額外的推斷。


unsigned int f_flags:文件標誌,如O_RDONLY/O_NONBLOCK/O_SYNC。為了檢查用戶請求是否是非堵塞式的操作。驅動程序須要檢查O_NONBLOCK標誌,而其它標誌非常少用到。

註意,檢查讀/寫權限應該查看f_mode而不是f_flags。全部這些標誌都定義在<linux/fcntl.h>中loff_t f_pos:當前的讀/寫位置。loff_t是一個64位的數。假設驅動程序須要知道文件裏的當前位置,能夠讀取這個值,但不要去改動它。read/write會使用它們接收到的最後那個指針參數來更新這個位置,而不是直接對file->f_pos進行操作。這個規則的一個例外是llseek方法,該方法的目的本身就是改動文件位置。
struct file_operations *f_op:與文件相關的操作。內核在運行open操作時對這個指針賦值。以後須要處理這些操作時讀取這個指針。file->f_op中的值決不會為方便引用而保存起來,也就是說。我們能夠在不論什麽須要的時候改動文件的關聯操作,在返回給調用者之後,新的操作方法就會馬上生效。
void *private_data:open系統調用在調用驅動程序的open方法前將這個指針置為NULL。驅動程序能夠將這個字段用於不論什麽目的或者忽略這個字段。
3.inode結構
內核用inode結構在內部表示文件。因此它和file結構不同。後者表示打開的文件描寫敘述符。對單個文件。可能會有很多個表示打開的文件描寫敘述符的file結構,但他們都指向單個inode結構。該結構中僅僅有兩個字段對編寫驅動程序實用:
dev_t i_rdev:對表示設備文件的inode結構。該字段包括了真正的設備編號
struct cdev *i_cdev:struct cdev是表示字符設備的內核的內部結構。當inode指向一個字符設備文件時,該字段包好了指向struct cdev結構的指針。

為了防止內核版本號的升級帶來的不兼容問題,一般不直接使用i_rdev。而是使用以下的宏獲得設備號:
unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);


<三>字符設備的註冊
在內核調用設備的操作之前,必須分配並註冊一個或多個struct cdev結構。

為此。必須包括<linux/cdev.h>,當中定義了該結構及相關的輔助函數:
分配、初始化struct cdev結構的兩種方式:
struct cdev *my_cdev = cdev_allo();
void cdev_init(struct cdev *cdev, struct file_operations *fops);
my_cdev->ops = &my_fops;
另一個struct cdev的字段須要初始化,和file_operations結構類似,struct cdev也有一個全部者字段。應被設為THIS_MODULE。


在cdev結構設置好之後。最後的步驟就是通過以下的調用告訴內核該結構的信息:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

dev是cdev結構,num是該設備相應的第一個設備編號,count是應該和該設備關聯的設備編號的數量,count常常取1。在使用cdev_add時,須要註意:

a.這個調用可能會失敗。

假設它返回一個負的錯誤滿,則設備不會被加入到系統中。

b.僅僅要cdev_add返回了,設備的操作就會被內核調用。因此。在驅動程序還沒有全然準備優點理設備上的操作時。就不能調用cdev_add。

要從系統中移除一個字符設備。做例如以下調用:void cdev_del(struct cdev *dev);將dev傳遞給cdev_del函數之後。就不應再訪問cdev結構了。


早起的辦法:

註冊字符設備驅動程序的經典方式:int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);假設使用register_chrdev函數,將自己的設備從系統移除的正確函數是:int unregister_chrdev(unsigned int major, const char *name);


<四>open和release
1.open方法:open方法提供給驅動程序以初始化的能力,從而為以後的操作完畢初始化做準備。在大部分驅動程序中。open應完畢例如以下工作:

a.檢查設備特定的錯誤

b.假設設備是首次打開。則對其進行初始化

c.假設有必要。更新f_op指針

d.分配並填寫置於filp->private_data裏的數據結構

首先要做的是確定要打開的詳細設備,open方法原型是:int (*open)(struct inode *inode,struct file *filp);當中的inode參數在其i_cdev字段中包括了我們所須要的信息,即我們先前設置的cdev結構。

唯一的問題是,我們通常不須要cdev結構本身,而是希望得到包括cdev結構的scull_dev結構。

通過定義在<linux/kernel.h>中的container_of宏實現:container_of(pointer, container_type, container_field);這個宏須要一個container_field字段的指針。該字段包括在container_type類型的結構,然後返回包括該字段的結構指針。
struct scull_dev *dev; /*device information*/
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /*for other methods*/
還有一個確定要打開的設備的方法是:檢查保存在inode結構體中的次設備號。
2.release方法:release方法的作用正好與open相反,這個設備方法都應該完畢以下的任務:

a.釋放由open分配的、保存在filp->private_data中的全部內容。

b.在最後一側關閉操作時關閉設備


<五>read 和 write
read和write方法完畢的任務相似,即拷貝數據到應用程序空間,或反過來從應用程序空間拷貝數據。
ssize_t read(struct file *filp, char __user *buff, size_t conut, loff_t *offp);
ssize_t write(struct file *filp, char __user *buff, size_t conut, loff_t *offp);
參數filp是文件指針。參數count是請求傳輸的數據長度。參數buff是指向用戶空間的緩沖區。這個緩沖區或者保護要寫入的數據,或者是一個存放讀入數據的空緩沖區。最後的offp是一個指向"long offset type"對象的指針。這個對象指明用戶在文件裏進行存取操作的位置。
須要指出,read和write方法的buff參數是用戶空間的指針,因此,內核代碼不能直接引用當中的內容。出現這樣的限制的原因有例如以下幾個:
a.隨著驅動程序所執行的架構的不同或者內核配置的不同,在內核模式中執行時,用戶空間的指針可能是無效的。

該地址可能根本無法被映射到內核空間,或者可能指向某些隨機數據。
b.即使該指針在內核空間中代表同樣的東西,但用戶空間的內存是分頁的,而在系統調用被調用時,涉及到的內存可能根本不在RAM中。對用戶空間內存的直接引用將導致頁錯誤,而這對內核代碼來說是不同意的發生的,其結果可能是一個"oops",它將導致調用該系統調用的進程的死亡。
c.我們討論的指針可能由用戶程序提供。而該程序可能存在缺陷或者是個惡意程序。假設驅動程序盲目引用用戶提供的指針,將導致系統出現打開的後門。從而同意用戶空間程序訪問或覆蓋系統中的內存。假設讀者不打算由於自己的驅動程序而危及用戶系統的安全性,則永遠不要直接引用用戶空間指針。
read和write代碼要做的就是在用戶地址空間和內核地址空間之間進行整段數據的拷貝。

read和write方法的實現核心在:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void __user *to, const void *from, unsigned long count);

這兩個函數的作用並不限於在內核空間和用戶空間之間拷貝數據。它們還檢查用戶空間的指針是否有效。

假設指針無效。就不會進行數據拷貝;還有一方面。假設在拷貝過程中遇到無效地址,則只會復制部分數據。在這兩種情況下,返回值還須要拷貝的內存數量值。至於實際的設備方法,read方法的任務是從設備拷貝數據到用戶空間。而write方法則是從用戶空間拷貝數據到設備上。每次read或write系統調用都會請求一定數目的字節傳輸,只是驅動程序也並不限制小數據量的傳輸。

不管傳輸多少數據。都應更新*offp所表示的文件位置,以便反應在新系統調用成功完畢之後當前的文件位置。出錯時,read和write方法都返回一個負值,大於等於0的返回值告訴調用程序成功傳輸了多少字節。

假設在正確傳輸部分數據之後發生了錯誤,則返回值必須是成功傳輸的字節數。雖然內核函數通過返回負值來表示錯誤,並且返回值表明了錯誤的類型,但執行在用戶空間的程序看到的時鐘是作為返回值的-1。


1.read方法:
調用程序對read的返回值解釋例如以下:
a.假設返回值等於傳遞給read系統調用的count參數。則說明所請求的字節數傳輸成功完畢了。
b.假設返回值是正的。可是比count小,則說明僅僅有部分數據成功傳送。

這樣的情況因設備的不同可能有很多原因。大部分情況下。程序會又一次讀數據。
c.假設返回值為0,則表示已經到達了文件尾。
d.負值意味著發生了錯誤,該值指明了發生了什麽錯誤。錯誤碼在<linux/errno.h>中定義。


2.write方法:

與read類似,依據例如以下返回值規則,write也能傳輸少於請求的數據量:
a.假設返回值等於count,則完畢了所請求數目的字節傳送
b.假設返回值是正的,但小於count,則僅僅傳輸了部分數據。程序可能再次試圖寫入余下的數據。


c.假設值為0,意味著什麽也沒寫入,這個結果不是錯誤。並且也沒有理由返回一個錯誤碼。

d.賦值意味著發生了錯誤,與read同樣。有效的錯誤碼定義在<linux/errno.h>中。

LINUX設備驅動程序筆記(三)字符設備驅動程序