目录
简介:
Linux内核主要包括三种驱动模型,字符设备驱动、块设备驱动以及网络设备驱动。本文主要介绍字符设备驱动的框架和机制。
一、字符设备驱动框架
Linux底层驱动一般都从入口函数开始分析。
1、字符设备驱动入口
驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。
static int __init xxx_init(void) { } static void __exit xxx_exit(void) { } module_init(xxx_init); module_exit(xxx_exit);
这段代码实现了通用驱动的 module_init 加载与 module_exit 卸载框架。
2、字符设备驱动加载过程
- 字符设备驱动加载过程
- 字符设备驱动卸载过程
2.1 申请设备号
每一类字符设备都有唯一的设备号,其中设备号又分为主设备号和次设备号。
-
主设备号:用于标识设备的类型,如:区分是
IIC
设备还是SPI
设备; -
次设备号:用于区分同类型的不同设备,如:区分
IIC
设备下,具体哪一个设备,是MPU6050
还是EEPROM
;
2.1.1 分配设备号函数
设备号的分配方式有两种,一种是静态分配,另一种是动态分配。
(1) 静态分配函数
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:以from设备号开始,连续分配count个同类型的设备号
参数:
-
from:表示已知的一个设备号
-
count:表示连续设备编号的个数,(同类型的设备有多少个)
-
name:表示设备或者驱动的名称
(2) 动态分配函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
功能:从baseminor次设备号开始,连续分配count个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给*dev
参数:
-
dev:设备号的指针,用于存放分配的设备号,可使用MAJOR和MINOR宏,将主设备号和次设备号分别提取出来;
-
baseminor:次设备号从第几开始分配;
-
count:表示连续次设备号的个数,(同类型的设备有多少个);
-
name:表示设备或者驱动的名称;
两个函数的区别:
-
register_chrdev_region:已给定主设备号和次设备号,调用该函数将设备号注册到内核;
-
alloc_chrdev_region:未给定设备的主设备号和次设备号,调用后,主设备号以
0
来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况;
(3) 注销设备号
设备号作为一种系统资源,使用完后需要将占用的设备号归还给系统,调用 unregister_chrdev_region 注销
void unregister_chrdev_region(dev_t from, unsigned count)
功能:要注销from主设备号下的连续count个设备
参数:
-
from:表示已知的一个设备号
-
count:表示连续设备编号的个数,(同类型的设备有多少个)
2.1.2 设备号中的主/次设备号
设备号的类型是dev_t ,内核 include/linux/types.h 可以看到dev_t 表示u32类型的数值。
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
dev_t 包括主设备号和次设备号,由 MINORBITS 宏定义指定分界线,主设备号占用高12bit
,次设备号占用低20bit。
头文件 include/linux/kdev_t.h 中定义
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从dev_t中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //从dev_t中获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //主/次设备号组成dev_t
2.1.3 申请设备号示例
static dev_t devid; //设备号
static int major; //主设备号
static int minor; //次设备号
#define DEV_CNT 1 //设备数量
#define DEV_NAME "test" //设备名字
/* 创建设备号 */
if (major) { /* 定义了设备号 */
devid = MKDEV(major, 0); //主设备号major和次设备号0组成dev_t
register_chrdev_region(devid, DEV_CNT, DEV_NAME); //dev_t设备号注册到内核
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, DEV_CNT, DEV_NAME); //申请设备号dev_t
major = MAJOR(devid); //获取dev_t的主设备号
minor = MINOR(devid); //获取dev_t的次设备号
}
2.2 注册字符设备
2.2.1 cdev结构体介绍
cdev 定义在 include/linux/cdev.h,用来抽象一个字符设备。
struct cdev{
struct kobject kobj; //表示一个内核对象
struct module *owner; //值为THIS_MODULE,表示模块
const struct file_operations *ops; //操作集,包括open、read、write等操作接口
struct list_head list; //用于将该设备加入到内核模块链表中
dev_t dev; //设备号(包括主设备号和次设备号)
unsigned int count; //次设备号个数
};
2.2.2 file_operations结构体介绍
file_operations 定义在 include/linux/fs.h,它是文件操作集,包含open、read、write等操作。
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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 (*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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
2.2.3 初始化、注册字符设备
(1) cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:初始化cdev结构体
参数:
-
cdev:struct cdev结构体字符设备
-
fops:文件操作集
(2) cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:添加一个字符设备到kernel
参数:
-
p:cdev结构体指针
-
dev:设备号
-
count:该类型设备的个数
(3) 注销字符设备
void cdev_del(struct cdev *p)
功能:从系统中移除该字符设备驱动
2.2.4 注册字符设备示例
调用 cdev_add 后,"cat /proc/devices" 命令能看到字符设备信息
/* 定义file_operations */
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
/* 初始化并注册 cdev */
struct cdev cdev;
cdev.owner = THIS_MODULE;
cdev_init(&cdev, &dev_fops); //初始化字符设备结构,设置file_operations
cdev_add(&cdev, devid, DEV_CNT); //添加至内核,"cat /proc/devices"能查看
2.3 自动创建设备节点
mknod 命令可以手动创建设备节点,简单介绍如下:
$ mknod /dev/test c 252 0 #生成/dev/test,c表示字符设备,主设备号为 252,次设备号为 0
主设备号可以用 "cat /proc/devices" 查看。下面主要介绍自动创建设备节点。
2.3.1 class_create创建类
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
void class_destroy(struct class *cls) //注销class
调用class_create后,会在/sys/class/下创建类文件夹,文件夹名由函数的第二个入参决定。
2.3.2 device_create创建设备
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
功能:调用device_create,会在/sys/class/xxx类下面创建一个设备,通过udev在/dev/目录下生成设备文件节点,文件名由fmt参数决定。
参数:
- class:表示设备要创建哪个类下面;
- parent:父设备, 一般为 NULL, 也就是没有父设备;
- devt:设备号;
- drvdata:设备可能会使用的一些数据, 一般为 NULL;
- fmt:设备名字, 如果设置 fmt=xxx 的话, 就会生成/dev/xxx 这个设备文件;
返回值:创建好的设备
删除设备时调用:
void device_destroy(struct class *class, dev_t devt) //注销device
udev工作过程如下:
(1)当内核检测到系统中出现新设备后,内核会通过netlink套接字发送uevent;
(2)设备模块加载时udev获取内核发送的信息,扫描/sys/class/下的设备目录进行规则匹配。从而在/dev/创建设备节点;
2.3.3 自动创建设备节点示例
static struct class *class; /* 类 */
static struct device *device; /* 设备 */
/* 4、创建类 */
class = class_create(THIS_MODULE, "test"); ///sys/class/目录下会创建一个新的文件夹
if (IS_ERR(class)) {
return PTR_ERR(class);
}
/* 5、创建设备 */
device = device_create(class, NULL, devid, NULL, "test");//dev目录下创建相应的设备节点
if (IS_ERR(device)) {
return PTR_ERR(device);
}
二、字符设备示例
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
static dev_t devid; /* 设备号 */
static int major; /* 主设备号 */
static int minor; /* 次设备号 */
static struct cdev cdev; /* cdev */
static struct class *class; /* 类 */
static struct device *device; /* 设备 */
#define CDEV_CNT 1 /* 设备号个数 */
#define CDEV_NAME "cdev_test" /* 名字 */
static int chardev_open(struct inode *inode, struct file *filp)
{
//filp->private_data = &cdev_data; /* 设置私有数据 */
printk("open...\n");
return 0;
}
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
printk("read...\n");
return 0;
}
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
printk("write...\n");
return cnt;
}
static int chardev_release(struct inode *inode, struct file *filp)
{
printk("release...\n");
return 0;
}
/* 设备操作函数 */
static struct file_operations chardev_fops = {
.owner = THIS_MODULE,
.open = chardev_open,
.read = chardev_read,
.write = chardev_write,
.release = chardev_release,
};
/* 入口函数 */
static int __init chardev_init(void)
{
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (major) { /* 定义了设备号 */
devid = MKDEV(major, 0);
register_chrdev_region(devid, CDEV_CNT, CDEV_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, CDEV_CNT, CDEV_NAME); /* 申请设备号 */
major = MAJOR(devid); /* 获取分配号的主设备号 */
minor = MINOR(devid); /* 获取分配号的次设备号 */
}
printk("major=%d, minor=%d\r\n", major, minor);
/* 2、初始化 cdev */
cdev.owner = THIS_MODULE;
cdev_init(&cdev, &chardev_fops); //file_operations
/* 3、添加一个 cdev */
cdev_add(&cdev, devid, CDEV_CNT);
/* 4、创建类 */
class = class_create(THIS_MODULE, CDEV_NAME); ///sys/class/目录下会创建一个新的文件夹
if (IS_ERR(class)) {
return PTR_ERR(class);
}
/* 5、创建设备 */
device = device_create(class, NULL, devid, NULL, CDEV_NAME);//dev目录下创建相应的设备节点
if (IS_ERR(device)) {
return PTR_ERR(device);
}
return 0;
}
/* 出口函数 */
static void __exit chardev_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&cdev); /* 删除 cdev */
unregister_chrdev_region(devid, CDEV_CNT); /* 注销 */
device_destroy(class, devid);
class_destroy(class);
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("donga");
本示例read、write函数只打印信息演示作用。在实际使用中,read函数通常会调用 copy_to_user 将内核空间的数据拷贝到用户空间,write函数则调用 copy_from_user 将用户空间数据拷贝到内核空间,再做处理。
1、命令测试
$ insmod chardev_drv.ko #加载驱动,生成/dev/cdev_test设备节点
$ cat /proc/devices #查看字符设备
$ cat /dev/cdev_test #读
$ echo "1" > /dev/cdev_test #写
/ # cat /dev/cdev_test #读,过程:open->read->release
open...
read...
release.../ # echo "1" > /dev/cdev_test #写,过程:open->write->release
open...
write...
release...
2、应用程序读写
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
if(argc != 2){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR); //打开设备文件
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
write(fd, NULL, 0); //写
read(fd, NULL, 0); //读
retvalue = close(fd); //关闭文件
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
执行过程: