字符驱动设备
字符设备架构

在 Linux 中使用 cdev 结构体表示一个字符设备,cdev中最重要的有设备号dev_t和file_operations这两个结构体 。file_operations中包含了字符设备的操作函数,其中比较重要的有的 read(),write(),ioctl(), open(),close()这几个函数。
cdev 结构体定义如下
c
<include/linux/cdev.h>
struct cdev {
//内嵌的内核对象.
struct kobject kobj;
//该字符设备所在的内核模块的对象指针,一般为THIS_MODULE,可以防止设备的方法正在被使用时,设备所在模块被卸载。
//如果 owner 变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。
struct module *owner;
//文件操作结构体,自己定义的那个
const struct file_operations *ops;
//用来将已经向内核注册的所有字符设备形成链表.
struct list_head list;
//字符设备的设备号,由主设备号和次设备号构成.
dev_t dev;
//隶属于同一主设备号的次设备号的个数.
unsigned int count;
};
字符设备开发流程
字符驱动的编写
驱动模块的装载和卸载
c
//注册模块加载函数
module_init(xxx_init);
//注册模块卸载函数
module_exit(xxx_exit);
- module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数
- "insmod"命令加载驱动的时候,xxx_init 这个函数就会被调用。
- modeprode命令相比于insmod更智能。insmod无法解决模块的依赖问题,即1.ko如果依赖2.ko。那你必须手先安装2.ko才能安装1.ko。modeprode会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中.modprobe 命令默认会去
/lib/modules/<kernel-version>目录中查找模块,自制的文件系统一般不会有这个目录,需要手动创建。推荐使用modeprode。
- module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数
- 当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
- modprobe -r drv.ko ,命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令 。
扩展链接:[[#驱动模块的装载方式]]
字符设备的注册与注销
一般驱动设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块
的出口函数 xxx_exit 中进行。
根据设备号的使用情况,和动态/静态申请设备号可分为以下几种情况。[[#主次设备号]]
设备号的申请
所有次设备号被占用
c
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
- register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号。使用该函数注册的设备会将一个主设备号的后的次设备号全部占用,造成设备号的浪费。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。 - unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
动态申请设备号
c
//major为0时,设备号动态申请,其会返回申请的设备号
//该函数不仅会完成设备号的申请,还会完成cdev_init.cdev_add.
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
//动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
- dev:保存申请到的设备号。
- baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
- count:要申请的设备号数量。
- name:设备名字。
静态申请设备号
c
//自己决定主次设备号传给register_chrdev_region
devno = MKDEV(major,minor);
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//可以自己决定主设备号
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
- register_chrdev_region把一段
(major, minor)设备号区间,登记到内核的"字符设备号管理表"里,声明:这段号我占了,别人别用。from:起始设备号(包含 major + minor)count:连续多少个 minor,一般为1name:显示在/proc/devices/ 内核表里的名字
设备号的注销
c
//注销一定范围内的设备号
//register_chrdev_region和alloc_chrdev_region申请的设备号,都用以下函数注销
void unregister_chrdev_region(dev_t from, unsigned count)
//register_chrdev申请的设备用这个注销
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
实现具体的操作函数
具体的操作函数即为file_operations 结构体
c
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
cdev结构体的注册和删除
在准备好设备号和file_operations后,就需要将二者与cdev结构体关联起来。[[#字符设备架构]]
- 创建一个cdev
- 初始化cdev,将file_operations填入结构cdev结构体中
- 将设备号填入结构体cdev中,同时向内核注册cdev
cdev_init
c
//创建一个结构体,用cdev结构体完成初始化
struct cdev test_cdev;
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- cdev_init:初始化一个
cdev结构体,并与file_operations关联- cdev:自己创建的字符设备结构体
- file_operations:具体的操作函数
- cdev_add:向内核注册一个
cdev结构体,并将其与设备号绑定。- p:cdev的结构体指针
- dev:设备号
- count:要添加设备的数量
cdev_alloc
c
test_cdev=struct cdev *cdev_alloc(void);
test_cdev.ops=xxx_ops;
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- cdev_alloc:返回一个初始化过的cdev结构体,但还没有与file_operations关联需显示指定。
cdev结构体的注销
c
void cdev_del(struct cdev *p)
cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函数。
类与设备节点的创建和删除
\[#如何创建和删除设备节点与类\]
创建在module_init()
删除在module_exit()
指定驱动相关信息
c
MODULE_AUTHOR("xxx"); //作者
MODULE_DESCRIPTION("SW DECODER DIRVER"); //描述
MODULE_LICENSE("GPL"); //许可证
modinfo ./xxx.ko可以查看驱动模块相关信息(注查看的不是已经装载在内核里的驱动,而是具体的.ko文件)
测试应用的编写
驱动与应用的编译
驱动的编译
c
KERNELDIR := /lib/modules/$(shell uname -r)/build //指定内核源码树的位置,uname表示当前的内核版本
CURRENT_PATH := $(shell pwd) //获取当前驱动源码的位置,方便后面告诉内核编译系统"我们要编译的代码就在这里"。
obj-m := chrdevbase.o //将chrdevbase.o,编译成一个可加载的内核模块
build: kernel_modules //编译的默认目标
kernel_modules:
// $(MAKE):调用系统 make 工具。
// -C $(KERNELDIR):这是"借鸡生蛋"。它让 make 先跳转到内核源码目录(KERNELDIR)去执行那里的 Makefile。
// M=$(CURRENT_PATH):告诉内核 Makefile,在处理完内核环境后,回到这个路径来编译我们的驱动源码。
//modules:这是内核 Makefile 里的一个目标,表示只编译模块,不编译整个内核
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
Make的流程:
- $(MAKE):调用系统 make 工具。
- -C $(KERNELDIR):切换到指定的内核目录去执行相应的Makefile
- M=$(CURRENT_PATH:告诉内核 Makefile要编译的源码在哪
- modules:告诉Makefile,只编译模块
obj-m := chrdevbase.o:告诉Makefile,将哪些文件编译成模块,以及编译的方式。- 其会根据xxx.o去自动查找,同名的xxx.c文件。[[universe/Software and Environment/Makefile#Makefile的隐式规则|Makefile的隐式编译规则]]
应用的编译
c
//指定的编译工具 源码.c -o 可执行文件
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
测试与调试
测试程序 设备节点 相应操作
字符设备相关函数/结构体详解
register_chrdev
- register_chrdev:其本质上是调用了__register_chrdev。
__register_chrdev在major为0时会动态申请设备号。register_chrdev给__register_chrdev传的次设备号的起始数为0,申请数量为256。故register_chrdev会浪费,一个主设备号下的所有次设备号。
c
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
/**
* __register_chrdev() - create and register a cdev occupying a range of minors
* @major: major device number or 0 for dynamic allocation
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: name of this range of devices
* @fops: file operations associated with this devices
*
* If @major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If @major > 0 this function will attempt to reserve a device with the given
* major number and will return zero on success.
*
* Returns a -ve errno on failure.
*
* The name of this device has nothing to do with the name of the device in
* /dev. It only helps to keep track of the different owners of devices. If
* your module name has only one type of devices it's ok to use e.g. the name
* of the module here.
*/
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
register_chrdev_region和alloc_chrdev_region
register_chrdev_region和alloc_chrdev_region
以上两者本质上都调用了__register_chrdev_region。最大的区别在于alloc_chrdev_region给__register_chrdev_region传major为0,register_chrdev_region传的major是具体的。
二者都只是先内核申请了设备号,并告诉内核与这设备号绑定的设备名字。
c
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
/*
* Register a single major with a specified minor range.
*
* If major == 0 this function will dynamically allocate an unused major.
* If major > 0 this function will attempt to reserve the range of minors
* with given major.
*
*/
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, *curr, *prev = NULL;
int ret;
int i;
if (major >= CHRDEV_MAJOR_MAX) {
pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
name, major, CHRDEV_MAJOR_MAX-1);
return ERR_PTR(-EINVAL);
}
if (minorct > MINORMASK + 1 - baseminor) {
pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",
name, baseminor, baseminor + minorct - 1, 0, MINORMASK);
return ERR_PTR(-EINVAL);
}
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
if (major == 0) {
ret = find_dynamic_major();
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;
}
major = ret;
}
ret = -EBUSY;
i = major_to_index(major);
for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {
if (curr->major < major)
continue;
if (curr->major > major)
break;
if (curr->baseminor + curr->minorct <= baseminor)
continue;
if (curr->baseminor >= baseminor + minorct)
break;
goto out;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
if (!prev) {
cd->next = curr;
chrdevs[i] = cd;
} else {
cd->next = prev->next;
prev->next = cd;
}
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
相关函数对比
表中同块的函数,功能相同。
