您的位置:首页 > 运维架构 > Linux

linux 字符设备驱动框架

2013-06-15 16:20 465 查看
一、字符设备结构及原理

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 {
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 取模插入相应的散列桶中。同一个散列桶中的字符设备编号范围是按起始次设备号递增排序的。

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 结构)。

register_chrdev_region( ) //分配指定的设备号范围

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
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: