驱动开发_2.字符设备驱动
目录
- 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