linux 字符设备驱动 c_dev分析
2011-11-18 10:31
190 查看
内核中每个字符设备都对应一个 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 结构本身。
随着内核不断增加新的功能,file_operations结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open()、release()、read()、write()、ioctl()、llseek()、poll()等。
open()函数:对设备特殊文件进行open()系统调用时,将调用驱动程序的open() 函数:
int (*open)(struct inode * ,struct file *);
其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode- i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误)等;
release()函数:当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release() 函数:
void (*release) (struct inode * ,struct file *) ;
release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
read()函数:当对设备特殊文件进行read() 系统调用时,将调用驱动程序read()函数:
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数类型)。
globalvar_read函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{…copy_to_user(buf, &global_var, sizeof(int));…}
write( ) 函数:当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。
globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off)
{…copy_from_user(&global_var, buf, sizeof(int));…}
ioctl() 函数:该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:
int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);
unsigned int参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,"No such ioctl fordevice,该设备无此ioctl 命令")。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。
llseek()函数:该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:
loff_t (*llseek) (struct file *, loff_t, int);
poll()函数:poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
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 结构本身。
随着内核不断增加新的功能,file_operations结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open()、release()、read()、write()、ioctl()、llseek()、poll()等。
open()函数:对设备特殊文件进行open()系统调用时,将调用驱动程序的open() 函数:
int (*open)(struct inode * ,struct file *);
其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode- i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误)等;
release()函数:当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release() 函数:
void (*release) (struct inode * ,struct file *) ;
release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
read()函数:当对设备特殊文件进行read() 系统调用时,将调用驱动程序read()函数:
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数类型)。
globalvar_read函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{…copy_to_user(buf, &global_var, sizeof(int));…}
write( ) 函数:当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。
globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off)
{…copy_from_user(&global_var, buf, sizeof(int));…}
ioctl() 函数:该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:
int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);
unsigned int参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,"No such ioctl fordevice,该设备无此ioctl 命令")。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。
llseek()函数:该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:
loff_t (*llseek) (struct file *, loff_t, int);
poll()函数:poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
相关文章推荐
- linux设备驱动--globalmem字符设备框架分析
- linux驱动开发之字符设备框架 -调用过程分析
- 11-S3C2440驱动学习(七)嵌入式linux-字符设备的另一种写法及RTC驱动程序分析和字符设备驱动框架总结
- linux 高级字符设备驱动 ioctl操作介绍 例程分析实现
- 基于mini6410的linux驱动学习总结(五 字符设备驱动程序实例分析(虚拟设备驱动))
- linux设备驱动--globalmem字符设备框架分析
- Linux字符设备驱动之Tiny6410 LED驱动分析
- Linux驱动学习5(详细分析字符设备驱动信号量实现互斥)
- linux 驱动程序 设备模块 设备号 设备文件创建 设备注册 字符驱动设备分析
- linux字符设备驱动程序源码(char_dev.c)分析
- linux 高级字符设备驱动 ioctl操作介绍 例程分析实现
- linux 驱动程序 设备模块 设备号 设备文件创建 设备注册 字符驱动设备分析
- linux 高级字符设备驱动 ioctl操作介绍 例程分析实现【转】
- Linux设备驱动之mmap设备操作(memdev.c字符设备驱动分析)
- Linux 4.33 字符设备驱动分析
- 用面向对象思想分析linux字符设备驱动开发
- linux字符设备驱动总结分析
- Linux字符设备驱动分析
- linux字符设备驱动解析
- Linux设备驱动之字符设备\interrupt\wait_queue实现