概述: 实现一个简单的字符设备驱动。
上一节大概介绍了字符设备的数据结构相关的概念,接下来就来实现一个简单字符设备驱动程序,话不多说,先上代码,和运行效果,然后再一一分析。
1. 程序
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/fs.h>
#define CDE_NAME "Rivotek_cdev"
struct my_char_dev
{
unsigned int maj; //主设备号
unsigned int mio; //次设备号
};
struct my_char_dev *lcdev;
/*open 函数,当应用调用open时,会调用到驱动里的这个函数*/
static int lcdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO"lcdev open\n");
return 0;
}
/*close 函数,当应用调用close时,会调用到驱动里的这个函数*/
static int lcdev_release (struct inode *inode, struct file *file)
{
printk(KERN_INFO"lcdev release\n");
return 0;
}
/* operatiron 函数的填充,告诉系统,我们这个驱动有哪些操作方法, 目前只实现了open 和 close*/
static const struct file_operations lcdev_fops = {
.owner = THIS_MODULE,
.open = lcdev_open,
.release = lcdev_release,
};
static int __init char_test_init(void)
{
int ret;
lcdev = kmalloc(sizeof(struct my_char_dev), GFP_KERNEL);
if(!lcdev) {
printk(KERN_ERR"No memory for lcdev");
ret = -ENOMEM;
goto out;
}
printk(KERN_ALERT"kmalloc ok \n");
lcdev->maj = 0;
lcdev->mio = 0;
ret = register_chrdev(lcdev->maj, CDE_NAME, &lcdev_fops); //注册字符设备
if(0 > ret) {
printk(KERN_ERR"register failed\n");
goto register_err;
}
printk(KERN_ALERT"register char dev ok ,ret:%d\n", ret);
lcdev->maj = MAJOR(ret);
lcdev->mio = MINOR(ret);
printk(KERN_ALERT"maj: %d ,mio:%d\n", lcdev->maj, lcdev->mio);
printk(KERN_ALERT"dev_t size: %d\n", sizeof(dev_t));
return 0;
register_err:
if(lcdev)
kfree(lcdev);
out:
return ret;
}
static void __exit char_test_exit(void)
{
unregister_chrdev(lcdev->maj, CDE_NAME); //注销字符设备
kfree(lcdev);
printk(KERN_ALERT"char test exit\n");
}
module_init(char_test_init);
module_exit(char_test_exit);
MODULE_LICENSE("GPL");
2.实现效果
加载驱动后打印对照
加载驱动后没有设备节点??
哈哈,这是驱动没有做这部分功能的实现,那么就手动添加,
/ # mknod /dev/Rivotek_cdev c 252 0
创建后的效果:
怎么确定这个mknod创建的设备节点就是驱动加载的这个设备?
这里我cat了一下,效果如下:
open 和 close 函数的打印对应上了,这样证明mknod 创建的设备节点和驱动对应上了。
这里我们使用的是register_chrcev() 函数来注册的字符设备驱动,那么总结一下使用这个接口的的情况下,注册一个字符设备驱动,我们需要做哪些事情?
1. 主、次设备号的准备(如果未知,那么可以和这里的程序一样,都填0, 系统自动分配)
2. 设备操作方法的编写(open、close)和填充(.open = xxx)
3. 注册字符设备
4.退出时收尾(注销字符设备、该释放内存的地方释放内存,避免内存泄露)
接下来梳理一下,register_chrcev() 方法的注册流程
1. include/linux/fs.h 中,定义如下:
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() 方法。
2. fs/char_dev.c 中,定义 __register_chrdev()
/**
* __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;
}
(1)先从 comment 看看该函数的用法
A、 函数的功能:创建并注册一个或者多个不同次设备号的字符设备(从调进来的参数可以知道,利用 __regiseter_chrdev(), 最多就注册255个次设备)
B、传入的是5个参数
major:主设备号,如果是0,则表示需要系统分配主设备号,也称之为动态分配。
baseminor: 次设备号的起始编号,这里在 __regiseter_chrdev() 方法中已经规定,次设备号从0开始计数。
count: 有多少个子设备,最大是255个
问题1: count 为 0时怎么处理的?
name: 字符设备的名字
fops: 驱动提供的操作方法指针
C、 返回值
动态分配设备号,那么返回设备号表示注册成功。
传入的major 大于0,则返回0表示注册成功。
放回负数,则表示注册失败。
(2)函数调用关系及对应函数的梳理
__register_chrdev()
__register_chrdev_region() //设备号处理和内核字符设备分配
cdev_alloc(); //字符设备内存分配,链表初始化
kobject_set_name() //字符设备名字设定
cdev_add() // 字符设备添加到系统中
A. __register_chrdev_region()
引入新的数据结构:struct char_device_struct
static struct char_device_struct {
struct char_device_struct *next; //指向下一个结构体指针
unsigned int major; //主设备号
unsigned int baseminor; //次设备号的起始数字
int minorct; //次设备个数
char name[64]; // 字符设备名字
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; // 字符设备数组,囊括kernel中所有字符设备
//PATH: include/linux/fs.h
#define CHRDEV_MAJOR_HASH_SIZE 255
从定义和成员可以知道:
- 这是维护在内核中对字符设备定义的结构体(static 定义,其他文件无法访问)
- 字符设备主设备个数最多是256个(数组中)
验证一下,假如定义主设备号为500,会怎样?
代码改动
@ -52,7 +52,7 @@ static int __init char_test_init(void)
}
printk(KERN_ALERT"kmalloc ok \n");
- lcdev->maj = 0;
+ lcdev->maj = 500;^M
lcdev->mio = 0;
@@ -62,9 +62,11 @@ static int __init char_test_init(void)
goto register_err;
}
printk(KERN_ALERT"register char dev ok ,ret:%d\n", ret);
-
- lcdev->maj = MAJOR(ret);
- lcdev->mio = MINOR(ret);
+^M
+ if(!lcdev->maj) {^M
+ lcdev->maj = MAJOR(ret);^M
+ lcdev->mio = MINOR(ret);^M
+ }^M
printk(KERN_ALERT"maj: %d ,mio:%d\n", lcdev->maj, lcdev->mio);
printk(KERN_ALERT"dev_t size: %d\n", sizeof(dev_t));
验证结果
没有什么影响,还是可以操作。
那可以理解为,数组只是记录着字符设备的个数,和设备号的大小没有关系。如上篇博客记录,主设备号是有12 bits 表示,那么应该有 4096个主设备号,0开始,最大到4095;
设置主设备号为 4096 时,
系统报错了,不让创建这样的设备,设备号超范围了;符合程序预期。
问题2: 动态分配的时候为什么是252?
代码梳理
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); //分配内存
if (cd == NULL)
return ERR_PTR(-ENOMEM);
.....
if (major == 0) { //进行动态分配主设备号
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL) //分配设备号是根据字符设
//备数组进行分配的,遍历整个数组,如果数组中的某个元素是 NULL(空),则将该元素的下标标号作为
//字符设备的设备号,所以动态分配设备号,设备号不会超过255.
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
}
答问题2: 如上代码梳理,看到动态分配策略
i = major_to_index(major); /*通过主设备号对255取余,确定字符*/
/*数组起始索引值*/
/*以索引值开始遍历,直到找到对应的设备*/
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major || //如果当前设备设备号大于需要待注册设备,
//执行 cp = &(*cp)-> next, 继续查找。
((*cp)->major == major && //当匹配到待注册的字符设备
//仅当字符设备的次设备号不小待注册字符设备次设备号或者
(((*cp)->baseminor >= baseminor) ||
//当前设备次设备号与次设备个数之和大于待注册次设备号,循环结束。
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
/*这里很神奇,将新生成的字符设备填充到数组的同时,又将next指向了自己*/
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
上述中的for循环主要是处理已知主设备号的同时添加次设备号的过程。这里就有点困惑,字符设备数组和next指针是怎么协同工作的,有如下的疑问
- 字符数组中只有主设备号还是所有字符设备都需要占用一个数组的元素?
- next 怎么指向了自己?
cdev_alloc() 和 cdev_add()主要是分配一个cdev结构体的内存和做一些内核层面的添加,稍后继续分析顺带深入到内核驱动的最底层,梳理一下最底层的逻辑。
这里呢只是以 register_chrdev() 为入口,初步梳理了字符设备注册的流程,接下来还是先以其他几个字符设备的注册函数为入口,先将字符设备注册梳理清楚。