字符设备驱动程序
Linux 中主要有三类设备的驱动程序,分别是字符设备驱动程序,块设备驱动程序和网络设备驱动程序
字符设备是指在I/O 传输过程中以字符为单位进行传输的设备,例如键盘,打印机等,字符设备的驱动程序结构如下图所示:
字符设备可以通过文件节点来访问,设备文件和普通文件差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道,当然也存在数据区特性的字符设备,访问它们可以前后移动访问位置,比如FrameBuffer Device 就是这样一个设备,app 可以用 mmap 或者 lseek 访问获取整个图像
字符设备文件类型是 c(char),设备文件没有文件大小,取而代之的是两个号:主设备号 和 次设备号
字符设备驱动程序实现逻辑
Linux 内部一切设备皆文件,所有的硬件设备操作到应用层都会抽象为文件的操作,Linux 访问文件的基本逻辑如下:
- Linux 文件系统中,每一个文件都用一个struct inode 结构体来描述,这个结构体里面记录这个文件的所有信息,比如文件类型,访问权限等
- Linux 操作系统中,每个驱动程序在应用层/dev目录下都会有一个设备文件和它对应,称为设备节点,该设备节点存在主设备号和次设备号
- 每打开一次文件,Linux 的 VFS都会分配一个 struct file 结构体来描述打开的文件,该结构体用于维护文件打开的权限,文件指针偏移值,私有的内存信息等
驱动程序的主设备号和次设备号信息保存在struct cdev 结构中
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
字符设备驱动的调用逻辑:
- open 函数打开设备文件,可以根据设备文件对应的struct inode 结构体描述的信息,判断当前的要操作的设备类型,还会分配一个 struct file 结构体
- 根据 struct inode 结构中记录的设备号,可以找到对应的驱动程序,Linux 中每个字符设备都有一个 struct cdev结构体来对应,struct cdev描述了字符设备的所有信息,其中最重要的就是字符设备的操作接口
- 找到struct cdev 结构体后,linux 内核会将struct cdev结构体所在的内存空间首地址记录在 struct inode 结构体的 i_cdev 成员中,将 struct cdev 结构体中记录的函数操作接口记录在 struct file 结构体的 fops 成员中
- 任务完成,VFS会给应用层返回一个文件描述符,这个fd 是和 struct file 结构体想对应的,上层应用程序的调用就可以通过 fd 找到对应的 struct file,由struct file找到操作字符设备接口的函数了
代码路径:/include/linux/fs.h
struct file 类型的实现:
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
u64 f_version;
const struct file_operations *f_op;
...
/* needed for tty driver, and maybe others */
void *private_data;
.....
};
private_data 可以用于保存驱动的私有数据,用于在驱动的各个操作函数之间共享
file 数据结构中包含 inode 数据结构,struct inode *f_inode
struct inode *inode = file_inode(file);
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
.....
dev_t i_rdev; //保存字符设备驱动的 cdev结构
}
其中的 dev_t i_rdev; 包含了设备的主次设备号
主次设备号 是 一个32位的数 12位表示主设备号 20位表示次设备号
/include/linux/fs.h 提供下面操作设备号的函数
// inlude/linux/kdev_t.h
#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))
//从 inode 中获取主次设备号 /include/linux/fs.h
static inline unsigned iminor(const struct inode *inode)
static inline unsigned imajor(const struct inode *inode)
struct 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 *);
......
}
可以看到驱动程序的操作函数中,参数都会带有struct file *,指向内核分配的struct file 类型
编写字符设备驱动
- 实现驱动模块的加载和卸载入口函数
module_init(drm_core_init);
module_exit(drm_core_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxx");
MODULE_DESCRIPTION("A module used for show task of each thread!");
module_init(fbmem_init);
在 linux 中,所有标志为 __init的函数在连接的时候都放在 .init.text 这个区段,此外,所有的__init 函数
在区段 .initcall.init 中还保存了一份函数指针,在初始化内核会通过这些函数指针调用这些 __init 函数,并
在初始化完成之后,释放 init区段,(包括 .init.text .initcall.init )
module_exit(fbmem_exit);
一般以 __exit 标志命名。 主要完成的工作:
- 注销设备
- 对于申请的内存需要动态释放
- 释放硬件资源 终端 DMA通道 I/O 端口 I/O 内存管理
- 开启了硬件一定要关闭
- 申请主设备号
extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
extern int register_chrdev_region(dev_t, unsigned, const char *);
3.手动/自动创建设备节点
设备节点可以手动创建 mknod /dev/hello 250 0
自动创建设备节点:
Linux 系统中还存在 udev,mdev机制,可以遍历 /sys/class/xxx 下的 uevent 文件,根据这些这些uevent文件获取创建设备节点的信息,然后调用 mknod 程序在 /dev 下创建设备节点,结束之后,udev就开始等待内核空间的event
创建设备类的一般流程:
static struct class *cls;
static struct device *test_device;
devno = MKDEV(major,minor);
cls = class_create(THIS_MODULE, "democlass");
if(IS_ERR(cls))
{
unregister_chrdev(major,"hello");
return result;
}
test_device = device_create(cls, NULL, devno, NULL, "hellodevice");
if(IS_ERR(test_device ))
{
class_destroy(cls);
unregister_chrdev(major,"hello");
return result;
}
- 实现 file_operation
- 定义一个结构体 static struct file_operations变量,在其中定义一些设备的打开,关闭,读,写,控制函数
字符设备提供给应用程序的流控制接口有 open,close,read,write,ioctl;添加一个字符设备驱动程序的过程,实际上就是给上述操作添加代码的过程,Linux 对这些设备操作统一做了抽象
struct file_operations s3c_rotator_fops = {
.owner = THIS_MODULE,
.open = dev_fifo_open,
.release = xxxx,
.mmap = xxx,
.ioctl = xxxx,
.poll = xxxx,
};