一、Linux設備分類
Linux系統為了管理方便,將設備分成三種基本類型:
字符設備
塊設備
網絡設備
字符設備:
字符(char)設備是個能夠像字節流(類似文件)一樣被訪問的設備,由字符設備驅動程序來實現這種特性。字符設備驅動程序通常至少要實現open、close、read和write的系統調用。
字符終端(/dev/console)和串口(/dev/ttyS0以及類似設備)就是兩個字符設備,它們能很好的說明“流”這種抽象概念。
字符設備可以通過文件節點來訪問,比如/dev/tty1和/dev/lp0等。這些設備文件和普通文件之間的唯一差別在于對普通文件的訪問可以前后移動訪問位置,而大多數字符設備是一個只能順序訪問的數據通道。然而,也存在具有數據區特性的字符設備,訪問它們時可前后移動訪問位置。例如framebuffer就是這樣的一個設備,app可以用mmap或lseek訪問抓取的整個圖像。
在/dev下執行ls -l ,可以看到很多創建好的設備節點:
字符設備文件(類型為c),設備文件是沒有文件大小的,取而代之的是兩個號碼:主設備號5 +次設備號1 。
塊設備:
和字符設備類似,塊設備也是通過/dev目錄下的文件系統節點來訪問。塊設備(例如磁盤)上能夠容納filesystem。在大多數的Unix系統中,進行I/O操作時塊設備每次只能傳輸一個或多個完整的塊,而每塊包含512字節(或2的更高次冪字節的數據)。
Linux可以讓app像字符設備一樣地讀寫塊設備,允許一次傳遞任意多字節的數據。因此,塊設備和字符設備的區別僅僅在于內核內部管理數據的方式,也就是內核及驅動程序之間的軟件接口,而這些不同對用戶來講是透明的。在內核中,和字符驅動程序相比,塊驅動程序具有完全不同的接口。
塊設備文件(類型為b):
網絡設備:
任何網絡事物都需要經過一個網絡接口形成,網絡接口是一個能夠和其他主機交換數據的設備。接口通常是一個硬件設備,但也可能是個純軟件設備,比如回環(loopback)接口。
網絡接口由內核中的網絡子系統驅動,負責發送和接收數據包。許多網絡連接(尤其是使用TCP協議的連接)是面向流的,但網絡設備卻圍繞數據包的傳送和接收而設計。網絡驅動程序不需要知道各個連接的相關信息,它只要處理數據包即可。
由于不是面向流的設備,因此將網絡接口映射到filesystem中的節點(比如/dev/tty1)比較困難。
Unix訪問網絡接口的方法仍然是給它們分配一個唯一的名字(比如eth0),但這個名字在filesystem中不存在對應的節點。內核和網絡設備驅動程序間的通信,完全不同于內核和字符以及塊驅動程序之間的通信,內核調用一套和數據包相關的函數socket,也叫套接字。
查看網絡設備使用命令ifconfig:
二、字符設備架構是如何實現的?
在Linux的世界里面一切皆文件,所有的硬件設備操作到應用層都會被抽象成文件的操作。我們知道如果應用層要訪問硬件設備,它必定要調用到硬件對應的驅動程序。Linux內核中有那么多驅動程序,應用層怎么才能精確的調用到底層的驅動程序呢?
在這里我們字符設備為例,來看一下應用程序是如何和底層驅動程序關聯起來的。必須知道的基礎知識:
1.在Linux文件系統中,每個文件都用一個struct inode結構體來描述,這個結構體里面記錄了這個文件的所有信息,例如:文件類型,訪問權限等。
2.在Linux操作系統中,每個驅動程序在應用層的/dev目錄下都會有一個設備文件和它對應,并且該文件會有對應的主設備號和次設備號。
3.在Linux操作系統中,每個驅動程序都要分配一個主設備號,字符設備的設備號保存在struct cdev結構體中。
structcdev{ structkobjectkobj; structmodule*owner; conststructfile_operations*ops;//接口函數集合 structlist_headlist;//內核鏈表 dev_tdev;//設備號 unsignedintcount;//次設備號個數 };
4.在Linux操作系統中,每打開一次文件,Linux操作系統在VFS層都會分配一個struct file結構體來描述打開的這個文件。該結構體用于維護文件打開權限、文件指針偏移值、私有內存地址等信息。
注意:
常常我們認為struct inode描述的是文件的靜態信息,即這些信息很少會改變。而struct file描述的是動態信息,即在對文件的操作的時候,struct file里面的信息經常會發生變化。典型的是struct file結構體里面的f_pos(記錄當前文件的位移量),每次讀寫一個普通文件時f_ops的值都會發生改變。
這幾個結構體關系如下圖所示:
通過上圖我們可以知道,如果想訪問底層設備,就必須打開對應的設備文件。也就是在這個打開的過程中,Linux內核將應用層和對應的驅動程序關聯起來。
1.當open函數打開設備文件時,可以根據設備文件對應的struct inode結構體描述的信息,可以知道接下來要操作的設備類型(字符設備還是塊設備)。還會分配一個struct file結構體。
2.根據struct inode結構體里面記錄的設備號,可以找到對應的驅動程序。這里以字符設備為例。在Linux操作系統中每個字符設備有一個struct cdev結構體。此結構體描述了字符設備所有的信息,其中最重要一項的就是字符設備的操作函數接口。
3.找到struct cdev結構體后,Linux內核就會將struct cdev結構體所在的內存空間首地記錄在struct inode結構體的i_cdev成員中。將struct cdev結構體的中記錄的函數操作接口地址記錄在struct file結構體的f_op成員中。
4.任務完成,VFS層會給應用層返回一個文件描述符(fd)。這個fd是和struct file結構體對應的。接下來上層的應用程序就可以通過fd來找到strut file,然后在由struct file找到操作字符設備的函數接口了。
三、字符驅動相關函數分析
/** *cdev_init()-initializeacdevstructure *@cdev:thestructuretoinitialize *@fops:thefile_operationsforthisdevice * *Initializes@cdev,remembering@fops,makingitreadytoaddtothe *systemwithcdev_add(). */ voidcdev_init(structcdev*cdev,conststructfile_operations*fops) 功能: 初始化cdev結構體 參數: @cdevcdev結構體地址 @fops操作字符設備的函數接口地址 返回值: 無
/** *register_chrdev_region()-registerarangeofdevicenumbers *@from:thefirstinthedesiredrangeofdevicenumbers;mustinclude *themajornumber. *@count:thenumberofconsecutivedevicenumbersrequired *@name:thenameofthedeviceordriver. * *Returnvalueiszeroonsuccess,anegativeerrorcodeonfailure. */ intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name) 功能: 注冊一個范圍()的設備號 參數: @from設備號 @count注冊的設備個數 @name設備的名字 返回值: 成功返回0,失敗返回錯誤碼(負數)
/** *cdev_add()-addachardevicetothesystem *@p:thecdevstructureforthedevice *@dev:thefirstdevicenumberforwhichthisdeviceisresponsible *@count:thenumberofconsecutiveminornumberscorrespondingtothis *device * *cdev_add()addsthedevicerepresentedby@ptothesystem,makingit *liveimmediately.Anegativeerrorcodeisreturnedonfailure. */ intcdev_add(structcdev*p,dev_tdev,unsignedcount) 功能: 添加一個字符設備到操作系統 參數: @pcdev結構體地址 @dev設備號 @count次設備號個數 返回值: 成功返回0,失敗返回錯誤碼(負數)
/** *cdev_del()-removeacdevfromthesystem *@p:thecdevstructuretoberemoved * *cdev_del()removes@pfromthesystem,possiblyfreeingthestructure *itself. */ voidcdev_del(structcdev*p) 功能: 從系統中刪除一個字符設備 參數: @pcdev結構體地址 返回值: 無
staticinlineintregister_chrdev(unsignedintmajor,constchar*name, conststructfile_operations*fops) 功能: 注冊或者分配設備號,并注冊fops到cdev結構體, 如果major>0,功能為注冊該主設備號, 如果major=0,功能為動態分配主設備號。 參數: @major:主設備號 @name:設備名稱,執行cat/proc/devices顯示的名稱 @fops:文件系統的接口指針 返回值 如果major>0成功返回0,失敗返回負的錯誤碼 如果major=0成功返回主設備號,失敗返回負的錯誤碼
該函數實現了對cdev的初始化和注冊的封裝,所以調用該函數之后就不需要自己操作cdev了。
相對的注銷函數為unregister_chrdev
staticinlinevoidunregister_chrdev(unsignedintmajor,constchar*name)
四、如何編寫字符設備驅動
參考上圖,編寫字符設備驅動步驟如下:
1. 實現模塊加載和卸載入口函數
module_init(hello_init); module_exit(hello_exit);
2. 申請主設備號
申請主設備號 (內核中用于區分和管理不同字符設備)
register_chrdev_region(devno,number_of_devices,"hello");
3. 創建設備節點
創建設備節點文件 (為用戶提供一個可操作到文件接口--open())創建設備節點有兩種方式:手動方式創建,函數自動創建。手動創建:
mknod/dev/helloc2500
自動創建設備節點
除了使用mknod命令手動創建設備節點,還可以利用linux的udev、mdev機制,而我們的ARM開發板上移植的busybox有mdev機制,那么就使用mdev機制來自動創建設備節點。
在etc/init.d/rcS文件里有一句:
echo/sbin/mdev>/proc/sys/kernel/hotplug
該名命令就是用來自動創建設備節點。
udev 是一個工作在用戶空間的工具,它能根據系統中硬件設備的狀態動態的更新設備文件,包括設備文件的創建,刪除,權限等。這些文件通常都定義在/dev 目錄下,但也可以在配置文件中指定。udev 必須有內核中的sysfs和tmpfs支持,sysfs 為udev 提供設備入口和uevent 通道,tmpfs 為udev 設備文件提供存放空間。
udev 運行在用戶模式,而非內核中。udev 的初始化腳本在系統啟動時創建設備節點,并且當插入新設備——加入驅動模塊——在sysfs上注冊新的數據后,udev會創新新的設備節點。
注意,udev 是通過對內核產生的設備文件修改,或增加別名的方式來達到自定義設備文件的目的。但是,udev 是用戶模式程序,其不會更改內核行為。也就是說,內核仍然會創建sda,sdb等設備文件,而udev可根據設備的唯一信息來區分不同的設備,并產生新的設備文件(或鏈接)。
例如:
如果驅動模塊可以將自己的設備號作為內核參數導出,在sysfs文件中就有一個叫做uevent文件記錄它的值。
由上圖可知,uevent中包含了主設備號和次設備號的值以及設備名字。
在Linux應用層啟動一個udev程序,這個程序的第一次運行的時候,會遍歷/sys目錄,尋找每個子目錄的uevent文件,從這些uevent文件中獲取創建設備節點的信息,然后調用mknod程序在/dev目錄下創建設備節點。結束之后,udev就開始等待內核空間的event。這個設備模型的東西,我們在后面再詳細說。這里大就可以這樣理解,在Linux內核中提供了一些函數接口,通過這些函數接口,我們可在sysfs文件系統中導出我們的設備號的值,導出值之后,內核還會向應用層上報event。此時udev就知道有活可以干了,它收到這個event后,就讀取event對應的信息,接下來就開始創建設備節點啦。
如何創建一個設備類?
第一步 :通過宏class_create() 創建一個class類型的對象;
/*Thisisa#definetokeepthecompilerfrommergingdifferent *instancesofthe__keyvariable*/ #defineclass_create(owner,name) ({ staticstructlock_class_key__key; __class_create(owner,name,&__key); }) 參數: @ownerTHIS_MODULE @name類名字 返回值 可以定義一個structclass的指針變量cls接受返回值,然后通過IS_ERR(cls)判斷 是否失敗,如果成功這個宏返回0,失敗返回非9值(可以通過PTR_ERR(cls)來獲得 失敗返回的錯誤碼)
在Linux內核中,把設備進行了分類,同一類設備可以放在同一個目錄下,該函數啟示就是創建了一個類,例如:
第二步:導出我們的設備信息到用戶空間
/** *device_create-createsadeviceandregistersitwithsysfs *@class:pointertothestructclassthatthisdeviceshouldberegisteredto *@parent:pointertotheparentstructdeviceofthisnewdevice,ifany *@devt:thedev_tforthechardevicetobeadded *@drvdata:thedatatobeaddedtothedeviceforcallbacks *@fmt:stringforthedevice'sname * *Thisfunctioncanbeusedbychardeviceclasses.Astructdevice *willbecreatedinsysfs,registeredtothespecifiedclass. * *A"dev"filewillbecreated,showingthedev_tforthedevice,if *thedev_tisnot0,0. *Ifapointertoaparentstructdeviceispassedin,thenewlycreated *structdevicewillbeachildofthatdeviceinsysfs. *Thepointertothestructdevicewillbereturnedfromthecall. *Anyfurthersysfsfilesthatmightberequiredcanbecreatedusingthis *pointer. * *Returns&structdevicepointeronsuccess,orERR_PTR()onerror. * *Note:thestructclasspassedtothisfunctionmusthavepreviously *beencreatedwithacalltoclass_create(). */ structdevice*device_create(structclass*class,structdevice*parent, dev_tdevt,void*drvdata,constchar*fmt,...)
自動創建設備節點使用實例:
staticstructclass*cls; staticstructdevice*test_device; devno=MKDEV(major,minor); cls=class_create(THIS_MODULE,"helloclass"); if(IS_ERR(cls)) { unregister_chrdev(major,"hello"); returnresult; } test_device=device_create(cls,NULL,devno,NULL,"hellodevice"); if(IS_ERR(test_device)) { class_destroy(cls); unregister_chrdev(major,"hello"); returnresult; }
4 實現file_operations
staticconststructfile_operationsfifo_operations={ .owner=THIS_MODULE, .open=dev_fifo_open, .read=dev_fifo_read, .write=dev_fifo_write, .unlocked_ioctl=dev_fifo_unlocked_ioctl, };
open、release對應應用層的open()、close()函數。實現比較簡單,
直接返回0即可。其中read、write、unloched_ioctrl 函數的實現需要涉及到用戶空間和內存空間的數據拷貝。
在Linux操作系統中,用戶空間和內核空間是相互獨立的。也就是說內核空間是不能直接訪問用戶空間內存地址,同理用戶空間也不能直接訪問內核空間內存地址。
如果想實現,將用戶空間的數據拷貝到內核空間或將內核空間數據拷貝到用戶空間,就必須借助內核給我們提供的接口來完成。
1. read接口實現
用戶空間-->內核空間
字符設備的write接口定義如下:
ssize_t(*write)(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos); 參數: filp:待操作的設備文件file結構體指針 buf:待寫入所讀取數據的用戶空間緩沖區指針 count:待讀取數據字節數 f_pos:待讀取數據文件位置,寫入完成后根據實際寫入字節數重新定位 返回: 成功實際寫入的字節數,失敗返回負值
如果該操作為空,將使得write系統調用返回負EINVAL失敗,正常返回實際寫入的字節數。
用戶空間向內核空間拷貝數據需要使用copy_from_user函數,該函數定義在arch/arm/include/asm/uaccess.h中。
staticinlineintcopy_from_user(void*to,constvoid__uservolatile*from,unsignedlongn) 參數: to:目標地址(內核空間) from:源地址(用戶空間) n:將要拷貝數據的字節數 返回: 成功返回0,失敗返回沒有拷貝成功的數據字節數
還可以使用get_user宏:
intget_user(data,ptr); 參數: data:可以是字節、半字、字、雙字類型的內核變量 ptr:用戶空間內存指針 返回: 成功返回0,失敗返回非0
2. write接口實現
內核空間-->用戶空間
字符設備的read接口定義如下:
ssize_t(*read)(structfile*filp,char__user*buf,size_tcount,lofft*f_pos); 參數: filp:待操作的設備文件file結構體指針 buf:待寫入所讀取數據的用戶空間緩沖區指針 count:待讀取數據字節數 f_pos:待讀取數據文件位置,讀取完成后根據實際讀取字節數重新定位 __user:是一個空的宏,主要用來顯示的告訴程序員它修飾的指針變量存放的是用戶空間的地址。 返回值: 成功實際讀取的字節數,失敗返回負值
注意:如果該操作為空,將使得read系統調用返回負EINVAL失敗,正常返回實際讀取的字節數。
用戶空間從內核空間讀取數據需要使用copy_to_user函數:
staticinlineintcopy_to_user(void__uservolatile*to,constvoid*from,unsignedlongn) 參數: to:目標地址(用戶空間) from:源地址(內核空間) n:將要拷貝數據的字節數 返回: 成功返回0,失敗返回沒有拷貝成功的數據字節數在這里插入圖片描述
還可以使用put_user宏:
intput_user(data,prt) 參數: data:可以是字節、半字、字、雙字類型的內核變量 ptr:用戶空間內存指針 返回: 成功返回0,失敗返回非0
這樣我們就可以實現read、write函數了,實例如下:
ssize_thello_read(structfile*filp,char*buff,size_tcount,loff_t*offp) { ssize_tresult=0; if(count>127) count=127; if(copy_to_user(buff,data,count)) { result=-EFAULT; } else { printk(KERN_INFO"wrote%dbytes ",count); result=count; } returnresult; } ssize_thello_write(structfile*filp,constchar*buf,size_tcount,loff_t*f_pos) { ssize_tret=0; //printk(KERN_INFO"Writing%dbytes ",count); if(count>127)return-ENOMEM; if(copy_from_user(data,buf,count)){ ret=-EFAULT; } else{ data[count]='?'; printk(KERN_INFO"Received:%s ",data); ret=count; } returnret; }
3. unlocked_ioctl接口實現
(1)為什么要實現xxx_ioctl ?
前面我們在驅動中已經實現了讀寫接口,通過這些接口我們可以完成對設備的讀寫。但是很多時候我們的應用層工程師除了要對設備進行讀寫數據之外,還希望可以對設備進行控制。例如:針對串口設備,驅動層除了需要提供對串口的讀寫之外,還需提供對串口波特率、奇偶校驗位、終止位的設置,這些配置信息需要從應用層傳遞一些基本數據,僅僅是數據類型不同。
通過xxx_ioctl函數接口,可以提供對設備的控制能力,增加驅動程序的靈活性。
(2)如何實現xxx_ioctl函數接口?
增加xxx_ioctl函數接口,應用層可以通過ioctl系統調用,根據不同的命令來操作dev_fifo。
kernel 2.6.35 及之前的版本中struct file_operations 一共有3個ioctl :ioctl,unlocked_ioctl和compat_ioctl 現在只有unlocked_ioctl和compat_ioctl 了
在kernel 2.6.36 中已經完全刪除了struct file_operations 中的ioctl 函數指針,取而代之的是unlocked_ioctl 。
· 2.6.36 之前的內核
long(ioctl)(structinodenode,structfile*filp,unsignedintcmd,unsignedlongarg)
· 2.6.36之后的內核
long(*unlocked_ioctl)(structfile*filp,unsignedintcmd,unsignedlongarg)
參數cmd: 通過應用函數ioctl傳遞下來的命令
先來看看應用層的ioctl和驅動層的xxx_ioctl對應關系:<1>應用層ioctl參數分析
intioctl(intfd,intcmd,...); 參數: @fd:打開設備文件的時候獲得文件描述符 @cmd:第二個參數:給驅動層傳遞的命令,需要注意的時候,驅動層的命令和應用層的命令一定要統一 @第三個參數:"..."在C語言中,很多時候都被理解成可變參數。 返回值 成功:0 失敗:-1,同時設置errno
小貼士:
當我們通過ioctl調用驅動層xxx_ioctl的時候,有三種情況可供選擇: 1:不傳遞數據給xxx_ioctl 2:傳遞數據給xxx_ioctl,希望它最終能把數據寫入設備(例如:設置串口的波特率) 3:調用xxxx_ioctl希望獲取設備的硬件參數(例如:獲取當前串口設備的波特率) 這三種情況中,有些時候需要傳遞數據,有些時候不需要傳遞數據。在C語言中,是 無法實現函數重載的。那怎么辦?用"..."來欺騙編譯器了,"..."本來的意思是傳 遞多參數。在這里的意思是帶一個參數還是不帶參數。 參數可以傳遞整型值,也可以傳遞某塊內存的地址,內核接口函數必須根據實際情況 提取對應的信息。
<2>驅動層xxx_ioctl參數分析
long(*unlocked_ioctl)(structfile*file,unsignedintcmd,unsignedlongarg); 參數: @file:vfs層為打開字符設備文件的進程創建的結構體,用于存放文件的動態信息 @cmd:用戶空間傳遞的命令,可以根據不同的命令做不同的事情 @第三個參數:用戶空間的數據,主要這個數據可能是一個地址值(用戶空間傳遞的是一個地址),也可能是一個數值,也可能沒值 返回值 成功:0 失敗:帶錯誤碼的負值
<3>如何確定cmd 的值。
該值主要用于區分命令的類型,雖然我只需要傳遞任意一個整型值即可,但是我們盡量按照內核規范要求,充分利用這32bite的空間,如果大家都沒有規矩,又如何能成方圓?
現在我就來看看,在Linux 內核中這個cmd是如何設計的吧!
具體含義如下:
設備類型 | 類型或叫幻數,代表一類設備,一般用一個字母或者1個8bit的數字 |
---|---|
序列號 | 代表這個設備的第幾個命令 |
方 向 | 表示是由內核空間到用戶空間,或是用戶空間到內核空間,入:只讀,只寫,讀寫,其他 |
數據尺寸 | 表示需要讀寫的參數大小 |
由上可以一個命令由4個部分組成,每個部分需要的bite都不完全一樣,制作一個命令需要在不同的位域寫不同的數字,Linux 系統已經給我們封裝好了宏,我們只需要直接調用宏來設計命令即可。
在這里插入圖片描述
通過Linux 系統給我們提供的宏,我們在設計命令的時候,只需要指定設備類型、命令序號,數據類型三個字段就可以了。
Linux 系統中已經設計了一場用的命令,可以通過查閱Linux 源碼中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已經被使用過了。
<4> 如何檢查命令?
可以通過宏_IOC_TYPE(nr)來判斷應用程序傳下來的命令type是否正確;
可以通過宏_IOC_DIR(nr)來得到命令是讀還是寫,然后再通過宏access_ok(type,addr,size)來判斷用戶層傳遞的內存地址是否合法。
使用方法如下:
if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){ pr_err("cmd%u,badmagic0x%x/0x%x. ",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)); elseif(_IOC_DIR(cmd)&_IOC_WRITE) ret=!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd)); if(ret){ pr_err("badaccess%ld. ",ret); return-EFAULT; }
5 注冊cdev
定義好file_operations結構體,就可以通過函數cdev_init()、cdev_add()注冊字符設備驅動了。
實例如下:
staticstructcdevcdev; cdev_init(&cdev,&hello_ops); error=cdev_add(&cdev,devno,1);
注意如果使用了函數register_chrdev(),就不用了執行上述操作,因為該函數已經實現了對cdev的封裝。
五、實例
千言萬語,全部匯總在這一個圖里,大家可以對照相應的層次來學習。
六、實例
好了,現在我們可以來實現一個完整的字符設備框架的實例,包括打開、關閉、讀寫、ioctrl、自動創建設備節點等功能。
#include#include #include #include #include #include #include #include"dev_fifo_head.h" //指定的主設備號 #defineMAJOR_NUM250 //自己的字符設備 structmycdev { intlen; unsignedcharbuffer[50]; structcdevcdev; }; MODULE_LICENSE("GPL"); //設備號 staticdev_tdev_num={0}; //全局gcd structmycdev*gcd; //設備類 structclass*cls; //獲得用戶傳遞的數據,根據它來決定注冊的設備個數 staticintndevices=1; module_param(ndevices,int,0644); MODULE_PARM_DESC(ndevices,"Thenumberofdevicesforregister. "); //打開設備 staticintdev_fifo_open(structinode*inode,structfile*file) { structmycdev*cd; printk("dev_fifo_opensuccess! "); //用structfile的文件私有數據指針保存structmycdev結構體指針 cd=container_of(inode->i_cdev,structmycdev,cdev); file->private_data=cd; return0; } //讀設備 staticssize_tdev_fifo_read(structfile*file,char__user*ubuf,size_t size,loff_t*ppos) { intn; intret; char*kbuf; structmycdev*mycd=file->private_data; printk("read*ppos:%lld ",*ppos); if(*ppos==mycd->len) return0; //請求大大小>buffer剩余的字節數:讀取實際記得字節數 if(size>mycd->len-*ppos) n=mycd->len-*ppos; else n=size; printk("n=%d ",n); //從上一次文件位置指針的位置開始讀取數據 kbuf=mycd->buffer+*ppos; //拷貝數據到用戶空間 ret=copy_to_user(ubuf,kbuf,n); if(ret!=0) return-EFAULT; //更新文件位置指針的值 *ppos+=n; printk("dev_fifo_readsuccess! "); returnn; } //寫設備 staticssize_tdev_fifo_write(structfile*file,constchar__user*ubuf,size_tsize,loff_t*ppos) { intn; intret; char*kbuf; structmycdev*mycd=file->private_data; printk("write*ppos:%lld ",*ppos); //已經到達buffer尾部了 if(*ppos==sizeof(mycd->buffer)) return-1; //請求大大小>buffer剩余的字節數(有多少空間就寫多少數據) if(size>sizeof(mycd->buffer)-*ppos) n=sizeof(mycd->buffer)-*ppos; else n=size; //從上一次文件位置指針的位置開始寫入數據 kbuf=mycd->buffer+*ppos; //拷貝數據到內核空間 ret=copy_from_user(kbuf,ubuf,n); if(ret!=0) return-EFAULT; //更新文件位置指針的值 *ppos+=n; //更新dev_fifo.len mycd->len+=n; printk("dev_fifo_writesuccess! "); returnn; } //linux內核在2.6以后,已經廢棄了ioctl函數指針結構,取而代之的是 longdev_fifo_unlocked_ioctl(structfile*file,unsignedintcmd, unsignedlongarg) { intret=0; structmycdev*mycd=file->private_data; if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){ pr_err("cmd%u,badmagic0x%x/0x%x. ",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)); elseif(_IOC_DIR(cmd)&_IOC_WRITE) ret=!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd)); if(ret){ pr_err("badaccess%ld. ",ret); return-EFAULT; } switch(cmd) { caseDEV_FIFO_CLEAN: printk("CMD:CLEAN "); memset(mycd->buffer,0,sizeof(mycd->buffer)); break; caseDEV_FIFO_SETVALUE: printk("CMD:SETVALUE "); mycd->len=arg; break; caseDEV_FIFO_GETVALUE: printk("CMD:GETVALUE "); ret=put_user(mycd->len,(int*)arg); break; default: return-EFAULT; } returnret; } //設備操作函數接口 staticconststructfile_operationsfifo_operations={ .owner=THIS_MODULE, .open=dev_fifo_open, .read=dev_fifo_read, .write=dev_fifo_write, .unlocked_ioctl=dev_fifo_unlocked_ioctl, }; //模塊入口 int__initdev_fifo_init(void) { inti=0; intn=0; intret; structdevice*device; gcd=kzalloc(ndevices*sizeof(structmycdev),GFP_KERNEL); if(!gcd){ return-ENOMEM; } //設備號:主設備號(12bit)|次設備號(20bit) dev_num=MKDEV(MAJOR_NUM,0); //靜態注冊設備號 ret=register_chrdev_region(dev_num,ndevices,"dev_fifo"); if(ret0){ ????//靜態注冊失敗,進行動態注冊設備號 ?????ret???=alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo"); ??????if(ret?0){ ????????printk("Fail?to?register_chrdev_region "); ????????goto???err_register_chrdev_region; ??????} ????} ????//創建設備類 ????cls???=?class_create(THIS_MODULE,?"dev_fifo"); ????if(IS_ERR(cls)){ ????????ret???=?PTR_ERR(cls); ????????goto???err_class_create; ????} ????printk("ndevices?:???%d ",ndevices); ????for(n?=?0;n?
頭文件內容:
dev_fifo_head.h
#ifndef_DEV_FIFO_HEAD_H #define_DEV_FIFO_HEAD_H #defineDEV_FIFO_TYPE'k' #defineDEV_FIFO_CLEAN_IO(DEV_FIFO_TYPE,0x10) #defineDEV_FIFO_GETVALUE_IOR(DEV_FIFO_TYPE,0x11,int) #defineDEV_FIFO_SETVALUE_IOW(DEV_FIFO_TYPE,0x12,int) #endif
Makefile :
ifeq($(KERNELRELEASE),) KERNEL_DIR?=/lib/modules/$(shelluname-r)/build PWD:=$(shellpwd) modules: $(MAKE)-C$(KERNEL_DIR)M=$(PWD)modules .PHONY:modulesclean clean: $(MAKE)-C$(KERNEL_DIR)M=$(PWD)clean else obj-m:=dev_fifo.o endif
應用程序:
#include#include #include #include #include #include intmain(intargc,constchar*argv[]) { intfd; intn; charbuf[1024]="helloword"; fd=open("/dev/dev_fifo0",O_RDWR); if(fd0){ ????????perror("Fail???ot?open"); ????????return???-1; ????} ????printf("open???successful?,fd?=?%d ",fd); ????n?=?write(fd,buf,strlen(buf)); ????if(n?0){ ????????perror("Fail???to?write"); ????????return???-1; ????} ????printf("write???%d?bytes! ",n); ????n?=?write(fd,buf,strlen(buf)); ????if(n?0){ ????????perror("Fail???to?write"); ????????return???-1; ????} ????printf("write???%d?bytes! ",n); ????return?0; }
測試步驟:
(1) 加載模塊
sudoinsmodhello.ko
(2) 創建設備節點
sudomknod/dev/helloc2500
如果代碼中增加了自動創建設備節點的功能,這個步驟不要執行。
(3) 測試字符設備
gcctest.c-orun sudo./run審核編輯:彭靜
-
數據
+關注
關注
8文章
6890瀏覽量
88826 -
Linux
+關注
關注
87文章
11227瀏覽量
208922 -
驅動程序
+關注
關注
19文章
826瀏覽量
47957
原文標題:一文帶你掌握Linux字符設備架構
文章出處:【微信號:zhuyandz,微信公眾號:FPGA之家】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論