共計 19027 個字符,預計需要花費 48 分鐘才能閱讀完成。
本篇內容主要講解“Linux 字符設備架構有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓丸趣 TV 小編來帶大家學習“Linux 字符設備架構有哪些”吧!
一、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 結構體中。
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops;// 接口函數集合 struct list_head list;// 內核鏈表 dev_t dev; // 設備號 unsigned int count;// 次設備號個數 };
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() - 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 結構體
參數:
@cdev cdev 結構體地址
@fops 操作字符設備的函數接口地址
返回值:
無
/**
* 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)
功能:
注冊一個范圍()的設備號
參數:
@from 設備號
@count 注冊的設備個數
@name 設備的名字
返回值:
成功返回 0, 失敗返回錯誤碼(負數)
/**
* 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)
功能:
添加一個字符設備到操作系統
參數:
@p cdev 結構體地址
@dev 設備號
@count 次設備號個數
返回值:
成功返回 0, 失敗返回錯誤碼(負數)
/**
* 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)
功能:
從系統中刪除一個字符設備
參數:
@p cdev 結構體地址
返回值:
無
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 功能: 注冊或者分配設備號,并注冊 fops 到 cdev 結構體, 如果 major 0,功能為注冊該主設備號, 如果 major=0,功能為動態分配主設備號。 參數: @major : 主設備號 @name : 設備名稱,執行 cat /proc/devices 顯示的名稱 @fops : 文件系統的接口指針 返回值 如果 major 0 成功返回 0,失敗返回負的錯誤碼 如果 major=0 成功返回主設備號,失敗返回負的錯誤碼
該函數實現了對 cdev 的初始化和注冊的封裝,所以調用該函數之后就不需要自己操作 cdev 了。
相對的注銷函數為 unregister_chrdev
static inline void unregister_chrdev(unsigned int major, const char *name)
四、如何編寫字符設備驅動
參考上圖,編寫字符設備驅動步驟如下:
1. 實現模塊加載和卸載入口函數
module_init (hello_init); module_exit (hello_exit);
2. 申請主設備號
申請主設備號 (內核中用于區分和管理不同字符設備)
register_chrdev_region (devno, number_of_devices, hello
3. 創建設備節點
創建設備節點文件 (為用戶提供一個可操作到文件接口 –open()) 創建設備節點有兩種方式:手動方式創建,函數自動創建。手動創建:
mknod /dev/hello c 250 0
自動創建設備節點
除了使用 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 的初始化腳本在系統啟動時創建設備節點,并且當插入新設備 mdash; mdash; 加入驅動模塊 mdash; mdash; 在 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 類型的對象;
/* 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); \ }) 參數: @owner THIS_MODULE @name 類名字 返回值 可以定義一個 struct class 的指針變量 cls 接受返回值,然后通過 IS_ERR(cls)判斷 是否失敗,如果成功這個宏返回 0,失敗返回非 9 值(可以通過 PTR_ERR(cls)來獲得 失敗返回的錯誤碼)
在 Linux 內核中,把設備進行了分類,同一類設備可以放在同一個目錄下,該函數啟示就是創建了一個類,例如:
第二步:導出我們的設備信息到用戶空間
/** * 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, ...)
自動創建設備節點使用實例:
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 實現 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 對應應用層的 open()、close()函數。實現比較簡單,
直接返回 0 即可。其中 read、write、unloched_ioctrl 函數的實現需要涉及到用戶空間 和內存空間的數據拷貝。
在 Linux 操作系統中,用戶空間和內核空間是相互獨立的。也就是說內核空間是不能直接訪問用戶空間內存地址,同理用戶空間也不能直接訪問內核空間內存地址。
如果想實現,將用戶空間的數據拷貝到內核空間或將內核空間數據拷貝到用戶空間,就必須借助內核給我們提供的接口來完成。
1. read 接口實現
用戶空間 – 內核空間
字符設備的 write 接口定義如下:
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos); 參數: filp: 待操作的設備文件 file 結構體指針 buf: 待寫入所讀取數據的用戶空間緩沖區指針 count: 待讀取數據字節數 f_pos: 待讀取數據文件位置,寫入完成后根據實際寫入字節數重新定位 返回: 成功實際寫入的字節數,失敗返回負值
如果該操作為空,將使得 write 系統調用返回負 EINVAL 失敗,正常返回實際寫入的字節數。
用戶空間向內核空間拷貝數據需要使用 copy_from_user 函數,該函數定義在 arch/arm/include/asm/uaccess.h 中。
static inline int copy_from_user(void *to, const void __user volatile *from,unsigned long n) 參數: to: 目標地址(內核空間) from: 源地址(用戶空間) n: 將要拷貝數據的字節數 返回: 成功返回 0,失敗返回沒有拷貝成功的數據字節數
還可以使用 get_user 宏:
int get_user(data, ptr); 參數: data: 可以是字節、半字、字、雙字類型的內核變量 ptr: 用戶空間內存指針 返回: 成功返回 0,失敗返回非 0
2. write 接口實現
內核空間 – 用戶空間
字符設備的 read 接口定義如下:
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, lofft *f_pos); 參數: filp: 待操作的設備文件 file 結構體指針 buf: 待寫入所讀取數據的用戶空間緩沖區指針 count: 待讀取數據字節數 f_pos: 待讀取數據文件位置,讀取完成后根據實際讀取字節數重新定位 __user :是一個空的宏,主要用來顯示的告訴程序員它修飾的指針變量存放的是用戶空間的地址。 返回值: 成功實際讀取的字節數,失敗返回負值
注意:如果該操作為空,將使得 read 系統調用返回負 EINVAL 失敗,正常返回實際讀取的字節數。
用戶空間從內核空間讀取數據需要使用 copy_to_user 函數:
static inline int copy_to_user(void __user volatile *to, const void *from,unsigned long n) 數: to: 目標地址(用戶空間) from: 源地址(內核空間) n: 將要拷貝數據的字節數 回: 成功返回 0,失敗返回沒有拷貝成功的數據字節數
在這里插入圖片描述
還可以使用 put_user 宏:
int put_user(data, prt) 參數: data: 可以是字節、半字、字、雙字類型的內核變量 ptr: 用戶空間內存指針 返回: 成功返回 0, 失敗返回非 0
這樣我們就可以實現 read、write 函數了,實例如下:
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 接口實現
(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。
middot; 2.6.36 之前的內核
long (ioctl) (struct inode node ,struct file* filp, unsigned int cmd,unsigned long arg)
middot; 2.6.36 之后的內核
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)
參數 cmd: 通過應用函數 ioctl 傳遞下來的命令
先來看看應用層的 ioctl 和驅動層的 xxx_ioctl 對應關系:
1 應用層 ioctl 參數分析
int ioctl(int fd, int cmd, ...); 參數: @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) (struct file *file, unsigned int cmd, unsigned long arg); 參數: @file: vfs 層為打開字符設備文件的進程創建的結構體,用于存放文件的動態信息 @ cmd: 用戶空間傳遞的命令,可以根據不同的命令做不同的事情 @第三個參數: 用戶空間的數據,主要這個數據可能是一個地址值(用戶空間傳遞的是一個地址),也可能是一個數值,也可能沒值 返回值 成功:0 失敗:帶錯誤碼的負值
3 如何確定 cmd 的值。
該值主要用于區分命令的類型,雖然我只需要傳遞任意一個整型值即可,但是我們盡量按照內核規范要求,充分利用這 32bite 的空間,如果大家都沒有規矩,又如何能成方圓?
現在我就來看看,在 Linux 內核中這個 cmd 是如何設計的吧!
具體含義如下:
由上可以一個命令由 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,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 結構體,就可以通過函數 cdev_init()、cdev_add()注冊字符設備驅動了。
實例如下:
static struct cdev cdev; cdev_init(cdev, hello_ops); error = cdev_add(cdev,devno,1);
注意如果使用了函數 register_chrdev(), 就不用了執行上述操作,因為該函數已經實現了對 cdev 的封裝。
五、實例
千言萬語,全部匯總在這一個圖里,大家可以對照相應的層次來學習。
六、實例
好了,現在我們可以來實現一個完整的字符設備框架的實例,包括打開、關閉、讀寫、ioctrl、自動創建設備節點等功能。
#include linux/init.h #include linux/module.h #include linux/cdev.h #include linux/fs.h #include linux/device.h #include linux/slab.h #include asm/uaccess.h #include dev_fifo_head.h // 指定的主設備號 #define MAJOR_NUM 250 // 自己的字符設備 struct mycdev { int len; unsigned char buffer[50]; struct cdev cdev; }; MODULE_LICENSE(GPL // 設備號 static dev_t dev_num = {0}; // 全局 gcd struct mycdev *gcd; // 設備類 struct class *cls; // 獲得用戶傳遞的數據,根據它來決定注冊的設備個數 static int ndevices = 1; module_param(ndevices, int, 0644); MODULE_PARM_DESC(ndevices, The number of devices for register.\n // 打開設備 static int dev_fifo_open(struct inode *inode, struct file *file) { struct mycdev *cd; printk( dev_fifo_open success!\n // 用 struct file 的文件私有數據指針保存 struct mycdev 結構體指針 cd = container_of(inode- i_cdev,struct mycdev,cdev); file- private_data = cd; return 0; } // 讀設備 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 剩余的字節數 : 讀取實際記得字節數 if(size mycd- len - *ppos) n = mycd- len - *ppos; else n = size; printk(n = %d\n ,n); // 從上一次文件位置指針的位置開始讀取數據 kbuf = mycd- buffer + *ppos; // 拷貝數據到用戶空間 ret = copy_to_user(ubuf,kbuf, n); if(ret != 0) return -EFAULT; // 更新文件位置指針的值 *ppos += n; printk(dev_fifo_read success!\n return n; } // 寫設備 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); // 已經到達 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_write success!\n return n; } //linux 內核在 2.6 以后,已經廢棄了 ioctl 函數指針結構,取而代之的是 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; } // 設備操作函數接口 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; } // 設備號 : 主設備號(12bit) | 次設備號(20bit) dev_num = MKDEV(MAJOR_NUM, 0); // 靜態注冊設備號 ret = register_chrdev_region(dev_num,ndevices, dev_fifo if(ret 0){ // 靜態注冊失敗,進行動態注冊設備號 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; } } // 創建設備類 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 ndevices;n ++) { // 初始化字符設備 cdev_init( gcd[n].cdev, fifo_operations); // 添加設備到操作系統 ret = cdev_add(gcd[n].cdev,dev_num + n,1); if (ret 0) { goto err_cdev_add; } // 導出設備信息到用戶空間(/sys/class/ 類名 / 設備名) 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: // 將已經導出的設備信息除去 for(i = 0;i n;i ++) { device_destroy(cls,dev_num + i); } err_cdev_add: // 將已經添加的全部除去 for(i = 0;i n;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 文件系統中的設備 for(i = 0;i ndevices;i ++) { device_destroy(cls,dev_num + i); } // 刪除系統中的設備類 class_destroy(cls); // 從系統中刪除添加的字符設備 for(i = 0;i ndevices;i ++) { cdev_del( gcd[i].cdev); } // 釋放申請的設備號 unregister_chrdev_region(dev_num, ndevices); return; } module_init(dev_fifo_init); module_exit(dev_fifo_exit);
頭文件內容:
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
應用程序:
#include stdio.h #include stdlib.h #include sys/types.h #include string.h #include sys/stat.h #include fcntl.h 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) 創建設備節點
sudo mknod /dev/hello c 250 0
如果代碼中增加了自動創建設備節點的功能,這個步驟不要執行。
(3) 測試字符設備
gcc test.c -o runsudo ./run
到此,相信大家對“Linux 字符設備架構有哪些”有了更深的了解,不妨來實際操作一番吧!這里是丸趣 TV 網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!