【嵌入式环境下linux内核及驱动学习笔记-(3-字符设备驱动详解)】

目录

  • 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文件
      • 5.2.1 Makefile文件
      • 5.2.2 Makefile工作顺序解读
  • 6、struct inode 及 struct file
    • 6.1 struct inode
    • 6.2 struct file
  • 7、字符设备驱动框架的总结
    • 7.1 5个重点数据类型
    • 7.2 框架的工作机制

设备驱动的学习,重在对框架的理解。因此以下会在涉及框架的重点部分用黄色块标注。

1、文件系统与设备驱动

【嵌入式环境下linux内核及驱动学习笔记-(3-字符设备驱动详解)】_第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的文件种类:

  1. -:普通文件
  2. d:目录文件
  3. p:管道文件
  4. s:本地socket文件
  5. l:链接文件
  6. c:字符设备
  7. b:块设备

2.2 设备分类

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率
  3. 网络设备:针对网络数据收发的设备

3、 设备号

3.1 dev_t类型

内核用设备号来区分不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:

  1. 主设备号:占高12位,用来表示驱动程序相同的一类设备
  2. 次设备号:占低20位,用来表示被操作的哪个具体设备

应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。

Linux把设备文件统一放在/dev目录内。用 ls -l命令可以看到如下的显示:
【嵌入式环境下linux内核及驱动学习笔记-(3-字符设备驱动详解)】_第2张图片
在日期的前面,图上的红框部分,就是主设备号与次设备号。前面的主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,同一驱动可以支持多个同类设备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。

设备号,是设备在操作层面的一个重要身份识别号,内核层面的操作都是以设备号来识别,定位,管理设备

3.2 与设备号相关的操作介绍

3.2.1 宏 MKDEV

项目 说明
语法 MKDEV(int major , int minor)
功能 将主设备号和次设备号转换成dev_t类型
头文件
参数 major为主设备号,minor为次设备号
宏定义 #define MKDEV(major,minor)(((major)<
返回值 成功执行返回dev_t类型的设备编号

3.2.2 宏 MAJOR

项目 说明
语法 MAJOR(dev_t dev)
功能 从内核设备号中取得主设备号
头文件
参数 设备号
宏定义 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
返回值 成功执行返回unsigned int 类型的主设备编号

3.2.3 宏 MINOR

项目 说明
语法 MINOR(dev_t dev)
功能 从内核设备号中取得次设备号
头文件
参数 设备号
宏定义 #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 
int register_chrdev_region(dev_t from, unsigned count, const char *name)

该函数用于向系统申请已知的设备号,函数的原型如下所示:

参数:

     from:所需设备编号范围内的第一个,必须包括主设备号

     count:所需要的连续设备编号数量

     name:设备或驱动程序的名称

返回值:

     成功:返回0

     失败:返回负的错误号

3.2.6 alloc_chrdev_region()

#include 
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 
void unregister_chrdev_region(dev_t from, unsigned count)

该函数用于释放掉原先申请的设备号,函数的原型如下所示:

参数:

     from:需要释放的设备号范围的第一个

     count:需要释放的设备号的数量

返回值:

4、cdev结构体与file_operations结构体

设备号是设备的身份标识,那么cdev结构体就是字符设备本体了。开发者需要主动构造cdev结构体去描述一个设备。file_operations结构体则相当于行为能力的集合,用于具体操作设备之用。

补充:
\qquad 实际上,驱动框架实际都是面向对象思维的。任何一种框架都会有一个代表设备的数据结构对象。字符设备驱动是struct cdev代表设备对象;platform虚拟总线框架是struct platform_device来代表设备对象;i2c总线框架是struct i2c_client代表设备对象;input框架则是struct input_dev代表设备对象。

4.1 struct cdev与struct file_operations

cdev原型:

	#include  

	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结构体代表了设备本体,需要开发者在驱动模块初始化阶段手动去构造,并通过操作函数去初始化和挂接到链表。

file_operations原型

	#include 
	
	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相关操作函数

关联头函数
【嵌入式环境下linux内核及驱动学习笔记-(3-字符设备驱动详解)】_第3张图片

4.2.1 cdev_init()函数

#include 
void cdev_init(struct cdev *cdev , const struct file_operations *fops);

cdev_init()的作用用来初始化一个cdev结构体,函数的代码如下所示:

参数:
cdev:要初始化化的cdev结构体
fops:设备的file_operations结构体

返回值:

4.2.2 cdev_alloc()

#include 
struct cdev *cdev_alloc(void);

cdev_alloc()的作用是用来动态分配一个cdev结构体,函数的代码如下所示:

参数:

返回值:

成功:返回cdev结构体的指针
失败:返回NULL

4.2.3 cdev_put()

在Linux内核中,cdev_put()函数用于释放字符设备(char device)对象。其定义如下:

#include 

void cdev_put(struct cdev *p)

cdev_put()函数的作用用来释放cdev,函数的代码如下所示:

参数:
p:cdev结构体指针

返回值:

在一个字符设备的生命周期中,其对象的引用计数会根据以下情况增加和减少:

  • 调用cdev_init()初始化字符设备对象时,其引用计数为1。
  • 调用cdev_add()将字符设备添加到系统中时,其引用计数加1。
  • 打开该字符设备对应节点的进程数加到其引用计数。
  • 关闭字符设备节点或调用cdev_del()将其从系统中删除时,其引用计数减1。
  • 调用cdev_put() 减少其引用计数。

所以,cdev_put()函数主要用于手动释放对字符设备对象的引用,一般在不再需要使用该字符设备对象时调用,比如在模块退出时Force卸载该模块注册的字符设备。

4.3.4 cdev_add()

#include 

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()

#include 
·
void cdev_del(struct cdev *);

cdev_del()向系统删除一个cdev,用于完成字符设备的注销,函数的代码如下所示:
参数:
p:要在系统中移除的cdev结构体指针
返回值:

5、字符设备驱动模板

5.1 字符设备驱动简单模板

到了这一步,了解了上面相关的内容后,可以直接写出字符设备驱动程序了。这里直接给出一个模板,供编程时直接拷贝使用。

/*************************************************************************
	> File Name: arch-char.c
	> 作用:字符设备驱动简单模板
 ************************************************************************/

#include 
#include 
#include 
#include 
/*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");

\qquad 上方为字符设备驱动程序的简单模板。里面除了内核模块原有的基础程式化框架以外,针对字符设备驱动又增加了5个步骤。而之所以叫模板,因为内部的结构已模式化了,开发者在这模板基础上只需要把注意力放在file_operations的成员函数的具体实现上即可。也就是那5个步骤里,真正需要开发者关心的是第5步,其它基本可以照抄,除非你要改变变量名称。

\qquad 模块中,的my_open()函数是与应用层内核调用open()函数一一对应的,也即内核调用open()函数后,到了内核底层实际是调用了my_open()函数完成对设备的操作。而如何操作设备,完成这个open的行为,这就因不同设备而异。这在后面的例子中会继续详细说明。

5.2 编译上述模块所要用到的Makefile文件

5.2.1 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

\qquad 这个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

5.2.2 Makefile工作顺序解读

\qquad KERNELRELEASE是在Linux内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE当然还没有被定义,所以顺序执行下面的各个目标语句。 如果make的目标是clean,直接执行clean操作,然后结束。

\qquad 当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内核源码目录"下读取那里的MakefileM=(PWD) 表明然后返回到当前目录(存放驱动代码的目录)继续读入、执行当前的Makefile。

\qquad 当从内核源码目录返回时,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

\qquad 头文件里定义的struct inode结构体如下,其与磁盘上的i-node相对应。当应用层用open()访问一个文件时,会在内核中为i-node创建一个副本,主要记录如下内容。这是管理一个文件的基本要素。其中与字符驱动密切相关的是i_rdev成员,存放了设备文件对应的设备号。i_cdev则是存放着字符设备对应的cdev结构体指针,该结构体由驱动程序模块建立的。

6.2 struct file

\qquad 头文件中定义的struct file结构体如下,和inode一样,其在文件被open()时,由系统创建。并获得由字符驱动模块构建的实际操作文件的函数入口file_operations结构体的指针,将指针存于f_op成员内。

\qquad 在应用层,每个进程都会有一个文件描述符表,这个表内会存放进程打开的每一个文件的所谓文件描述符fd。实质,文件描述符表是一个数组,而fd则是这个数组元素的下标,也就是,如果fd = 0,则‘0’是指的该数组的第0个元素。fd=10,指的是该数组的第10个元素,该元素所存的内容是每个对应文件的struct file结构体指针,该结构体又存有文件的inodefile_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结构体指针 文件状态等控制信息,用于向底层操作函数传递这些状态信息

\qquad 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,和 struct inode,生成了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()

你可能感兴趣的:(Linux内核与驱动,linux,内核与驱动,字符设备)