linux字符設備放在“/dev”目錄中。字符設備是指只能一個字節一個字節進行讀寫操作的設備,一般每個字符設備或者塊設備都會在“/dev”目錄下對應一個設備文件,并且每個設備文件都必須有主/次設備號,主設備號相同的設備是同類設備,使用同一個驅動程序。
本教程操作環境:linux7.3系統、Dell G3電腦。
1、Linux設備驅動分類
Linux系統將設備分為三個類:字符設備、塊設備、網絡設備,在這三大類中,字符設備相對比較簡單,應用程序通過字符設備文件來訪問字符設備,本講主要介紹字符設備,如果對塊設備和網絡設備感興趣的話,可以參看相關資料,并對其進行深入了解。
2、什么是字符設備?
字符設備是指只能一個字節一個字節進行讀寫操作的設備,不能隨機讀取設備中的某一數據、讀取數據要按照先后順序。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制臺和LED等。
一般每個字符設備或者塊設備都會在/dev目錄下對應一個設備文件,并且每個設備文件都必須有主/次設備號,主設備號相同的設備是同類設備,使用同一個驅動程序。
Linux用戶層程序通過設備文件來使用驅動程序操作字符設備或塊設備。
可以通過
cat /proc/devices
命令查看當前已經加載的設備驅動程序的主設備號。
通過在/dev目錄下執行命令
ls?-l
可以看到所有設備文件的主設備號和次設備號:
對常見設備文件作如下說明:
/dev/hd[a-t]:IDE設備 /dev/sd[a-z]:SCSI設備 /dev/fd[0-7]:標準軟驅 /dev/md[0-31]:軟raid設備 /dev/loop[0-7]:本地回環設備 /dev/mem:內存 /dev/null:無限數據接收設備,相當于黑洞 /dev/zero:無限零資源 /dev/tty[0-63]:虛擬終端 /dev/ttyS[0-3]:串口 /dev/lp[0-3]:并口 /dev/console:控制臺 /dev/fb[0-31]:framebuffer /dev/cdrom?=>?/dev/hdc /dev/modem?=>?/dev/ttyS[0-9] /dev/pilot?=>?/dev/ttyS[0-9]
3、如何建立設備文件?
建立設備文件有兩種方式,一是通過系統調用mknod(),編程中調用該函數可以建立一個新的設備文件名,另外一種就是通過mknod命令,命令的第一個參數為設備文件名,第二個參數為設備類型,比如c表示字符設備,第三、四個參數為設備文件的主設備號和次設備號,比如231和0。主設備號和次設備號合起來唯一的確定一個設備,同一個設備不同類型的主設備號是一樣的,次設備號不同,比如一個硬盤的多個分區就有不同的次設備號,通過主設備號就可以把設備文件與驅動程序關聯起來。
mknod filename type major minor
- filename:要創建的設備文件名;
- type:設備類型,c代表一個字符設備,b代表一個塊設備;
- major:主設備號;
- minor:次設備號;
4、如何描述字符設備?
Linux內核中抽象出Struct cdev結構體來表示一個字符設備,cdev 定義于
struct?cdev?{ ????????struct?kobject?kobj;???//??內嵌內核對象 ????????struct?module?*owner;??//該字符設備所在的內核模塊 ????????const?struct?file_operations?*ops;?//文件操作結構體 ????????struct?list_head?list;??//已注冊字符設備鏈表 ????????dev_t?dev;?//由主、次設備號構成的設備號 ????????unsigned?int?count;//同一主設備號的次設備號的個數 };
Linux使用file_operations結構訪問驅動程序的函數,這個結構的每一個成員的名字都對應著一個系統調用。
struct?file_operations?{ ???????struct?module?*owner; ???????loff_t?(*llseek)?(struct?file?*,?loff_t,?int); ???????ssize_t?(*read)?(struct?file?*,?char?*,?size_t,?loff_t?*); ???????ssize_t?(*write)?(struct?file?*,?const?char?*,?size_t,?loff_t?*); ???????int?(*readdir)?(struct?file?*,?void?*,?filldir_t); ???????unsigned?int?(*poll)?(struct?file?*,?struct?poll_table_struct?*); ???????int?(*ioctl)?(struct?inode?*,?struct?file?*,?unsigned?int,?unsigned?long); ???????int?(*mmap)?(struct?file?*,?struct?vm_area_struct?*); ???????int?(*open)?(struct?inode?*,?struct?file?*); ???????int?(*flush)?(struct?file?*); ???????int?(*release)?(struct?inode?*,?struct?file?*); ???????int?(*fsync)?(struct?file?*,?struct?dentry?*,?int?datasync); ???????int?(*fasync)?(int,?struct?file?*,?int); ???????int?(*lock)?(struct?file?*,?int,?struct?file_lock?*); ???????ssize_t?(*readv)?(struct?file?*,?const?struct?iovec?*,?unsigned?long,loff_t?*); ???????ssize_t?(*writev)?(struct?file?*,?const?struct?iovec?*,?unsigned?long,????loff_t?*); ????};
用戶進程利用在對設備文件進行諸如read,write操作的時候,系統調用通過設備文件的主設備號找到相應的設備驅動程序,然后讀取這個數據結構相應的函數指針,接著把控制權交給該函數,這是Linux的設備驅動程序工作的基本原理。
5、字符設備與文件系統的接口
如圖,在Linux內核中,最左邊, 使用cdev結構體來描述字符設備;通過其成員dev_t來定義設備號(分為主、次設備號)以確定字符設備的唯一性;通過其成員file_operations來定義字符設備驅動提供給虛擬文件系統VFS的接口函數,如常見的open()、read()、write()等,這些函數真正的操作硬件設備。
在上一個圖的基礎上我們看這個圖,字符設備驅動程序是以內核模塊的形式加載到內核中的,首先模塊加載函數按靜態或者動態方式獲取設備號;然后字符設備初始化函數建立cdev與 file_operations之間的連接, 通過注冊函數向系統添加一個cdev以完成注冊; 模塊卸載時與加載對應,要注銷cdev,并釋放設備號。
在用戶程序中,可以通過系統調用open(), read(), write()等調用驅動程序在內核中所實現的這些函數。這樣用戶態到內核驅動之間的通路就打通了。
6、編寫簡單的字符設備驅動程序
如圖,編寫字符設備驅動分為三大步驟:
- 驅動的初始化,其中又分為四個步驟,調用相關的函數達到。
- 實現設備的操作,具體的操作取決于你自己所要實現的功能,這里只列出了基本的操作
- 驅動的注銷,注銷就是釋放資源。
其中調用的接口函數功能如下:
第1個函數是分配函數,動態申請cdev的內存,給該結構分配內存空間。
第2個函數是初始化函數,初始化cdev的成員,并建立cdev和file_operations之間關聯.
第3個函數注冊cdev設備對象,也就是把字符設備添加到字符設備表中,就像大家入學時進行注冊一樣。
第4個函數是注銷驅動程序調用,將cdev對象從系統中刪除。
第5個函數釋放cdev數據結構所占的內存。
一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。注冊時申請設備號,注銷時釋放設備號,就像大家入學是有一個學號,畢業離開時就釋放掉這個學號。
當我們在用戶程序中調用read()函數時,陷入內核空間,實際上要通過內核的copy_to_user()函數把內核空間緩沖區中的數據拷貝到用戶空間的緩沖區,反之,當我們調用write()函數時,內核通過調用copy_from_user()函數把用戶空間的數據拷貝到內核緩沖區。