linux設備驅動三(字符設備驅動)

主設備號和次設備號

  • 主設備號標示設備對應的驅動程序,次設備號用來確定設備文件指向的設備。
  • 現代linux允許多個驅動程序共享主設備號,但是大多數設備仍延用一個主設備號對應一個驅動程序

設備號內部表達

  • 2.6版本中以後的主設備12,次設備20,兼容性的做法是使用宏,而不對設備號的組織進行任何假定
  • MAJOR(dev_t dev) MINOR(dev_t dev) MKDEV(int major, int minor)
    分配和釋放設備號
  • 如果count非常大,則所請求的範圍可能和下一個主設備號重疊,但只要我們所請求的編號範圍是可用的,則不會有什麼問題。疑問:這句話該怎麼理解, 指定了主設備號和次設備號,count很大的時候會分配到下一個主設備號上嗎?請求範圍可用是指區域會跨兩個主設備號?
  • 設備號在使用結束之後需要釋放。
  • 設備號分配完成,但是並沒有和內部函數關聯。

動態分配主設備號

  • Documentation/devices.txt中記錄了系統已經靜態分配的主設備號。
  • 將一個已經分配的靜態編號用於新設備驅動的機會是非常小的。
  • 在使用未分配的設備號時有兩種方式,一種是隨機指定未分配的設備號,另外一種是動態分配。
  • 隨機指定:當一個設備驅動被廣泛使用時,隨機指定也會造成衝突和麻煩。
  • 動態分配:主設備號不能始終保持一致,因此不能預先創建節點。但是可以在/proc/devices中通過name來查找。
  • 爲了加載一個使用動態主設備號的設備驅動程序,對insmod的調用可替換爲簡單的腳本,該腳本在調用insmod之後讀取/proc/devices以獲得新分配的主設備號後創建對應的設備文件。疑問:一般調用register_device之後不是會在dev路徑下生成設備嗎,爲什麼要自己創建?他的意思是不是隻註冊設備驅動程序,而不註冊設備,通過指定主設備號來創建設備,該設備再由driver來處理?
  • 在第一次分配主設備號之後,如果沒有其他模塊併發的情況下rmmod之後再insmod,仍然可以分配到相同的主設備號。
  • 最佳方法是可以通過insmod參數來指定主設備號,不指定時默認爲0,才用動態分配。這樣可以避免反覆創建和刪除設備文件。

一些重要的數據結構

大部分基本的驅動程序操作涉及到三個重要的內部數據結構,file_operations、file和inode
file_operations的第一個字段是一個模塊指針,內核通過該字段來避免在模塊的操作正在進行的時候卸載模塊。
unsigned int (*poll) (struct file *, struct poll_table_struct );
poll,額破裂了,select三個系統調用的後端實現,用來查詢某個或者多個文件描述符上的讀寫操作是否會阻塞,poll返回一個位掩碼,用來支出非阻塞讀寫是否可能,如果將pool定義爲NULL,則設備被認既及可讀也可寫,並且不會阻塞。
int (ioctl)(struct inoode, struct file
, unsigned int, unsigend long);執行設備特定命令的方法,內核還能識別一部分,而不必調用fops中的ioctl,如果設備不支持ioctl,則對於內核未定義的ioctl系統調用將返回錯誤-ENOTTY 設備無此ioctl命令。
int (*mmap)(struct file *, struct vm_area_struct *);將設備內存映射到進程地址空間,沒有實現則返回錯誤-ENODEV
int (*open)(struct inode *, struct file ); 設置爲NULL,設備代開操作永遠成功,但系統不會通知驅動程序。
int (release)(struct inode, struct file
); file結構被釋放時將調用該操作
int (*flush)(struct file *);發生在進程關閉設備文件描述符時,等待設備上尚未完成的操作。和fsync操作不同。如果flush爲NULL,內核忽略請求。
int (fsync)(struct file , struct dentry , int);fsync系統調用的後端實現,刷新待處理的數據。設置爲NULL,系統調用返回-EINVAL。
int (aio_fsync)(struct kiocb, int);fsync異步實現
int (fasync)(int, struct file , int) 通知設備其FASYNC標誌發生了變化。異步通知。不支持可以設置爲NULL
int (lock)(struct file , int, struct file_lock)文件鎖定,設備不會實現該方法
ssize (readv)(struct file, const struct iovec
, unsigned long, loff_t
)
ssize (writev)(struct file , const struct iovec, unsigned long, loff_t); 設置爲NULL,可能多次調用read和write方法
ssize (sendfile)(struct file, loff_t
, size_t, read_actor_t, void
) sendfile系統調用實現,用以最小的複製操作將數據從一個文件描述符移動到另一個。web服務器調用它把文件發送到網絡。驅動通常設置爲NULL
ssize (sendpage)(struct file, struct page
, int, size_t ,loff_t
, int);sendpage是sendfile系統調用的另外一半,由內核調用將數據發送到對應文件,每次一頁,驅動通常設置爲NULL
unsigned long (get_unmapped_area)(struct file, unsigned long, unsigned long, unsigend long, unsigned long);進程地址空間中找到一個合適的位置,吧底層設備中的內存段映射到該位置。通常由內存管理代碼完成,但該方法存在可允許驅動程序強制滿足特定設備需要的任何對其需求。大部分驅動程序可設置爲NULL
int (*check_flags)(int) 該方法允許模塊檢查傳遞給fcntl(F_SETFL…)調用的標誌
int (dir_notify) (struct file, unsigned long);當應用程序使用fcntl來請求目錄改變通知時,該方法將被調用。驅動程序不必實現
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,

}
這種初始化方法是推薦的,緊湊易讀,允許對結構成員進行重新排列,某些場合,把頻繁訪問成員放在相同硬件緩存行上,將大大提高性能。

file結構,驅動程序從不自己填寫file結構,而只是對別處創建的file結構進行訪問

inode結構中 驅動程序有用的兩個字段
dev_t i_rdev; 設備文件對應的設備編號
struct cdev *i_cdev inode指向一個字符設備文件時使用,通過宏來訪問設備號

字符設備註冊

使用單獨的cdev_alloc函數,則不需要使用cdev_init初始化,如果不是單獨申請,而是嵌入到其他結構中,則使用cdev_init初始化cdev結構
所有初始化完成後再調用cdev_add,cdev_add返回之後設備即被激活了,避免併發訪問
cdev_del後不能再訪問cdev對象
通常cdev_add的count參數爲1,也有特例,比如scsi磁盤驅動,通過每個物理設備的多個次設備號來允許用戶空間選擇不同使用模式(比如密度)

早期方法:

int register_chrdev(unsigned int major, const char* name, struct file_operations fios);
major是主設備號,name是驅動程序的名稱。register_chrdev將爲指定的主設備號註冊0-255作爲此設備號,併爲每個設備監理一個對應的cdev結構。使用這一接口的驅動程序必須能夠處理所有256個次設備號上的open調用。
移除調用int unregister_chrdev(unsigned int major, const char
name);major和name必須一致,疑問,每次移除驅動上的所有255個設備?

open和release

  • open方法:
    檢查設備特定錯誤
    如果設備首次打開,進行初始化
    如有必要,更新f_op
    分配並填寫置於filp->private_data裏的數據結構

  • release方法:
    釋放由open分配的,保存在filp->private_data中的所有內容
    在最後一次關閉操作時關閉設備

注意:flush方法hi在應用程序每次調用close時會被調用,不過很少驅動程序會實現flush,因爲在close時並沒有什麼事情去做,除非releae被調用。
內核在進程退出的時候,通過在內部使用close系統調用來關閉所有相關文件。

scull的內存使用

  • NULL指針傳遞給kfree是合法的
  • 通過cp /dev/zero /dev/scull0用光所有的系統RAM
  • 驅動程序中仍和不確定的或與策略相關的值,都可以使用編譯期間和加載截斷進行配置。

內核不能直接引用用戶空間內存地址

  • 原因:架構和內核配置的不同,在內核模式中運行時,用戶空間的指針可能是無效。該地址可能根本無法映射到內核空間,或者指向某些隨機數。
    用戶空間內存是分頁,可能被換出了,根本不在RAM中,
    用戶程序可能存在缺陷或者是個惡意程序,可能允許隨意訪問內核資源或者覆蓋系統中的內存,危及系統安全

  • 函數:unsigned long copy_to_user
    unsigned long copy_from_user

    使用時可以使用這兩個函數來檢查指針是否有效,無效就不會拷貝
    拷貝過程遇到無效地址則僅僅複製部分數據,
    在這兩種情況下,返回值是還需要拷貝的數值,通常在使用時,如果不爲0,返回給用戶-EFAULT錯誤。

    確認不需要內核檢查用戶空間指針(其他地方已經檢查過了),可以使用函數:
    __copy_to_user 和 __copy_from_user

  • 注意:內核中訪問用戶空間的內存,可能將該進程轉入睡眠狀態,
    所以訪問用戶空間的任何函數必須是可重入的,
    必須能夠和其他驅動程序函數併發執行,
    必須處於能夠合法休眠的狀態。
    read和write會將文件位置的變化傳回file結構,pread和pwrite系統調用不會修改文件位置。
    strace監控應用程序調用的系統調用以及它們的返回值。

總結:

完成一個字符設備的簡單過程可以描述如下:

  • 註冊設備號,指定register_chrdev_region或者動態分配alloc_chrdev_region
  • 創建設備對象cdev cdev_alloc
  • 初始化設備 cdev_init,是否單獨調用時可選的,設置其他值owner,操作函數表ops等
  • cdev_add,把創建的設備及它對應的設備號告知內核

需要注意

  • 設置的操作函數表ops中每個函數的具體語義要認識準確
  • 用戶空間的地址不能隨便使用,要有限制及理解清楚限制條件
  • 註冊設備使用的早起方法應該避免使用,後續可能從內核中移除
  • 設備註冊過程中可能發生的併發訪問
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章