目录
- 1、文件系统与设备驱动
- 2、设备文件
- 2.1 linux的文件种类:
- 2.2 设备分类
- 3、 设备号
- 3.1 dev_t类型
- 3.2 与设备号相关的操作介绍
- 3.2.1 宏 MKDEV
- 3.2.2 宏 MAJOR
- 3.2.3 宏 MINOR
- 3.2.4 命令mknod
- 3.2.5 register_chrdev_region()
- 3.2.6 alloc_chrdev_region()
- 3.2.7 unregister_chrdev_region()
- 4、cdev结构体与file_operations结构体
- 4.1 struct cdev与struct file_operations
- 4.2 cdev操作函数
- 4.2.1 cdev_init()函数
- 4.2.2 cdev_alloc()
- 4.2.3 cdev_put()
- 4.3.4 cdev_add()
- 4.2.5 cdev_del()
- 5、字符设备驱动模板
- 5.1 字符设备驱动简单模板
- 5.2 编译上述模块所要用到的Makefile文件
- 6、struct inode 及 struct file
- 6.1 struct inode
- 6.2 struct file
- 7、字符设备驱动框架的总结
- 7.1 5个重点数据类型
- 7.2 框架的工作机制
设备驱动的学习,重在对框架的理解。因此以下会在涉及框架的重点部分用黄色块标注。
1、文件系统与设备驱动
Linux一切皆文件,通过VFS虚拟文件系统,把所有外设的细节都隐藏起来,最后都以文件的形态呈现于应用层。这种方式完美的统一了对用户的接口,极大方便了应用层的调用方式。
应用程序与VFS之间的接口是系统调用。VFS向下与具体的文件系统或设备文件之间的接口是file_operations结构体成员函数。file_operations是一个内核的结构体,其成员函数为真正实现对文件的打开、关闭、读写、控制的一系列成员函数。
对字符设备而言,file_operations成员函数就直接是设备驱动,在函数内完成对字符设备的读写等操作。这部分由驱动工程师直接编写。
对块设备有两种访问方法:
- 不通过文件系统直接访问块设备,这是通过内核已实现的file_operations类型的变量def_blk_fops。典型的应用就是dd 这个命令。
- 另一方法是通过文件系统来访问块设备,file_operations的实现位于文件系统内。
因此,以下是针对字符驱动的框架说明重点一:
从上图可以看出,当应用层面要使用某个字符设备时(比如LED灯),只需要通过统一的系统函数open()、read()、write()、close()等函数来操作与该设备所对应的字符设备文件(比如/dev/led)即可。
而从内核层面,实际是一一对应的执行了xxx_open()、xxx_read()、xxx_write)、xxx_close()等函数来具体的操作硬件设备。因此驱动程序的开发本质就是完成xxx_open()等内核驱动程序。
2、设备文件
2.1 linux的文件种类:
- -:普通文件
- d:目录文件
- p:管道文件
- s:本地socket文件
- l:链接文件
- c:字符设备
- b:块设备
2.2 设备分类
Linux内核按驱动程序实现模型框架的不同,将设备分为三类:
- 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
- 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率
- 网络设备:针对网络数据收发的设备
3、 设备号
3.1 dev_t类型
内核用设备号来区分不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:
- 主设备号:占高12位,用来表示驱动程序相同的一类设备
- 次设备号:占低20位,用来表示被操作的哪个具体设备
应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
Linux把设备文件统一放在/dev目录内。用 ls -l命令可以看到如下的显示:
在日期的前面,图上的红框部分,就是主设备号与次设备号。前面的主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,同一驱动可以支持多个同类设备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。
设备号,是设备在操作层面的一个重要身份识别号,内核层面的操作都是以设备号来识别,定位,管理设备
3.2 与设备号相关的操作介绍
3.2.1 宏 MKDEV
项目 | 说明 |
---|---|
语法 | MKDEV(int major , int minor) |
功能 | 将主设备号和次设备号转换成dev_t类型 |
头文件 | <linux/kdev_t.h> |
参数 | major为主设备号,minor为次设备号 |
宏定义 | #define MKDEV(major,minor)(((major)<<MINORBITS) |
返回值 | 成功执行返回dev_t类型的设备编号 |
3.2.2 宏 MAJOR
项目 | 说明 |
---|---|
语法 | MAJOR(dev_t dev) |
功能 | 从内核设备号中取得主设备号 |
头文件 | <linux/kdev_t.h> |
参数 | 设备号 |
宏定义 | #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) |
返回值 | 成功执行返回unsigned int 类型的主设备编号 |
3.2.3 宏 MINOR
项目 | 说明 |
---|---|
语法 | MINOR(dev_t dev) |
功能 | 从内核设备号中取得次设备号 |
头文件 | <linux/kdev_t.h> |
参数 | 设备号 |
宏定义 | #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) |
返回值 | 成功执行返回unsigned int 类型的次设备编号 |
3.2.4 命令mknod
项目 | 说明 |
---|---|
命令 | mknod name c或b 主设备号 次设备号 |
功能 | 创建字符设备文件和块设备文件 |
参数 | 【name:要创建的设备名】【c: 表示创建的是字符设备】【 b:表示创建的是块设备】【 主设备号:创建设备的主设备号】【次设备号:创建设备次设备号】 |
举例 | sudo mknod /dev/mydevice c 20 0 |
3.2.5 register_chrdev_region()
#include <linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name)
该函数用于向系统申请已知的设备号,函数的原型如下所示:
参数:
from:所需设备编号范围内的第一个,必须包括主设备号
count:所需要的连续设备编号数量
name:设备或驱动程序的名称
返回值:
成功:返回0
失败:返回负的错误号
3.2.6 alloc_chrdev_region()
#include <linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
该函数用于向系统动态申请未被占用的设备号,能自动避开设备号重复的冲突,函数的原型如下:
参数:
dev:传出参数,传出第一个分配的设备号
baseminor:所需要的第一个次设备号
count:需要的次设备号的数量
name:设备或驱动的名称
返回值:
成功:返回0
失败:返回负的错误号
3.2.7 unregister_chrdev_region()
#include <linux/fs.h>
void unregister_chrdev_region(dev_t from, unsigned count)
该函数用于释放掉原先申请的设备号,函数的原型如下所示:
参数:
from:需要释放的设备号范围的第一个
count:需要释放的设备号的数量
返回值:
无
4、cdev结构体与file_operations结构体
设备号是设备的身份标识,那么cdev结构体就是字符设备本体了。开发者需要主动构造cdev结构体去描述一个设备。file_operations结构体则相当于行为能力的集合,用于具体操作设备之用。
4.1 struct cdev与struct file_operations
#include <linux/cdev.h>
struct cdev
{
struct kobject kobj; //相当于父类,表示该类型实体是一种内核对象
struct module *owner; //填THIS_MODULE,表示该字符设备从属于哪个内核模块
const struct file_operations *ops; //指向空间存放着针对该设备的各种操作函数地址
struct list_head list; //链表指针域,各个cdev通过该链表指针串起来
dev_t dev; //设备号
unsigned int count; //设备数量
};
在内核里,每一个设备都有一个对应的cdev结构体,所有的结构体在内核中以链表的形式串接起来,结构体中的 struct list_head list成员就是链表的指针域,在初始化时,由系统完成链表的挂接。
cdev结构体代表了设备本体,需要开发者在驱动模块初始化阶段手动去构造,并通过操作函数去初始化和挂接到链表。
#include <linux/fs.h>
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 (*iterate) (struct file *, struct dir_context *);
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 *, loff_t, loff_t, 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 (*show_fdinfo)(struct seq_file *m, struct file *f);
};
常用成员指针函数简单介绍:
llseek():用来修改一个文件的当前读写位置,并将新的位置进行返回,如果出错,则函数返回一个负值;
read():用来从设备中读取数据,成功时返回读取到的字节数,出错时,则函数返回一个负值;
write():用于向设备发送数据,成功时返回写入的字节数,若该函数未实行时,用户进行函数调用,将得到-EINVAL返回值;
unlocked_ioctl():提供设备相关的控制命令的实现(不是读和写操作),调用成功时,返回给调用程序一个非负值,与应用程序调用fcntl和ioctl函数相对应;
mmap():函数将设备内存映射到进程的虚拟地址空间中,当设备驱动未实现此函数时,用户进行调用将会得到-ENODEV返回值;
open():用于打开驱动设备,若驱动程序中不实现此函数,则设备的打开操作永远成功;
release():与open相反,用于关闭设备;
poll():一般用于询问设备是否可被非阻塞地立即读写;
aio_read():对文件描述符对应的设备进行异步读操作;
aio_write():对文件描述符对应的设备进行异步写操作。
file_operations结构体如上所示,其成员几乎全部是函数指针,这些函数指针所指向的操作函数需要由开发者编写,完成直接操作设备的能力。而这些能力又是与系统调用接口open()、read()、write()、close()等一一对应的。
成员 struct module *owner; 填THIS_MODULE,表示该结构体对象从属于哪个内核模块
file_openations结构体需要开发者在驱动模块的初始化环节完成手工构建
4.2 cdev操作函数
#include <linux/cdev.h>
4.2.1 cdev_init()函数
void cdev_init(struct cdev *cdev , const struct file_operations *fops);
cdev_init()的作用用来初始化一个cdev结构体,函数的代码如下所示:
参数:
cdev:要初始化化的cdev结构体
fops:设备的file_operations结构体
返回值:无
4.2.2 cdev_alloc()
struct cdev *cdev_alloc(void);
cdev_alloc()的作用是用来动态分配一个cdev结构体,函数的代码如下所示:
参数: 无
返回值:
成功:返回cdev结构体的指针
失败:返回NULL
4.2.3 cdev_put()
cdev_put()函数的作用用来释放cdev,函数的代码如下所示:
参数:
p:cdev结构体指针
返回值:
无
4.3.4 cdev_add()
int cdev_add(struct cdev *p , dev_t dev , unsigned int count);
cdev_add()函数用于向系统添加一个cdev,完成字符设备的注册,函数的代码如下所示:
参数:
p:字符设备的cdev结构体指针
dev:此设备负责的第一个设备号
count:与此对应的次设备号的数量
返回值:
成功:返回0
失败:返回error号
4.2.5 cdev_del()
void cdev_del(struct cdev *);
cdev_del()向系统删除一个cdev,用于完成字符设备的注销,函数的代码如下所示:
参数:
p:要在系统中移除的cdev结构体指针
返回值: 无
5、字符设备驱动模板
5.1 字符设备驱动简单模板
到了这一步,了解了上面相关的内容后,可以直接写出字符设备驱动程序了。这里直接给出一个模板,供编程时直接拷贝使用。
/*************************************************************************
> File Name: arch-char.c
> 作用:字符设备驱动简单模板
************************************************************************/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
/*1、定义重要的变量及结构体*/
static dev_t devno; //设备号变量
int major, minor; //主设备号,次设备号变量
struct cdev my_dev; //cdev设备描述结构体变量
int my_open(struct inode *pnode , struct file *pf); //函数声明
int my_close(struct inode *pnode , struct file *pf); //函数声明
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
.open = my_open,
.release = my_close,
};
static int __init my_init(void){
int unsucc =0;
/*2、创建 devno */
unsucc = alloc_chrdev_region(&devno , 0 , 1 , "arch-char");
if (unsucc){
printk(" creating devno faild\n");
return -1;
}
major = MAJOR(devno);
minor = MINOR(devno);
/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
cdev_init(&my_dev , &fops);
my_dev.owner = THIS_MODULE;
/*4、注册cdev结构体到内核链表中*/
unsucc = cdev_add(&my_dev,devno,1);
if (unsucc){
printk("cdev add aild \n");
return 1;
}
printk("the driver arch-char initalization completed\n");
return 0;
}
static void __exit my_exit(void)
{
cdev_del(&my_dev);
unregister_chrdev_region(devno , 1);
printk("***************the driver arch-char exit************\n");
}
/*5、具体操作硬件的函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int my_open(struct inode *pnode , struct file *pf){
printk("arch-char is opened\n");
return 0;
}
/*file_operations结构全成员函数.release的具体实现*/
int my_close(struct inode *pnode , struct file *pf){
printk("arch-char is closed \n");
return 0;
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
上方为字符设备驱动程序的简单模板。里面除了内核模块原有的基础程式化框架以外,针对字符设备驱动又增加了5个步骤。而之所以叫模板,因为内部的结构已模式化了,开发者在这模板基础上只需要把注意力放在file_operations的成员函数的具体实现上即可。也就是那5个步骤里,真正需要开发者关心的是第5步,其它基本可以照抄,除非你要改变变量名称。
模块中,的my_open()函数是与应用层内核调用open()函数一一对应的,也即内核调用open()函数后,到了内核底层实际是调用了my_open()函数完成对设备的操作。而如何操作设备,完成这个open的行为,这就因不同设备而异。这在后面的例子中会继续详细说明。
5.2 编译上述模块所要用到的Makefile文件
- Makefile文件容
ROOTFS_DIR = /opt/4412/rootfs
ifeq ($(KERNELRELEASE), )
KERNEL_DIR := /home/mao/linux/linux-3.14
CUR_DIR := $(shell pwd)
all :
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
clean :
make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
install:
cp -raf *.ko $(ROOTFS_DIR)/drv
else
obj-m += arch-char.o
endif
这个Makefile文件适用于驱动源码与linux内核源码不在同一个目录结构下的情况。比如,这里的linux内核源码在/home/mao/linux/inux-3.14目录内。而驱动代码和在/home/mao/driver/5-arch/arch-char.c。Makefile文件也与arch-char.c在相同的目录中,编译完成后,会形成archchar.ko文件,这个就是内核模块了。
在Makefile 中 obj-m用来指定模块名,注意模块名加.o而不是.ko
可以用 模块名-objs 变量来指定编译到ko中的所有.o文件名(每个同名的.c文件对应的.o目标文件)
一个目录下的Makefile可以编译多个模块,则每个模块都用一条如下的指令就行。
添加:obj-m += 下一个模块名.o
- Makefile工作顺序解读
KERNELRELEASE是在Linux内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE当然还没有被定义,所以顺序执行下面的各个目标语句。 如果make的目标是clean,直接执行clean操作,然后结束。
当make的目标为all时,-C ( K E R N E L D I R ) 指明跳转到 " l i n u x 内核源码目录 " 下读取那里的 M a k e f i l e ; M = (KERNEL_DIR)指明跳转到"linux内核源码目录"下读取那里的Makefile;M= (KERNELDIR)指明跳转到"linux内核源码目录"下读取那里的Makefile;M=(PWD) 表明然后返回到当前目录(存放驱动代码的目录)继续读入、执行当前的Makefile。
当从内核源码目录返回时,KERNELRELEASE已被定义(因为刚才系统-C $(ERNELDIR)就是执行了内核源码的makefile),make将继续读取else之后的内容,obj-m += arch-char.o表示编译连接后将生成arch-char.ko模块。
6、struct inode 及 struct file
这里需要继续解释的是结构体struct inode 以及 struct file。
6.1 struct inode
在<linux/fs.h>头文件里定义的struct inode 结构体如下,其与磁盘上的i-node相对应。当应用层用open()访问一个文件时,会在内核中为i-node创建一个副本,主要记录如下内容。这是管理一个文件的基本要素。其中与字符驱动密切相关的是i_rdev成员,存放了设备文件对应的设备号。i_cdev则是存放着字符设备对应的cdev结构体指针,该结构体由驱动程序模块建立的。
6.2 struct file
在<linux/fs.h>头文件中定义的struct file 结构体如下,和inode一样,其在文件被open()时,由系统创建。并获得由字符驱动模块构建的实际操作文件的函数入口file_operations结构体的指针,将指针存于f_op成员内。
在应用层,每个进程都会有一个文件描述符表,这个表内会存放进程打开的每一个文件的所谓文件描述符fd。实质,文件描述符表是一个数组,而fd则是这个数组元素的下标,也就是,如果fd = 0,则‘0’是指的该数组的第0个元素。fd=10,指的是该数组的第10个元素,该元素所存的内容是每个对应文件的struct file结构体指针,该结构体又存有文件的inode 与file_operations 结构体指针。
这就达到一个目的,当应用的任何一个操作设备文件的指令,如read(fd) , write(fd)等,都可以通过文件描述符表数组的fd下标对应的元素找到内核 的file_operations结构体指针,这样就可以调用该结构体内对应.read和.write的成员函数指针,从而完成实质的对字符设备的读,写操作。
7、字符设备驱动框架的总结
7.1 5个重点数据类型
与框架相关的5个重点数据类型如下:
变量 | 用途 | 说明 |
---|---|---|
dev_t devno | 设备号,标识设备的身份标号,可以解析出主设备号与次设备号 | 创建于驱动加载之时,也存于cdev结构体内 |
struct cdev | 表示设备的结构体,将设备号与操作函数结构体file_operations关联起来 | 创建于驱动加载之时,加载到内核的cdev链表中 |
struct file_operations | 驱动实际操作的函数入口,是具体的设备驱动函数集合的入口 | 创建于驱动加载之时,会被其它结构体引用 |
struct inode | 集中了设备文件的相关属性,内部存有devno及cdev结构体指针 | 创建于设备文件被open之时,应用层打开文件时,通过inode去寻找cdev |
struct file | 保存了文件操作的状态等属性,关键是存储了file_operations结构体指针 | 文件状态等控制信息,用于向底层操作函数传递这些状态信息 |
5个数据类型,devno , struct cdev , struct file_operations是在驱动程序加载时创建的。与设备驱动是一一对应的。一个设备驱动对应一套这些数据对象。
struct inode 与 struct file 是在应用层open()设备文件时,创建的。数据的关联关系如下:
驱动加载时:生成并注册devno,创建struct cdev , 创建file_operations 。并在cdev中把devno与file_operations关联起来。
应用层open(设备文件名):创建struct inode 关联了devno与struct cdev , 创建了struct file ,关联了file_operations,生成了fd关联了格struct file
应用层 read(fd):通过fd 找到struct file ,通过struct file关联file_operations找到了硬件操作函数指针.read()函数。完成硬件操作。
7.2 框架的工作机制
- 图:字符设备驱动框架图
- 图的解释:
左侧,为驱动加载时完成的工作。加载后,建立了struct cdev 以及struct file_operations 。cdev会挂在内核 的cdev链表里,等着被使用。
右侧,为用户用了 open()函数后的动作过程,会建立空struct inode和空struct file。然后,读出cdev后,把cdev中的file_operations写入struct file中。
这里,每个驱动有一个cdev,每个设备有一个inode ,每次open会产生一个fd文件描述符以及对应的struct file。
最后,每个应用层里调用read() close() ioctl()等就可以对应有如下动作 (以read举例): fd -> struct file -> file_operations.xxx_read()