目錄
- 1. 什么是字符設備
- 2. 設備號
- 2.1 設備號概念
- 2.2 通過設備號dev分別獲取主、次設備號的宏函數
- 2.3 主設備號的申請
- 靜態申請
- 動態分配
- 2.4 注銷設備號
- 3. 字符設備
- 3.1 注冊字符設備
- 3.2 注銷字符設備
- 3.3 應用程序和驅動程序的關系
- 3.4 file_opertaions結構體
- 3.5 class_create
- 3.6 創建設備文件的2種方法
- 4. 字符設備驅動程序的開發步驟
- 5. 應用層與驅動間拷貝數據
- 5.1 copy_to_user
- 5.2 copy_to_user
- 6. 驅動中字符設備文件的相關3個結構體
- 7. 例子
- 7.1 手動創建和自動創建的區別
- 7.2 手動創建
- 7.3 自動創建
1. 什么是字符設備
字符設備是Linux三大設備之一(另外兩種是塊設備,網絡設備),字符設備就是字節流形式通訊的I/O設備, 在讀寫時是按字節順序進行的,絕大部分設備都是字符設備,常見的字符設備包括鼠標、鍵盤、顯示器、串口等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
2. 設備號
2.1 設備號概念
為方便管理, Linux中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成:
主設備號:用來標識某種設備類型。
次設備號:用來區分同一種設備類型的不同設備。
Linux使用一個結構描述設備號:dev_t 高12位是主設備號,低20位是次設備號
2.2 通過設備號dev分別獲取主、次設備號的宏函數
關于設備號操作的 3 個宏函數。這三個宏是 Linux 內核(通常在 <linux/kdev_t.h> 中定義)中用于操作設備號 dev_t 的核心工具。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))
功能
MAJOR(dev):獲取設備號 dev 中的主設備號(通過右移去除次設備號位)。
MINOR(dev):獲取設備號 dev 中的次設備號(通過與掩碼屏蔽掉主設備號位)。
MKDEV(ma, mi):將主設備號 ma 和次設備號 mi 組合成一個完整的設備號(通過左移主設備號后與次設備號合并)。
2.3 主設備號的申請
靜態申請
在內核源碼路徑下確定一個沒有使用的主設備號(可查看Documentation/devices.txt文檔確定未使用的設備號),通過函數 register_chrdev_region 或register_chrdev注冊設備號:
1. register_chrdev_region
int register_chrdev_region(dev_t from, unsigned count, const char *name);
參數
from:要分配的起始設備編號,from的次設備編號部分常常是 0, 但是沒有強制要求
count:請求的連續設備編號的總數
name:設備的名稱,它會出現在 /proc/devices 和sysfs 中
返回值
成功返回0,否則返回小于0
2. register_chrdev
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
參數
major:major為0時,此函數將動態分配一個主設備號,并返回它的值,若major>0時,此函數將嘗試使用給定的設備號數值保留該設備號
name:設備的名稱,它會出現在/proc/devices和sysfs中
fops:struct file_operations結構體,為應用程序提供的訪問本驅動的操作方法指針
返回值
成功返回0,否則返回小于0。
動態分配
alloc_chrdev_region
int alloc_chrdev_region(dev_t * dev, unsigned baseminor, unsigned count, const char *name);
參數
dev :設備號,動態分配,若調用成功用此參數返回分配的設備號>0。
baseminor:起始的次設備號
count:需要分配的設備號數目
name:設備名(體現在/proc/devices中)
返回值
成功返回0,否則返回非0。
2.4 注銷設備號
void unregister_chrdev_region(dev_t from, unsigned count);
釋放從 from 開始的 count 個設備號
3. 字符設備
3.1 注冊字符設備
在linux內核中用struct cdev來表示一個字符設備:
struct cdev {struct kobject kobj; //內核中的最頂層的基類struct module *owner; //表示該字符設備屬于哪個模塊,一般值設置為THIS_MODULE。const struct file_operations *ops; //注冊驅動的關鍵, 要填充成這個結構體變量struct list_head list; //內核鏈表, 內核用來統一管理字符設備dev_t dev; //設備號(包括主設備號和次設備號)unsigned int count; //次設備號個數
};
在編寫字符驅動之前,需要先注冊一個字符設備,示例如下:
struct cdev {struct kobject kobj;struct module * owner; //值為THIS_MODULE, 代表當前模塊const struct file_operations; //應用程序的訪問接口, 驅動要負責實體此結構體中的函數struct list_head list;dev_t dev //設備號unsigned int cout; //次設備號的數據
};
//自行查找 內核鏈表的實現
在編寫字符驅動之前, 必須注冊一個字符設備//定義一個cdev對象
struct cdev test_cdev;//動態分配一個cdev對象
struct cdev *cdev_alloc(void);//字符設備文件操作函數集合
struct file_operations test_fops = {.owner = THIS_MODULE,.open = xxx_open,.read = xxx_open,.write = xxx_write,......
};/*初始化cdev結構體成員*/
test_cdev.owner = THIS_MODULE;
cdev_init(&test_cdev, &test_fops);/*向內核中添加字符設備*/
cdev_add(&test_cdev, dev_number, dev_count);
3.2 注銷字符設備
void cdev_del(struct cdev *pcdev);
釋放cdev結構體空間,cdev_del函數內部能夠判斷struct cdev定義的對象是用的堆內存還是棧內存還是數據段內存的。這個函數cdev_del調用時,會先去檢查有沒有使用堆內存,如果有用到的話,會先去釋放掉所占用的堆內存,然后在注銷掉這個設備驅動。所以,如果struct cdev要用堆內存一定要用內核提供的這個cdev_alloc去分配堆內存,因為內部會做記錄,這樣在cdev_del注銷掉這個驅動的時候,才會去釋放掉那段堆內存。
3.3 應用程序和驅動程序的關系
3.4 file_opertaions結構體
關于 C庫以及如何通過系統調用調入到內核空間不用去管,重點關注的是應用程序和具體的驅動,應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了 open這個函數,那么在驅動程序中也得有一個名為 open的函數。每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 Linux內核文件 include/linux/fs.h中有個叫做file_operations的結構體,此結構體就是Linux內核驅動操作函數集合,內容如下所示:
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);int (*show_fdinfo)(struct seq_file *m, struct file *f);/* get_lower_file is for stackable file system */struct file* (*get_lower_file)(struct file *f);
};
file_operations 結構體函數成員太多,幾個常用的:
llseek() 函數用于修改文件指針偏移量
read() 函數用于讀取設備文件
write() 函數用于向設備文件寫入數據
poll() 函數用于輪詢監聽設備狀態
unlocked_ioctl() 函數提供對于設備的控制功能,與應用程序中的 ioctl 函數對應
compat_ioctl() 函數與 unlocked_ioctl() 函數功能一樣,區別在于在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是unlocked_ioctl()
mmap() 函數用于將設備的內存映射到進程空間中(也就是用戶空間)
open() 函數用于打開設備文件
release() 函數用于釋放(關閉)設備文件,與應用程序中的 close 函數對應
fasync() 函數用于刷新待處理的數據,用于將緩沖區中的數據刷新到磁盤中
aio_fsync() 函數與 fasync 函數的功能類似,只是 aio_fsync 是異步刷新待處理的數據
3.5 class_create
宏class_create()用于動態創建設備的邏輯類,并完成部分字段的初始化,然后將其添加進Linux內核系統中。此函數的執行效果就是在/sys/class/目錄下創建一個新的文件夾,此文件夾的名字為此函數的第二個輸入參數,但此文件夾是空的。宏class_create()在實現時,調用了函數__class_create()。
函數實現
<內核源碼路徑>/include/linux/device.h中:
#define class_create(owner, name) \
({ \static struct lock_class_key __key; \__class_create(owner, name, &__key); \
})
參數說明
owner:一個struct module結構體類型的指針,指向函數__class_create()
即將創建的、“擁有”這個struct class的模塊。一般賦值為THIS_MODULE,
此結構體的詳細定義見文件include/linux/module.h。name:char類型的指針,代表即將創建的struct class變量的名字,
用于給struct class的name字段賦值。
通俗地說,就是指向struct class名稱的字符串的指針。
返回值
與函數__class_create()的返回值相同,都代表新創建的邏輯類。
3.6 創建設備文件的2種方法
1.使用mknod命令手動創建
命令格式:
mknod devname type major minor
drvname:設備文件名
type:設備文件類型 c字符設備/d塊設備
major:主設備號
minor:次設備號
2.自動創建(加載模塊時注冊創建,卸載模塊時釋放)
1)函數register_chrdev
自動獲取注冊一個字符設備并申請設備號,同時注冊file_operations
操作方法集
2)函數class_create
創建一個struct class類,這個類存放在sysfs下面,一旦創建好了這個類, 調用device_create
函數在/dev目錄下創建相應的設備節點。這樣加載驅動程序模塊時,用戶空間中udev會自動響應device_create函數,去/sysfs下尋找對用的類從而創建設備節點。
3)函數device_create返回值是創建一個struct device型設備關聯設備號,class,和設備節點。
4. 字符設備驅動程序的開發步驟
在內核模塊中使用一個結構來描述字符設備對象:
struct cdev {struct kobject kobj; // 文件系統相關,由系統來管理,不需要自己添加struct module *owner; // 用于模塊計數const struct file_operations *ops; // 對硬件的操作方法集struct list_head list; // 用于管理字符設備的鏈表dev_t dev; // 設備號;把軟件和硬件結合起來;主設備號<<20 +次設備號unsigned int count; // 同一主設備號下的,次設備號的個數
};
4.1 注冊字符設備
- 定義和分配struct cdev
- 初始化字符設備對象(對硬件操作的方法集):cdev_init(struct cdev *, const struct file_operations *)
- 向內核申請設備號:register_chrdev_region(dev_t from, unsigned count, const char * name) 或者 alloc_chrdev_region()
- 添加當前的字符設備到內核中:cdev_add(struct cdev *, dev_t, unsigned)
4.2 實現 struct file_operations函數,此函數向應用程序提供調用接口。
4.3 卸載字符設備
- cdev_del(struct cdev *); // 刪除字符設備
- unregister_chrdev_region(dev_t from, unsigned count); //刪除
5. 應用層與驅動間拷貝數據
用戶訪問內核時,進程需要從用戶態切換為內核態,然后再訪問內核,這么做的目的是防止用戶隨意篡改內核數據。在編寫某個外設的驅動時,需要實現內核中的 read 和 write 函數,此時站在內核的角度,無法直接讀取用戶緩沖區的數據也無法直接向用戶緩沖區寫入數據。因此,就需要用到copy_to_user
和copy_from_user
函數。
copy_to_user()
:完成內核空間到用戶空間的復制
copy_from_user()
:完成用戶空間到內核空間的復制
#include <linux/uaccess.h> // copy_to_user & copy_from_user 需要包含的頭文件
5.1 copy_to_user
函數聲明
調用該函數需站在內核的角度,即調用該函數所在文件中的緩沖區,都屬于內核緩沖區
ulong copy_to_user(void __user * to, const void * from, unsigned long n);
第一個參數 to:要向用戶空間返回數據的地址
第二個參數 from: 內核空間地址,將此地址處的數據拷貝給用戶空間的to指針指向的地址。
第三個參數 n:要寫入數據到用戶空間中的字節數
返回值:成功返回 0
具體使用:
static char readbuf[100]; /* 讀緩沖區 *//** 該函數一般用于內核中的 read 函數的定義中,將內核接收到的內容拷貝到用戶緩沖區*/static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{int ret = copy_to_user(buf, readbuf, cnt); // 將readbuf中的內容拷貝到buf// 拷貝的字節數大小為cntif(ret == 0){printk("kernel send data ok!\n");}else{printk("kernel send data failed!\n"); } return 0;
}
5.2 copy_to_user
6. 驅動中字符設備文件的相關3個結構體
struct inode: 記錄文件的物理信息。
struct file:代表一個打開的文件,由內核打開時創建,關閉時釋放。
struct file_operations: 函數指針集合,是對硬件操作的方法集,通過在驅動程序中重新實現并注冊到內核中,供應用層調用。
7. 例子
hello_cdev.c
#include <linux/module.h> // 模塊相關頭文件
#include <linux/init.h> // 模塊初始化和退出宏
#include <linux/kernel.h> // 內核核心功能
#include <linux/cdev.h> // 字符設備結構
#include <linux/fs.h> // 文件系統接口#define BUF_LEN 32 // 設備緩沖區大小
#define HELLO_CDEV_NAME "hello_cdev" // 設備名稱static dev_t devno; // 設備號變量(包含主/次設備號)
struct cdev hello_cdev; // 字符設備結構體
static char gbuffer[BUF_LEN] = "HelloWorld"; // 設備數據緩沖區
static int curlen = 0; // 當前有效數據長度/* 設備打開函數 */
static int hello_cdev_open(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__); // 打印函數名(調試用)return 0; // 成功打開
}/* 設備關閉函數 */
static int hello_cdev_release(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__); // 打印函數名(調試用)return 0; // 成功關閉
}/* 設備讀取函數 */
static ssize_t hello_cdev_read(struct file * filp, char __user * buf, size_t size, loff_t * ppos)
{int ret;size_t copy_len = size;/* 防止讀取超出實際數據范圍 */if(copy_len > curlen) {copy_len = curlen;}/* 將內核數據復制到用戶空間 */ret = copy_to_user(buf, gbuffer, copy_len);if(ret) { // 復制失敗處理printk("%s copy_to_user failed\n", __func__);return -EFAULT; // 返回錯誤碼(錯誤地址)}return copy_len; // 返回實際讀取字節數
}/* 設備寫入函數 */
static ssize_t hello_cdev_write(struct file * filp, const char __user * buf, size_t size, loff_t * ppos)
{int ret;size_t copy_len = size;/* 限制寫入不超過緩沖區大小 */if(copy_len > BUF_LEN) {copy_len = BUF_LEN;}/* 將用戶數據復制到內核空間 */ret = copy_from_user(gbuffer, buf, copy_len);if(ret) { // 復制失敗處理printk("%s copy_from_user failed\n", __func__);return -EFAULT; // 返回錯誤碼(錯誤地址)}curlen = copy_len; // 更新有效數據長度return copy_len; // 返回實際寫入字節數
}/* 設備操作函數集 */
static struct file_operations hello_ops = {.owner = THIS_MODULE, // 指向當前模塊.open = hello_cdev_open, // 打開設備回調.release = hello_cdev_release, // 關閉設備回調.read = hello_cdev_read, // 讀設備回調.write = hello_cdev_write, // 寫設備回調
};/* 模塊初始化函數 */
static int __init hello_cdev_init(void)
{int ret;/* 1. 動態申請設備號 */ret = alloc_chrdev_region(&devno, 0, 1, HELLO_CDEV_NAME);if(ret) {printk(KERN_ERR "%s alloc_chrdev_region failed\n", __func__);return ret; // 返回錯誤碼}printk("%s major:%d, minor:%d\n", __func__, MAJOR(devno), MINOR(devno)); // 打印主/次設備號/* 2. 初始化字符設備 */hello_cdev.owner = THIS_MODULE;cdev_init(&hello_cdev, &hello_ops); // 關聯操作函數集/* 3. 添加設備到系統 */ret = cdev_add(&hello_cdev, devno, 1);if(ret) {printk(KERN_ERR "%s cdev_add failed\n", __func__);goto cdev_err; // 跳轉到錯誤處理}curlen = strlen(gbuffer); // 初始化有效數據長度return 0; // 初始化成功cdev_err:/* 錯誤處理:釋放設備號 */unregister_chrdev_region(devno, 1);return ret;
}/* 模塊退出函數 */
static void __exit hello_cdev_exit(void)
{/* 1. 從系統移除字符設備 */cdev_del(&hello_cdev);/* 2. 釋放設備號資源 */unregister_chrdev_region(devno, 1);printk("Bye... %s\n", __func__); // 退出提示
}/* 模塊注冊 */
module_init(hello_cdev_init); // 指定初始化函數
module_exit(hello_cdev_exit); // 指定退出函數
MODULE_LICENSE("GPL"); // 聲明GPL許可證
簡潔版
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>#define BUF_LEN 32
#define HELLO_CDEV_NAME "hello_cdev"static dev_t devno;
struct cdev hello_cdev;
static char gbuffer[BUF_LEN] = "HelloWorld";
static int curlen = 0;static int hello_cdev_open(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static int hello_cdev_release(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static ssize_t hello_cdev_read(struct file * filp, char __user * buf, size_t size, loff_t * ppos)
{int ret;size_t copy_len = size;/*若用戶要讀取的數據量超過了gbuffer中實際的數據量,則以實際 gbuffer中存放的數據量為準*/if(copy_len > curlen) {copy_len = curlen;}/*將內核中的數據返回給用戶空間*/ret = copy_to_user(buf, gbuffer, copy_len);if(ret) {printk("%s copy_to_user failed\n", __func__);return -EFAULT;}return copy_len;
}static ssize_t hello_cdev_write(struct file * filp, const char __user * buf, size_t size, loff_t * ppos)
{int ret;size_t copy_len = size;if(copy_len > BUF_LEN) {copy_len = BUF_LEN;}/*將用戶空間傳入的數據寫入到gbuffer中*/ret = copy_from_user(gbuffer, buf, copy_len);if(ret) {printk("%s copy_from_user failed\n", __func__);return -EFAULT;}curlen = copy_len;return copy_len;
}/*2, 實現struct file_operations中的函數*/
static struct file_operations hello_ops = {.owner = THIS_MODULE,.open = hello_cdev_open, //用戶空間調用open API函數打開設備文件時.release = hello_cdev_release, //用戶空間調用close API函數關閉設備文件時.read = hello_cdev_read, //用戶空間調用read API函數讀取設備文件時.write = hello_cdev_write, //用戶空間調用write API函數寫入設備文件時
};static int __init hello_cdev_init(void)
{/*1, 申請設備號2, 實現struct file_operations中的函數3, 初始化cdev, 并將cdev對象注冊到內核中*/int ret;/*1, 申請設備號*/ret = alloc_chrdev_region(&devno, 0, 1, HELLO_CDEV_NAME);//devno設備號,次設備號,設備號數目,設備名if(ret) {//成功返回0,不成功非0printk(KERN_ERR "%s alloc_chrdev_region failed\n", __func__);}printk("%s major:%d, minor:%d\n", __func__, MAJOR(devno), MINOR(devno));/*3, 初始化cdev, 添加cdev對象到內核中*/hello_cdev.owner = THIS_MODULE;cdev_init(&hello_cdev, &hello_ops);ret = cdev_add(&hello_cdev, devno, 1);if(ret) {printk(KERN_ERR "%s cdev_add failed\n", __func__);goto cdev_err;}curlen = strlen(gbuffer);return 0;
cdev_err:/*退出時釋放設備號*/unregister_chrdev_region(devno, 1);return ret;
}static void __exit hello_cdev_exit(void)
{/*1, 注銷cdev 對象*/cdev_del(&hello_cdev);/*2, 釋放設備號*/unregister_chrdev_region(devno, 1);printk("Bye... %s\n", __func__);
}module_init(hello_cdev_init);
module_exit(hello_cdev_exit);
MODULE_LICENSE("GPL");
7.1 手動創建和自動創建的區別
7.2 手動創建
hello_cdev.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>#define BUF_LEN 64
#define HELLO_CDEV_NAME "hello_cdev"
static dev_t devno;
static struct cdev hello_cdev;
static char kbuffer[BUF_LEN] = "HelloWorld";static int hello_cdev_open(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static int hello_cdev_release(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static ssize_t hello_cdev_read(struct file * filp, char __user * user_buf, size_t count, loff_t * f_ops)
{int ret = 0;ssize_t copy_len = 0;int buflen = strlen(kbuffer);printk("%s\n", __func__);/*若用戶空間要讀取的數據量超過了buffer中的字符數, 則以buffer中的實際數據量為準*/copy_len = count < buflen ? count : buflen;ret = copy_to_user(user_buf, kbuffer, copy_len);if(ret) {printk(KERN_ERR "%s copy_to_user failed, copy_len:%ld\n", __func__, copy_len);return -ENOSPC;}return copy_len;
}static ssize_t hello_cdev_write(struct file * filp, const char __user * user_buf, size_t count, loff_t * f_ops)
{int ret = 0;ssize_t copy_len = count;int buflen = sizeof(kbuffer);printk("%s\n", __func__);if(copy_len > buflen) {copy_len = buflen;}/*將內核中的數據返回給用戶程序*/ret = copy_from_user(kbuffer, user_buf, copy_len);if(ret) {printk(KERN_ERR "%s copy_from_user failed, copy_len:%ld\n", __func__, copy_len);return -ENOSPC;}return copy_len;
}static struct file_operations hello_fops = {.owner = THIS_MODULE,.open = hello_cdev_open,.release = hello_cdev_release,.read = hello_cdev_read,.write = hello_cdev_write,
};static int __init hello_cdev_init(void)
{/*1, 申請設備號2, 初始化cdev對象3, 添加cdev對象到內核中4, 實現 struct file_operations函數集*//*1, 申請設備號*/int ret = 0;ret = alloc_chrdev_region(&devno, 0, 1, HELLO_CDEV_NAME);if(ret) {/*返回值 不為0, 申請 設備號失敗, 退出內核模塊的加載流程*/printk(KERN_ERR "%s, alloc_chrdev_region failed\n", __func__);return -EFAULT;}printk("%s, major:%d , minor:%d\n", __func__, MAJOR(devno), MINOR(devno));/*2, 初始化 cdev */hello_cdev.owner = THIS_MODULE;/*關聯cdev 和 file_operations*/cdev_init(&hello_cdev, &hello_fops);ret = cdev_add(&hello_cdev, devno, 1);if(ret) {printk(KERN_ERR "%s, cdev_add filed\n", __func__);ret = -EFAULT;goto cdev_err;}return 0;
cdev_err:unregister_chrdev_region(devno, 1);return ret;
}static void __exit hello_cdev_exit(void)
{/*注銷字符設備*/cdev_del(&hello_cdev);/*釋放設備號*/unregister_chrdev_region(devno, 1);printk("Bye %s\n", __func__);
}module_init(hello_cdev_init);
module_exit(hello_cdev_exit);MODULE_LICENSE("GPL");
test_hello.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>#define DEV_NAME "/dev/hello-dev"
int main()
{int fd, ret;char buf[32] = {0};fd = open(DEV_NAME, O_RDWR);if(fd < 0) {perror("open");return -1;}//lseek(fd, 1, SEEK_SET);ret = read(fd, buf, sizeof(buf)); printf("read 1 count:%d, data:%s\n", ret, buf);memset(buf, 0, sizeof(buf));sprintf(buf, "%s", "This is a test");ret = write(fd, buf, strlen(buf));ret = read(fd, buf, sizeof(buf));printf("read 2 count:%d, data:%s\n", ret, buf);close(fd);return 0;
}
Makefile
APP=test_hello
#如果$(KERNELRELEASE)是空的
ifeq ($(KERNELRELEASE),)
#獲得內核源碼根目錄KERNELDIR ?= /lib/modules/$(shell uname -r)/build#獲取當前路徑 PWD:=$(shell pwd)
modules:
# $(MAKE) -C $(KERNELDIR) 解釋: $(MAKE) 就是 make , -C 進入一個子目錄 /lib/modules/5.15.0-139-generic/build/arch, 再執行子目錄的Makefile
# M=$(PWD) 解釋: M是一個變量,值為當前目錄, 將當前M的值傳遞給 $(KERNELDIR) 目錄下的Makefile
# modules 是執行$(KERNELDIR) 目錄下的Makefile時傳遞的目標$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesgcc $(APP).c -o $(APP)
elseobj-m=hello_cdev.o
endifclean:rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.cmd .*.*.d $(APP)
終端
1. 進入目錄下
2. sudo dmesg -C
3. make
4. insmod hello_cdev.ko
5. dmesg查看主設備號cdev_t
6. ※mknod /dev/hello_cdev c cdev_t(上條命令查看的) 0
7. sudo chmod 666 /dev/hello_cdev
8. ./test_hello(可以在另一個終端上測)
9. 輸入測試
10.dmesg
7.3 自動創建
hello_cdev.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>#define BUF_LEN 64
#define HELLO_CDEV_NAME "hello_cdev"
static dev_t devno;
static struct cdev hello_cdev;
static char kbuffer[BUF_LEN] = "HelloWorld";static struct class * clsp;
static struct device * devp;static int hello_cdev_open(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static int hello_cdev_release(struct inode * inodep, struct file * filp)
{printk("%s\n", __func__);return 0;
}static ssize_t hello_cdev_read(struct file * filp, char __user * user_buf, size_t count, loff_t * f_ops)
{int ret = 0;ssize_t copy_len = 0;int buflen = strlen(kbuffer);printk("%s\n", __func__);/*若用戶空間要讀取的數據量超過了buffer中的字符數, 則以buffer中的實際數據量為準*/copy_len = count < buflen ? count : buflen;ret = copy_to_user(user_buf, kbuffer, copy_len);if(ret) {printk(KERN_ERR "%s copy_to_user failed, copy_len:%ld\n", __func__, copy_len);return -ENOSPC;}return copy_len;
}static ssize_t hello_cdev_write(struct file * filp, const char __user * user_buf, size_t count, loff_t * f_ops)
{int ret = 0;ssize_t copy_len = count;int buflen = sizeof(kbuffer);printk("%s\n", __func__);if(copy_len > buflen) {copy_len = buflen;}/*將內核中的數據返回給用戶程序*/ret = copy_from_user(kbuffer, user_buf, copy_len);if(ret) {printk(KERN_ERR "%s copy_from_user failed, copy_len:%ld\n", __func__, copy_len);return -ENOSPC;}return copy_len;
}static struct file_operations hello_fops = {.owner = THIS_MODULE,.open = hello_cdev_open,.release = hello_cdev_release,.read = hello_cdev_read,.write = hello_cdev_write,
};static int __init hello_cdev_init(void)
{/*1, 申請設備號2, 初始化cdev對象3, 添加cdev對象到內核中4, 實現 struct file_operations函數集*//*1, 申請設備號*/int ret = 0;ret = alloc_chrdev_region(&devno, 0, 1, HELLO_CDEV_NAME);if(ret) {/*返回值 不為0, 申請 設備號失敗, 退出內核模塊的加載流程*/printk(KERN_ERR "%s, alloc_chrdev_region failed\n", __func__);return -EFAULT;}printk("%s, major:%d , minor:%d\n", __func__, MAJOR(devno), MINOR(devno));/*2, 初始化 cdev */hello_cdev.owner = THIS_MODULE;/*關聯cdev 和 file_operations*/cdev_init(&hello_cdev, &hello_fops);ret = cdev_add(&hello_cdev, devno, 1);if(ret) {printk(KERN_ERR "%s, cdev_add filed\n", __func__);ret = -EFAULT;goto cdev_err;}/*創建設備節點*//*1, 創建設備類*/clsp = class_create(THIS_MODULE, "hello_cls");if(IS_ERR(clsp)) {ret = PTR_ERR(clsp); /*獲得錯誤碼*/printk("%s, class_create failed\n", __func__);goto cls_err;}/*2, 創建設備文件*/devp = device_create(clsp, NULL, devno, NULL, "hello_autnode_dev");if(IS_ERR(devp)) {ret = PTR_ERR(devp); /*獲得錯誤碼*/printk("%s, device_create failed\n", __func__);goto dev_err;}return 0;
dev_err:class_destroy(clsp);
cls_err:cdev_del(&hello_cdev);
cdev_err:unregister_chrdev_region(devno, 1);return ret;
}static void __exit hello_cdev_exit(void)
{/*1, 刪除設備文件*/device_del(devp);/*2, 注銷設備類*/class_destroy(clsp);/*3, 注銷字符設備*/cdev_del(&hello_cdev);/*4, 釋放設備號*/unregister_chrdev_region(devno, 1);printk("Bye %s\n", __func__);
}module_init(hello_cdev_init);
module_exit(hello_cdev_exit);MODULE_LICENSE("GPL");
test_hello.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>#define DEV_NAME "/dev/hello_autnode_dev"
int main()
{int fd, ret;char buf[32] = {0};fd = open(DEV_NAME, O_RDWR);if(fd < 0) {perror("open");return -1;}//lseek(fd, 1, SEEK_SET);ret = read(fd, buf, sizeof(buf)); printf("read 1 count:%d, data:%s\n", ret, buf);memset(buf, 0, sizeof(buf));sprintf(buf, "%s", "This is a test");ret = write(fd, buf, strlen(buf));ret = read(fd, buf, sizeof(buf));printf("read 2 count:%d, data:%s\n", ret, buf);close(fd);return 0;
}
Makefile
APP=test_hello
#如果$(KERNELRELEASE)是空的
ifeq ($(KERNELRELEASE),)
#獲得內核源碼根目錄KERNELDIR ?= /lib/modules/$(shell uname -r)/build#獲取當前路徑 PWD:=$(shell pwd)
modules:
# $(MAKE) -C $(KERNELDIR) 解釋: $(MAKE) 就是 make , -C 進入一個子目錄 /lib/modules/5.15.0-139-generic/build/arch, 再執行子目錄的Makefile
# M=$(PWD) 解釋: M是一個變量,值為當前目錄, 將當前M的值傳遞給 $(KERNELDIR) 目錄下的Makefile
# modules 是執行$(KERNELDIR) 目錄下的Makefile時傳遞的目標$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesgcc $(APP).c -o $(APP)
elseobj-m=hello_cdev.o
endifclean:rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.cmd .*.*.d $(APP)
終端
1. 進入目錄
2. make 生成內核文件
3. sudo dmesg -C
4. insmod hello_cdev.ko
5. dmesg
6. ./test_hello.c (可以再開一個終端)
7. 輸入測試
8. dmesg查看
9. rmmod helloc_cdev