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

linux 字符设备驱动框架

2014-05-10 00:34 417 查看
一、字符设备结构

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;

}

二、字符设备的注册

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,
但是有多个设备号对应于一个特定的设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模式(例如密度),
通过安排多个次编号给每一个物理设备.

//在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中.
它几乎会一直成功, 但是, 并且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作.
除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add.

(5)源码:

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;

}

三、分配设备号

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 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

}

http://blog.chinaunix.net/uid-27717694-id-3630313.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: