- 论坛徽章:
- 0
|
字符设备驱动学习
一.字符设备类型:
dev_t: 定义于include/linux/types.h中,下面是摘自types.h:
typedef __u32 __kernel_dev_t;
typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t;
typedef __kernel_ino_t ino_t;
typedef __kernel_mode_t mode_t;
typedef __kernel_nlink_t nlink_t;
typedef __kernel_off_t off_t;
typedef __kernel_pid_t pid_t;
.....
可以看出其中dev_t和pid_t都是32为的无符号数.
dev_t dev=MKDEV(ma,mi);
MKDEV(ma,mi)和相关函数定义在include/linux/kdev_t.h中,如下摘取:
#define MINORBITS 20
#define MINORMASK ((1U > MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) 由此可以看出字符设备的主设备号是12为位的无符号整型数,次设备号是20位的无符号整型数.这里面的转换在源代码里看应该是十分清楚的了
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 file_operations *fops; //在词设备上的操作
struct cdev *cdev; //定义在include/linux/cdev.h
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
#define CHRDEV_MAJOR_HASH_SIZE 255
struct cdev:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
二.设备号的申请
字符设备设备号的申请有两种方式:(这里是说主设备号)
1.指定申请:就是说你事先找到一个未使用的字符设备号,然后再向内核申请这个指定的设备号. 这中方式的申请函数是:register_chrdev_region(),这个函数定义在fs/char_dev.c中,下面是内核源码:
/** * register_chrdev_region() - register a range of device numbers
* @from: 要申请的第一个设备的设备号,必须要有主设备号
* @count: 要申请的有连续设备号的设备的个数
* @name: 设备名称
* 返回0表示申请成功,返回负数说明申请失败
*/
//在2.6内核中申请设备号和注册字符设备采用的函数是
//register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
//当参数major 大于0时,表示设备号是用户定义的。major=0,表示要动态申请设备号。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count; //要申请的最后一个设备的设备号.
dev_t n, next;
for (n = from; n
next = MKDEV(MAJOR(n)+1, 0);
if (next > to) next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),next - n, name); //这个函数在
alloc_chrdev_region中也调用了,具体解释见下面解释1.
if (IS_ERR(cd)) goto fail;见解释2.
}
return 0;
fail:
to = n;
for (n = from; n 见解释3.
}
解释1:__register_chrdev_region//注册主设备号和次设备号
/*major == 0此函数动态分配主设备号
major > 0 则是申请分配指定的主设备号
返回0表示申请成功,返回负数说明申请失败*/
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);
//kzalloc分配内存并且全部初始化为0,见include/linux/lab.h
//kzalloc其实是 kmalloc(size, flags | __GFP_ZERO);的封装
if (cd == NULL) return ERR_PTR(-ENOMEM);
//include/asm-generic/errno-base.h #define ENOMEM 12
mutex_lock(&chrdevs_lock);
if (major == 0) { //下面是动态申请主设备号
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i—)
{
// ARRAY_SIZE(arr)是://(sizeof(arr)/sizeof((arr)[0])+__must_be_array(arr)
// #define __must_be_array(a) //BUILD_BUG_ON_ZERO(__builtin_types_compatible_p(typeof(a), typeof(&a[0])))
//__builtin_types_compatible_p gcc内嵌函数用于判断一个变量的类型是
//否为某指定的类型,假如是就返回1,否则返回0。
//#define BUILD_BUG_ON_ZERO(e) (sizeof(char[1 - 2 * !!(e)]) - 1)
// chrdevs是内核中所有已经注册了设备号的设备的一个数组
if (chrdevs == NULL) break;
}
if (i == 0) { ret = -EBUSY; goto out;}
major = i; ret = major; //这里取到一个未使用的设备号
}
cd->major = major; cd->baseminor = baseminor;
cd->minorct = minorct; strncpy(cd->name,name, 64);
//以上是申请到设备后对设备数据结构的填充
i = major_to_index(major); //将其转换为哈希表索引
//return major % CHRDEV_MAJOR_HASH_SIZE;
//#define CHRDEV_MAJOR_HASH_SIZE 255
for (cp = &chrdevs; *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_min) {
ret = -EBUSY;
goto out;
}
}
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock); //在对这样的数组进行遍历的时候应该加锁的.
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
解释2:
static inline long IS_ERR(const void *ptr)
{
return IS_ERR_VALUE((unsigned long)ptr);
}
IS_ERR_VALUE:
#define IS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO)
//MAX_ERRNO:这里可以看出它的值是0x80000fff
#define MAX_ERRNO 4095 //系统内的错误状态的最大值
解释3:
static inline long PTR_ERR(const void *ptr)
{
return (long) ptr;
}
2.内核自动分配:也就是事先你并不知道哪一个设备号未使用,在申请时就须内核为你挑选一个未使用的设备号.
这中方式的申请函数是:alloc_chrdev_region(),这个函数定义在
fs/char_dev.c中,下面是内核源码:
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: 要申请的有连续设备号的第一个设备的次设备号
* @count: 要申请的有连续设备号的设备的个数
* @name: 设备或驱动的名称 *
* 动态申请设备号,申请成功通常将第一个设备的设备号(包括主,次设备号)
*传递给@dev,并且函数返回0,失败则函数返回负数
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
三、注册字符设备
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义:
linux-2.6.22/include/linux/cdev.h
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 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
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;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。
下面贴出了两个函数的代码,以具体看一下它们之间的差异。
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
由此可见,两个函数完成都功能基本一致,只是 cdev_init() 还多赋了一个 cdev->ops 的值。
初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add() 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。
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 字段。
当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u2/77869/showart_2013442.html |
|