相信大家通过上节的了解,对字符设备也有了感性上的认识。接下来我们就要对字符设备驱动进行剖析了(
基于Linux3.0.1版本内核)
一、字符设备结构struct cdev说明
在Linux内核中,是使用struct cdev这个数据结构来表示字符设备的。
定义在
<linux/fs.h>中
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //操作函数集
struct list_head list;
dev_t dev; //设备号
unsigned int count;// 支持的设备数,如设备中支持三个串口
};
我们只对该结构中的主要成员进行说明:
a )操作函数集file_operationsops的说明--------------------------------------------start
定义在<linux/fs.h>中
以下为file_operations 结构的内容,同时file_operations 所支持的操作也被称为设备方法。
struct file_operations {
struct module *owner; //指向拥有该结构模块的指针
loff_t (*llseek) (struct file *, loff_t, int); //修改文件的当前读写位置,并将新位置作为返回值返回
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备中写入数据
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化设备上的异步读取操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化设备上的异步写入操作
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);//打开设备,始终是对设备文件执行的第一个操作
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
file_operations 作用:是一个函数指针的集合,主要完成应用程序的系统调用对在设备驱动上的操作的映射。这个结构中的每一个成员都必须指向驱动程序中实现特定的操作函数(方法)
,即此结构中的函数指针指向驱动中的实现函数(方法),不同函数实现针对设备的不同操作,对不支持的操作,可设置对应函数指针为NULL。
如下;
struct file_operations memfpos = {
.read = dev_read,
.write = dev_write,
.ioctl = dev_ioctl,
.llseek = NULL,
} ;
关于操作函数集file_operations 的说明----------------------------------------------end
b)设备号的说明--------------------------------------------start
(在/dev/目录下查看)
crw-rw----. 1 root video 10,175 Mar 19 09:36 agpgart
crw-rw----. 1 root root 10, 56Mar 19 09:37 autofs
drwxr-xr-x. 2 root root 640 Mar 19 09:36 block
drwxr-xr-x. 2 root root 80 Mar 19 09:36 bsg
红色10 : 代表
主设备号。通过主设备号把
设备文件和
设备驱动程序一 一对应起来。
黄色0:代表次
设备号。区分同类型的设备。譬如串口驱动程序通过此设备号去识别不同的串口。。。
注:第一列为“d”,代表块设备
若是普通文件则会被文件大小所代替。如下:
-rw-r--r--
1 root root
441
Mar 19 09:36 a.c
由上述结构可知,设备号的数据类型为
dev_t。
经追踪得:dev_t ==unsigned int ,即是32位数据
在这32位中,其中高12位是主设备号,低20位是次设备号。
操作设备号:----存在于结构体struct inode中;成员名为i_rdev。
1>将
主设备号与
次设备号合成为
dev_t类型
dev_t = MKDEV(主设备号, 次设备号)
2>从
dev_t中分解出主设备号
主设备号 = MAJOR(dev_t dev)
3>从
dev_t中分解出次设备号
次设备号 = MINOR(dev_t dev)
例:
num = MINOR(inode->i_rdev);
分配设备号:--->主要是分配主设备号
1>静态分配
开发者自己选择一个数字作为主设备号,然后通过
函数register_chrdev_region向内核申请使用。
缺点:如果使用的设备号已经被内核中的其他驱动使用,则申请失败。
2>动态分配
使用
alloc_chrdev_region由内核分配一个可用的主设备号。
优势:不会分配到已经使用的号
缺点:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。所以对于驱动程序来讲,一般在分配设备号,就可以在/proc/devices读到。回忆上一节。
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数:
* @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--->关于该设备即驱动的名字
注销设备号:
不管使用哪种分配得到的设备号,都应该在驱动退出时使用
unregister_chrdev_region函数释放这些设备号。
注销设备号函数原型:void unregister_chrdev_region(dev_t from, unsigned count)
参数:
* @from: the first in the range of numbers to unregister ---》注销的主设备号
* @count: the number of device numbers to unregister---》注销的设备数量
关于设备号的说明-------------------------------------------------end
在了解了以上设备驱动结构 cdev 的主要成员后,我们就可以进行设备驱动程序的编写了
二、字符设备驱动程序编写:
㈠、驱动的初始化:
1.1
分配设备描述结构(
struct cdev
)---两种分配方式:静态分配和动态分配
1>静态分配:
struct cdev mdev;
2>动态分配:
struct cdev *pdev = cdev_alloc();
注:注册设备号(动态)
alloc_chrdev_region
1.2
初始化
设备描述结构----
cdev_init()函数;<linux/cdev.h>
函数原型:
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
参数:
cdev: 待初始化的cdev结构
fops:设备对应的操作函数集
注:将设备结构与操作其设备的函数集相关联。
1.3注册
设备描述结构---
cdev_add()函数;<linux/cdev.h>
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:
p:待添加到内核的字符设备结构
dev:设备号
count:该类设备的设备个数
1.4硬件初始化---根据相应硬件的芯片手册,完成初始化。
在这里我们使用数组去模拟寄存器,所以不需要进行硬件的初始化。
以上步骤对应实现代码:
int dev1_regs[5];//用数组模拟设备的操作
int dev2_regs[5];
struct cdev mdev; //静态分配设备结构
dev_t devno;//静态分配设备号
const struct file_operations memfops = //实现的一些重要的设备方法
{
.read = dev_read,
.write = dev_write,
.llseek = dev_lseek,
.open = dev_open,
.release = dev_close,
};
static int memdev_init(void)
{
/*1.初始化设备结构*/
cdev_init(&mdev, &memfops);
/*动态申请设备号*/
alloc_chrdev_region(&devno, 0, 2, "memdev");
/*2.注册设备描述结构*/
cdev_add(&mdev, devno, 2);
return 0;
}
㈡、实现设备的操作
即实现函数操作集file_operations 的设备方法
file_operations比较重要的成员:
1>int (*open)(struct inode *, struct file *)
打开设备,响应open系统调用
2>int (*release)(struct inode *, struct file *);
关闭设备,响应close系统调用
3>loff_t (*llseek)(struct file *, loff_t , int )
重定位读写指针,响应lseek系统调用
参数1:file指针
参数2:请求偏移量
参数3:文件定位的起始位置,一般为0或1, 0:表文件开头 1:表文件结尾
4>ssize_t (*read)(struct file *filp, char __user *buff, size_t count, loff_t *offp)
从设备中读取数据,响应read系统调用
共做2件事。
--->从设备中读取数据(属于硬件访问类操作)
--->将读取的数据返回给应用程序;
参数说明:
filp: 与字符设备文件关联的file结构指针,由内核创建
buff:从设备(内核空间)读取到数据,需要保存到的位置,------由read系统调用提供该参数
count:请求传输的数据量---由read系统调用提供该参数
offp:文件的读写位置,由内核从file结构中取出。
注:buff参数是由用户空间传递进来的指针,这类指针不能被内核代码直接引用,即不可以使用memcpy函数进行数据复制,必须使用专门函数:
---->利用copy_to_user()函数----位于#include <linux/uaccess.h>
这个函数的行为虽然类似于memcpy函数,但当内核空间内运行的代码访问用户空间时要多加小心。被寻址的用户空间的页面可能当前并不在内存中,于是虚拟内存子系统会将该进程转入睡眠状态。
这个函数以及下面将要介绍的函数(
copy_from_user())的作用都不仅限于在内核空间和用户空间传递数据,它们还可以检查用户空间的指针是否有效,如无效,则不予拷贝。另外,在拷贝过程中,如果遇到无效用户指针,,则仅仅会拷贝部分数据。从而避免了导致内核崩溃会建立安全漏洞的因素。
参数对应情况如下图:
5>ssize_t (*write)(struct file *,const char __user *, size_t , loff_t *)
向设备写入数据,响应write系统调用。
同read相似:也做2件事
---->从应用程序(用户空间)提供的地址取出数据
---->将数据写入设备(属于硬件访问类
)
参数参考read设备方法.
利用copy_from_user()函数
从用户空间拿到数据后,传递给内核空间。
涉及设备操作的两个重要参数--结构体file、inode;定义在<linux/fs.h>中:
1>struct file
在Linux系统中,每个打开的文件,在内核中都会关联一个struct file,它由内核在打开文件时创建,在文件关闭后释放。
重要的成员:
loff_t f_pos //文件的读写指针---联系文件定位函数lseek()
struct file_operations *f_op //该文件所对应的操作
2>struct inode
每个存在于文件系统里的文件都会关联一个inode结构,该结构主要用来记录文件物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是会关联一个inode结构。
重要成员:
dev_t i_rdev:设备号
实现的部分函数操作集代码:
loff_t dev_lseek(struct file *filp, loff_t offset, int whence)
{
loff_t new_pos = 0;
switch (whence)
{
case SEEK_SET:
new_pos = 0 + offset;
break;
case SEEK_CUR:
new_pos = filp->f_pos + offset;
break;
case SEEK_END:
new_pos = 5*sizeof(int)-1 + offset;
break;
default:
return -EINVAL;
}
filp->f_pos = new_pos;
return new_pos; //此处返回的局部变量是可以的,局部变量存储在栈中,只是返回给调用者此值的拷贝
}
ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
int *reg_base = filp->private_data;
/*将数据从内核空间复制到应用空间*/
copy_to_user(buf, reg_base+(*pos), size);
filp->f_pos += size;
return size;
}
ssize_t dev_write(struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
int *reg_base = filp->private_data;
/*将数据从应用空间复制到内核空间*/
copy_from_user(reg_base+(*pos), buf, size);
filp->f_pos += size;
return size;
}
/*因为是模拟操作,不对真正的寄存器操作,所以open函数内部只是实现了读取设备 的次设备号*/
int dev_open(struct inode *node, struct file *filp)
{
int num = MINOR(node->i_rdev);
if(num == 0)
{
filp->private_data = dev1_regs;//把从inode中提取出来的次设备号传递给与打开文件关联的struct file中去
}
if(num == 1)
{
filp->private_data = dev2_regs;
}
return 0;
}
int dev_close(struct inode *node, struct file *filp)
{
return 0;
}
㈢、驱动注销
3.1》当我们从内核中卸载驱动程序的时候,使用cdev_del函数来完成字符设备结构的注销
3.2》使用unregister_chrdev_region注销设备号。
static void memdev_exit(void)
{
cdev_del(&mdev);
unregister_chrdev_region(devno, 2);
}