一、Linux device driver 的概念 系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能: 1、对设备初始化和释放; 2、把数据从内核传送到硬件和从硬件读取数据; 3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据; 4、检测和处理设备出现的错误。 在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。 已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。 最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。 二、实例剖析 我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。 由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢这需要了解一个非常关键的数据结构: STruct file_operatiONs { int (*seek) (struct inode * ,struct file *, off_t ,int)int (*read) (struct inode * ,struct file *, char ,int)int (*write) (struct inode * ,struct file *, off_t ,int)int (*readdir) (struct inode * ,struct file *, struct dirent * ,int)int (*select) (struct inode * ,struct file *, int ,select_table *)int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long)int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *)int (*open) (struct inode * ,struct file *)int (*release) (struct inode * ,struct file *)int (*fsync) (struct inode * ,struct file *)int (*fasync) (struct inode * ,struct file *,int)int (*check_media_change) (struct inode * ,struct file *)int (*revalidate) (dev_t dev)} 这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。 下面就开始写子程序。 #include 基本的类型定义 #include 文件系统使用相关的头文件 #include #include #include unsigned int test_major = 0static int read_test(struct inode *inode,struct file *file,char *buf,int count) { int left用户空间和内核空间 if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT ) return -EFAULTfor(left = count left >0 left--) { __put_user(1,buf,1)buf++} return count} 这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。 static int write_test(struct inode *inode,struct file *file,const char *buf,int count) { return count} static int open_test(struct inode *inode,struct file *file ) { MOD_INC_USE_COUNT模块计数加以,表示当前内核有个设备加载内核当中去 return 0} static void release_test(struct inode *inode,struct file *file ) { MOD_DEC_USE_COUNT} 这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。 struct file_operations test_fops = { read_test, write_test, open_test, release_test, }设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。 int init_module(void) { int resultresult = register_chrdev(0, "test", &test_fops)对设备操作的整个接口 if (result <0) { printk(KERN_INFO "test: can't get major numbern")return result} if (test_major == 0) test_major = result return 0} 在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。 如果登记成功,返回设备的主设备号,不成功,返回一个负值。 void cleanup_module(void) { unregister_chrdev(test_major,"test")} 在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。 一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。 下面编译 : $ gcc -O2 -DMODULE -D__KERNEL__ -c test.c –c表示输出制定名,自动生成.o文件 得到文件test.o就是一个设备驱动程序。 如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后 ld -r file1.o file2.o -o modulename。 驱动程序已经编译好了,现在把它安装到系统中去。 $ insmod –f test.o 如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 : $ rmmod test 下一步要创建设备文件。 mknod /dev/test c major minor c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。 用shell命令 $ cat /proc/devices 就可以获得主设备号,可以把上面的命令行加入你的shell script中去。 minor是从设备号,设置成0就可以了。 我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。 #include #include #include #include main() { int testdevint ichar buf[10]testdev = open("/dev/test",O_RDWR)if ( testdev == -1 ) { printf("Cann't open file n")exit(0)} read(testdev,buf,10)for (i = 0i <10i++) printf("%dn",buf[i])close(testdev)} 编译运行,看看是不是打印出全1 以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。上述给出了一个简单的字符设备驱动编写的框架和原理,更为复杂的编写需要去认真研究LINUX内核的运行机制和具体的设备运行的机制等等。希望大家好好掌握LINUX设备驱动程序编写的方法。
linux查看包含的头文件源文件
继续看docker的devicemapper(一种存储驱动,而Device Mapper是内核的一种块设备映射框架),docker默认使用的是loop-lvm,初始化时创建了两个比较大的稀疏文件(100GB的data和2GB的metadata,实际使用多少占用多少),并使用losetup命令将这两个文件"attach/mount"为两个loop设备(把文件映射为两个伪设备,虽然是伪设备,但是可以像普通块设备一样,对其进行格式化想要的文件系统,比如xfs,奇幻吧,loop这个词就是这样来的),然后基于这两个loop伪设备创建100GB大小的thin-pool(又映射了一层,这个时候用到了linux内核提供的dm机制,可以使用dmsetup info命令查看),基于这个thin-pool,每启动一个容器就从100GB里面分出来10GB空间给该容器,擦,整个来龙去脉终于搞懂了。
loop-lvm为默认,安装方便,只用于测试环境(使用docker info可以看到警告,不能用于生产环境,笑,我们生产环境就是用的默认的,不过一个vm只有一个容器,可能没有暴露问题,但是性能还是差一些的),基于稀疏文件不仅读写性能差,也没有直接使用"块设备"稳定,于是devicemapper驱动程序还有另外一种方式,direct-lvm,准确说,这个才是真正的lvm,配置方式和配置lvm的方式一样的(比较繁琐和麻烦),当然docker也提供了工具。另外,还看了docker的另外一个存储driver,overlay(最新的为overlay2,它也是分层的,不过只有两层,镜像层和容器层,aufs是很多层,devicemapper没有分层的概念,而是快照,不过快照好像也是分层的,前两者是基于文件系统的,后者也是基于块设备),可以通
Docker 的联合文件系统
linux下的内核头文件放在/usr/include/linux/和/usr/include/sys/linux目录树:scripts目录该目录中不包含任何核心代码,该目录下存放了用来配置内核的脚本和应用程序源码。lib目录该目录主要包含两部分内容:gnuzip解压缩算法,用于在系统启动过程中将压缩的内核镜像解压缩;剩余的文件用于实现一个C库的子集,主要包括字符串和内存操作等相关函数。mm目录该目录包含了体系结构无关的内存管理代码,包括通用的分页模型的框架、伙伴算法的实现和对象缓冲器slab的实现代码。include目录这个目录包含了Linux源代码目录树中绝大部分头文件,每个体系架构都在该目录下对应一个子目录,该子目录中包含了给定体系结构所必需的宏定义和内联函数。init目录该目录中存放的是系统核心初始化代码,内核初始化入口函数start_kernel就是在该目录中的文件main.c内实现的。kernel目录该目录中存放的是Linux内核的最核心的代码,用于实现系统的核心模块,这些模块包括:进程管理、进程调度器、中断处理、系统时钟管理、同步机制等。arch目录该目录中的每个子目录中都与某种体系结构相对应,用于存放体系结构相关代码,向平台无关的系统核心模块提供所需的功能接口。每个体系结构对应的子目录下通常至少包含以下几个子目录:kernel子目录、lib子目录、mm子目录、boot子目录。系统次核心组件包括:block目录:用于实现块设备的基本框架和块设备的I/O调度算法。usr目录:该目录中的代码为内核尚未完全启动时执行用户空间代码提供了支持ipc目录:该目录中的文件用于实现SystemV的进程间通信模块。driver目录:用于存放各类设备的驱动程序。sound目录:存放了声音系统架构,如OpenSoundSystem(OSS)、AdvancedLinuxSoundArchitecture(ALSA)的相关代码和具体声卡的设备驱动程序。security目录:存放了Security-EnhancedLinux(SELinux)安全框架的实现代码crypto目录:该目录中存放了相关的加密算法的代码。documentation目录:存放了与内核相关的文档。net和fs目录:包含linux内核支持的众多网络协议和文件系统。
解释一下linux驱动程序结构框架及工作原理
Devicemapper 是 Linux 内核提供的框架,从 Linux 内核 2.6.9 版本开始引入,Devicemapper 与 AUFS 不同,AUFS 是一种文件系统,而Devicemapper 是一种映射块设备的技术框架。
Devicemapper 提供了一种将物理块设备映射到虚拟块设备的机制,目前 Linux 下比较流行的 LVM (Logical Volume Manager 是 Linux 下对磁盘分区进行管理的一种机制)和软件磁盘阵列(将多个较小的磁盘整合成为一个较大的磁盘设备用于扩大磁盘存储和提供数据可用性)都是基于 Devicemapper 机制实现的。
Devicemapper是如何数据存储的?
当 Docker 使用 Devicemapper 作为文件存储驱动时,Docker 将镜像和容器的文件存储在瘦供给池(thinpool)中,并将这些内容挂载在 /var/lib/docker/devicemapper/` 目录下。
这些目录储存 Docker 的容器和镜像相关数据,目录的数据内容和功能说明如下:
Docker 的 Devicemapper 模式有两种:第一种是 loop-lvm 模式,该模式主要用来开发和测试使用;第二种是 direct-lvm 模式,该模式推荐在生产环境中使用。
默认使用 loop-lvm 模式。
1.使用以下命令停止已经运行的 Docker:
2.编辑 /etc/docker/daemon.json 文件,如果该文件不存在,则创建该文件,并添加以下配置:
其中 directlvm_device 指定需要用作 Docker 存储的磁盘路径,Docker 会动态为我们创建对应的存储池。例如这里我想把 /dev/xdf 设备作为我的 Docker 存储盘,directlvm_device 则配置为 /dev/xdf。
3.启动 Docker:
4.验证 Docker 的文件驱动模式:
使用 docker info 查看,当看到 Storage Driver 为 devicemapper,并且 Pool Name 为 docker-thinpool 时,这表示 Devicemapper 的 direct-lvm 模式已经配置成功。
overlay2 是目前Docker 官方推荐的文件系统,也是目前安装 Docker 时默认的文件系统。
overlay2 和 AUFS 类似,它将所有目录称之为层(layer),overlay2 的目录是镜像和容器分层的基础,而把这些层统一展现到同一的目录下的过程称为联合挂载(union mount)。overlay2 把目录的下一层叫作lowerdir,上一层叫作upperdir,联合挂载后的结果叫作merged。
l目录包含缩短的层标识符作为符号链接。这些标识符用于避免达到mount命令参数的长度限制。
镜像层的 link 文件内容为该镜像层的短 ID,diff 文件夹为该镜像层的改动内容,lower 文件为该层的所有父层镜像的短 ID。diff 目录为容器的读写层,容器内修改的文件都会在 diff 中出现,merged 目录为分层文件联合挂载后的结果,也是容器内的工作目录。
通过 docker image inspect 命令来查看某个镜像的层级关系
使用 docker inspect 命令来查看容器的工作目录:
overlay2 的工作过程中对文件的操作分为读取文件和修改文件。
容器内进程读取文件分为以下三种情况。
overlay2 对文件的修改采用的是写时复制的工作机制,这种工作机制可以最大程度节省存储空间。具体的文件操作机制如下。
如何清理docker呢?由于版本较古老,没有 prune 命令。使用以下脚本来清理:
一、Linux device driver 的概念
系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:
1、对设备初始化和释放;
2、把数据从内核传送到硬件和从硬件读取数据;
3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;
4、检测和处理设备出现的错误。
在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
二、实例剖析
我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。
由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:
STruct file_operatiONs {
int (*seek) (struct inode * ,struct file *, off_t ,int)
int (*read) (struct inode * ,struct file *, char ,int)
int (*write) (struct inode * ,struct file *, off_t ,int)
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int)
int (*select) (struct inode * ,struct file *, int ,select_table *)
int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long)
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *)
int (*open) (struct inode * ,struct file *)
int (*release) (struct inode * ,struct file *)
int (*fsync) (struct inode * ,struct file *)
int (*fasync) (struct inode * ,struct file *,int)
int (*check_media_change) (struct inode * ,struct file *)
int (*revalidate) (dev_t dev)
}
这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
下面就开始写子程序。
#include <linux/types.h>基本的类型定义
#include <linux/fs.h>文件系统使用相关的头文件
#include <linux/mm.h>
#include <linux/errno.h>
#include <asm/segment.h>
unsigned int test_major = 0
static int read_test(struct inode *inode,struct file *file,char *buf,int count)
{
int left用户空间和内核空间
if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )
return -EFAULT
for(left = count left >0 left--)
{
__put_user(1,buf,1)
buf++
}
return count
}
这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。
static int write_test(struct inode *inode,struct file *file,const char *buf,int count)
{
return count
}
static int open_test(struct inode *inode,struct file *file )
{
MOD_INC_USE_COUNT模块计数加以,表示当前内核有个设备加载内核当中去
return 0
}
static void release_test(struct inode *inode,struct file *file )
{
MOD_DEC_USE_COUNT
}
这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
struct file_operations test_fops = {?
read_test,
write_test,
open_test,
release_test,
}
设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。
int init_module(void)
{
int result
result = register_chrdev(0, "test", &test_fops)对设备操作的整个接口
if (result <0) {
printk(KERN_INFO "test: can't get major numbern")
return result
}
if (test_major == 0) test_major = result
return 0
}
在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。
如果登记成功,返回设备的主设备号,不成功,返回一个负值。
void cleanup_module(void)
{
unregister_chrdev(test_major,"test")
}
在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。
一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。
下面编译 :
$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c –c表示输出制定名,自动生成.o文件
得到文件test.o就是一个设备驱动程序。
如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后
ld ?-r ?file1.o ?file2.o ?-o ?modulename。
驱动程序已经编译好了,现在把它安装到系统中去。
$ insmod ?–f ?test.o
如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 :
$ rmmod test
下一步要创建设备文件。
mknod /dev/test c major minor
c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。
用shell命令
$ cat /proc/devices
就可以获得主设备号,可以把上面的命令行加入你的shell script中去。
minor是从设备号,设置成0就可以了。
我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
int testdev
int i
char buf[10]
testdev = open("/dev/test",O_RDWR)
if ( testdev == -1 )
{
printf("Cann't open file n")
exit(0)
}
read(testdev,buf,10)
for (i = 0i <10i++)
printf("%dn",buf[i])
close(testdev)
}
编译运行,看看是不是打印出全1
以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。上述给出了一个简单的字符设备驱动编写的框架和原理,更为复杂的编写需要去认真研究LINUX内核的运行机制和具体的设备运行的机制等等。希望大家好好掌握LINUX设备驱动程序编写的方法。
以上就是关于模块选煤设备框架指什么?全部的内容,如果了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!



















