文章目录
思路来源—sys文件系统
sys文件系统的设计思想:
将内核的一些信息,以文件的形式列在sys目录下。应用层通过读写该目录下的文件,可以和内核进行一些信息交互。
由此设计跨设备的内存共享,实现用户态和内核态的信息交互。
设备类:
- /sys/class,内核将驱动进行分类,比如和声音有关的、和时钟有关的;
- class_creat() 创建的;
- /sys/class/xxx/ 目录下有dev,event,power,subsystem等文件,例如cat dev,得知dev中内容是设备号;例如event是内核给udev传递的信息的地方。
字符设备驱动的创建
A. 字符设备创建
1.**创建设备号 **
dev_t是一个32位的整数,其中的12位用来标示主设备号,其余的20位用来标示次设备号;
#include <linux/types.h>
dev_t dev_id;
拓展:
我们可以使用两个宏来获得设备的主设备号及次设备号,其中,major为主设备号,minor为次设备号。
MAJOR(dev_t dev_id);
MINOR(dev_t dev_id);
将主设备号和次设备号转换为dev_t类型,则可以使用下面的宏:
MKDEV(int major, int minor);
2.注册设备
静态注册设备号, 用于已知设备号的情况
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:
from为要分配设备号的起始值,count是所请求的连续设备号的个数,name为设备名称。
分配成功时返回值为0,否则返回错误编号
动态分配设备号,用于未知设备号的情况
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数:
dev用于保存申请成功后的第一个设备号,baseminor第一个要使用的次设备号,常常为0,另外参数同上。该函数可以自动避开设备号重复的冲突,会使用一个未使用的设备号。返回值:小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来。
使用动态注册方法,注册设备
alloc_chrdev_region(&dev_id, 0, 1, "dev_demo");
3.申请字符设备结构体
结构体原型:
struct cdev {
struct kobject kobj; //内嵌的 kobject 对象
struct module *owner; //所属模块
const struct file_operations *ops;//文件操作结构体
struct list_head list;
dev_t dev; //设备号
unsigned int count;
};
申请字符设备结构体实现:
#include <linux/cdev.h>
struct cdev my_chrdev = cdev_alloc() ;
4.定义一个文件操作集
文件操作集原型:
struct file_operations {
struct module *owner;
/* 拥有该结构的模块的指针,一般为 THIS_MODULES */
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 *, char _ _user *, size_t, loff_t);
/* 初始化一个异步的读取操作*/
ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t);
/* 初始化一个异步的写入操作*/
int(*readdir)(struct file *, void *, filldir_t);
/* 仅用于读取目录,对于设备文件,该字段为 NULL */
unsigned int(*poll)(struct file *, struct poll_table_struct*);
/* 轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
/* 执行设备 I/O 控制命令*/
long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
/* 不使用 BLK 的文件系统,将使用此种函数指针代替 ioctl */
long(*compat_ioctl)(struct file *, unsigned int, unsigned long);
/* 在 64 位系统上, 32 位的 ioctl 调用,将使用此函数指针代替*/
int(*mmap)(struct file *, struct vm_area_struct*);
/* 用于请求将设备内存映射到进程地址空间*/
int(*open)(struct inode *, struct file*);
/* 打开 */
int(*flush)(struct file*);
int(*release)(struct inode *, struct file*);
/* 关闭*/
int (*fsync) (struct file *, struct dentry *, int datasync);
/* 刷新待处理的数据*/
int(*aio_fsync)(struct kiocb *, int datasync);
/* 异步 fsync */
int(*fasync)(int, struct file *, int);
/* 通知设备 FASYNC 标志发生变化*/
int(*lock)(struct file *, int, struct file_lock*);
ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);
/* 通常为 NULL */
unsigned long(*get_unmapped_area)(struct file *,unsigned long, nsigned long,
unsigned long, unsigned long);
/* 在当前进程地址空间找到一个未映射的内存段 */
int(*check_flags)(int);
/* 允许模块检查传递给 fcntl(F_SETEL...)调用的标志 */
int(*dir_notify)(struct file *filp, unsigned long arg);
/* 对文件系统有效,驱动程序不必实现*/
int(*flock)(struct file *, int, struct file_lock*);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,
unsigned int); /* 由 VFS 调用,将管道数据粘接到文件 */
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,
unsigned int); /* 由 VFS 调用,将文件数据粘接到管道 */
int (*setlease)(struct file *, long, struct file_lock **);
};
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行 write()系统调用时,将得到-EINVAL 返回值。
readdir()函数仅用于目录,设备节点不需要实现它。
ioctl()提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。
mmap()函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap()系统调用时将获得-ENODEV 返回值。这个函数对于帧缓冲等设备特别有意义。
当用户空间调用 Linux API 函数 open()打开设备文件时,设备驱动的 open()函数最终被调用。
驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与 open()函数对应的是 release()函数。
poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行 select()和 poll()系统调用将引起进程的阻塞。
aio_read()和 aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用 aio_read()、 aio_write()等系统调用进行读写。
文件操作结构体声明及初始化,需要对函数进行实现
int my_chrdev_open(struct inode *inode, struct file *filp)
{
printk("enter my_chrdev_open!\n");
return 0;
}
int my_chrdev_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter my_chrdev_ioctl!\n");
return 0;
}
struct file_operations my_chrdev_fops =
{
.owner = THIS_MODULE,
.open = my_chrdev_open,
.read = my_chrdev_read,
.write = my_chrdev_write,
.ioctl = my_chrdev_ioctl,
.release = my_chrdev_release,
};
B. 字符设备注册到内核
5.字符设备初始化
函数原型:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
实现:
my_chrdev.ops = &my_chrdev_fops;
cdev_init(&my_chrdev, &my_chrdev_fops);
6.将字符设备加入到内核
函数原型:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
实现:
cdev_add(&my_chrdev, dev_id, 1);
C. 注册设备节点
内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,在/sys/class/下创建类目录,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。
#include <linux/device.h>
7.创建设备类
函数原型:
struct class *class_create(struct module *owner, const char *name)
实现:
struct class *my_chrdev_class = NULL;
my_chrdev_class = class_create(THIS_MODULE, "my_chrdev_class");
会在/sys/class目录下创建名为"my_chrdev_class"的文件夹
8.创建设备文件
函数原型:
struct device *device_create(struct class *class, struct device *parent, dev_t devt,void drvdata, const char *fmt,...)
class:类 – 在文件系统中用来管理设备节点文件的类
parent:当前设备的父设备 – (无:NULL)
devt:设备号 – 创建设备节点文件所关联的设备号
drvdata:当前设备驱动的私有数据 – 任何数据(无:NULL)
const charfmt, … :设备节点文件名(格式化字符串传参)
实现:
struct device *my_chrdev_device = NULL;
my_chrdev_device = device_create(my_chrdev_class, NULL, dev_id, NULL, "my_chrdev_device");
会在/dev目录下创建创建名为"my_chrdev_device"的文件夹
D. 注销相关字符设备
9.注销一个设备号
函数原型:
void unregister_chrdev_region(dev_t from, unsigned count)
10.**注销一个字符设备 **
函数原型:
void cdev_del(struct cdev *p)