1、字符设备基础
Linux系统将设备分为三大类:字符设备、块设备和网络设备。字符设备是其中较为基础的一类,它的读写操作需要一个字节一个字节的进行,不能随机读取设备中的某一数据,即要按照先后顺序。举例来说,比较常见的字符设备有鼠标、键盘、串口等。
字符设备驱动所做的工作主要是添加、初始化、删除cdev结构体,申请、释放设备号,填充file_operations结构体中的功能函数,比如open()、read()、write()、close()等。当我们创建一个字符设备时,一般会在/dev目录下生成一个设备文件,Linux用户层的程序就可以通过这个设备文件来操作这个字符设备。
2、设备设备驱动结构
2.1 cdev结构体
在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体在/include/linux/cdev.h中定义。
struct cdev {
struct kobject kobj; /* 内嵌的内核对象 */
struct module *owner; /* 模块所有者,一般为THIS OWNER */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list; /* 把所有向内核注册的字符设备形成链表 */
dev_t dev; /* 设备号,由主设备号和次设备号构成 */
unsigned int count; /* 属于同主设备号的次设备号的个数 */
} __randomize_layout;
cdev结构体的成员dev_t定义了设备号,一共32位,主设备号高12位,次设备号低20位。主设备号用来区分设备类型,次设备号用来区分同类型的不同设备。在/include/linux/kdev_t.h中定义了一些宏:
MAJOR(dev) /* 从dev_t中获取主设备号 */
MINOR(dev) /* 从dev_t中获取次设备号 */
MKDEV(ma,mi) /* 通过主设备号ma和次设备号mi生成dev_t */
cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。Linux内核提供了一组函数以用于操作cdev结构体:
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_set_parent(struct cdev *p, struct kobject *kobj);
int cdev_device_add(struct cdev *cdev, struct device *dev);
void cdev_device_del(struct cdev *cdev, struct device *dev);
void cdev_del(struct cdev *);
void cd_forget(struct inode *);
这里只简单介绍几个函数,就不贴上代码了,有兴趣可以查。cdev_init()函数用来初始化cdev,并建立cdev与file_operations之间的连接;cdev_alloc()用来给cdev结构体动态申请内存;cdev_add()函数和cdev_del()函数用来向系统添加和删除一个cdev,完成字符设备的注册和注销。
2.1 分配和释放设备号
在向系统注册字符设备之前,需要先为字符设备申请一个设备号,分为动态申请和静态申请。函数原型为:
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/* 静态申请 */
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/* 动态申请 */
register_chrdev_region()函数用于已知设备号的情况,alloc_chrdev_region()函数用于未知设备号、向系统动态申请一个未被使用的设备号的情况,申请到之后会把设备号放到第一个参数dev中。这两个函数相比,后者的优点就是不会产生设备号冲突。
当注销字符设备时,需要释放设备号linux系统框图,函数原型为:
/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count);
2.2 file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,主要功能函数都在这里实现。这些函数在用户层程序调用Linux时被内核调用。file_operations结构体代码清单为:
struct file_operations {
struct module *owner;
/* 模块拥有者,一般为THIS MODULE */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
/* 从设备中读取数据,成功时返回读取的字节数,出错返回负值 */
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/* 向设备发送数据,成功时该函数返回写入字节数。若未被实现,用户调层用write()时系统将返回 -EINVAL*/
int (*mmap) (struct file *, struct vm_area_struct *);
/* 将设备内存映射内核空间进程内存中,若未实现,用户层调用mmap()系统将返回 -ENODEV */
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);
/* 提供设备相关控制命令(读写设备参数、状态,控制设备进行读写...)的实现,当调用成功时返回一个非负值 */
int (*open) (struct inode *, struct file *);
/* 打开设备 */
int (*release) (struct inode *, struct file *);
/* 关闭设备 */
int (*flush) (struct file *, fl_owner_t id);
/* 刷新设备 */
loff_t (*llseek) (struct file *, loff_t, int);
/* 用来修改文件读写位置,并将新位置返回,出错时返回一个负值 */
int (*fasync) (int, struct file *, int);
/* 通知设备 FASYNC 标志发生变化 */
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* POLL机制,用于询问设备是否可以被非阻塞地立即读写。当询问的条件未被触发时,用户空间进行select()和poll()系统调用将引起进程阻塞 */
...
};
3、字符设备驱动组成
3.1 模块加载函数和模块卸载函数
我们为设备定义一个结构体,将cdev、私有数据等信息包含进去。在模块加载函数中,为字符设备申请设备号、注册cdev;在模块加载函数中,为字符设备释放设备号、注销cdev。
/* 设备结构体 */
struct xxx_dev {
struct cdev cdev;
...
}
/* 设备驱动模块加载函数 */
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); /* 注册设备 */
...
}
/* 设备驱动模块卸载函数 */
static void _ _exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */
cdev_del(&xxx_dev.cdev); /* 注销设备 */
...
}
3.2 file_operations结构体的成员函数
file_operations结构体的成员函数是内核与用户层的接口,用户层对Linux系统的调用最终会调用到这些函数,下面列出了常用的一些函数:
/* 打开设备 */
static int xxx_open(struct inode *inode, struct file *filp)
{
...
}
/* 释放设备 */
static int xxx_release(struct inode *inode, struct file *filp)
{
...
}
/* ioctl */
static long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch (cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
/* 不能支持的命令 */
return - ENOTTY;
...
}
/* 读
* filp:文件结构体指针
* buf:读取的内存地址
* size:字节大小
* ppos:对文件操作的起始位置
*/
static ssize_t xxx_read(struct file *filp, char __user * buf, size_t size, loff_t * ppos)
{
...
copy_to_user(buf, ..., ...);
...
}
/* 写
* filp:文件结构体指针
* buf:写入的内存地址
* size:字节大小
* ppos:对文件操作的起始位置
*/
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
...
copy_from_user(..., buf, ...);
...
}
/* 文件操作结构体 */
static const struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.release = xxx_release,
.read = xxx_read,
.write = xxx_write,
.unlocked_ioctl = xxx_ioctl,
};
需要注意的是,在xxx_read()和xxx_write()函数中的第二个参数内存地址指的是用户空间的地址,不能在内核中直接进行读写。这时候需要借助两个函数来完成,copy_from_user()函数用来完成将数据从用户空间拷贝到内核,copy_to_user()函数用来将数据从内核拷贝到用户空间。函数原型为:
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
*to是内核空间的指针,*from是用户空间指针,n表示拷贝数据的字节数。上述函数如果完全复制成功, 返回值为0。 如果复制失败, 则返回负值。
3.3 结构框图

参考:
《Linux设备驱动程序开发详解》宋宝华
Linux字符设备驱动 - GreenHand# - 博客园
(编辑:威海站长网)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|