linux 内核学习15-字符设备驱动详解
1. 字符设备驱动的抽象
这里将上面学习的内容进行解析
<include/linux/cdev.h>
struct cdev{
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}
参数 | 含义 |
---|---|
kobj | 用于Linux设备驱动模型 |
owner | 字符设备驱动所在的内核模块对象指针程序 |
ops | 字符设备驱动程序中最关键的一个操作函数,在和应用程序交互过程中起到了桥梁枢纽的作用 |
kobjlist | 用来将字符设备串成一个链表 |
dev | 字符设号备的设备号,由 主设备号和 次设备号组成 |
count | 同属于一个主设备号的次设备号的个数 |
主设备号和次设备号通常可以通过如下宏来获取,也就是高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)))
设备驱动程序可以由两种方式来产生struct cdev
,一种是使用全局静态变量,另一种是使用内核提供的cdev_alloc()
接口函数。
static struct cdev mydemo_cdev;
struct mydemo_cdev =cdev_alloc();
初次之外,Linux内核还提供了若干个与cdev相关的API函数。
- cdev_init()函数,初始化cdev数据结构,并且建立该设备与驱动操作方法集,file_operations之间的连接关系。
void cdev_init(struct cdev *cdev,const struct file_operations *fops)
- cdev_add()函数,把一个字符设备添加到系统中,通常在驱动程序的probe函数里会调用该接口来注册一个字符设备。
int cdev_add(struct cdev *p,dev_t devd,unsigned count)
参数 | 含义 |
---|---|
p | 表示一个设备的cdev数据结构 |
dev | 表示设备的设备号 |
count | 表示这个 主设备号里可以由多少个次设备号,通常同一个主设备号可以由多个次设备号不相同的设备,如系统中同时有多个串口,他们都是名为tty的设备,主设备都是4 |
- cdev_del()函数,从系统中删除一个cdev,通常在驱动程序的卸载函数里会调用该接口
void cdev_del(struct cdev *p)
2. 设备号的管理
字符这杯驱动的初始化函数(probe函数)有一个很重要的工作,即为设备分配设备号。设备号是系统中珍贵的资源,内核必须避免发生两个设备驱动使用同一个主设备号的情况,因此在编写驱动程序要小心。Linux内核提供两个接口函数完成设备的申请。
int register_chrdev_region(dev_t from,unsigned count,const char *name)
register_chrdev_region()函数需要指定主设备号,可以连续分配多个。也就是说,在使用该函数之前,驱动程序编写者必须保证分配的主设备号在系统中没有使用。内核文档documentation/devices.txt
文件描述了系统中已经分配出去的设备号,因此使用该接口函数的程序员应该事先约定文档,避免使用已经被系统占用的主设备号。
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
alloc_chrdev_region()函数会自动分配一个主设备号,可以避免和系统占用的主设备号重复,建议驱动开发者使用这个函数来分配主设备号。
在驱动程序的卸载函数中需要把主设备号释放给系统,可以调用如下接口函数。
void unregister_chrdev_region(dev_t from,unsigned count)
3. 设备结点
在Linux系统中有一个原则,就是万物皆文件。设备结点也算是一个特殊的文件,称之为设备文件,是连接内核驱动空间和用户空间的桥梁。
主设备号:代表一类设备
次设备号:代表同一类设备的不同个体,每个次设备号都有一个不同的设备结点。
按照Linux的习惯,系统中所有的设备结点都存放在/dev/ 目录中,dev目录是一个动态生成的,使用devtmpfs虚拟文件系统挂在的、基于RAM的虚拟文件系统。
/mnt # ls -l /dev/
total 0
crw-rw---- 1 0 0 14, 4 Sep 2 10:07 audio
crw-rw---- 1 0 0 5, 1 Sep 2 10:07 console
crw-rw---- 1 0 0 10, 63 Sep 2 10:07 cpu_dma_latency
crw-r--r-- 1 0 0 252, 0 Sep 2 10:14 demo_drv
第一列中c表示字符设备,d表示块设备,后面还会有更多的主设备号和次设备号。
设备结点的生成方式有两种方式:一种是使用mknod命令手工生成,另一个使用udev机制动态生成。
mknod filename type major minor
举个例子
mkdir -p /dev/cobing
mknod /dev/cobing/mydev1 c 128 512
4. 字符设备操作方法集
在mydemo例子中,实现了一个demodrv_fops的操作方法集,里面包含open,release,read和write等方法。从C语言的角度,就是抽象和定义了一堆函数指针,这些函数指针称为file_operations方法,是在Linux内核发展过程中不断扩充壮大的。
static const struct file_operations demodrv_fops={
.owner=THIS_MODULE,
.open=demodrv_open,
.release=demodrv_release,
.read=demodrv_read,
.write=demodrv_write
};
这个方法是通过cdev_init()
函数和设备监理的一个连接关系,因此在用户空间的test程序中,直接使用open()函数打开这个设备结点。
#define DEMO_DEV_NAME "/dev/demo_drv"
fd=open(DEMO_DEV_NAME,O_RDONLY);
open函数的第一个参数是设备文件名,第二个参数用来指定打开文件的属性。open函数执行成功会返回一个文件描述符,俗称文件句柄否则返回-1.
应用程序的open函数执行时,会通过系统调用进入内核空间,在内核空间的虚拟文件系统层(VFS)经过复杂的转换,最后调用设备驱动的file_operations
方法集中的open方法。因此,驱动开发者有必要了解file_operations
结构体的组成,该结构体定义在include/linux/fs.h
头文件中,字符设备驱动程序的核心开发工作是实现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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, 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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
参数 | 含义 |
---|---|
llseek | 修改文件的当前读写位置,并返回新位置 |
read | 从设备驱动中读取数据到用户空间,函数返回成功读取的字节数,如果返回负数,则说明读取失败 |
write | 把用户空间的数据写入设备中,函数返回成功写入的字节数 |
poll | 查询设备是否可以立即读写,该方法主要用于阻塞型I?O |
unlocked_ioctl和compat_ioctl | 提供与设备相关的控制命令的实现 |
mmap | 设备内存射到进程的虚拟地址中 |
open | 打开设备 |
release | 关闭设备 |
aio_read和aio_write | 所谓的异步I/O就是提交完I/O请求之后立即返回,不需要等到I/O操作完成之后再去做别的事情,因此具有非阻塞特性。设备驱动完成I/O操作之后,可以通过发送信号或者回调函数等方式来通知 |
fsync | 实现一种称为异步通知的方法 |