字符设备基础了解
一.字符设备驱动
- 使用一个结构描述字符设备对象:
struct cdev {
struct kobject kobj; // 文件系统相关,由系统来管理,不需要自己添加
struct module *owner; // 用于模块计数
const struct file_operations *ops; // 对硬件的操作方法集
struct list_head list; // 用于管理字符设备的链表
dev_t dev; // 设备号;把软件和硬件结合起来;主设备号<<20 +次设备号
unsigned int count; // 同一主设备号下的,次设备号的个数
};
2. 字符设备注册分 3 个步骤
分配cdev
初始化cdev
添加cdev
3. 驱动中字符设备文件的相关 3 个结构
struct file 代表一个打开的文件,由内核打开时创建,关闭时释放
struct inode 记录文件的物理上信息inode号
struct file_operations 函数指针集合
4. 字符设备结构中的 struct file_operations 结构是对硬件操作的方法集,通过在驱动程序中重新实现并注册到内核中,供应用层调用
方法一:手动申请字符设备cdev,静态创建设备节点
- 对字符设备的操作流程:总结编写一个字符设备驱动要必做的几步:
(1)定义一个字符设备:struct cdev *cdev;
(2)为字符设备分配空间:cdev_alloc(void);
(3)初始化字符设备对象(对硬件操作的方法集):cdev_init(struct cdev *, const struct file_operations *);
(4)向内核申请一个设备号:register_chrdev_region(dev_t from, unsigned count, const char * name);
(5)添加当前的字符设备到内核中:cdev_add(struct cdev *, dev_t, unsigned);
(6)卸载字符设备对象:
a) cdev_del(struct cdev *); // 删除字符设备
b) unregister_chrdev_region(dev_t from, unsigned count); //删除
2. 设备号静态申请:int register_chrdev_region(dev_t from, unsigned count, const char *name)
3. 手动创建设备节点: mknod /dev/devchar0 c 222 0
命令 设备节点名称 字符设备 主设备号 次设备号
方法二:自动申请cdev获取主设备号,动态创建设备节点( 3个步骤 )
1.通过 register_chrdev 自动申请一个cdev字符设备,动态获取主设备号,并注册方法集,就完成了上面方法一中的前 5 步
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
参数:major:0时自动获取主设备号,>0 时静态获取不一定成功获取
name:设备名称
fops:对硬件操作的方法集
返回值:return major ? 0 : cd->major;,成功返回主设备号,失败返回0
2.创建放设备节点的目录:创建一个目录在 /sys/class/xxx
class_create(owner, name) 这是一个宏函数,具体通过下面函数实现
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key)
参数:owner:是一个宏表示这个模块
name:创建的目录名,放在 /sys/class/ 下,如name=“mychar”-- /sys/class/mychar
返回值:创建的这个目录对象的指针,根据这个指针找这个目录
3.创建设备节点,代替mknod的操作
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
参数:class:创建存放设备节点的目录
parent:指向此新设备的父结构设备的指针(如果有的话),一般NULL
devt:要添加的设备号,包含主设备号和次设备号
drvdata:要添加到设备中的回调的数据
fmt:用于设备名称的字符串,后面是可变参数,用于格式化fmt的;
返回值:成功返回device ;失败返回ERR_PTR(retval). 看内核中的例子怎么使用返回值
如device_create(cls, NULL, MKDEV(major, 0), NULL, "Demochar%d",0); 这个设备节点的名称:“Demochar0”
4. 对于向系统申请的资源要记得归还
void class_destroy(struct class *cls):删除cls创建的目录
void device_destroy(struct class *class, dev_t devt):功能:释放设备节点
参数:cls : 申请到的class
devt: 设备号,包含主设备号和次设备号;通过宏函数 MKDEV(major,minor)制作
二.关于设备号:
主设备号用来标识与设备文件相连的驱动程序;反映的是设备类型。
次设备号被驱动程序用来识别操作的是哪一个设备;用来区分同类型的设备。
Linux使用一个结构描述设备号:dev_t 高12位是主设备号,低20位是次设备号
关于设备号操作的 3 个宏函数
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
主设备号的 2 种获取方法
1.静态申请
在内核源码路径下确定一个没有使用的主设备号 Documentation/devices.txt
通过函数 register_chrdev_region 注册设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
2. 动态分配:
通过函数 alloc_chrdev_region 动态分配
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数: dev :设备号,0 动态分配; >0
baseminor:起始的次设备号
count:需要分配的设备号数目
name:设备名(体现在/proc/devices中)
注销设备:void unregister_chrdev_region(dev_t from, unsigned count)
释放从 from 开始的 count 个设备号
有了设备号,创建设备文件 2 种方法
3.使用 mknod 命令手动创建
mknod drvname type major minor
drvname:设备文件名
type:设备文件类型 c字符设备; d块设备
major:主设备号
minor:次设备号
4.自动创建(加载模块时注册创建,卸载模块时释放)
1) 函数 register_chrdev 自动获取注册一个字符设备并申请设备号,同时注册file_operations操作方法集
2) 函数class_create创建一个struct class类 ,这个类存放在sysfs下面,一旦创建好了这个类, 调用device_create函数在/dev目录下创建相应的设备节点。这样加载模块时,用户空间中udev会自动响应device_create函数,去/sysfs下寻找对用的类从而创建设备节点。
3) 函数device_create返回值是创建一个struct device型设备关联设备号,class,和设备节点
三.应用层与驱动间拷贝数据
从底层拷贝数据给应用层:copy_to_user ;
从应用层拷贝数据给底层:copy_from_user
内核提供了专门的函数用于访问用户号空间指针
unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
__builtin_constant_p(EXP) 是GCC编译器的内嵌函数,用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0。
--------驱动程序demo.c
#include <linux/module.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/cdev.h> #include <linux/kdev_t.h> #include <linux/uaccess.h> #include <linux/err.h> #include <linux/kernel.h> /* 变量定义区域 */ const char * name = "demochdrev"; // 字符驱动名字 unsigned int major; // 主设备号 const char *clsname = "mychr"; // 创建class类的名字(不清楚具体什么用途) struct class *mycls; struct device *mydev; // read 系统调用 ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t * ppos) { printk(KERN_INFO "kernel read \n"); return 0; } // write 系统调用 ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { printk(KERN_INFO "kernel write \n"); return 0; } // close 系统调用 int demo_release(struct inode *iod, struct file *filp) { printk(KERN_INFO "kernel release \n"); return 0; } // open 系统调用 int demo_open(struct inode *iod, struct file *filp) { printk(KERN_INFO "kernel open \n"); return 0; } // 对文件的操作方法集 struct file_operations fops = { .owner = THIS_MODULE, .read = demo_read, // 函数实现要在其上面,不然在此处找不到函数的声明 .write = demo_write, .open = demo_open, .release = demo_release, }; /* 模块 3 步操作 */ // 模块入口,申请字符设备用到的资源 static int __init demo_init(void) { printk(KERN_INFO "module init \n"); // 1. 注册一个字符设备 cdev major = register_chrdev(0, name, &fops); // major=0 表示自动分配主设备号,其返回值是主设备号 if(major <= 0){ // 注册字符设备失败 printk(KERN_INFO "register chrdev fail \n"); } // 2.自动创建设备节点 /sys/class 目录下的文件夹名 // 2.1 创建一个 class 类 mycls = class_create(THIS_MODULE, clsname); // 返回值 struct class* 类型 if (IS_ERR(mycls)){ // 创建 class 失败 printk(KERN_INFO "class create fail \n"); unregister_chrdev(major, name); // 在每步检测申请失败了,就要释放前面申请的资源 return PTR_ERR(mycls); } // 2.2 创建设备节点的名字 mydev = device_create(mycls, NULL,MKDEV(major, 0) , NULL, "demochr%d", 0); // 最后 2 个参数会制作这个驱动的可见的名字,安装模块时会看见/dev/demochr0 if (IS_ERR(mydev)) { // 创建设备节点失败 printk(KERN_INFO "failed to create device\n"); unregister_chrdev(major, name); class_destroy(mycls); return PTR_ERR(mydev); } return 0; } // 模块出口,释放申请的资源 static void __exit demo_exit(void) { printk(KERN_INFO "module exit \n"); device_destroy(mycls, MKDEV(major, 0)); // 上面创建的顺序反向操作,栈操作 class_destroy(mycls); unregister_chrdev(major, name); } // 模块三要素 module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL");
----makefile文件
#KERNELDIR=/lib/modules/3.13.0-32-generic/build # Ubuntu系统上的内核源码路径 KERNELDIR=/home/linux/share/kernel-3.4.39 # 目标板上使用内核源码路径 PWD=$(shell pwd) // 当前路径 all: make -C $(KERNELDIR) M=$(PWD) modules clean: make -C $(KERNELDIR) M=$(PWD) clean obj-m +=demo.o # 生成的目标 demo.ko