一文帶你掌握Linux字符設(shè)備架構(gòu)
一、Linux設(shè)備分類
Linux系統(tǒng)為了管理方便,將設(shè)備分成三種基本類型:
字符設(shè)備 塊設(shè)備 網(wǎng)絡(luò)設(shè)備
字符設(shè)備:
字符(char)設(shè)備是個(gè)能夠像字節(jié)流(類似文件)一樣被訪問的設(shè)備,由字符設(shè)備驅(qū)動(dòng)程序來實(shí)現(xiàn)這種特性。字符設(shè)備驅(qū)動(dòng)程序通常至少要實(shí)現(xiàn)open、close、read和write的系統(tǒng)調(diào)用。
字符終端(/dev/console)和串口(/dev/ttyS0以及類似設(shè)備)就是兩個(gè)字符設(shè)備,它們能很好的說明“流”這種抽象概念。
字符設(shè)備可以通過文件節(jié)點(diǎn)來訪問,比如/dev/tty1和/dev/lp0等。這些設(shè)備文件和普通文件之間的唯一差別在于對普通文件的訪問可以前后移動(dòng)訪問位置,而大多數(shù)字符設(shè)備是一個(gè)只能順序訪問的數(shù)據(jù)通道。然而,也存在具有數(shù)據(jù)區(qū)特性的字符設(shè)備,訪問它們時(shí)可前后移動(dòng)訪問位置。例如framebuffer就是這樣的一個(gè)設(shè)備,app可以用mmap或lseek訪問抓取的整個(gè)圖像。
在/dev下執(zhí)行l(wèi)s -l ,可以看到很多創(chuàng)建好的設(shè)備節(jié)點(diǎn):
字符設(shè)備文件(類型為c),設(shè)備文件是沒有文件大小的,取而代之的是兩個(gè)號碼:主設(shè)備號5 +次設(shè)備號1 。
塊設(shè)備:
和字符設(shè)備類似,塊設(shè)備也是通過/dev目錄下的文件系統(tǒng)節(jié)點(diǎn)來訪問。塊設(shè)備(例如磁盤)上能夠容納filesystem。在大多數(shù)的Unix系統(tǒng)中,進(jìn)行I/O操作時(shí)塊設(shè)備每次只能傳輸一個(gè)或多個(gè)完整的塊,而每塊包含512字節(jié)(或2的更高次冪字節(jié)的數(shù)據(jù))。
Linux可以讓app像字符設(shè)備一樣地讀寫塊設(shè)備,允許一次傳遞任意多字節(jié)的數(shù)據(jù)。因此,塊設(shè)備和字符設(shè)備的區(qū)別僅僅在于內(nèi)核內(nèi)部管理數(shù)據(jù)的方式,也就是內(nèi)核及驅(qū)動(dòng)程序之間的軟件接口,而這些不同對用戶來講是透明的。在內(nèi)核中,和字符驅(qū)動(dòng)程序相比,塊驅(qū)動(dòng)程序具有完全不同的接口。
塊設(shè)備文件(類型為b):
網(wǎng)絡(luò)設(shè)備:
任何網(wǎng)絡(luò)事物都需要經(jīng)過一個(gè)網(wǎng)絡(luò)接口形成,網(wǎng)絡(luò)接口是一個(gè)能夠和其他主機(jī)交換數(shù)據(jù)的設(shè)備。接口通常是一個(gè)硬件設(shè)備,但也可能是個(gè)純軟件設(shè)備,比如回環(huán)(loopback)接口。
網(wǎng)絡(luò)接口由內(nèi)核中的網(wǎng)絡(luò)子系統(tǒng)驅(qū)動(dòng),負(fù)責(zé)發(fā)送和接收數(shù)據(jù)包。許多網(wǎng)絡(luò)連接(尤其是使用TCP協(xié)議的連接)是面向流的,但網(wǎng)絡(luò)設(shè)備卻圍繞數(shù)據(jù)包的傳送和接收而設(shè)計(jì)。網(wǎng)絡(luò)驅(qū)動(dòng)程序不需要知道各個(gè)連接的相關(guān)信息,它只要處理數(shù)據(jù)包即可。
由于不是面向流的設(shè)備,因此將網(wǎng)絡(luò)接口映射到filesystem中的節(jié)點(diǎn)(比如/dev/tty1)比較困難。
Unix訪問網(wǎng)絡(luò)接口的方法仍然是給它們分配一個(gè)唯一的名字(比如eth0),但這個(gè)名字在filesystem中不存在對應(yīng)的節(jié)點(diǎn)。內(nèi)核和網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序間的通信,完全不同于內(nèi)核和字符以及塊驅(qū)動(dòng)程序之間的通信,內(nèi)核調(diào)用一套和數(shù)據(jù)包相關(guān)的函數(shù)socket,也叫套接字。
查看網(wǎng)絡(luò)設(shè)備使用命令ifconfig:
二、字符設(shè)備架構(gòu)是如何實(shí)現(xiàn)的?
在Linux的世界里面一切皆文件,所有的硬件設(shè)備操作到應(yīng)用層都會被抽象成文件的操作。我們知道如果應(yīng)用層要訪問硬件設(shè)備,它必定要調(diào)用到硬件對應(yīng)的驅(qū)動(dòng)程序。Linux內(nèi)核中有那么多驅(qū)動(dòng)程序,應(yīng)用層怎么才能精確的調(diào)用到底層的驅(qū)動(dòng)程序呢?
在這里我們字符設(shè)備為例,來看一下應(yīng)用程序是如何和底層驅(qū)動(dòng)程序關(guān)聯(lián)起來的。必須知道的基礎(chǔ)知識:
1.在Linux文件系統(tǒng)中,每個(gè)文件都用一個(gè)struct inode結(jié)構(gòu)體來描述,這個(gè)結(jié)構(gòu)體里面記錄了這個(gè)文件的所有信息,例如:文件類型,訪問權(quán)限等。
2.在Linux操作系統(tǒng)中,每個(gè)驅(qū)動(dòng)程序在應(yīng)用層的/dev目錄下都會有一個(gè)設(shè)備文件和它對應(yīng),并且該文件會有對應(yīng)的主設(shè)備號和次設(shè)備號。
3.在Linux操作系統(tǒng)中,每個(gè)驅(qū)動(dòng)程序都要分配一個(gè)主設(shè)備號,字符設(shè)備的設(shè)備號保存在struct cdev結(jié)構(gòu)體中。
?struct?cdev?{
????????struct?kobject?kobj;
????????struct?module?*owner;
????????const?struct?file_operations?*ops;//接口函數(shù)集合
????????struct?list_head?list;//內(nèi)核鏈表
????????dev_t?dev;????//設(shè)備號
????????unsigned?int?count;//次設(shè)備號個(gè)數(shù)
????};
4.在Linux操作系統(tǒng)中,每打開一次文件,Linux操作系統(tǒng)在VFS層都會分配一個(gè)struct file結(jié)構(gòu)體來描述打開的這個(gè)文件。該結(jié)構(gòu)體用于維護(hù)文件打開權(quán)限、文件指針偏移值、私有內(nèi)存地址等信息。
注意:
常常我們認(rèn)為struct inode描述的是文件的靜態(tài)信息,即這些信息很少會改變。而struct file描述的是動(dòng)態(tài)信息,即在對文件的操作的時(shí)候,struct file里面的信息經(jīng)常會發(fā)生變化。典型的是struct file結(jié)構(gòu)體里面的f_pos(記錄當(dāng)前文件的位移量),每次讀寫一個(gè)普通文件時(shí)f_ops的值都會發(fā)生改變。
這幾個(gè)結(jié)構(gòu)體關(guān)系如下圖所示:
通過上圖我們可以知道,如果想訪問底層設(shè)備,就必須打開對應(yīng)的設(shè)備文件。也就是在這個(gè)打開的過程中,Linux內(nèi)核將應(yīng)用層和對應(yīng)的驅(qū)動(dòng)程序關(guān)聯(lián)起來。
1.當(dāng)open函數(shù)打開設(shè)備文件時(shí),可以根據(jù)設(shè)備文件對應(yīng)的struct inode結(jié)構(gòu)體描述的信息,可以知道接下來要操作的設(shè)備類型(字符設(shè)備還是塊設(shè)備)。還會分配一個(gè)struct file結(jié)構(gòu)體。
2.根據(jù)struct inode結(jié)構(gòu)體里面記錄的設(shè)備號,可以找到對應(yīng)的驅(qū)動(dòng)程序。這里以字符設(shè)備為例。在Linux操作系統(tǒng)中每個(gè)字符設(shè)備有一個(gè)struct cdev結(jié)構(gòu)體。此結(jié)構(gòu)體描述了字符設(shè)備所有的信息,其中最重要一項(xiàng)的就是字符設(shè)備的操作函數(shù)接口。
3.找到struct cdev結(jié)構(gòu)體后,Linux內(nèi)核就會將struct cdev結(jié)構(gòu)體所在的內(nèi)存空間首地記錄在struct inode結(jié)構(gòu)體的i_cdev成員中。將struct cdev結(jié)構(gòu)體的中記錄的函數(shù)操作接口地址記錄在struct file結(jié)構(gòu)體的f_op成員中。
4.任務(wù)完成,VFS層會給應(yīng)用層返回一個(gè)文件描述符(fd)。這個(gè)fd是和struct file結(jié)構(gòu)體對應(yīng)的。接下來上層的應(yīng)用程序就可以通過fd來找到strut file,然后在由struct file找到操作字符設(shè)備的函數(shù)接口了。
三、字符驅(qū)動(dòng)相關(guān)函數(shù)分析
/**
?*?cdev_init()?-?initialize?a?cdev?structure
?*?@cdev:?the?structure?to?initialize
?*?@fops:?the?file_operations?for?this?device
?*
?*?Initializes?@cdev,?remembering?@fops,?making?it?ready?to?add?to?the
?*?system?with?cdev_add().
?*/
void?cdev_init(struct?cdev?*cdev,?const?struct?file_operations?*fops)
功能:
??初始化cdev結(jié)構(gòu)體
參數(shù):
??@cdev?cdev結(jié)構(gòu)體地址
??@fops?操作字符設(shè)備的函數(shù)接口地址
返回值:
??無
/**
?*?register_chrdev_region()?-?register?a?range?of?device?numbers
?*?@from:?the?first?in?the?desired?range?of?device?numbers;?must?include
?*????????the?major?number.
?*?@count:?the?number?of?consecutive?device?numbers?required
?*?@name:?the?name?of?the?device?or?driver.
?*
?*?Return?value?is?zero?on?success,?a?negative?error?code?on?failure.
?*/??????????????????????????????????????????????
int?register_chrdev_region(dev_t?from,?unsigned?count,?const?char?*name)
功能:
??注冊一個(gè)范圍()的設(shè)備號
參數(shù):
??@from?設(shè)備號
??@count?注冊的設(shè)備個(gè)數(shù)
??@name?設(shè)備的名字
返回值:
??成功返回0,失敗返回錯(cuò)誤碼(負(fù)數(shù))
/**
?*?cdev_add()?-?add?a?char?device?to?the?system
?*?@p:?the?cdev?structure?for?the?device
?*?@dev:?the?first?device?number?for?which?this?device?is?responsible
?*?@count:?the?number?of?consecutive?minor?numbers?corresponding?to?this
?*?????????device
?*
?*?cdev_add()?adds?the?device?represented?by?@p?to?the?system,?making?it
?*?live?immediately.??A?negative?error?code?is?returned?on?failure.
?*/
int?cdev_add(struct?cdev?*p,?dev_t?dev,?unsigned?count)
功能:
??添加一個(gè)字符設(shè)備到操作系統(tǒng)
參數(shù):
??@p?cdev結(jié)構(gòu)體地址
??@dev?設(shè)備號
??@count?次設(shè)備號個(gè)數(shù)
返回值:
??成功返回0,失敗返回錯(cuò)誤碼(負(fù)數(shù))
/**
?*?cdev_del()?-?remove?a?cdev?from?the?system
?*?@p:?the?cdev?structure?to?be?removed
?*
?*?cdev_del()?removes?@p?from?the?system,?possibly?freeing?the?structure
?*?itself.
?*/
void?cdev_del(struct?cdev?*p)
功能:
??從系統(tǒng)中刪除一個(gè)字符設(shè)備
參數(shù):
??@p?cdev結(jié)構(gòu)體地址
返回值:
??無
static?inline?int?register_chrdev(unsigned?int?major,?const?char?*name,
??????????const?struct?file_operations?*fops)
功能:
??注冊或者分配設(shè)備號,并注冊fops到cdev結(jié)構(gòu)體,
??如果major>0,功能為注冊該主設(shè)備號,
??如果major=0,功能為動(dòng)態(tài)分配主設(shè)備號。
參數(shù):
??@major?:?主設(shè)備號
??@name?:?設(shè)備名稱,執(zhí)行?cat?/proc/devices顯示的名稱
??@fops??:?文件系統(tǒng)的接口指針
返回值
??如果major>0???成功返回0,失敗返回負(fù)的錯(cuò)誤碼
??如果major=0??成功返回主設(shè)備號,失敗返回負(fù)的錯(cuò)誤碼
該函數(shù)實(shí)現(xiàn)了對cdev的初始化和注冊的封裝,所以調(diào)用該函數(shù)之后就不需要自己操作cdev了。
相對的注銷函數(shù)為unregister_chrdev
static?inline?void?unregister_chrdev(unsigned?int?major,?const?char?*name)
四、如何編寫字符設(shè)備驅(qū)動(dòng)

參考上圖,編寫字符設(shè)備驅(qū)動(dòng)步驟如下:
1. 實(shí)現(xiàn)模塊加載和卸載入口函數(shù)
module_init?(hello_init);
module_exit?(hello_exit);
2. 申請主設(shè)備號
申請主設(shè)備號 ?(內(nèi)核中用于區(qū)分和管理不同字符設(shè)備)
register_chrdev_region?(devno,?number_of_devices,?"hello");
3. 創(chuàng)建設(shè)備節(jié)點(diǎn)
創(chuàng)建設(shè)備節(jié)點(diǎn)文件 (為用戶提供一個(gè)可操作到文件接口--open()) 創(chuàng)建設(shè)備節(jié)點(diǎn)有兩種方式:手動(dòng)方式創(chuàng)建,函數(shù)自動(dòng)創(chuàng)建。手動(dòng)創(chuàng)建:
mknod?/dev/hello?c?250?0
自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)
除了使用mknod命令手動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn),還可以利用linux的udev、mdev機(jī)制,而我們的ARM開發(fā)板上移植的busybox有mdev機(jī)制,那么就使用mdev機(jī)制來自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)。
在etc/init.d/rcS文件里有一句:
echo?/sbin/mdev?>?/proc/sys/kernel/hotplug
該名命令就是用來自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)。
udev 是一個(gè)工作在用戶空間的工具,它能根據(jù)系統(tǒng)中硬件設(shè)備的狀態(tài)動(dòng)態(tài)的更新設(shè)備文件,包括設(shè)備文件的創(chuàng)建,刪除,權(quán)限等。這些文件通常都定義在/dev 目錄下,但也可以在配置文件中指定。udev 必須有內(nèi)核中的sysfs和tmpfs支持,sysfs 為udev 提供設(shè)備入口和uevent 通道,tmpfs 為udev 設(shè)備文件提供存放空間。
udev 運(yùn)行在用戶模式,而非內(nèi)核中。udev 的初始化腳本在系統(tǒng)啟動(dòng)時(shí)創(chuàng)建設(shè)備節(jié)點(diǎn),并且當(dāng)插入新設(shè)備——加入驅(qū)動(dòng)模塊——在sysfs上注冊新的數(shù)據(jù)后,udev會創(chuàng)新新的設(shè)備節(jié)點(diǎn)。
注意,udev 是通過對內(nèi)核產(chǎn)生的設(shè)備文件修改,或增加別名的方式來達(dá)到自定義設(shè)備文件的目的。但是,udev 是用戶模式程序,其不會更改內(nèi)核行為。也就是說,內(nèi)核仍然會創(chuàng)建sda,sdb等設(shè)備文件,而udev可根據(jù)設(shè)備的唯一信息來區(qū)分不同的設(shè)備,并產(chǎn)生新的設(shè)備文件(或鏈接)。
例如:
如果驅(qū)動(dòng)模塊可以將自己的設(shè)備號作為內(nèi)核參數(shù)導(dǎo)出,在sysfs文件中就有一個(gè)叫做uevent文件記錄它的值。
由上圖可知,uevent中包含了主設(shè)備號和次設(shè)備號的值以及設(shè)備名字。
在Linux應(yīng)用層啟動(dòng)一個(gè)udev程序,這個(gè)程序的第一次運(yùn)行的時(shí)候,會遍歷/sys目錄,尋找每個(gè)子目錄的uevent文件,從這些uevent文件中獲取創(chuàng)建設(shè)備節(jié)點(diǎn)的信息,然后調(diào)用mknod程序在/dev目錄下創(chuàng)建設(shè)備節(jié)點(diǎn)。結(jié)束之后,udev就開始等待內(nèi)核空間的event。這個(gè)設(shè)備模型的東西,我們在后面再詳細(xì)說。這里大就可以這樣理解,在Linux內(nèi)核中提供了一些函數(shù)接口,通過這些函數(shù)接口,我們可在sysfs文件系統(tǒng)中導(dǎo)出我們的設(shè)備號的值,導(dǎo)出值之后,內(nèi)核還會向應(yīng)用層上報(bào)event。此時(shí)udev就知道有活可以干了,它收到這個(gè)event后,就讀取event對應(yīng)的信息,接下來就開始創(chuàng)建設(shè)備節(jié)點(diǎn)啦。
如何創(chuàng)建一個(gè)設(shè)備類?
第一步 :通過宏class_create() 創(chuàng)建一個(gè)class類型的對象;
/*?This?is?a?#define?to?keep?the?compiler?from?merging?different
?*?instances?of?the?__key?variable?*/
#define?class_create(owner,?name)????\
({????????????\
??static?struct?lock_class_key?__key;??\
??__class_create(owner,?name,?&__key);??\
})
參數(shù):
??@owner??THIS_MODULE
??@name???類名字
返回值
??可以定義一個(gè)struct?class的指針變量cls接受返回值,然后通過IS_ERR(cls)判斷
??是否失敗,如果成功這個(gè)宏返回0,失敗返回非9值(可以通過PTR_ERR(cls)來獲得
??失敗返回的錯(cuò)誤碼)
在Linux內(nèi)核中,把設(shè)備進(jìn)行了分類,同一類設(shè)備可以放在同一個(gè)目錄下,該函數(shù)啟示就是創(chuàng)建了一個(gè)類,例如:
第二步:導(dǎo)出我們的設(shè)備信息到用戶空間
/**
?*?device_create?-?creates?a?device?and?registers?it?with?sysfs
?*?@class:?pointer?to?the?struct?class?that?this?device?should?be?registered?to
?*?@parent:?pointer?to?the?parent?struct?device?of?this?new?device,?if?any
?*?@devt:?the?dev_t?for?the?char?device?to?be?added
?*?@drvdata:?the?data?to?be?added?to?the?device?for?callbacks
?*?@fmt:?string?for?the?device's?name
?*
?*?This?function?can?be?used?by?char?device?classes.??A?struct?device
?*?will?be?created?in?sysfs,?registered?to?the?specified?class.
?*
?*?A?"dev"?file?will?be?created,?showing?the?dev_t?for?the?device,?if
?*?the?dev_t?is?not?0,0.
?*?If?a?pointer?to?a?parent?struct?device?is?passed?in,?the?newly?created
?*?struct?device?will?be?a?child?of?that?device?in?sysfs.
?*?The?pointer?to?the?struct?device?will?be?returned?from?the?call.
?*?Any?further?sysfs?files?that?might?be?required?can?be?created?using?this
?*?pointer.
?*
?*?Returns?&struct?device?pointer?on?success,?or?ERR_PTR()?on?error.
?*
?*?Note:?the?struct?class?passed?to?this?function?must?have?previously
?*?been?created?with?a?call?to?class_create().
?*/
struct?device?*device_create(struct?class?*class,?struct?device?*parent,
???????????dev_t?devt,?void?*drvdata,?const?char?*fmt,?...)
自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)使用實(shí)例:
static?struct?class?*cls;
static?struct?device?*test_device;
??devno?=?MKDEV(major,minor);
??cls?=?class_create(THIS_MODULE,"helloclass");
??if(IS_ERR(cls))
??{
????unregister_chrdev(major,"hello");
????return?result;
??}
??test_device?=?device_create(cls,NULL,devno,NULL,"hellodevice");
??if(IS_ERR(test_device?))
??{
????class_destroy(cls);
????unregister_chrdev(major,"hello");
????return?result;
??}
4 實(shí)現(xiàn)file_operations
static?const?struct?file_operations?fifo_operations?=?{
????.owner?=???THIS_MODULE,
????.open?=???dev_fifo_open,
????.read?=???dev_fifo_read,
????.write?=???dev_fifo_write,
????.unlocked_ioctl?=???dev_fifo_unlocked_ioctl,
};
open、release對應(yīng)應(yīng)用層的open()、close()函數(shù)。實(shí)現(xiàn)比較簡單,
直接返回0即可。其中read、write、unloched_ioctrl 函數(shù)的實(shí)現(xiàn)需要涉及到用戶空間 和內(nèi)存空間的數(shù)據(jù)拷貝。
在Linux操作系統(tǒng)中,用戶空間和內(nèi)核空間是相互獨(dú)立的。也就是說內(nèi)核空間是不能直接訪問用戶空間內(nèi)存地址,同理用戶空間也不能直接訪問內(nèi)核空間內(nèi)存地址。
如果想實(shí)現(xiàn),將用戶空間的數(shù)據(jù)拷貝到內(nèi)核空間或?qū)?nèi)核空間數(shù)據(jù)拷貝到用戶空間,就必須借助內(nèi)核給我們提供的接口來完成。
1. read接口實(shí)現(xiàn)
用戶空間-->內(nèi)核空間
字符設(shè)備的write接口定義如下:
ssize_t?(*write)(struct?file?*filp,?const?char?__user?*buf,?size_t?count,?loff_t?*f_pos);
參數(shù):
??filp:待操作的設(shè)備文件file結(jié)構(gòu)體指針
??buf:待寫入所讀取數(shù)據(jù)的用戶空間緩沖區(qū)指針
??count:待讀取數(shù)據(jù)字節(jié)數(shù)
??f_pos:待讀取數(shù)據(jù)文件位置,寫入完成后根據(jù)實(shí)際寫入字節(jié)數(shù)重新定位
返回:
??成功實(shí)際寫入的字節(jié)數(shù),失敗返回負(fù)值
如果該操作為空,將使得write系統(tǒng)調(diào)用返回負(fù)EINVAL失敗,正常返回實(shí)際寫入的字節(jié)數(shù)。
用戶空間向內(nèi)核空間拷貝數(shù)據(jù)需要使用copy_from_user函數(shù),該函數(shù)定義在arch/arm/include/asm/uaccess.h中。
static?inline?int?copy_from_user(void?*to,?const?void?__user?volatile?*from,unsigned?long?n)
參數(shù):
??to:目標(biāo)地址(內(nèi)核空間)
??from:源地址(用戶空間)
??n:將要拷貝數(shù)據(jù)的字節(jié)數(shù)
返回:
??成功返回0,失敗返回沒有拷貝成功的數(shù)據(jù)字節(jié)數(shù)

還可以使用get_user宏:
int?get_user(data,?ptr);
參數(shù):
??data:可以是字節(jié)、半字、字、雙字類型的內(nèi)核變量
??ptr:用戶空間內(nèi)存指針
返回:
??成功返回0,失敗返回非0
2. write接口實(shí)現(xiàn)
內(nèi)核空間-->用戶空間
字符設(shè)備的read接口定義如下:
ssize_t?(*read)(struct?file?*filp,?char?__user?*buf,?size_t??count,?lofft?*f_pos);
參數(shù):
??filp:?待操作的設(shè)備文件file結(jié)構(gòu)體指針
??buf:??待寫入所讀取數(shù)據(jù)的用戶空間緩沖區(qū)指針
??count:待讀取數(shù)據(jù)字節(jié)數(shù)
??f_pos:待讀取數(shù)據(jù)文件位置,讀取完成后根據(jù)實(shí)際讀取字節(jié)數(shù)重新定位
??__user :是一個(gè)空的宏,主要用來顯示的告訴程序員它修飾的指針變量存放的是用戶空間的地址。
返回值:
??成功實(shí)際讀取的字節(jié)數(shù),失敗返回負(fù)值
注意:如果該操作為空,將使得read系統(tǒng)調(diào)用返回負(fù)EINVAL失敗,正常返回實(shí)際讀取的字節(jié)數(shù)。
用戶空間從內(nèi)核空間讀取數(shù)據(jù)需要使用copy_to_user函數(shù):
?static?inline?int?copy_to_user(void?__user?volatile?*to,?const?void?*from,unsigned?long?n)
參數(shù):
??to:目標(biāo)地址(用戶空間)
??from:源地址(內(nèi)核空間)
??n:將要拷貝數(shù)據(jù)的字節(jié)數(shù)
返回:
??成功返回0,失敗返回沒有拷貝成功的數(shù)據(jù)字節(jié)數(shù)

還可以使用put_user宏:
int?put_user(data,?prt)
參數(shù):
??data:可以是字節(jié)、半字、字、雙字類型的內(nèi)核變量
??ptr:用戶空間內(nèi)存指針
返回:
??成功返回0,?失敗返回非0
這樣我們就可以實(shí)現(xiàn)read、write函數(shù)了,實(shí)例如下:
ssize_t?hello_read?(struct?file?*filp,?char?*buff,???size_t?count,?loff_t?*offp)
{
??ssize_t???result?=?0;
??if?(count???>?127)?
????count?=?127;
??if???(copy_to_user?(buff,?data,?count))
??{
????result?=???-EFAULT;
??}
??else
??{
????printk???(KERN_INFO?"wrote?%d?bytes\n",?count);
????result?=???count;
??}?
??return???result;
}
ssize_t?hello_write?(struct?file?*filp,const?char?*buf,?size_t?count,?loff_t?*f_pos)
{
??ssize_t?ret???=?0;
??//printk???(KERN_INFO?"Writing?%d?bytes\n",?count);
??if?(count???>?127)?return?-ENOMEM;
??if???(copy_from_user?(data,?buf,?count))?{
????ret?=???-EFAULT;
??}
??else?{
????data[count]?=?'\0';
????printk???(KERN_INFO"Received:?%s\n",?data);
????ret?=???count;
??}
??return?ret;
}
3. unlocked_ioctl接口實(shí)現(xiàn)
(1)為什么要實(shí)現(xiàn)xxx_ioctl ?
前面我們在驅(qū)動(dòng)中已經(jīng)實(shí)現(xiàn)了讀寫接口,通過這些接口我們可以完成對設(shè)備的讀寫。但是很多時(shí)候我們的應(yīng)用層工程師除了要對設(shè)備進(jìn)行讀寫數(shù)據(jù)之外,還希望可以對設(shè)備進(jìn)行控制。例如:針對串口設(shè)備,驅(qū)動(dòng)層除了需要提供對串口的讀寫之外,還需提供對串口波特率、奇偶校驗(yàn)位、終止位的設(shè)置,這些配置信息需要從應(yīng)用層傳遞一些基本數(shù)據(jù),僅僅是數(shù)據(jù)類型不同。
通過xxx_ioctl函數(shù)接口,可以提供對設(shè)備的控制能力,增加驅(qū)動(dòng)程序的靈活性。
(2)如何實(shí)現(xiàn)xxx_ioctl函數(shù)接口?
增加xxx_ioctl函數(shù)接口,應(yīng)用層可以通過ioctl系統(tǒng)調(diào)用,根據(jù)不同的命令來操作dev_fifo。
kernel 2.6.35 及之前的版本中struct file_operations 一共有3個(gè)ioctl :ioctl,unlocked_ioctl和compat_ioctl 現(xiàn)在只有unlocked_ioctl和compat_ioctl 了
在kernel 2.6.36 中已經(jīng)完全刪除了struct file_operations 中的ioctl 函數(shù)指針,取而代之的是unlocked_ioctl 。
· ? ? ? ? 2.6.36 之前的內(nèi)核
long?(ioctl)?(struct?inode?node?,struct?file*?filp,?unsigned?int?cmd,unsigned?long?arg)
· ? ? ? ? 2.6.36之后的內(nèi)核
long?(*unlocked_ioctl)?(struct?file?*filp,?unsigned?int?cmd,?unsigned?long?arg)
參數(shù)cmd: 通過應(yīng)用函數(shù)ioctl傳遞下來的命令
先來看看應(yīng)用層的ioctl和驅(qū)動(dòng)層的xxx_ioctl對應(yīng)關(guān)系:
<1>應(yīng)用層ioctl參數(shù)分析
int?ioctl(int?fd,?int?cmd,?...);
參數(shù):
@fd:打開設(shè)備文件的時(shí)候獲得文件描述符?
@?cmd:第二個(gè)參數(shù):給驅(qū)動(dòng)層傳遞的命令,需要注意的時(shí)候,驅(qū)動(dòng)層的命令和應(yīng)用層的命令一定要統(tǒng)一
@第三個(gè)參數(shù):?"..."在C語言中,很多時(shí)候都被理解成可變參數(shù)。
返回值
???????成功:0
???????失敗:-1,同時(shí)設(shè)置errno
小貼士:
當(dāng)我們通過ioctl調(diào)用驅(qū)動(dòng)層xxx_ioctl的時(shí)候,有三種情況可供選擇:
1:?不傳遞數(shù)據(jù)給xxx_ioctl?
2:?傳遞數(shù)據(jù)給xxx_ioctl,希望它最終能把數(shù)據(jù)寫入設(shè)備(例如:設(shè)置串口的波特率)
3:?調(diào)用xxxx_ioctl希望獲取設(shè)備的硬件參數(shù)(例如:獲取當(dāng)前串口設(shè)備的波特率)
這三種情況中,有些時(shí)候需要傳遞數(shù)據(jù),有些時(shí)候不需要傳遞數(shù)據(jù)。在C語言中,是
無法實(shí)現(xiàn)函數(shù)重載的。那怎么辦?用"..."來欺騙編譯器了,"..."本來的意思是傳
遞多參數(shù)。在這里的意思是帶一個(gè)參數(shù)還是不帶參數(shù)。
參數(shù)可以傳遞整型值,也可以傳遞某塊內(nèi)存的地址,內(nèi)核接口函數(shù)必須根據(jù)實(shí)際情況
提取對應(yīng)的信息。
<2>驅(qū)動(dòng)層xxx_ioctl參數(shù)分析
long?(*unlocked_ioctl)?(struct?file?*file,?unsigned?int?cmd,?unsigned?long?arg);
參數(shù):
@file:???vfs層為打開字符設(shè)備文件的進(jìn)程創(chuàng)建的結(jié)構(gòu)體,用于存放文件的動(dòng)態(tài)信息?
@?cmd:?用戶空間傳遞的命令,可以根據(jù)不同的命令做不同的事情
@第三個(gè)參數(shù):?用戶空間的數(shù)據(jù),主要這個(gè)數(shù)據(jù)可能是一個(gè)地址值(用戶空間傳遞的是一個(gè)地址),也可能是一個(gè)數(shù)值,也可能沒值
返回值
???????成功:0
???????失?。簬уe(cuò)誤碼的負(fù)值
<3>如何確定cmd 的值。
該值主要用于區(qū)分命令的類型,雖然我只需要傳遞任意一個(gè)整型值即可,但是我們盡量按照內(nèi)核規(guī)范要求,充分利用這32bite的空間,如果大家都沒有規(guī)矩,又如何能成方圓?
現(xiàn)在我就來看看,在Linux 內(nèi)核中這個(gè)cmd是如何設(shè)計(jì)的吧!
具體含義如下:
| 設(shè)備類型 | 類型或叫幻數(shù),代表一類設(shè)備,一般用一個(gè)字母或者1個(gè)8bit的數(shù)字 |
|---|---|
| 序列號 | 代表這個(gè)設(shè)備的第幾個(gè)命令 |
| 方 向 | 表示是由內(nèi)核空間到用戶空間,或是用戶空間到內(nèi)核空間,入:只讀,只寫,讀寫,其他 |
| 數(shù)據(jù)尺寸 | 表示需要讀寫的參數(shù)大小 |
由上可以一個(gè)命令由4個(gè)部分組成,每個(gè)部分需要的bite都不完全一樣,制作一個(gè)命令需要在不同的位域?qū)懖煌臄?shù)字,Linux 系統(tǒng)已經(jīng)給我們封裝好了宏,我們只需要直接調(diào)用宏來設(shè)計(jì)命令即可。

通過Linux 系統(tǒng)給我們提供的宏,我們在設(shè)計(jì)命令的時(shí)候,只需要指定設(shè)備類型、命令序號,數(shù)據(jù)類型三個(gè)字段就可以了。
Linux 系統(tǒng)中已經(jīng)設(shè)計(jì)了一場用的命令,可以通過查閱Linux 源碼中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已經(jīng)被使用過了。
<4> 如何檢查命令?
可以通過宏_IOC_TYPE(nr)來判斷應(yīng)用程序傳下來的命令type是否正確;
可以通過宏_IOC_DIR(nr)來得到命令是讀還是寫,然后再通過宏access_ok(type,addr,size)來判斷用戶層傳遞的內(nèi)存地址是否合法。
使用方法如下:
??if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
????pr_err("cmd???%u,bad?magic?0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
????return-ENOTTY;
??}
??if(_IOC_DIR(cmd)&_IOC_READ)
????ret=!access_ok(VERIFY_WRITE,(void?__user*)arg,_IOC_SIZE(cmd));
??else?if(?_IOC_DIR(cmd)&_IOC_WRITE?)
????ret=!access_ok(VERIFY_READ,(void???__user*)arg,_IOC_SIZE(cmd));
??if(ret){
????pr_err("bad???access?%ld.\n",ret);
????return-EFAULT;
??}
5 注冊cdev
定義好file_operations結(jié)構(gòu)體,就可以通過函數(shù)cdev_init()、cdev_add()注冊字符設(shè)備驅(qū)動(dòng)了。
實(shí)例如下:
static?struct?cdev?cdev;
cdev_init(&cdev,&hello_ops);
error?=?cdev_add(&cdev,devno,1);
注意如果使用了函數(shù)register_chrdev(),就不用了執(zhí)行上述操作,因?yàn)樵摵瘮?shù)已經(jīng)實(shí)現(xiàn)了對cdev的封裝。
五、實(shí)例
千言萬語,全部匯總在這一個(gè)圖里,大家可以對照相應(yīng)的層次來學(xué)習(xí)。
六、實(shí)例
好了,現(xiàn)在我們可以來實(shí)現(xiàn)一個(gè)完整的字符設(shè)備框架的實(shí)例,包括打開、關(guān)閉、讀寫、ioctrl、自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)等功能。
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?"dev_fifo_head.h"
//指定的主設(shè)備號
#define???MAJOR_NUM?250
//自己的字符設(shè)備
struct?mycdev
{
????int?len;
????unsigned???char?buffer[50];
????struct???cdev?cdev;
};
MODULE_LICENSE("GPL");
//設(shè)備號
static?dev_t???dev_num?=?{0};
//全局gcd
struct?mycdev?*gcd;
//設(shè)備類
struct?class?*cls;
//獲得用戶傳遞的數(shù)據(jù),根據(jù)它來決定注冊的設(shè)備個(gè)數(shù)
static?int?ndevices?=?1;
module_param(ndevices,?int,?0644);
MODULE_PARM_DESC(ndevices,?"The?number?of?devices?for?register.\n");
//打開設(shè)備
static?int?dev_fifo_open(struct???inode?*inode,???struct?file?*file)
{
????struct???mycdev?*cd;??
????printk("dev_fifo_open???success!\n");??
????//用struct?file的文件私有數(shù)據(jù)指針保存struct?mycdev結(jié)構(gòu)體指針
????cd???=?container_of(inode->i_cdev,struct???mycdev,cdev);
????file->private_data?=???cd;??
????return???0;
}
//讀設(shè)備
static?ssize_t???dev_fifo_read(struct?file?*file,?char???__user?*ubuf,???size_t
size,?loff_t?*ppos)
{
????int?n;
????int?ret;
????char???*kbuf;
????struct???mycdev?*mycd?=???file->private_data;
????printk("read?*ppos?:???%lld\n",*ppos);?
????if(*ppos?==?mycd->len)
????????return???0;
????//請求大大小?>?buffer剩余的字節(jié)數(shù)???:讀取實(shí)際記得字節(jié)數(shù)
????if(size?>?mycd->len?-?*ppos)
????????n?=?mycd->len?-?*ppos;
????else
????????n?=?size;
????printk("n?=???%d\n",n);
????//從上一次文件位置指針的位置開始讀取數(shù)據(jù)
????kbuf???=?mycd->buffer???+?*ppos;
????//拷貝數(shù)據(jù)到用戶空間
????ret???=?copy_to_user(ubuf,kbuf,?n);
????if(ret?!=?0)
????????return???-EFAULT;
????//更新文件位置指針的值
????*ppos?+=?n;
????printk("dev_fifo_read???success!\n");
????return???n;
}
//寫設(shè)備
static?ssize_t???dev_fifo_write(struct?file?*file,?const?char?__user?*ubuf,size_t?size,?loff_t?*ppos)
{
????int?n;
????int?ret;
????char???*kbuf;
????struct???mycdev?*mycd?=???file->private_data;
????printk("write?*ppos?:???%lld\n",*ppos);
????//已經(jīng)到達(dá)buffer尾部了
????if(*ppos?==?sizeof(mycd->buffer))
???????return???-1;
????//請求大大小?>?buffer剩余的字節(jié)數(shù)(有多少空間就寫多少數(shù)據(jù))
????if(size?>?sizeof(mycd->buffer)?-?*ppos)
????????n?=?sizeof(mycd->buffer)?-?*ppos;
????else
????????n?=?size;
????//從上一次文件位置指針的位置開始寫入數(shù)據(jù)
????kbuf???=?mycd->buffer???+?*ppos;
????//拷貝數(shù)據(jù)到內(nèi)核空間
????ret???=?copy_from_user(kbuf,?ubuf,?n);
????if(ret?!=?0)
????????return???-EFAULT;
????//更新文件位置指針的值
????*ppos?+=?n;
????//更新dev_fifo.len
????mycd->len?+=?n;
????printk("dev_fifo_write???success!\n");
????return???n;
}
//linux?內(nèi)核在2.6以后,已經(jīng)廢棄了ioctl函數(shù)指針結(jié)構(gòu),取而代之的是
long???dev_fifo_unlocked_ioctl(struct?file?*file,???unsigned?int?cmd,
????unsigned???long?arg)
{
??int?ret?=?0;
??struct?mycdev?*mycd???=?file->private_data;
??if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
????pr_err("cmd???%u,bad?magic?0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
????return-ENOTTY;
??}
??if(_IOC_DIR(cmd)&_IOC_READ)
????ret=!access_ok(VERIFY_WRITE,(void?__user*)arg,_IOC_SIZE(cmd));
??else?if(?_IOC_DIR(cmd)&_IOC_WRITE?)
????ret=!access_ok(VERIFY_READ,(void???__user*)arg,_IOC_SIZE(cmd));
??if(ret){
????pr_err("bad???access?%ld.\n",ret);
????return-EFAULT;
??}?
????switch(cmd)
????{
??????case?DEV_FIFO_CLEAN:
?????????printk("CMD:CLEAN\n");
??????memset(mycd->buffer,?0,?sizeof(mycd->buffer));
?????????break;
??????case?DEV_FIFO_SETVALUE:
?????????printk("CMD:SETVALUE\n");
?????????mycd->len?=?arg;
?????????break;
??????case?DEV_FIFO_GETVALUE:
?????????printk("CMD:GETVALUE\n");
?????????ret???=?put_user(mycd->len,?(int?*)arg);
?????????break;
??????default:
?????????return???-EFAULT;
????}
????return???ret;
}
//設(shè)備操作函數(shù)接口
static?const?struct?file_operations?fifo_operations?=?{
????.owner?=???THIS_MODULE,
????.open?=???dev_fifo_open,
????.read?=???dev_fifo_read,
????.write?=???dev_fifo_write,
????.unlocked_ioctl?=???dev_fifo_unlocked_ioctl,
};
//模塊入口
int?__init?dev_fifo_init(void)
{
????int?i?=?0;
????int?n?=?0;
????int?ret;
????struct???device?*device;
??gcd???=?kzalloc(ndevices???*?sizeof(struct???mycdev),?GFP_KERNEL);
????if(!gcd){
????????return???-ENOMEM;
????}
????//設(shè)備號?:?主設(shè)備號(12bit)?|?次設(shè)備號(20bit)
????dev_num???=?MKDEV(MAJOR_NUM,?0);
????//靜態(tài)注冊設(shè)備號
????ret???=?register_chrdev_region(dev_num,ndevices,"dev_fifo");
????if(ret?0){
????//靜態(tài)注冊失敗,進(jìn)行動(dòng)態(tài)注冊設(shè)備號
?????ret???=alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
??????if(ret?0){
????????printk("Fail?to?register_chrdev_region\n");
????????goto???err_register_chrdev_region;
??????}
????}
????//創(chuàng)建設(shè)備類
????cls???=?class_create(THIS_MODULE,?"dev_fifo");
????if(IS_ERR(cls)){
????????ret???=?PTR_ERR(cls);
????????goto???err_class_create;
????}
????printk("ndevices?:???%d\n",ndevices);
????for(n?=?0;n?????{
??????//初始化字符設(shè)備
??????cdev_init(&gcd[n].cdev,&fifo_operations);
??????//添加設(shè)備到操作系統(tǒng)
??????ret???=?cdev_add(&gcd[n].cdev,dev_num?+?n,1);
??????if?(ret?0)
??????{
?????????goto???err_cdev_add;
??????}
?????//導(dǎo)出設(shè)備信息到用戶空間(/sys/class/類名/設(shè)備名)
??????device???=?device_create(cls,NULL,dev_num?+n,NULL,"dev_fifo%d",n);
??????if(IS_ERR(device)){
?????????ret???=?PTR_ERR(device);
?????????printk("Fail?to?device_create\n");
?????????goto???err_device_create;????
??????}
????}
????printk("Register???dev_fito?to?system,ok!\n");
????return???0;
err_device_create:
????//將已經(jīng)導(dǎo)出的設(shè)備信息除去
????for(i?=?0;i?????{
???????device_destroy(cls,dev_num?+?i);????
????}
err_cdev_add:
????//將已經(jīng)添加的全部除去
????for(i?=?0;i?????{
???????cdev_del(&gcd[i].cdev);
????}
err_class_create:
????unregister_chrdev_region(dev_num,???ndevices);
err_register_chrdev_region:
????return???ret;
}
void?__exit?dev_fifo_exit(void)
{
????int?i;
????//刪除sysfs文件系統(tǒng)中的設(shè)備
????for(i?=?0;i?????{
????????device_destroy(cls,dev_num?+?i);????
????}
????//刪除系統(tǒng)中的設(shè)備類
????class_destroy(cls);
????//從系統(tǒng)中刪除添加的字符設(shè)備
????for(i?=?0;i?????{
???????cdev_del(&gcd[i].cdev);
????}?
????//釋放申請的設(shè)備號
????unregister_chrdev_region(dev_num,???ndevices);
????return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);???
頭文件內(nèi)容:
dev_fifo_head.h
#ifndef?_DEV_FIFO_HEAD_H
#define?_DEV_FIFO_HEAD_H
#define?DEV_FIFO_TYPE?'k'
#define?DEV_FIFO_CLEAN?_IO(DEV_FIFO_TYPE,0x10)
#define?DEV_FIFO_GETVALUE?_IOR(DEV_FIFO_TYPE,0x11,int)
#define?DEV_FIFO_SETVALUE?_IOW(DEV_FIFO_TYPE,0x12,int)
#endif
Makefile :
ifeq?($(KERNELRELEASE),)
KERNEL_DIR??=/lib/modules/$(shell?uname?-r)/build??
PWD?:=$(shell?pwd)
modules:
????$(MAKE)?-C?$(KERNEL_DIR)???M=$(PWD)?modules
.PHONY:modules?clean
clean:
????$(MAKE)?-C?$(KERNEL_DIR)???M=$(PWD)?clean
else
????obj-m?:=?dev_fifo.o??
endif
應(yīng)用程序:
#include?
#include?
#include?
#include?
#include?
#include?
int?main(int?argc,?const?char?*argv[])
{
????int?fd?;
????int?n;
????char?buf[1024]?=?"hello???word";
????
????fd?=?open("/dev/dev_fifo0",O_RDWR);
????if(fd?0){
????????perror("Fail???ot?open");
????????return???-1;
????}
????printf("open???successful?,fd?=?%d\n",fd);
????n?=?write(fd,buf,strlen(buf));
????if(n?0){
????????perror("Fail???to?write");
????????return???-1;
????}
????printf("write???%d?bytes!\n",n);
????n?=?write(fd,buf,strlen(buf));
????if(n?0){
????????perror("Fail???to?write");
????????return???-1;
????}
????printf("write???%d?bytes!\n",n);
????return?0;
}
測試步驟:
(1) ? 加載模塊
sudo?insmod?hello.ko
(2) ? 創(chuàng)建設(shè)備節(jié)點(diǎn)
sudo?mknod?/dev/hello?c?250?0
如果代碼中增加了自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)的功能,這個(gè)步驟不要執(zhí)行。
(3) ? 測試字符設(shè)備
gcc?test.c?-o?run
sudo?./run
?推薦閱讀:
蘋果開源代碼中驚現(xiàn)“wechat”,老外注釋的吐槽亮了!
17個(gè)在 Linux 運(yùn)維中定要掌握的實(shí)用技巧
5T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機(jī),樹莓派,等等。在公眾號內(nèi)回復(fù)「1024」,即可免費(fèi)獲?。?!


