在Linux系统中,设备的类型非常多。比如:字符设备,块设备,网络设备接口设备,PCI设备,USB设备,平台设备,混杂设备。设备类型不同,对应的驱动模型也不同。Linux下开发设备驱动程序要遵循内核模块的编写规范,在编写字符设备驱动程序时,有一个统一的框架,也就是字符设备驱动模型。下面我们来看下整个字符设备驱动模型是怎样的。
从思维导图中我们可以看到整个设备驱动模型分为了三个部分,分别是驱动初始化,实现设备操作,还有驱动注销。下面重点分别介绍每个部分的具体实现。
Part1:驱动初始化
- 分配设备描述结构
- 初始化设备描述结构
- 注册设备描述结构
- 硬件初始化
1.分配设备描述结构
在任何一种驱动模型中,设备都会通过内核的一种结构来描述,通过source in sight阅读内核源码我们可以看到这个字符描述设备结构体的定义如下
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;//设备操作集
struct list_head list;
dev_t dev;//设备号
unsigned int count;//设备数量
};
我们重点关注其中的三个变量。
1.第一个是count,这个变量代表了有多少个相同的设备,比如开发板上可能对应有多个串口,这个count值就代表了串口的数量。
2.第二个是dev,这个变量代表了这个设备的设备号,通过dev_t来定义,dev_t实际上是unsigned int无符号的32位整形内核规定,每个设备都要对应一个设备号,设备号又分为主设备号和次设备号。不同设备对应的主设备号是不相同的,多个同种设备是通过次设备号来区分开的。也就是说我们定义的这样一个cdev,是可以同时支持多个同种设备的,我们通过设置不同次设备号来将它们区分开。在linux终端下输入ls -l /dev 来查看当前接入系统的设备。设备在linux文件系统中都是以设备文件来表示的。一个设备文件即代表了一个设备。
矩形框里的数值代表主设备号,椭圆内的数值代表次设备号。我们可以看到这是几个同类型的设备,所以它们的主设备号都是1,而次设备号是不同的。字符设备文件和字符设备驱动程序是通过主设备号来建立起联系的。一个设备号由高12位的主设备号和低20位的次设备号组成。内核中通过几个宏拓展的实现来完成设备号的分解和合成,代码写的非常有意思,我们可以来看一下。
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
通过MKDEV将主设备号和次设备号合成为设备号,其实就是将主设备号左移20位后位或低20位的次设备号。
dev_t dev=MKDEV(主设备号,次设备号)
MAJOR用来提取设备号中的主设备号,宏定义中我们可以看到就是将主设备号右移了20位,剔除了次设备号,同时将主设备号右对齐。
MINOR用来提取设备号中的次设备号,宏定义中我们可以看到次设备号是通过主设备号位与一个次设备号掩码来获得,这个次设备号写的很有意思,将1左移20位后再减去1,从而得到一个低20位都为1的掩码。从而次设备号得到提取。
那么如何为一个设备分配一个设备号呢?Linux内核提供了两种分配设备号的方法。第一种是静态分配,第二种是动态分配。
- 静态分配:开发者可以自己选择一个数字作为主设备号,然后通过内核提供的函数register_chrdev_region()向内核申请使用。缺点:如果申请使用的设备号被内核中的其他驱动占用了,那么申请失败。
- 动态分配:通过使用alloc_chrdev_region()函数让内核为设备分配一个可用的主设备号。优点:因为内核知道哪些主设备号已经被使用了,所以不会出现设备号重复导致失败的情况。
3.第三个是ops操作函数集
2.初始化设备描述结构
cdev设备描述结构也可以有两种分配方式
- 静态分配:struct cdev mdev;
- 动态分配:struct cdev *pdev=cdev_alloc();
通过cdev_init()函数来初始化设备描述结构
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
/*cdev:待初始化的设备描述结构 fops:设备对应的操作函数集*/
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
3.注册设备描述结构
字符设备描述结构的注册通过cdev_add()函数来完成
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
/*p待添加到内核的设备描述结构 dev设备号 count同类型设备的数量*/
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
4.硬件初始化
Part2:实现设备操作
内核定义了一个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);
};
- int (open) (struct inode , struct file *);打开设备,响应open系统调用
- int (release) (struct inode , struct file *);关闭设备,响应close系统调用
- ssize_t (read) (struct file , char __user , size_t, loff_t );从设备读取数据,响应read系统调用
- ssize_t (write) (struct file , const char __user , size_t, loff_t );向设备写入数据,响应write系统调用
- loff_t (llseek) (struct file , loff_t, int);重定位文件读写指针,响应lseek系统调用
在Linux系统中,每一个打开的文件,在内核中都会关联一个struct file结构体,这个结构体有几个重要成员需要关注。
loff_t f_pos;/*文件读写指针*/
const struct file_operations *f_op;/*该文件对应的操作*/
在Linux系统中,每一个存在于文件系统里的文件都会关联一个struct inode结构体,该结构主要用来记录文件一些物理上的信息。注意和上面的struct file区分开,struct file结构体只有当文件打开时才会被关联,一个文件没有被打开时是不会关联struct file的。重要的成员为
dev_t i_rdev;/*设备号*/
open设备方法主要用来为以后的操作完成初始化准备工作的。在大部分的驱动程序中,open完成如下操作:
- 标明次设备号
- 启动设备
release设备方法和open正好相反。这个设备方法有时也称为close。
- 关闭设备
read设备方法通常完成两件事情:
- 从设备中读取数据(属于硬件访问类操作)
- 将读取到的数据返回给应用程序
ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *ppos);
参数分析:
filp:与字符设备文件关联的file结构指针,由内核创建
buff:从设备读取到的数据,需要保存到位置,由read系统调用提供该参数
count:请求传输的数据量,由read系统调用提供该参数
ppos:文件的读写位置,由内核从filp结构取出后,传递进来
需要注意的是,buff参数是来源于用户空间的指针,这类指针都不能被内核代码直接引用,必须使用内核专门的函数进行安全的传递。
int copy_from_user(void *to,const void __user *from,int n)
int copy_to_user(void __user *to,const void *from,int n)
从下面这个图我们也可以清楚的看到read设备方法在linux系统中的整体执行情况。
write设备方法通常完成两件事情: - 从应用程序中提供的地址中取出数据
- 将数据写入设备(属于硬件访问类操作)
ssize_t (*write) (struct file *filp, char __user *buff, size_t count, loff_t *ppos);
参数类似于read。
Part3:驱动注销
无论使用何种方式分配设备号,都要在设备挂起驱动退出时注销设备号。我们使用内核提供的unregister_chrdev_region()函数来释放这些设备号。
void unregister_chrdev_region(dev_t from, unsigned count)
{ /*from 要注销的设备号 count 设备数*/
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
void cdev_del(struct cdev *p)
{/*p要注销的设备描述结构*/
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}