在上一節完成NFS開發環境的搭建后,本節將探討Linux字符設備驅動的開發。字符設備驅動作為Linux內核的重要組成部分,主要負責管理與字符設備(如串口、鍵盤等)的交互,并為用戶空間程序提供統一的讀寫操作接口。
驅動代碼
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/cdev.h> #define DEVICE_NAME "hello_chrdev"
#define BUFFER_SIZE 100// 設備結構體
typedef struct {char buffer[BUFFER_SIZE];struct class *class;struct device *device;dev_t dev_num;struct cdev cdev;
} HelloDevice;static HelloDevice hello_dev;// 打開設備
static int hello_open(struct inode *inode, struct file *filp) {printk(KERN_INFO "Hello device opened\n");return 0;
}// 讀取設備
static ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {size_t len = strlen(hello_dev.buffer);if (*f_pos >= len) {return 0;}if (count > len - *f_pos) {count = len - *f_pos;}if (copy_to_user(buf, hello_dev.buffer + *f_pos, count)) {return -EFAULT;}*f_pos += count;return count;
}// 寫入設備
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {if (count > BUFFER_SIZE - 1) {count = BUFFER_SIZE - 1;}if (copy_from_user(hello_dev.buffer, buf, count)) {return -EFAULT;}hello_dev.buffer[count] = '\0';*f_pos += count;return count;
}// 關閉設備
static int hello_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "Hello device closed\n");return 0;
}// 文件操作結構體
static struct file_operations hello_fops = {.owner = THIS_MODULE,.open = hello_open,.read = hello_read,.write = hello_write,.release = hello_release,
};// 模塊初始化函數
static int __init hello_init(void) {int ret;// 分配設備號ret = alloc_chrdev_region(&hello_dev.dev_num, 0, 1, DEVICE_NAME);if (ret < 0) {printk(KERN_ERR "Failed to allocate character device number\n");return ret;}// 創建類hello_dev.class = class_create(THIS_MODULE, DEVICE_NAME);if (IS_ERR(hello_dev.class)) {unregister_chrdev_region(hello_dev.dev_num, 1);printk(KERN_ERR "Failed to create class\n");return PTR_ERR(hello_dev.class);}// 創建設備hello_dev.device = device_create(hello_dev.class, NULL, hello_dev.dev_num, NULL, DEVICE_NAME);if (IS_ERR(hello_dev.device)) {class_destroy(hello_dev.class);unregister_chrdev_region(hello_dev.dev_num, 1);printk(KERN_ERR "Failed to create device\n");return PTR_ERR(hello_dev.device);}// 初始化 cdev 結構體cdev_init(&hello_dev.cdev, &hello_fops);hello_dev.cdev.owner = THIS_MODULE;// 添加字符設備到系統ret = cdev_add(&hello_dev.cdev, hello_dev.dev_num, 1);if (ret < 0) {device_destroy(hello_dev.class, hello_dev.dev_num);class_destroy(hello_dev.class);unregister_chrdev_region(hello_dev.dev_num, 1);printk(KERN_ERR "Failed to add character device\n");return ret;}printk(KERN_INFO "Hello device initialized. Major: %d, Minor: %d\n", MAJOR(hello_dev.dev_num), MINOR(hello_dev.dev_num));return 0;
}// 模塊卸載函數
static void __exit hello_exit(void) {cdev_del(&hello_dev.cdev);device_destroy(hello_dev.class, hello_dev.dev_num);class_destroy(hello_dev.class);unregister_chrdev_region(hello_dev.dev_num, 1);printk(KERN_INFO "Hello device removed\n");
}module_init(hello_init);
module_exit(hello_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world character device driver");
函數接口詳解
1. 模塊初始化與退出相關函數
alloc_chrdev_region
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- 功能:動態分配一組連續的字符設備號。
- 參數
dev
:用于存儲分配到的設備號。baseminor
:起始的次設備號。count
:要分配的設備號數量。name
:設備的名稱,用于在/proc/devices
中顯示。
- 返回值:成功返回 0,失敗返回負數錯誤碼。
class_create
struct class *class_create(struct module *owner, const char *name);
- 功能:在
/sys/class
目錄下創建一個設備類。 - 參數
owner
:指向模塊的指針,通常為THIS_MODULE
。name
:類的名稱。
- 返回值:成功返回指向
struct class
的指針,失敗返回錯誤指針。
device_create
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
- 功能:在
/sys/class/<class_name>
目錄下創建設備節點,并在/dev
目錄下創建對應的設備文件。 - 參數
class
:指向設備類的指針。parent
:父設備指針,通常為NULL
。devt
:設備號。drvdata
:設備驅動數據,通常為NULL
。fmt
:設備名稱的格式化字符串。
- 返回值:成功返回指向
struct device
的指針,失敗返回錯誤指針。
cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
- 功能:初始化字符設備結構體
struct cdev
,并關聯文件操作結構體struct file_operations
。 - 參數
cdev
:指向struct cdev
的指針。fops
:指向struct file_operations
的指針。
cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
- 功能:將字符設備添加到內核中。
- 參數
p
:指向struct cdev
的指針。dev
:設備號。count
:設備數量。
- 返回值:成功返回 0,失敗返回負數錯誤碼。
module_init
和 module_exit
module_init(hello_init);
module_exit(hello_exit);
- 功能:分別指定模塊加載和卸載時調用的函數。
2. 文件操作相關函數
-
在 Linux 內核中,
struct file_operations
結構體是字符設備驅動與用戶空間進行交互的關鍵橋梁,其中open
、read
、write
和release
是比較常用的操作函數。struct file_operations` 結構體中相關成員介紹
open
函數int (*open) (struct inode *inode, struct file *filp);
- 功能:當用戶空間使用
open()
系統調用打開設備文件時,內核會調用驅動中注冊的open
函數。該函數通常用于執行設備的初始化操作,如分配資源、檢查設備狀態等。 - 參數
struct inode *inode
:指向文件對應的索引節點,包含了文件的元信息,如文件類型、權限等。struct file *filp
:指向文件對象,代表了一個打開的文件實例,包含了文件的當前狀態、偏移量等信息。
- 返回值:成功時返回 0,失敗時返回負數錯誤碼。
read
函數ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
- 功能:當用戶空間使用
read()
系統調用從設備文件讀取數據時,內核會調用驅動中的read
函數。該函數負責將設備中的數據復制到用戶空間的緩沖區。 - 參數
struct file *filp
:指向文件對象。char __user *buf
:用戶空間的緩沖區指針,用于存儲從設備讀取的數據。size_t count
:用戶請求讀取的字節數。loff_t *f_pos
:文件的當前偏移量指針,可通過修改該指針來更新文件的讀寫位置。
- 返回值:成功時返回實際讀取的字節數,返回 0 表示已到達文件末尾,失敗時返回負數錯誤碼。
write
函數ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
- 功能:當用戶空間使用
write()
系統調用向設備文件寫入數據時,內核會調用驅動中的write
函數。該函數負責將用戶空間緩沖區中的數據復制到設備中。 - 參數
struct file *filp
:指向文件對象。const char __user *buf
:用戶空間的緩沖區指針,包含了要寫入設備的數據。size_t count
:用戶請求寫入的字節數。loff_t *f_pos
:文件的當前偏移量指針。
- 返回值:成功時返回實際寫入的字節數,失敗時返回負數錯誤碼。
release
函數int (*release) (struct inode *inode, struct file *filp);
- 功能:當用戶空間使用
close()
系統調用關閉設備文件時,內核會調用驅動中的release
函數。該函數通常用于執行設備的清理操作,如釋放資源、關閉設備等。 - 參數
struct inode *inode
:指向文件對應的索引節點。struct file *filp
:指向文件對象。
- 返回值:成功時返回 0,失敗時返回負數錯誤碼。
- 功能:當用戶空間使用
3. 模塊卸載相關函數
cdev_del
void cdev_del(struct cdev *p);
- 功能:從內核中移除字符設備。
- 參數
p
:指向struct cdev
的指針。
device_destroy
void device_destroy(struct class *class, dev_t devt);
- 功能:銷毀
/sys/class/<class_name>
目錄下的設備節點和/dev
目錄下的設備文件。 - 參數
class
:指向設備類的指針。devt
:設備號。
class_destroy
void class_destroy(struct class *cls);
- 功能:銷毀
/sys/class
目錄下的設備類。 - 參數
cls
:指向struct class
的指針。
unregister_chrdev_region
void unregister_chrdev_region(dev_t from, unsigned count);
- 功能:釋放之前分配的字符設備號。
- 參數
from
:起始的設備號。count
:要釋放的設備號數量。
編譯和測試
編寫 Makefile
obj-m += helloworld.o
KDIR := linux-5.15.18/
PWD := $(shell pwd)
default:$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:$(MAKE) -C $(KDIR) M=$(PWD) clean
將 linux-5.15.18/ 替換為實際的 Linux 5.15.18 內核源碼路徑。
編譯驅動
在終端中執行 make
命令編譯驅動模塊。
測試驅動
在 QEMU 終端中:
- 使用
insmod helloworld.ko
加載驅動模塊。 - 使用 echo “Hello World” > /dev/helloworld 向設備寫入數據。
- 使用 cat /dev/helloworld 從設備讀取數據。
- 使用
rmmod hello_chrdev.ko
卸載驅動模塊。