内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 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 file_operations *fops; // 没有使用
struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
注意,内核并不是为每一个字符设备编号都定义一个char_device_struct 结构,而是为一组对应同一个字符设备驱动定义一个 char_device_struct 结构。chrdevs 散列表的大小是255,散列算法是把每组字符设备编号范围的主设备号以 255 取模插入相应的散列桶中。同一个散列桶中的字符设备编号范围是按起始次设备号递增排序的。
第一步:申请设备号
设备号分为主设备号和次设备号,关于设备号的申请也分为动态申请和自己指定,常用的申请函数有
静态分配
int register_chrdev_region(dev_t from, unsigned count, const char *name)
from :要分配的设备编号范围的初始值(次设备号常设为0);
count:连续编号范围;
Name:编号相关联的设备名称. (/proc/devices和/dev下的名字);
动态分配
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
Firstminor : 通常为0;
*dev:存放返回的设备号;
count:连续编号范围;
这两个函数都会调用一个共用的 __register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)
函数 __register_chrdev_region() 主要执行以下步骤:
1. 分配一个新的 char_device_struct 结构,并用 0 填充。
2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
3. 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
4. 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
5. 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
第二步:注册字符设备
内核中每个字符设备都对应一个 cdev结构的变量,下面是它的定义:
struct cdev {
struct kobject kobj; // 每个 cdev都是一个 kobject
struct module *owner; //指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev对应的字符设备文件的inode->i_devices的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
初始化的两种方式:cdev_init() , cdev_allonc()
一个 cdev一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。源码中cdev_init和cdev_alloc两个函数完成的功能基本一致,只是 cdev_init()还多赋了一个 cdev->ops的值。
还有一种组合使用的方式:
struct cdev *pdev = cdev_alloc(); //cdev_alloc用来分配一个struct cdev的结构体
cdev_init(pdev, &fops); //通过cdev_init指定pdev->ops = &fops;
cdev_add添加到系统中
初始化cdev后,需要把它添加到系统中去。为此可以调用 cdev_add()函数。传入cdev结构的指针,设备起始编号,以及设备编号范围。示例用法如下:
cdev_add(pdev,devno,3);
注意,传入cdev_add函数的编号范围跟次设备有关
下面看看cdev_add内核源码:
int cdev_add(struct cdev *p, dev_t dev,unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
可见,这个函数只是调用了kobj_map函数,内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map()函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup()函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
第三步:生成设备节点
当我们申请完设备号和向系统注册字符设备后,是不是证明我们可以调用我们的字符设备了呢?
我们还需要创建自己写的驱动程序的设备节点,这样在用户程序中open我们创建的设备节点后就可以进行read/write/ioctl等操作了。
有两种方式生成设备节点:
1、通过mknod命令
mknod /dev/yourname c major minor
这种方法是在系统命令行进行的,需要了解关于mknod命令的使用
2、通过udevd自动生成
Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。
在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。
用法实例
struct class * myclass = NULL;
struct device *mdevice = NULL;
myclass = class_create(THIS_MODULE, "hello");
mdevice = device_create(myclass, //创建设备属于哪个设备类
NULL, //创建的设备的父设备
devno,// 设备号
NULL, // 驱动的私有数据
"hello" //设备结点的名字,类似于printf ,"tty%d", i => /dev/ttyX
);
注意,若要自动创建不同的次设备节点的话,需要多次调用者两个函数,以在/dev下生成相应设备
这三步工作是任何字符设备驱动编写都需要的,一般是在模块入口函数调用
int __init char_test_init(void)
module_init(char_test_init);
模块卸载,释放内存空间
void __exit char_test_exit(void)
module_exit(char_test_exit);
当我们在卸载模块时,需要把我们注册模块时申请的内存空间给释放掉,否则就会造成内存泄漏。
首先,释放自动生成设备节点是占用的内存空间
//释放自动生成的设备节点
device_destroy(myclass, devno);
class_destroy(myclass);
其次,释放注册字符设备占用的内存空间
//注销字符设备
cdev_del(pdev);
最后,释放设备号
//释放设备号
unregister_chrdev_region(devno, 3);
同样模块卸载还牵扯到我们具体驱动函数中使用到的内存空间的释放,根据自己的代码情况进行相应操作。
具体驱动代码实现,file_operations字段
当我们完成这三步工作后,基本上就能完成了一个可以被用户程序调用的字符设备了,但是这个字符设备要实现什么样的功能,需要我们编写驱动需要实现的ops字段了。
Linux为所有的设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设 的种类较多,操作方式各不相同。Struct file_operations结构体中的成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。
const struct file_operations fops =
{
.open = test_open,
.release = test_close,
.read = test_read,
.write = test_write,
、、、、、、
};
最后总结一下自己这两天一直纠结的次设备号
关于字符设备次设备的生成需要注意三个地方:1、申请设备号时,需要指定次设备号的起始编号和数量。2、向系统注册字符设备时,需要指定设备编号和设备数量。3、生成设备节点时,有多少次设备,就要调用几次mknod或class_create+device_create