1.内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个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; //设备范围号大小
};
2.内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,它的定义如下:
static struct char_device_struct {注意,内核并不是为每一个字符设备编号定义一个 char_device_struct 结构,而是为一组对应同一个字符设备驱动的设备编号范围定义一个 char_device_struct 结构。chrdevs 散列表的大小是255,散列算法是把每组字符设备编号范围的主设备号以 255 取模插入相应的散列桶中。同一个散列桶中的字符设备编号范围是按起始次设备号递增排序的。
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];
3. kobj_map结构体是用来管理设备号及其对应的设备的。 内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。
当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
kobj_map()函数就是将指定的设备号加入到该数组,kobj_lookup()则查找该结构体,然后返回对应设备号的kobject对象,利用该kobject对象,我们可以得到包含它的对象如cdev。struct probe *probes[255];。
struct kobj_map {
struct probe {
struct probe *next; //这样形成了链表结构
dev_t dev; //设备号
unsigned long range; //设备号的范围
struct module *owner;
kobj_probe_t *get;
int (*lock) (dev_t, void *);
void *data; //指向struct cdev对象
} *probes[255];
struct mutex *lock;
}
4
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
//将cdev结构添加到cdev_map的数组中
error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
if (error)
return error;
//父kobject结构计数加1
kobject_get(p->kobj.parent);
return 0;
}
//内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。
//kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,
//根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range, struct module *module, kobj_probe_t *probe, int (*lock)(dev_t, void *), void *data)
{
//dev_t的前12位为主设备号,后20位为次设备号。
//n = MAJOR(dev + range - 1) - MAJOR(dev) + 1 表示设备号范围(dev, dev+range)中不同的主设备号的个数。通常n的值为1。
unsigned n = MAJOR(dev+range-1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);//主设备号
unsigned i;
struct probe *p;
if (n > 255)//若n > 255,则超出了kobj_map中probes数组的大小
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);//分配n个struct probe
if(p == NULL)
return -ENOMEM;
for(i = 0; i < n; i++, p++) {//用函数的参数初始化probe
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;//保存的是cdev结构
}
mutex_lock(domain->lock);
//从for循环可以看出kobj_map中的probes数组中每个元素为一个struct probe链表的头指针。
for(i = 0, p-=n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];//从数组中找到主设备号为index的probe结构链表,在此链表中每个probe结构都是相同的主设备号index
//链表中的元素是按照range值从小到大排列的。while循环即是找出该将p插入的位置。
while(*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;//插入链表
}
mutex_unlock(domain->lock);
return 0;
}
二、字符设备的注册
1.一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
(1)静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
(2)动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。
2.注册一个独立的cdev设备的基本过程如下:
(1)、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)
struct cdev *my_cdev = cdev_alloc();my_cdev->ops=&my_ops;
(2)、初始化struct cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops)(3)、初始化cdev.owner
cdev.owner = THIS_MODULE;
(4)、添加cdev,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)//p是cdev结构, dev是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1,
//在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并 且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add.
三、分配设备号
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。这三个函数都会调用一个共用的__register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)。
alloc_chrdev_region( ) //动态分配设备范围
register_chrdev( ) //申请指定的设备号,并且将其注册到字符设备驱动模型中.是一个老式分配设备编号范围的函数
//内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构
static struct char_device_struct * __register_chrdev_region(unsigned int major,unsigned int baseminor, int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//分配一个新的 char_device_struct 结构
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
//如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。
//动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。
//所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--)
if (chrdevs[i] == NULL)
break;
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;
}
//根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strncpy(cd->name,name, 64);
i = major_to_index(major);
//计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||((*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;
}
}
//将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
四.注册cdev设备老方法
1.如果你深入浏览 2.6 内核的大量驱动代码, 你可能注意到有许多字符驱动不使用我们刚刚描述过的 cdev 接口. 你见到的是还没有更新到 2.6 内核接口的老代码. 因为那个代码实际上能用, 这个更新可能很长时间不会发生. 为完整, 我们描述老的字符设备注册接口, 但是新代码不应当使用它; 这个机制在将来内核中可能会消失.
注册一个字符设备的经典方法是使用:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
这里, major 是感兴趣的主设备号, name 是驱动的名子(出现在 /proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给定的主编号注册 0 - 255 的次编号, 并且为每一个建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.register_chrdev函数的major参数如果等于0,则表示采用系统动态分配的主设备号。
它所做的事情为:
(1). 注册设备号, 通过调用 __register_chrdev_region() 来实现
(2). 分配一个cdev, 通过调用 cdev_alloc() 来实现
(3). 将cdev添加到驱动模型中, 这一步将设备号和驱动关联了起来. 通过调用 cdev_add() 来实现
(4). 将第一步中创建的 struct char_device_struct 对象的 cdev 指向第二步中分配的cdev. 由于register_chrdev()是老的接口,这一步在新的接口中并不需要.
2.如果你使用 register_chrdev, 从系统中去除你的设备的正确的函数是:
int unregister_chrdev(unsigned int major, const char *name);//major 和 name 必须和传递给 register_chrdev 的相同, 否则调用会失败.
五、字符设备驱动模板
(1)设置驱动文件操作结构体
static struct file_operations XXX_fops =
{
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
.ioctl = xxx_ioctl,
...
};
(2)编写字符设备驱动模块加载与卸载函数
static int _ _init xxx_init(void){ //模块加载--〉申请设备号,添加设备
...
cdev_init(&xxx_dev.cdev, &xxx_fops); //初始化cdev
xxx_dev.cdev.owner = THIS_MODULE;
if (xxx_major){//获取字符设备号
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
}
else{
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); //注册设备
...//可能申请中断号request_irq}static void _ _exit xxx_exit(void){/*设备驱动模块卸载函数*/ unregister_chrdev_region(xxx_dev_no, 1); //释放占用的设备号 cdev_del(&xxx_dev.cdev); //注销设备 ...//释放中断号free_irq}