linux设备驱动概述AlanTu

(1)Linux应用软件工程师(ApplicationSoftwareEngineer):

主要利用C库函数和LinuxAPI进行应用软件的编写;

从事这方面的开发工作,主要需要学习:符合linuxposix标准的API函数及系统调用,linux的多任务编程技巧:多进程、多线程、进程间通信、多任务之间的同步互斥等,嵌入式数据库的学习,UI编程:QT、miniGUI等。

(2)Linux固件工程师(FirmwareEngineer):

主要进行Bootloader、Linux的移植及Linux设备驱动程序的设计工作。

在任何一个计算机系统中,大至服务器、PC机、小至手机、mp3/mp4播放器,无论是复杂的大型服务器系统还是一个简单的流水灯单片机系统,都离不开驱动程序的身影,没有硬件的软件是空中楼阁,没有软件的硬件只是一堆废铁,硬件是底层的基础,是所有软件得以运行的平台,代码最终会落实到硬件上的逻辑组合。

但是硬件与软件之间存在一个驳论:为了快速、优质的完成软件功能设计,应用程序工程师不想也不愿关心硬件,而硬件工程师也很难有功夫去处理软件开发中的一些应用。例如软件工程师在调用printf的时候,不许也不用关心信息到底是通过什么样的处理,走过哪些通路显示在该显示的地方,硬件工程师在写完了一个4*4键盘驱动后,无需也不必管应用程序在获得键值后做哪些处理及操作。

也就是说软件工程师需要看到一个没有硬件的纯软件世界,硬件必须透明的提供给他,谁来实现这一任务?答案是驱动程序,驱动程序从字面解释就是:“驱使硬件设备行动”。驱动程序直接与硬件打交道,按照硬件设备的具体形式,驱动设备的寄存器,完成设备的轮询、中断处理、DMA通信,最终让通信设备可以收发数据,让显示设备能够显示文字和画面,让音频设备可以完成声音的存储和播放。

可见,设备驱动程序充当了硬件和软件之间的枢纽,因此驱动程序的表现形式可能就是一些标准的、事先协定好的API函数,驱动工程师只需要去完成相应函数的填充,应用工程师只需要调用相应的接口完成相应的功能。无论有没有操作系统,驱动程序都有其存在价值,只是在裸机情况下,工作环境比较简单、完成的工作较单一,驱动程序完成的功能也就比较简单,同时接口只要在小范围内符合统一的标准即可。但是在有操作系统的情况下,此问题就会被放大:硬件来自不同的公司、千变万化,全世界每天都会有大量的新芯片被生产,大量的电路板被设计出来,如果没有一个很好的统一标准去规范这一程序,操作系统就会被设计的非常冗余,效率会非常低。

下图反映了应用程序、linux内核、驱动程序、硬件的关系。

1)应用程序调用一系列函数库,通过对文件的操作完成一系列功能:

应用程序以文件形式访问各种硬件设备(linux特有的抽象方式,把所有的硬件访问抽象为对文件的读写、设置)

函数库:

部分函数无需内核的支持,由库函数内部通过代码实现,直接完成功能

部分函数涉及到硬件操作或内核的支持,由内核完成对应功能,我们称其为系统调用

2)内核处理系统调用,根据设备文件类型、主设备号、从设备号(后面会讲解),调用设备驱动程序;

3)设备驱动直接与硬件通信;

二、设备类型

硬件是千变万化的,没有八千也有一万了,就像世界上有三种人:男人、女人、女博士一样,linux做了一个很伟大也很艰难的分类:把所有的硬件设备分为三大类:字符设备、块设备、网络设备。

1)字符设备:字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。

对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生;

字符设备驱动程序通常至少要实现open、close、read和write系统调用。

比如我们常见的lcd、触摸屏、键盘、led、串口等等,就像男人是用来干活的一样,他们一般对应具体的硬件都是进行出具的采集、处理、传输。

2)块设备:一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。

块设备通过buffercache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。

块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。

只有一个块设备可以支持一个安装的文件系统。

比如我们常见的电脑硬盘、SD卡、U盘、光盘等,就像女人一样是用来存储信息的。

3)网络接口:任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。

访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。

比如我们常见的网卡设备、蓝牙设备,就像女博士一样,数量稀少但又不可或缺。

linux中所有的驱动程序最终都能归到这三种设备中,当然他们之间也没有非常严格的界限,这些都是程序中对他们的划分而已,比如一个sd卡,我们也可以把它封装成字符设备去操作也是没有问题的。就像。。。

三、设备文件、主设备号、从设备号

有了设备类型的划分,那么应用程序应该怎样访问具体的硬件设备呢?

或者说已经确定他是一个男人了,那么怎么从万千世界中区分他与他的不同呢?

答案是:姓名,在linux驱动中也就是设备文件名。

那么重名怎么办?

答案是:身份证号,在linux驱动中也就是设备号(主、从)。

设备文件:

在linux

系统

中有一个约定俗成的说法:“一切皆文件”,

主设备号、从设备号

在设备管理中,除了设备类型外,内核还需要一对被称为主从设备号的参数,才能唯一标识一个设备,类似人的身份证号

主设备号:

用于标识驱动程序,相同的主设备号使用相同的驱动程序,例如:S3C2440有串口、LCD、触摸屏三种设备,他们的主设备号各不相同;

从设备号:

用于标识同一驱动程序的不同硬件

例:PC的IDE设备,主设备号用于标识该硬盘,从设备号用于标识每个分区,2440有三个串口,每个串口的主设备号相同,从设备号用于区分具体属于那一个串口。

应用程序以main开始

驱动程序没有main,它以一个模块初始化函数作为入口

应用程序从头到尾执行一个任务

驱动程序完成初始化之后不再运行,等待系统调用

应用程序可以使用glibc等标准C函数库

驱动程序不能使用标准C库

五、用户态与内核态的区分

驱动程序是内核的一部分,工作在内核态

应用程序工作在用户态

数据空间访问问题

无法通过指针直接将二者的数据地址进行传递

系统提供一系列函数帮助完成数据空间转换get_userput_usercopy_from_usercopy_to_user

对设备初始化和释放资源

把数据从内核传送到硬件和从硬件读取数据

读取应用程序传送给设备文件的数据和回送应用程序请求的数据

检测和处理设备出现的错误(底层协议)

用于区分具体设备的实例

一、linux内核模块简介

linux内核整体结构非常庞大,其包含的组件也非常多。我们怎么把需要的部分都包含在内核中呢?

一种办法是把所有的需要的功能都编译到内核中。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,不得不重新编译内核,工作效率会非常的低,同时如果编译的模块不是很完善,很有可能会造成内核崩溃。

linux提供了另一种机制来解决这个问题,这种集中被称为模块,可以实现编译出的内核本身并不含有所有功能,而在这些功能需要被使用的时候,其对应的代码可以被动态的加载到内核中。

二、模块特点:

1)模块本身并不被编译入内核,从而控制了内核的大小。

2)模块一旦被加载,他就和内核中的其他部分完全一样。

注意:模块并不是驱动的必要形式:即:驱动不一定必须是模块,有些驱动是直接编译进内核的;同时模块也不全是驱动,例如我们写的一些很小的算法可以作为模块编译进内核,但它并不是驱动。就像烧饼不一定是圆的,圆的也不都是烧饼一样。

三、最简单的模块分析

1)以下是一个最简单的模块例子

2)以下是编译上述模块所需的编写的makefile

最终会编译得到:hello.ko文件

使用insmodhello.ko将模块插入内核,然后使用dmesg即可看到输出提示信息。

常用的几种模块操作:

3)linux内核模块的程序结构

1.模块加载函数:

模块加载函数的名字可以随便取,但必须以“module_init(函数名)”的形式被指定;

执行insmod命令时被执行,用于初始化模块所必需资源,比如内存空间、硬件设备等;

它返回整形值,若初始化成功,应返回0,初始化失败返回负数。

2.模块卸载函数

典型的模块卸载函数形式如下:

模块卸载函数在模块卸载的时候执行,不返回任何值,需用”module_exit(函数名)”的形式被指定。

卸载模块完成与加载函数相反的功能:

若加载函数注册了XXX,则卸载函数应当注销XXX

若加载函数申请了内存空间,则卸载函数应当释放相应的内存空间

若加载函数申请了某些硬件资源(中断、DMA、I/0端口、I/O内存等),则卸载函数应当释放相应的硬件资源

若加载函数开启了硬件,则卸载函数应当关闭硬件。

其中__init、__exit为系统提供的两种宏,表示其所修饰的函数在调用完成后会自动回收内存,即内核认为这种函数只会被执行1次,然后他所占用的资源就会被释放。

在linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_TABLE、MODULE_ALIA,分别描述模块的作者、描述、版本、设备表号、别名等。

四、有关模块的其它特性

1)模块参数:

我们可以利用module_param(参数名、参数类型、参数读写属性)为模块定义一个参数,例如:

在装载模块时,用户可以给模块传递参数,形式为:”insmod模块名参数名=参数值”,如果不传递,则参数使用默认的参数值

参数的类型可以是:byte,short,ushort,int,uint,long,ulong,charp,bool;

权限:定义在linux/stat.h中,控制存取权限,S_IRUGO表示所有用户只读;

模块被加载后,在sys/module/下会出现以此模块命名的目录,当读写权限为零时:表示此参数不存在sysfs文件系统下的文件节点,当读写权限不为零时:此模块的目录下会存在parameters目录,包含一系列以参数名命名的文件节点,这些文件节点的权限值就是传入module_param()的“参数读/写权限“,而该文件的内容为参数的值。

除此之外,模块也可以拥有参数数组,形式为:”module_param_array(数组名、数组类型、数组长、参数读写权限等)”,当不需要保存实际的输入的数组元素的个数时,可以设置“数组长“为0。

运行insmod时,使用逗号分隔输入的数组元素。

下面是一个实际的例子,来说明模块传参的过程。

当执行insmodhello_param.ko时,执行dmesg查看内核输出信息:

当执行insmodhello_param.konum_test=2000string_test=“editbydengwei”,执行dmesg查看内核输出信息:

2)导出模块及符号的相互引用

Linux2.6内核的“/proc/kallsyms“文件对应内核符号表,它记录了符号以及符号所在的内存地址,模块可以使用下列宏导到内核符号表中。

EXPORT_SYMBOL(符号名);任意模块均可

EXPORT_SYMBOL_GPL(符号名);只使用于包含GPL许可权的模块

下面给出一个简单的例子:将addsub符号导出到内核符号表中,这样其它的模块就可以利用其中的函数

执行cat/proc/kallsyms|greptest即可找到以下信息,表示模块确实被加载到内核表中。

在其它模块中可以引用此符号

前面我们介绍模块编程的时候介绍了驱动进入内核有两种方式:模块和直接编译进内核,并介绍了模块的一种编译方式——在一个独立的文件夹通过makefile配合内核源码路径完成

那么如何将驱动直接编译进内核呢?

在我们实际内核的移植配置过程中经常听说的内核裁剪又是怎么麽回事呢?

我们在进行linux内核配置的时候经常会执行makemenuconfig这个命令,然后屏幕上会出现以下界面:

这个界面是怎么生成的呢?

跟我们经常说的内核配置与与编译又有什么关系呢?

下面我们借此来讲解一下linux内核的配置机制及其编译过程。

一、配置系统的基本结构

Linux内核的配置系统由三个部分组成,分别是:

1、Makefile:分布在Linux内核源代码根目录及各层目录中,定义Linux内核的编译规则;

2、配置文件(config.in(2.4内核,2.6内核)):给用户提供配置选择的功能;

3、配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供基于字符界面、基于Ncurses图形界面以及基于Xwindows图形界面的用户配置界面,各自对应于Makeconfig、Makemenuconfig和makexconfig)。

这些配置工具都是使用脚本语言,如Tcl/TK、Perl编写的(也包含一些用C编写的代码)。本文并不是对配置系统本身进行分析,而是介绍如何使用配置系统。所以,除非是配置系统的维护者,一般的内核开发者无须了解它们的原理,只需要知道如何编写Makefile和配置文件就可以。

二、makefilemenuconfig过程讲解

当我们在执行makemenuconfig这个命令时,系统到底帮我们做了哪些工作呢?

这里面一共涉及到了一下几个文件我们来一一讲解

Linux内核根目录下的scripts文件夹

arch/$ARCH/Kconfig文件、各层目录下的Kconfig文件

Linux内核根目录下的makefile文件、各层目录下的makefile文件

Linux内核根目录下的的.config文件、arm/$ARCH/下的config文件

Linux内核根目录下的include/generated/autoconf.h文件

2)当我们执行makemenuconfig命令出现上述蓝色配置界面以前,系统帮我们做了以下工作:

首先系统会读取arch/$ARCH/目录下的Kconfig文件生成整个配置界面选项(Kconfig是整个linux配置机制的核心),那么ARCH环境变量的值等于多少呢?

它是由linux内核根目录下的makefile文件决定的,在makefile下有此环境变量的定义:

或者通过makeARCH=armmenuconfig命令来生成配置界面,默认生成的界面是所有参数都是没有值的

比如教务处进行考试,考试科数可能有外语、语文、数学等科,这里相当于我们选择了arm科可进行考试,系统就会读取arm/arm/kconfig文件生成配置选项(选择了arm科的卷子),系统还提供了x86科、milps科等10几门功课的考试题

3)假设教务处比较“仁慈”,为了怕某些同学做不错试题,还给我们准备了一份参考答案(默认配置选项),存放在arch/$ARCH/configs下,对于arm科来说就是arch/arm/configs文件夹:

此文件夹中有许多选项,系统会读取哪个呢?内核默认会读取linux内核根目录下.config文件作为内核的默认选项(试题的参考答案),我们一般会根据开发板的类型从中选取一个与我们开发板最接近的系列到Linux内核根目录下(选择一个最接近的参考答案)

#cparch/arm/configs/s3c2410_defconfig.config

4).config

假设教务处留了一个心眼,他提供的参考答案并不完全正确(.config文件与我们的板子并不是完全匹配),这时我们可以选择直接修改.config文件然后执行makemenuconfig命令读取新的选项

但是一般我们不采取这个方案,我们选择在配置界面中通过空格、esc、回车选择某些选项选中或者不选中,最后保存退出的时候,Linux内核会把新的选项(正确的参考答案)更新到.config中,此时我们可以把.config重命名为其它文件保存起来(当你执行makedistclean时系统会把.config文件删除),以后我们再配置内核时就不需要再去arch/arm/configs下考取相应的文件了,省去了重新配置的麻烦,直接将保存的.config文件复制为.config即可.

5)经过以上两步,我们可以正确的读取、配置我们需要的界面了

那么他们如何跟makefile文件建立编译关系呢?

当你保存makemenuconfig选项时,系统会除了会自动更新.config外,还会将所有的选项以宏的形式保存在

Linux内核根目录下的include/generated/autoconf.h文件下

内核中的源代码就都会包含以上.h文件,跟宏的定义情况进行条件编译。

当我们需要对一个文件整体选择如是否编译时,还需要修改对应的makefile文件,例如:

我们选择是否要编译s3c2410_ts.c这个文件时,makefile会根据CONFIG_TOUCHSCREEN_S3C2410来决定是编译此文件,此宏是在Kconfig文件中定义,当我们配置完成后,会出现在.config及autconf中,至此,我们就完成了整个linux内核的编译过程。

最后我们会发现,整个linux内核配置过程中,留给用户的接口其实只有各层Kconfig、makefile文件以及对应的源文件。

比如我们如果想要给内核增加一个功能,并且通过makemenuconfig控制其声称过程

首先需要做的工作是:修改对应目录下的Kconfig文件,按照Kconfig语法增加对应的选项;

其次执行makemenuconfig选择编译进内核或者不编译进内核,或者编译为模块,.config文件和autoconf.h文件会自动生成;

最后修改对应目录下的makefile文件完成编译选项的添加;

最后的最后执行makezImage命令进行编译。

三、具体实例

下面我们以前面做过的模块实验为例,讲解如何通过makemenuconfig机制将前面单独编译的模块编译进内核或编译为模块

假设我已经有了这么一个驱动:

modules.c

CONFIG_MODULES必须跟上面的Kconfig中保持一致,系统会自动添加CONFIG_前缀

modules.o必须跟你加入的.c文件名一致

最后执行:makezImagemodules就会被编译进内核中

第三步:

把星号在配置界面通过空格改为M,最后执行makemodules,在driver/char/目录下会生成一个modules.ko文件

跟我们前面讲的单独编译模块效果一样,也会生成一个模块,将它考入开发板执行insmodmoudles.ko,即可将生成的模块插入内核使用

1、概述:

通常在Linux中,把SoC系统中集成的独立外设单元(如:I2C、IIS、RTC、看门狗等)都被当作平台设备来处理。

从Linux2.6起,引入了一套新的驱动管理和注册机制:Platform_device和Platform_driver,来管理相应设备。

Linux中大部分的设备驱动,都可以使用这套机制,设备用platform_device表示,驱动用platform_driver进行注册。

Linuxplatformdriver机制和传统的device_driver机制相比,一个十分明显的优势在于platform机制将本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform_device提供的标准接口进行申请并使用。

这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安全性。

/*定义两个变量来分别保存挂起时的WTCON和WTDAT值,到恢复的时候使用*/staticunsignedlongwtcon_save;staticunsignedlongwtdat_save;/*Watchdog平台驱动的设备挂起接口函数的实现*/staticintwdt_suspend(structplatform_device*dev,pm_message_tstate){/*保存挂起时的WTCON和WTDAT值*/wtcon_save=readl(wdt_base+S3C2410_WTCON);wtdat_save=readl(wdt_base+S3C2410_WTDAT);/*停止看门狗定时器*/wdt_start_or_stop(0);return0;}/*Watchdog平台驱动的设备恢复接口函数的实现*/staticintwdt_resume(structplatform_device*dev){/*恢复挂起时的WTCON和WTDAT值,注意这个顺序*/writel(wtdat_save,wdt_base+S3C2410_WTDAT);writel(wtdat_save,wdt_base+S3C2410_WTCNT);writel(wtcon_save,wdt_base+S3C2410_WTCON);return0;}#else/*配置内核时没选上电源管理,Watchdog平台驱动的设备挂起和恢复功能均无效,这两个函数也就无需实现了*/#definewdt_suspendNULL#definewdt_resumeNULL#endif

在Linux2.6内核中,devfs被认为是过时的方法,并最终被抛弃,udev取代了它。Devfs的一个很重要的特点就是可以动态创建设备结点。那我们现在如何通过udev和sys文件系统动态创建设备结点呢?用udev在/dev/下动态生成设备文件,这样用户就不用手工调用mknod了。

利用的kernelAPI:class_create:创建classclass_destroy:销毁classclass_device_create:创建deviceclass_device_destroy:销毁device

#include#include#include#include#include#include#include#include#include#include#include#include#include#include#include#include#include#include#defineTHIS_DESCRIPTION"\Thismoduleisadummydevicedriver,itregister\n\\t\tachardevice,andutilizeudevtocreate/destroy\n\\t\tdevicenodeunder/dev/dynamicallly."

/***file_operationsof'dummy_dev'*/staticstructfile_operationsdummy_dev_ops={.owner=THIS_MODULE,.open=dummy_open,.read=dummy_read,.write=dummy_write,.ioctl=dummy_ioctl,};

/***structcdevof'dummy_dev'*/structcdev*my_cdev;structclass*my_class;staticint__initmy_init(void){interr,devno=MKDEV(DUMMY_MAJOR,DUMMY_MINOR);

/*registerthe'dummy_dev'chardevice*/my_cdev=cdev_alloc();cdev_init(my_cdev,&dummy_dev_ops);my_cdev->owner=THIS_MODULE;err=cdev_add(my_cdev,devno,1);if(err!=0)printk("dummypcideviceregisterfailed!\n");/*creatingyourownclass*/my_class=class_create(THIS_MODULE,"dummy_class");if(IS_ERR(my_class)){printk("Err:failedincreatingclass.\n");return-1;}/*registeryourowndeviceinsysfs,andthiswillcauseudevdtocreatecorrespondingdevicenode*/class_device_create(my_class,NULL,MKDEV(DUMMY_MAJOR,DUMMY_MINOR),NULL,DUMMY_NAME"%d",DUMMY_MINOR);return0;}staticvoid__exitmy_fini(void){printk("bye\n");cdev_del(my_cdev);/ree(my_cdev);nouse.becausethatcdev_del()willcallkfreeifneccessary.

class_device_destroy(my_class,MKDEV(DUMMY_MAJOR,DUMMY_MINOR));class_destroy(my_class);}

module_init(my_init);module_exit(my_fini);

一、字符设备基础知识

1、设备驱动分类

linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:

字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

如图,在Linux内核中:

a--使用cdev结构体来描述字符设备;

b--通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c--通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

在Linux字符设备驱动中:

a--模块加载函数通过register_chrdev_region()或alloc_chrdev_region()来静态或者动态获取设备号;

b--通过cdev_init()建立cdev与file_operations之间的连接,通过cdev_add()向系统添加一个cdev以完成注册;

c--模块卸载函数通过cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号;

用户空间访问该设备的程序:

a--通过Linux系统调用,如open()、read()、write(),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

3、字符设备驱动模型

二、cdev结构体解析

在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

内核给出的操作structcdev结构的接口主要有以下几个:

a--voidcdev_init(structcdev*,conststructfile_operations*);

其源代码如代码清单如下:

该函数主要对structcdev结构体做初始化,最重要的就是建立cdev和file_operations之间的连接:

(1)将整个结构体清零;

(2)初始化list成员使其指向自身;

(3)初始化kobj成员;

(4)初始化ops成员;

b--structcdev*cdev_alloc(void);

该函数主要分配一个structcdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即:.ops=xxx_ops).

其源代码清单如下:

在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,structmodule是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化.owner=THIS_MODULE,该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

c--intcdev_add(structcdev*p,dev_tdev,unsignedcount);

该函数向内核注册一个structcdev结构,即正式通知内核由structcdev*p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

(1)第一个设备号dev,

(2)和该设备关联的设备编号的数量。

这两个参数直接赋值给structcdev的dev成员和count成员。

d--voidcdev_del(structcdev*p);

该函数向内核注销一个structcdev结构,即正式通知内核由structcdev*p代表的字符设备已经不可以使用了。

从上述的接口讨论中,我们发现对于structcdev的初始化和注册的过程中,我们需要提供几个东西

(1)structfile_operations结构指针;

(2)dev设备号;

(3)count次设备号个数。

但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!

三、设备号相应操作

1--主设备号和次设备号(二者一起为设备号):

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

typedefu_longdev_t;

在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t:

1)--从设备号中提取major和minor

MAJOR(dev_tdev);

MINOR(dev_tdev);

2)--通过major和minor构建设备号

MKDEV(intmajor,intminor);

注:这只是构建设备号。并未注册,需要调用register_chrdev_region静态申请;

2、分配设备号(两种方法):

a--静态申请:

intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name);

b--动态分配:

intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);

可以看到二者都是调用了__register_chrdev_region函数,其源代码如下:

通过这个函数可以看出register_chrdev_region和alloc_chrdev_region的区别,register_chrdev_region直接将Major注册进入,而alloc_chrdev_region从Major=0开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

二者应用可以简单总结如下:

register_chrdev_regionalloc_chrdev_region

可以看到,除了前面两个函数,还加了一个register_chrdev函数,可以发现这个函数的应用非常简单,只要一句就可以搞定前面函数所做之事;

下面分析一下register_chrdev函数,其源代码定义如下:

调用了__register_chrdev(major,0,256,name,fops)函数:

可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev的初始化以及cdev的注册;

3、注销设备号:

voidunregister_chrdev_region(dev_tfrom,unsignedcount);

4、创建设备文件:

利用cat/proc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknodfilenametypemajorminor

2)自动创建设备节点:

利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

下面看一个实例,练习一下上面的操作:

hello.c

测试程序test.c

makefile:

编译成功后,使用insmod命令加载:

然后用cat/proc/devices查看,会发现设备号已经申请成功;

上一篇我们介绍到创建设备文件的方法,利用cat/proc/devices查看申请到的设备名,设备号。

第一种是使用mknod手工创建:mknodfilenametypemajorminor

第二种是自动创建设备节点:利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。

在驱动用加入对udev的支持主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。

内核中定义的structclass结构体,顾名思义,一个structclass结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。

这样,加载模块的时候,用户空间中的udev会自动响应device_create()函数,去/sysfs下寻找对应的类从而创建设备节点。

下面是两个函数的解析:

1、class_create(...)函数

功能:创建一个类;

下面是具体定义:

owner:THIS_MODULEname:名字

__class_create(owner,name,&__key)源代码如下:

销毁函数:voidclass_destroy(structclass*cls)

2、device_create(...)函数

structdevice*device_create(structclass*class,structdevice*parent,dev_tdevt,void*drvdata,constchar*fmt,...)

功能:创建一个字符设备文件

参数:

structclass*class:类structdevice*parent:NULLdev_tdevt:设备号void*drvdata:null、constchar*fmt:名字

返回:

structdevice*

下面是源码解析:

device_create_vargs(class,parent,devt,drvdata,fmt,vargs)解析如下:

现在就不继续往下跟了,大家可以继续往下跟;

下面是一个实例:

test.c

makefile

下面可以看几个class几个名字的对应关系:

先看下面这张图,这是Linux中虚拟文件系统、一般的设备文件与设备驱动程序值间的函数调用关系;

上面这张图展现了一个应用程序调用字符设备驱动的过程,在设备驱动程序的设计中,一般而言,会关心file和inode这两个结构体

用户空间使用open()函数打开一个字符设备fd=open("/dev/hello",O_RDWR),这一函数会调用两个数据结构structinode{...}与structfile{...},二者均在虚拟文件系统VFS处,下面对两个数据结构进行解析:

一、file文件结构体

在设备驱动中,这也是个非常重要的数据结构,必须要注意一点,这里的file与用户空间程序中的FILE指针是不同的,用户空间FILE是定义在C库中,从来不会出现在内核中。而structfile,却是内核当中的数据结构,因此,它也不会出现在用户层程序中。

file结构体指示一个已经打开的文件(设备对应于设备文件),其实系统中的每个打开的文件在内核空间都有一个相应的structfile结构体,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数,直至文件被关闭。如果文件被关闭,内核就会释放相应的数据结构。

在内核源码中,structfile要么表示为file,或者为filp(意指“filepointer”),注意区分一点,file指的是structfile本身,而filp是指向这个结构体的指针。

下面是几个重要成员:

a--fmode_tf_mode;

此文件模式通过FMODE_READ,FMODE_WRITE识别了文件为可读的,可写的,或者是二者。在open或ioctl函数中可能需要检查此域以确认文件的读/写权限,你不必直接去检测读或写权限,因为在进行octl等操作时内核本身就需要对其权限进行检测。

b--loff_tf_pos;

当前读写文件的位置。为64位。如果想知道当前文件当前位置在哪,驱动可以读取这个值而不会改变其位置。对read,write来说,当其接收到一个loff_t型指针作为其最后一个参数时,他们的读写操作便作更新文件的位置,而不需要直接执行filp->f_pos操作。而llseek方法的目的就是用于改变文件的位置。

c--unsignedintf_flags;

文件标志,如O_RDONLY,O_NONBLOCK以及O_SYNC。在驱动中还可以检查O_NONBLOCK标志查看是否有非阻塞请求。其它的标志较少使用。特别地注意的是,读写权限的检查是使用f_mode而不是f_flog。所有的标量定义在头文件中

d--structfile_operations*f_op;

e--void*private_data;

在驱动调用open方法之前,open系统调用设置此指针为NULL值。你可以很自由的将其做为你自己需要的一些数据域或者不管它,如,你可以将其指向一个分配好的数据,但是你必须记得在filestruct被内核销毁之前在release方法中释放这些数据的内存空间。private_data用于在系统调用期间保存各种状态信息是非常有用的。

二、inode结构体

内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file文件结构)是不同的,我们可以使用多个file文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须只能指向一个inode结构体。

(1)dev_ti_rdev;

表示设备文件的结点,这个域实际上包含了设备号。

(2)structcdev*i_cdev;

structcdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。

下面是源代码:

三、chardevs数组

从图中可以看出,通过数据结构structinode{...}中的i_cdev成员可以找到cdev,而所有的字符设备都在chrdevs数组中

下面先看一下chrdevs的定义:

可以看到全局数组chrdevs包含了255(CHRDEV_MAJOR_HASH_SIZE的值)个structchar_device_struct的元素,每一个对应一个相应的主设备号。

如果分配了一个设备号,就会创建一个structchar_device_struct的对象,并将其添加到chrdevs中;这样,通过chrdevs数组,我们就可以知道分配了哪些设备号。

register_chrdev_region()分配指定的设备号范围

alloc_chrdev_region()动态分配设备范围

他们都主要是通过调用函数__register_chrdev_region()来实现的;要注意,这两个函数仅仅是注册设备号!如果要和cdev关联起来,还要调用cdev_add()。

register_chrdev()申请指定的设备号,并且将其注册到字符设备驱动模型中.

它所做的事情为:

a--注册设备号,通过调用__register_chrdev_region()来实现

b--分配一个cdev,通过调用cdev_alloc()来实现

c--将cdev添加到驱动模型中,这一步将设备号和驱动关联了起来.通过调用cdev_add()来实现

d--将第一步中创建的structchar_device_struct对象的cdev指向第二步中分配的cdev.由于register_chrdev()是老的接口,这一步在新的接口中并不需要。

四、cdev结构体

五、文件系统中对字符设备文件的访问

下面看一下上层应用open()调用系统调用函数的过程

对于一个字符设备文件,其inode->i_cdev指向字符驱动对象cdev,如果i_cdev为NULL,则说明该设备文件没有被打开.

由于多个设备可以共用同一个驱动程序.所以,通过字符设备的inode中的i_devices和cdev中的list组成一个链表

首先,系统调用open打开一个字符设备的时候,通过一系列调用,最终会执行到chrdev_open

(最终是通过调用到def_chr_fops中的.open,而def_chr_fops.open=chrdev_open.这一系列的调用过程,本文暂不讨论)

intchrdev_open(structinode*inode,structfile*filp)

chrdev_open()所做的事情可以概括如下:

1.根据设备号(inode->i_rdev),在字符设备驱动模型中查找对应的驱动程序,这通过kobj_lookup()来实现,kobj_lookup()会返回对应驱动程序cdev的kobject.

2.设置inode->i_cdev,指向找到的cdev.

3.将inode添加到cdev->list的链表中.

4.使用cdev的ops设置file对象的f_op

5.如果ops中定义了open方法,则调用该open方法

6.返回

执行完chrdev_open()之后,file对象的f_op指向cdev的ops,因而之后对设备进行的read,write等操作,就会执行cdev的相应操作。

struct_file_operations

struct_file_operations在Fs.h这个文件里面被定义的,如下所示:

Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。

用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

下面是各成员解析:

1、structmodule*owner

第一个file_operations成员根本不是一个操作,它是一个指向拥有这个结构的模块的指针。

2、loff_t(*llseek)(structfile*filp,loff_tp,intorig);

(指针参数filp为进行读取信息的目标文件结构体指针;参数p为文件定位的目标偏移量;参数orig为对文件定位的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))

llseek方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值.

loff_t参数是一个"longoffset",并且就算在32位平台上也至少64位宽.错误由一个负返回值指示;如果这个函数指针是NULL,seek调用会以潜在地无法预知的方式修改file结构中的位置计数器(在"file结构"一节中描述).

3、ssize_t(*read)(structfile*filp,char__user*buffer,size_tsize,loff_t*p);

(指针参数filp为进行读取信息的目标文件,指针参数buffer为对应放置信息的缓冲区(即用户空间内存地址),参数size为要读取的信息长度,参数p为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)

这个函数用来从设备中获取数据。在这个位置的一个空指针导致read系统调用以-EINVAL("Invalidargument")失败。一个非负返回值代表了成功读取的字节数(返回值是一个"signedsize"类型,常常是目标平台本地的整数类型).

4、ssize_t(*aio_read)(structkiocb*,char__user*buffer,size_tsize,loff_tp);

可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同的,异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);初始化一个异步读--可能在函数返回前不结束的读操作.如果这个方法是NULL,所有的操作会由read代替进行(同步地).(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)

5、ssize_t(*write)(structfile*filp,constchar__user*buffer,size_tcount,loff_t*ppos);

(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)

发送数据给设备.。如果NULL,-EINVAL返回给调用write系统调用的程序.如果非负,返回值代表成功写的字节数。

(注:这个操作和上面的对文件进行读的操作均为阻塞操作)

6、ssize_t(*aio_write)(structkiocb*,constchar__user*buffer,size_tcount,loff_t*ppos);

初始化设备上的一个异步写.参数类型同aio_read()函数;

7、int(*readdir)(structfile*filp,void*,filldir_t);

对于设备文件这个成员应当为NULL;它用来读取目录,并且仅对文件系统有用.

8、unsignedint(*poll)(structfile*,structpoll_table_struct*);

(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)

这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。

(poll方法是3个系统调用的后端:poll,epoll,和select,都用作查询对一个或多个文件描述符的读或写是否会阻塞.poll方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能地,提供给内核信息用来使调用进程睡眠直到I/O变为可能.如果一个驱动的poll方法为NULL,设备假定为不阻塞地可读可写.

9、int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg);

(inode和filp指针是对应应用程序传递的文件描述符fd的值,和传递给open方法的相同参数.cmd参数从用户那里不改变地传下来,并且可选的参数arg参数以一个unsignedlong的形式传递,不管它是否由用户给定为一个整数或一个指针.如果调用程序不传递第3个参数,被驱动操作收到的arg值是无定义的.因为类型检查在这个额外参数上被关闭,编译器不能警告你如果一个无效的参数被传递给ioctl,并且任何关联的错误将难以查找.)

ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写).另外,几个ioctl命令被内核识别而不必引用fops表.如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,"设备无这样的ioctl"),系统调用返回一个错误.

10、int(*mmap)(structfile*,structvm_area_struct*);

mmap用来请求将设备内存映射到进程的地址空间。如果这个方法是NULL,mmap系统调用返回-ENODEV.

(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)

11、int(*open)(structinode*inode,structfile*filp);

(inode为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)

12、int(*flush)(structfile*);

flush操作在进程关闭它的设备文件描述符的拷贝时调用;

它应当执行(并且等待)设备的任何未完成的操作.这个必须不要和用户查询请求的fsync操作混淆了.当前,flush在很少驱动中使用;SCSI磁带驱动使用它,例如,为确保所有写的数据在设备关闭前写到磁带上.如果flush为NULL,内核简单地忽略用户应用程序的请求.

13、int(*release)(structinode*,structfile*);

release()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:

voidrelease(structinodeinode,structfile*file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。在文件结构被释放时引用这个操作.如同open,release可以为NULL.

14、int(*synch)(structfile*,structdentry*,intdatasync);

刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。

15、int(*aio_fsync)(structkiocb*,int);

16、int(*fasync)(int,structfile*,int);

这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:

此操作用来通知设备它的FASYNC标志的改变.异步通知是一个高级的主题,在第6章中描述.这个成员可以是NULL如果驱动不支持异步通知.

17、int(*lock)(structfile*,int,structfile_lock*);

lock方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它.

18、ssize_t(*readv)(structfile*,conststructiovec*,unsignedlong,loff_t*);

ssize_t(*writev)(structfile*,conststructiovec*,unsignedlong,loff_t*);

这些方法实现发散/汇聚读和写操作.应用程序偶尔需要做一个包含多个内存区的单个读或写操作;这些系统调用允许它们这样做而不必对数据进行额外拷贝.如果这些函数指针为NULL,read和write方法被调用(可能多于一次).

19、ssize_t(*sendfile)(structfile*,loff_t*,size_t,read_actor_t,void*);

这个方法实现sendfile系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个.

例如,它被一个需要发送文件内容到一个网络连接的web服务器使用.设备驱动常常使sendfile为NULL.

20、ssize_t(*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);

sendpage是sendfile的另一半;它由内核调用来发送数据,一次一页,到对应的文件.设备驱动实际上不实现sendpage.

21、unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedlong,unsignedlong);

这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中。这个任务通常由内存管理代码进行;这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求.大部分驱动可以置这个方法为NULL.[10]

22、int(*check_flags)(int)

这个方法允许模块检查传递给fnctl(F_SETFL...)调用的标志.

23、int(*dir_notify)(structfile*,unsignedlong);

这个方法在应用程序使用fcntl来请求目录改变通知时调用.只对文件系统有用;驱动不需要实现dir_notify.

现在,我们来编写自己第一个字符设备驱动——点亮LED。(不完善,后面再完善)

硬件平台:Exynos4412(FS4412)

编写驱动分下面几步:

a--查看原理图、数据手册,了解设备的操作方法;

b--在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;

c--实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名,内核才能找到相应的驱动程序;

d--设计所要实现的操作,比如open、close、read、write等函数;

e--实现中断服务(中断不是每个设备驱动所必须的);

f--编译该驱动程序到内核中,或者用insmod命令加载;

g--测试驱动程序;

下面是一个点亮LED的驱动:

第一步,当然是查看手册,查看原理图,找到相应寄存器;

查看手册,四个LED所用寄存器为:

led2

GPX2CON0x11000c40GPX2DAT0x11000c44

led3

GPX1CON0x11000c20GPX1DAT0x11000c24

led43-43-5

GPF3CON0x114001e0GPF3DAT0x114001e4

这里要注意:arm体系架构是io内存,必须要映射ioremap();其作用是物理内存向虚拟内存的映射。用到writelreadl这两个函数,详细解释会在后面不上,先看一下简单用法:以LED2为例,下面是地址映射及读写:

下面是驱动程序,后面会更完善

测试程序:

编译结束后,将a.out和hello.ko拷贝到开发板中:

#insmodhello.ko

#mknod/dev/helloc2500

#./a.out

会看到跑马灯效果。

后面会对该驱动完善。

编写驱动的第一步仍是看原理图:

可以看到,该蜂鸣器由GPD0_0来控制,查手册可知该I/O口由Time0来控制,找到相应的寄存器:

a--I/O口寄存器及地址

GPD0CON0x114000a0

b--Time0寄存器及地址

基地址为:TIMER_BASE0x139D0000

这些物理寄存器地址都是相邻的,我们这里用偏移量来表示:

寄存器名地址偏移量所需配置

TCFG00x0000[7-0]0XFF

TCFG10x0004[3-0]0X2

TCON0x0008[3-0]0X20X90X0

TCNTB00x000C500

TCMPB00x0010250

前面已经知道,驱动是无法直接操纵物理地址的,所以这里仍需物理地址向虚拟地址的转换,用到ioremap()函数、writel()函数、readl()函数:

1、地址映射操作

2、Time0初始化操作(这里使用的已经是虚拟地址)

3、装载数据,配置占空比

下面是驱动程序,这里我们用到了write()read()ioctl()函数,具体解析移步:

驱动程序:beep.c

下面是是个简单的测试程序test.c,仅实现蜂鸣器响6秒的功能:

这是个音乐播放测试程序,慎听!!分别为《大长今》、《世上只有妈妈好》、《渔船》,这个单独编译一下

附所用头文件:

编译好程序后

#insmodbeep.ko

#mknod/dev/beepc2500

#./music

便会听到悦耳的音乐了!

我们在前面讲到了file_operations,其是一个函数指针的集合,用于存放我们定义的用于操作设备的函数的指针,如果我们不定义,它默认保留为NULL。其中有最重要的几个函数,分别是open()、read()、write()、ioctl(),下面分别对其进行解析

一、打开和关闭设备函数

a--打开设备

int(*open)(structinode*,structfile*);

在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open返回0。

b--关闭设备

int(*release)(structinode*,structfile*);

当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。

这两个函数已经讲过,这里不再赘述,主要看下面几个函数

二、read()、write()函数

现在把read()、write()两个函数放一起讲,因为两个函数非密不可分的,先看一下两个函数的定义

a--read()函数

filp:为进行读取信息的目标文件,

buffer:为对应放置信息的缓冲区(即用户空间内存地址);

size:为要读取的信息长度;

p:为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,

移动的值为要读取信息的长度值

b--write()函数

filp:为目标文件结构体指针;

buffer:为要写入文件的信息缓冲区;

count:为要写入信息的长度;

ppos:为当前的偏移位置,这个值通常是用来判断写文件是否越界

两个函数的作用分别是从设备中获取数据及发送数据给设备,应用程序中与之对应的也有write()函数及read()函数:

我们知道,应用程序工作在用户空间,而驱动工作在内核空间,二者不能直接通信的,那我们用何种方法进行通信呢?下面介绍一下内核中的memcpy---copy_from_user和copy_to_user,虽然说内核中不能使用C库提供的函数,但是内核也有一个memcpy的函数,用法跟C库中的一样。

下面看一下copy_from_user()及copy_to_user()函数的定义:

可以看到两个函数均是调用了_memcpy()函数:

其实在这里,我们可以思考,既然拷贝的功能上面的_memcpy()函数就可以实现,为什么还要封装成copy_to_user()和copy_from_user()呢答案是_memcpy()函数是有缺陷的,譬如我们在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃。

出于上面的原因,内核和用户态之间交互的数据时必须要先对数据进行检测,如果数据是安全的,才可以进行数据交互。上面的函数就是memcpy的改进版,在memcpy功能的基础上加上的检查传入参数的功能,防止有些人有意或者无意的传入无效的参数。

现在我们可以审视一下这两个函数了:

用法:

和memcpy的参数一样,但它根据传参方向的不同分开了两个函数。

"to"是相对于内核态来说的。所以,to函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。

"from"也是相对于内核来说的。所以,from函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。

返回值:函数的返回值是指定要读取的n个字节中还剩下多少字节还没有被拷贝。

注意:

一般的,如果返回值不为0时,调用copy_to_user的函数会返回错误号-EFAULT表示操作出错。当然也可以自己决定。

又到了摆实例的时候了,这里只列出部分代码,看看这两个函数的用法:

到这里open、close、read、write四个函数已经学完,下面我们来看一下四个函数使用时,到底经历了一个怎样的过程:

注:箭头方向是从调用的一方指向受作用的一方

解析完open、close、read、write四个函数后,终于到我们的ioctl()函数了

一、什么是ioctl

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。下面是其源代码定义:

函数名:ioctl

功能:控制I/O设备

用法:intioctl(inthandle,intcmd,[int*argdx,intargcx]);

include/asm/ioctl.h中定义的宏的注释:

二、ioctl的必要性

如果不用ioctl的话,也可以实现对设备I/O通道的控制。例如,我们可以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。所以,我们就使用ioctl来实现控制的功能。要记住,用户程序所作的只是通过命令码(cmd)告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。

三、ioctl如何实现

在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情。因为设备都是特定的,这里也没法说。关键在于怎样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径。

命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。这些错误都会导致不可预料的事情发生,而当程序员发现了这些奇怪的事情的时候,再来调试程序查找错误,那将是非常困难的事情。所以在Linux核心中是这样定义一个命令码的:

|设备类型|序列号|方向|数据尺寸|

|-------------|----------|-------|------------|

|8bit|8bit|2bit|8~14bit|

|-------------|----------|-------|-------------|

这样一来,一个命令就变成了一个整数形式的命令码;但是命令码非常的不直观,所以LinuxKernel中提供了一些宏。这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。

比如上面展现的:

我们在前面PWM驱动程序中也定义了命令宏:

这里必须要提一下的,就是"幻数"MAGIC_NUMBER,"幻数"是一个字母,数据长度也是8,用一个特定的字母来标明设备类型,这和用一个数字是一样的,只是更加利于记忆和理解。

四、cmd参数如何得出

这里确实要说一说,cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过switch{case}结构进行相应的操作。

实例时刻,当然只是部分代码:

测试代码如下:

一、ioremap()函数基础概念

几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:

a--I/O映射方式(I/O-mapped)

典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。

b--内存映射方式(Memory-mapped)

RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。

但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。

一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。

1、ioremap函数

ioremap宏定义在asm/io.h内:

#defineioremap(cookie,size)__ioremap(cookie,size,0)

__ioremap函数原型为(arm/mm/ioremap.c):

void__iomem*__ioremap(unsignedlongphys_addr,size_tsize,unsignedlongflags);

phys_addr:要映射的起始的IO地址

size:要映射的空间的大小

flags:要映射的IO空间和权限有关的标志

该函数返回映射后的内核虚拟地址(3G-4G).接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

2、iounmap函数

iounmap函数用于取消ioremap()所做的映射,原型如下:

voidiounmap(void*addr);

在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。

读写I/O的函数如下所示:

a--writel()

writel()往内存映射的I/O空间上写数据,wirtel()I/O上写入32位数据(4字节)。

原型:voidwritel(unsignedchardata,unsignedshortaddr)

b--readl()

readl()从内存映射的I/O空间上读数据,readl从I/O读取32位数据(4字节)。原型:unsignedcharreadl(unsignedintaddr)

变量addr是I/O地址。

返回值:从I/O空间读取的数值。

具体定义如下:

三、使用实例

还是拿我们写PWM驱动的实例来解析

1、这里我们先定义了一些寄存器,这里使用的地址均是物理地址:

2、为了使用内存映射,我们需先定义指针用来保存内存映射后的地址:

注意:这里timer_base指针指向的类型设为void,主要因为上面使用了地址偏移,使用void更有利于我们使用;

3、使用ioremap()函数进行内存映射,并将映射的地址赋给我们刚才定义的指针

4、得到地址后,可以调用writel()、readl()函数进行相应的操作

可以看到,这里先从相应的地址中读取数据,修改完毕后,再利用writel函数进行数据写入。

一、VFS虚拟文件系统基础概念

Linux允许众多不同的文件系统共存,并支持跨文件系统的文件操作,这是因为有虚拟文件系统的存在。虚拟文件系统,即VFS(VirtualFileSystem)是Linux内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如ext2,vfat提供接口机制。

Linux有两个特性:

a--跨文件系统的文件操作

Linux中允许众多不同的文件系统共存,如ext2,ext3,vfat等。通过使用同一套文件I/O系统调用即可对Linux中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,对文件的操作可以跨文件系统而执行。如图1所示,我们可以使用cp命令从vfat文件系统格式的硬盘拷贝数据到ext3文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统。

图1.跨文件系统的文件操作

b--一切皆是文件

“一切皆是文件”是Unix/Linux的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、套接字等在Unix/Linux中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。

图2.一切皆是文件

而虚拟文件系统正是实现上述两点Linux特性的关键所在。虚拟文件系统(VirtualFileSystem,简称VFS),是Linux内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,允许不同的文件系统共存。系统中所有的文件系统不但依赖VFS共存,而且也依靠VFS协同工作。

为了能够支持各种实际文件系统,VFS定义了所有文件系统都支持的基本的、概念上的接口和数据结构;同时实际文件系统也提供VFS所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被Linux支持,就必须提供一个符合VFS标准的接口,才能与VFS协同工作。就像《老炮儿》里面的一样,“要有规矩”,想在Linux下混,就要按照Linux所定的规矩来办事。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS层和内核的其他部分看来,所有文件系统都是相同的。

图3显示了VFS在内核中与实际的文件系统的协同关系。

图3.VFS在内核中与其他的内核模块的协同关系

总结虚拟文件系统的作用:

虚拟文件系统(VFS)是linux内核和存储设备之间的抽象层,主要有以下好处。

-简化了应用程序的开发:应用通过统一的系统调用访问各种存储介质

-简化了新文件系统加入内核的过程:新文件系统只要实现VFS的各个接口即可,不需要修改内核部分

二、VFS数据结构

1、一些基本概念

为了描述这个结构,Linux引入了一些基本概念:

文件一组在逻辑上具有完整意义的信息项的系列。在Linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待。总之,“一切皆文件”。

目录项在一个文件路径中,路径中的每一部分都被称为目录项;如路径/home/source/helloworld.c中,目录/,home,source和文件helloworld.c都是一个目录项。

超级块用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。如上的几个概念在磁盘中的位置关系如图4所示。

图4.磁盘与文件系统

2、VFS数据结构

VFS依靠四个主要的数据结构和一些辅助的数据结构来描述其结构信息,这些数据结构表现得就像是对象;每个主要对象中都包含由操作函数表构成的操作对象,这些操作对象描述了内核针对这几个主要的对象可以进行的操作。

a--超级块对象

存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统;每次一个实际的文件系统被安装时,内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。超级块通过其结构中的一个域s_type记录它所属的文件系统类型。

超级块的定义在:

b--索引节点对象

索引节点定义在:

c--目录项

和超级块和索引节点不同,目录项并不是实际存在于磁盘上的。在使用的时候在内存中创建目录项对象,其实通过索引节点已经可以定位到指定的文件,但是索引节点对象的属性非常多,在查找,比较文件时,直接用索引节点效率不高,所以引入了目录项的概念。这里可以看做又引入了一个抽象层,目录项是对索引节点的抽象!!!路径中的每个部分都是一个目录项,比如路径:/mnt/cdrom/foo/bar其中包含5个目录项,/mntcdromfoobar

每个目录项对象都有3种状态:被使用,未使用和负状态

-被使用:对应一个有效的索引节点,并且该对象由一个或多个使用者

-未使用:对应一个有效的索引节点,但是VFS当前并没有使用这个目录项

-负状态:没有对应的有效索引节点(可能索引节点被删除或者路径不存在了)

目录项定义在:

d--文件对象

文件对象是已打开的文件在内存中的表示,主要用于建立进程和磁盘上的文件的对应关系。

即文件对象并不是一个文件,只是抽象的表示一个打开的文件对。文件对象和物理文件的关系有点像进程和程序的关系一样。

它由sys_open()现场创建,由sys_close()销毁。当我们站在用户空间来看待VFS,我们像是只需与文件对象打交道,而无须关心超级块,索引节点或目录项。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已经打开的文件,它反过来指向目录项对象(反过来指向索引节点)。

一个文件对应的文件对象可能不是惟一的,但是其对应的索引节点和目录项对象无疑是惟一的。

文件对象的定义在:

上面分别介绍了4种对象分别的属性和方法,下面用图来展示这4个对象的和VFS之间关系以及4个对象之间的关系。

VFS中4个主要对象

前面我们讲到,超级块和索引节点都是真实存在的,是一个实际的物理文件。即使基于内存的文件系统,也是一种抽象的实际物理文件;而目录项对象和文件对象是运行时才被创建的。

下面是VFS中4个主要对象之间的关系:

超级块是一个文件系统,一个文件系统中可以有多个索引节点;索引节点和目录的关系又是N:M;一个目录中可以有多个文件对象,但一个文件对象却只能有一个目录;

三、基于VFS的文件I/O

在深入sys_open()和sys_read()之前,我们先概览下调用sys_read()的上下文。下图描述了从用户空间的read()调用到数据从磁盘读出的整个流程。

当在用户应用程序调用文件I/Oread()操作时,系统调用sys_read()被激发,sys_read()找到文件所在的具体文件系统,把控制权传给该文件系统,最后由具体文件系统与物理介质交互,从介质中读出数据。

1、sys_open()

sys_open()系统调用打开或创建一个文件,成功返回该文件的文件描述符。图8是sys_open()实现代码中主要的函数调用关系图。

图8.sys_open函数调用关系图

由于sys_open()的代码量大,函数调用关系复杂,以下主要是对该函数做整体的解析;而对其中的一些关键点,则列出其关键代码。

a--从sys_open()的函数调用关系图可以看到,sys_open()在做了一些简单的参数检验后,就把接力棒传给do_sys_open():

1)、首先,get_unused_fd()得到一个可用的文件描述符;通过该函数,可知文件描述符实质是进程打开文件列表中对应某个文件对象的索引值;

2)、接着,do_filp_open()打开文件,返回一个file对象,代表由该进程打开的一个文件;进程通过这样的一个数据结构对物理文件进行读写操作。

3)、最后,fd_install()建立文件描述符与file对象的联系,以后进程对文件的读写都是通过操纵该文件描述符而进行。b--do_filp_open()用于打开文件,返回一个file对象;而打开之前需要先找到该文件:

1)、open_namei()用于根据文件路径名查找文件,借助一个持有路径信息的数据结构nameidata而进行;

2)、查找结束后将填充有路径信息的nameidata返回给接下来的函数nameidata_to_filp()从而得到最终的file对象;当达到目的后,nameidata这个数据结构将会马上被释放。

c--open_namei()用于查找一个文件:

1)、path_lookup_open()实现文件的查找功能;要打开的文件若不存在,还需要有一个新建的过程,则调用path_lookup_create(),后者和前者封装的是同一个实际的路径查找函数,只是参数不一样,使它们在处理细节上有所偏差;

2)、当是以新建文件的方式打开文件时,即设置了O_CREAT标识时需要创建一个新的索引节点,代表创建一个文件。在vfs_create()里的一句核心语句dir->i_op->create(dir,dentry,mode,nd)可知它调用了具体的文件系统所提供的创建索引节点的方法。注意:这边的索引节点的概念,还只是位于内存之中,它和磁盘上的物理的索引节点的关系就像位于内存中和位于磁盘中的文件一样。此时新建的索引节点还不能完全标志一个物理文件的成功创建,只有当把索引节点回写到磁盘上才是一个物理文件的真正创建。想想我们以新建的方式打开一个文件,对其读写但最终没有保存而关闭,则位于内存中的索引节点会经历从新建到消失的过程,而磁盘却始终不知道有人曾经想过创建一个文件,这是因为索引节点没有回写的缘故。

3)、path_to_nameidata()填充nameidata数据结构;

4)、may_open()检查是否可以打开该文件;一些文件如链接文件和只有写权限的目录是不能被打开的,先检查nd->dentry->inode所指的文件是否是这一类文件,是的话则错误返回。还有一些文件是不能以TRUNC的方式打开的,若nd->dentry->inode所指的文件属于这一类,则显式地关闭TRUNC标志位。接着如果有以TRUNC方式打开文件的,则更新nd->dentry->inode的信息

d--__path_lookup_intent_open()

清单8.ext3_read_inode

e--nameidata_to_filp子函数:__dentry_open

这是VFS与实际的文件系统联系的一个关键点。从3.1.1小节分析中可知,调用实际文件系统读取索引节点的方法读取索引节点时,实际文件系统会根据文件的不同类型赋予索引节点不同的文件操作函数集,如普通文件有普通文件对应的一套操作函数,设备文件有设备文件对应的一套操作函数。这样当把对应的索引节点的文件操作函数集赋予文件对象,以后对该文件进行操作时,比如读操作,VFS虽然对各种不同文件都是执行同一个read()操作界面,但是真正读时,内核却知道怎么区分对待不同的文件类型。

清单9.__dentry_open

2、sys_read()

sys_read()系统调用用于从已打开的文件读取数据。如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。图9是sys_read()实现代码中的函数调用关系图。

对文件进行读操作时,需要先打开它。从3.1小结可知,打开一个文件时,会在内存组装一个文件对象,希望对该文件执行的操作方法已在文件对象设置好。所以对文件进行读操作时,VFS在做了一些简单的转换后(由文件描述符得到其对应的文件对象;其核心思想是返回current->files->fd[fd]所指向的文件对象),就可以通过语句file->f_op->read(file,buf,count,pos)轻松调用实际文件系统的相应方法对文件进行读操作了。

四、解决问题

1、跨文件系统的文件操作的基本原理

到此,我们也就能够解释在Linux中为什么能够跨文件系统地操作文件了。举个例子,将vfat格式的磁盘上的一个文件a.txt拷贝到ext3格式的磁盘上,命名为b.txt。这包含两个过程,对a.txt进行读操作,对b.txt进行写操作。读写操作前,需要先打开文件。由前面的分析可知,打开文件时,VFS会知道该文件对应的文件系统格式,以后操作该文件时,VFS会调用其对应的实际文件系统的操作方法。

所以,VFS调用vfat的读文件方法将a.txt的数据读入内存;在将a.txt在内存中的数据映射到b.txt对应的内存空间后,VFS调用ext3的写文件方法将b.txt写入磁盘;从而实现了最终的跨文件系统的复制操作。

2、“一切皆是文件”的实现根本

不论是普通的文件,还是特殊的目录、设备等,VFS都将它们同等看待成文件,通过同一套文件操作界面来对它们进行操作。操作文件时需先打开;打开文件时,VFS会知道该文件对应的文件系统格式;当VFS把控制权传给实际的文件系统时,实际的文件系统再做出具体区分,对不同的文件类型执行不同的操作。这也就是“一切皆是文件”的根本所在。

五、总结

VFS即虚拟文件系统是Linux文件系统中的一个抽象软件层;因为它的支持,众多不同的实际文件系统才能在Linux中共存,跨文件系统操作才能实现。

VFS借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向Linux中不管是普通的文件还是目录、设备、套接字等都提供同样的操作界面,如打开、读写、关闭等。只有当把控制权传给实际的文件系统时,实际的文件系统才会做出区分,对不同的文件类型执行不同的操作。由此可见,正是有了VFS的存在,跨文件系统操作才能执行,Unix/Linux中的“一切皆是文件”的口号才能够得以实现。

一、什么是Linux设备文件系统

首先我们不看定义,定义总是太抽象很难理解,我们先看现象。当我们往开发板上移植了一个新的文件系统之后(假如各种设备驱动也移植好了),启动开发板,我们用串口工具进入开发板,查看系统/dev目录,往往里面没有或者就只有null、console等几个系统必须的设备文件在这儿外,没有任何设备文件了。那我们移植好的各种设备驱动的设备文件怎么没有啊?如果要使用这些设备,那不是要一个一个的去手动的创建这些设备的设备文件节点,这给我们使用设备带来了极为的不便(在之前篇幅中讲的各种设备驱动的移植都是这样)。

设备文件系统就是给我们解决这一问题的关键,他能够在系统设备初始化时动态的在/dev目录下创建好各种设备的设备文件节点(也就是说,系统启动后/dev目录下就有了各种设备的设备文件,直接就可使用了)。除此之外,他还可以在设备卸载后自动的删除/dev下对应的设备文件节点(这对于一些热插拔设备很有用,插上的时候自动创建,拔掉的时候又自动删除)。还有一个好处就是,在我们编写设备驱动的时候,不必再去为设备指定主设备号,在设备注册时用0来动态的获取可用的主设备号,然后在驱动中来实现创建和销毁设备文件(一般在驱动模块加载和卸载函数中来实现)。

二、关于udev

2.4内核使用devfs(设备文件系统)在设备初始化时创建设备文件,设备驱动程序可以指定设备号、所有者、用户空间等信息,devfs运行在内核环境中,并有不少缺点:可能出现主/辅设备号不够,命名不灵活,不能指定设备名称等问题。

而自2.6内核开始,引入了sysfs文件系统。sysfs把连接在系统上的设备和总线组织成一个分级的文件,并提供给用户空间存取使用。udev运行在用户模式,而非内核中。udev的初始化脚本在系统启动时创建设备节点,并且当插入新设备——加入驱动模块——在sysfs上注册新的数据后,udev会创新新的设备节点。

udev是一个工作在用户空间的工具,它能根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的创建,删除,权限等。这些文件通常都定义在/dev目录下,但也可以在配置文件中指定。udev必须内核中的sysfs和tmpfs支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。

注意,udev是通过对内核产生的设备文件修改,或增加别名的方式来达到自定义设备文件的目的。但是,udev是用户模式程序,其不会更改内核行为。也就是说,内核仍然会创建sda,sdb等设备文件,而udev可根据设备的唯一信息来区分不同的设备,并产生新的设备文件(或链接)。而在用户的应用中,只要使用新产生的设备文件即可。

三、udev和devfs设备文件的对比

提到udev,不能不提的就是devfs,下面看一下udev与devfs的区别:

1、udev能够实现所有devfs实现的功能。但udev运行在用户模式中,而devfs运行在内核中。

2、当一个并不存在的/dev节点被打开的时候,devfs一样自动加载驱动程序而udev确不能。udev设计时,是在设备被发现的时候加载模块,而不是当它被访问的时候。devfs这个功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生hotplug事件、加载恰当的驱动,而udev将会注意到这点并且为它创建对应的设备节点。如果你不想让所有的设备驱动停留在内存之中,应该使用其它东西来管理你的模块(如脚本,modules.conf,等等)。其中devfs用的方法导致了大量无用的modprobe尝试,以此程序探测设备是否存在。每个试探性探测都新建一个运行modprobe的进程,而几乎所有这些都是无用的

3、udev是通过对内核产生的设备名增加别名的方式来达到上述目的的。前面说过,udev是用户模式程序,不会更改内核的行为。

因此,内核依然会我行我素地产生设备名如sda,sdb等。但是,udev可以根据设备的其他信息如总线(bus),生产商(vendor)等不同来区分不同的设备,并产生设备文件。udev只要为这个设备文件取一个固定的文件名就可以解决这个问题。在后续对设备的操作中,只要引用新的设备名就可以了。但为了保证最大限度的兼容,一般来说,新设备名总是作为一个对内核自动产生的设备名的符号链接(link)来使用的。

例如:内核产生了sda设备名,而根据信息,这个设备对应于是我的内置硬盘,那我就可以制定udev规则,让udev除了产生/dev/sda设备文件外,另外创建一个符号链接叫/dev/internalHD。这样,我在fstab文件中,就可以用/dev/internalHD来代替原来的/dev/sda了。下次,由于某些原因,这个硬盘在内核中变成了sdb设备名了,那也不用着急,udev还会自动产生/dev/internalHD这个链接,并指向正确的/dev/sdb设备。所有其他的文件像fstab等都不用修改。

而在在2.6内核以前一直使用的是devfs,devfs挂载于/dev目录下,提供了一种类似于文件的方法来管理位于/dev目录下的所有设备,但是devfs文件系统有一些缺点,例如:不确定的设备映射,有时一个设备映射的设备文件可能不同,例如我的U盘可能对应sda有可能对应sdb。

四、udev的工作流程图

下面先看一张流程图:

前面提到设备文件系统udev的工作过程依赖于sysfs文件系统。udev文件系统在用户空间工作,它可以根据sysfs文件系统导出的信息(设备号(dev)等),动态建立和删除设备文件。

sysfs文件系统特点:sysfs把连接在系统上的设备和总线组织成为一个分级的目录及文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性,这其中就包括设备的主次设备号。

那么udev是如何建立设备文件的呢?

a--对于已经编入内核的驱动程序

当被内核检测到的时候,会直接在sysfs中注册其对象;对于编译成模块的驱动程序,当模块载入的时候才会这样做。一旦挂载了sysfs文件系统(挂载到/sys),内建的驱动程序在sysfs注册的数据就可以被用户空间的进程使用,并提供给udev以创建设备节点。

udev初始化脚本负责在Linux启动的时候创建设备节点,该脚本首先将/sbin/udevsend注册为热插拔事件处理程序。热插拔事件本不应该在这个阶段发生,注册udev只是为了以防万一。

然后udevstart遍历/sys文件系统(其属性文件dev中记录这设备的主设备号,与次设备号),并在/dev目录下创建符合描述的设备文件。

例如,/sys/class/tty/vcs/dev里含有"7:0"字符串,udevstart就根据这个字符串创建主设备号为7、次设备号为0的/dev/vcs设备。udevstart创建的每个设备的名字和权限由/etc/udev/rules.d/目录下的文件指定的规则来设置。如果udev找不到所创建设备的权限文件,就将其权限设置为缺省的660,所有者为root:root。

b--编译成模块的驱动程序

前面我们提到了"热插拔事件处理程序"的概念,当内核检测到一个新设备连接时,内核会产生一个热插拔事件,并在/proc/sys/kernel/hotplug文件里查找处理设备连接的用户空间程序。udev初始化脚本将udevsend注册为该处理程序。当产生热插拔事件的时候,内核让udev在/sys文件系统里检测与新设备的有关信息,并为新设备在/dev里创建项目。

大多数Linux发行版通过/etc/modules.conf配置文件来处理模块加载,对某个设备节点的访问导致相应的内核模块被加载。对udev这个方法就行不通了,因为在模块加载前,设备节点根本不存在。为了解决这个问题,在LFS-Bootscripts软件包里加入了modules启动脚本,以及/etc/sysconfig/modules文件。通过在modules文件里添加模块名,就可以在系统启动的时候加载这些模块,这样udev就可以检测到设备,并创建相应的设备节点了。如果插入的设备有一个驱动程序模块但是尚未加载,Hotplug软件包就有用了,它就会响应上述的内核总线驱动热插拔事件并加载相应的模块,为其创建设备节点,这样设备就可以使用了。

五、创建和配置mdev

mdev是udev的简化版本,是busybox中所带的程序,最适合用在嵌入式系统,而udev一般都用在PC上的Linux中,相对mdev来说要复杂些;

1、我们应该明白,不管是udev还是mdev,他们就是一个应用程序,就跟其他应用程序一样(比如:Boa服务),配置了就可以使用了。为了方便起见,我们就使用busybox自带的一个mdev,这样在配置编译busybox时,只要将mdev的支持选项选上,编译后就包含了mdev设备文件系统的应用(当然你也可以不使用busybox自带的,去下载udev的源码进行编译移植。

#cdbusybox-1.13.0/#makemenuconfig

LinuxSystemUtilities--->[*]mdev[*]Support/etc/mdev.conf[*]Supportsubdirs/symlinks[*]Supportregularexpressionssubstitutionswhenrenamingdevice[*]Supportcommandexecutionatdeviceaddition/removal

2、udev或者mdev需要内核sysfs和tmpfs虚拟文件系统的支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。所以在/etc/fstab配置文件中添加如下内容(红色部分):

#devicemount-pointtypeoptionsdumpfsckorder#----------------------------------------------------------------procfs/procprocdefaults00sysfs/syssysfsdefaults00tmpfs/dev/shmtmpfsdefaults00usbfs/proc/bus/usbusbfsdefaults00ramfs/devramfsdefaults00none/dev/ptsdevptsmode=062200

然后启动/sbin目录下的mdev应用对系统的设备进行搜索(红色部分)。

#Mountvirtualfilesystem/bin/mount-tprocprocfs/proc/bin/mount-n-tsysfssysfs/sys/bin/mount-n-tusbfsusbfs/proc/bus/usb/bin/mount-tramfsramfs/dev#Makedir/bin/mkdir-p/dev/pts/bin/mkdir-p/dev/shm/bin/mkdir-p/var/log/bin/mount-n-tdevptsnone/dev/pts-omode=0622/bin/mount-n-ttmpfstmpfs/dev/shm#Makedevicenodeecho/sbin/mdev>/proc/sys/kernel/hotplug/sbin/mdev-s

a--"mdev-s"的含义是扫描/sys中所有的类设备目录,如果在目录中有含有名为“dev”的文件,且文件中包含的是设备号,则mdev就利用这些信息为该设备在/dev下创建设备节点文件;

b--"echo/sbin/mdev>/proc/sys/kernel/hotplug"的含义是当有热插拔事件产生时,内核就会调用位于/sbin目录的mdev。这时mdev通过环境变量中的ACTION和DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的那个目录。接着,会看这个目录中是否有“dev”的属性文件。如果有就利用这些信息为这个设备在/dev下创建设备节点文件。

例如在之前篇幅的按键驱动中添加(红色部分):

5、至于mdev的配置文件/etc/mdev.conf,这个可有可无,只是设定设备文件的一些规则。我这里就不管他了,让他为空好了。6、完成以上步骤后,重新编译文件系统,下载到开发板上,启动开发板后进入开发板的/dev目录查看,就会有很多系统设备节点在这里产生了,我们就可以直接使用这些设备节点了。

提到sysfs文件系统,必须先需要了解的是Linux设备模型,什么是Linux设备模型呢?

一、Linux设备模型

1、设备模型概述

从2.6版本开始,Linux开发团队便为内核建立起一个统一的设备模型。在以前的内核中没有独立的数据结构用来让内核获得系统整体配合的信息。尽管缺乏这些信息,在多数情况下内核还是能正常工作的。然而,随着拓扑结构越来越复杂,以及要支持诸如电源管理等新特性的需求,向新版本的内核明确提出了这样的要求:需要有一个对系统结构的一般性抽象描述,即设备模型。

目的

I设备、驱动、总线等彼此之间关系错综复杂。如果想让内核运行流畅,那就必须为每个模块编码实现这些功能。如此一来,内核将变得非常臃肿、冗余。而设备模型的理念即是将这些代码抽象成各模块共用的框架,这样不但代码简洁了,也可让设备驱动开发者摆脱这本让人头痛但又必不可少的一劫,将有限的精力放于设备差异性的实现。

II设备模型用类的思想将具有相似功能的设备放到一起管理,并将相似部分萃取出来,使用一份代码实现。从而使结构更加清晰,简洁。

III动态分配主从设备号,有效解决设备号的不足。设备模型实现了只有设备在位时才为其分配主从设备号,这与之前版本为每个设备分配一个主从设备号不同,使得有限的资源得到合理利用。

IV设备模型提供sysfs文件系统,以文件的方式让本是抽象复杂而又无法捉摸的结构清晰可视起来。同时也给用户空间程序配置处于内核空间的设备驱动提供了一个友善的通道。

V程序具有随意性,同一个功能,不同的人实现的方法和风格各不相同,设备驱动亦是如此。大量的设备亦若实现方法流程均不相同,对以后的管理、重构将是难以想象的工作量。设备模型恰是提供了一个模板,一个被证明过的最优的思路和流程,这减少了开发者设计过程中不必要的错误,也给以后的维护扫除了障碍。

2、设备模型结构

如表,Linux设备模型包含以下四个基本结构:

类型

所包含的内容

内核数据结构

对应/sys项

设备(Devices)

设备是此模型中最基本的类型,以设备本身的连接按层次组织

structdevice

/sys/devices/*/*/.../

驱动

(Drivers)

在一个系统中安装多个相同设备,只需要一份驱动程序的支持

structdevice_driver

/sys/bus/pci/drivers/*/

总线

(Bus)

在整个总线级别对此总线上连接的所有设备进行管理

structbus_type

/sys/bus/*/

类别(Classes)

这是按照功能进行分类组织的设备层次树;如USB接口和PS/2接口的鼠标都是输入设备,都会出现在/sys/class/input/下

structclass

/sys/class/*/

device、driver、bus、class是组成设备模型的基本数据结构。kobject是构成这些基本结构的核心,kset又是相同类型结构kobject的集合。kobject和kset共同组成了sysfs的底层数据体系。本节采用从最小数据结构到最终组成一个大的模型的思路来介绍。当然,阅读时也可先从Device、Driver、Bus、Class的介绍开始,先总体了解设备模型的构成,然后再回到kobject和kset,弄清它们是如何将Device、Driver、Bus、Class穿插链接在一起的,以及如何将这些映像成文件并最终形成一个sysfs文件系统。

二、sys文件系统

sysfs是一个基于内存的文件系统,它的作用是将内核信息以文件的方式提供给用户程序使用。

sysfs可以看成与proc,devfs和devpty同类别的文件系统,该文件系统是虚拟的文件系统,可以更方便对系统设备进行管理。它可以产生一个包含所有系统硬件层次视图,与提供进程和状态信息的proc文件系统十分类似。

sysfs把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核的数据结构以及它们的属性。sysfs的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括block,bus,drivers,class,power和firmware等.

sysfs提供一种机制,使得可以显式的描述内核对象、对象属性及对象间关系。sysfs有两组接口,一组针对内核,用于将设备映射到文件系统中,另一组针对用户程序,用于读取或操作这些设备。表2描述了内核中的sysfs要素及其在用户空间的表现:

sysfs在内核中的组成要素

在用户空间的显示

内核对象(kobject)

目录

对象属性(attribute)

文件

对象关系(relationship)

链接(SymbolicLink)

sysfs目录结构:

/sys下的子目录

/sys/devices

这是内核对系统中所有设备的分层次表达模型,也是/sys文件系统管理设备的最重要的目录结构;

/sys/dev

这个目录下维护一个按字符设备和块设备的主次号码(major:minor)链接到真实的设备(/sys/devices下)的符号链接文件;

/sys/bus

这是内核设备按总线类型分层放置的目录结构,devices中的所有设备都是连接于某种总线之下,在这里的每一种具体总线之下可以找到每一个具体设备的符号链接,它也是构成Linux统一设备模型的一部分;

/sys/class

这是按照设备功能分类的设备模型,如系统所有输入设备都会出现在/sys/class/input之下,而不论它们是以何种总线连接到系统。它也是构成Linux统一设备模型的一部分;

/sys/kernel

这里是内核所有可调整参数的位置,目前只有uevent_helper,kexec_loaded,mm,和新式的slab分配器等几项较新的设计在使用它,其它内核可调整参数仍然位于sysctl(/proc/sys/kernel)接口中;

/sys/module

这里有系统中所有模块的信息,不论这些模块是以内联(inlined)方式编译到内核映像文件(vmlinuz)中还是编译为外部模块(ko文件),都可能会出现在/sys/module中:

/sys/power

这里是系统中电源选项,这个目录下有几个属性文件可以用于控制整个机器的电源状态,如可以向其中写入控制命令让机器关机、重启等。

表3:sysfs目录结构

三、深入理解sysfs文件系统

sysfs是一个特殊文件系统,并没有一个实际存放文件的介质。

1、kobject结构

sysfs文件系统与kobject结构紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。

Kobject是Linux2.6引入的新的设备管理机制,在内核中由structkobject表示。通过这个数据结构使所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux2.6设备模型的核心结构,Kobject是组成设备模型的基本结构。类似于C++中的基类,它嵌入于更大的对象的对象中,用来描述设备模型的组件。如bus,devices,drivers等。都是通过kobject连接起来了,形成了一个树状结构。这个树状结构就与/sys向对应。

2、sysfs如何读写kobject结构

sysfs就是利用VFS的接口去读写kobject的层次结构,建立起来的文件系统。kobject的层次结构的注册与注销XX_register()形成的。文件系统是个很模糊广泛的概念,linux把所有的资源都看成是文件,让用户通过一个统一的文件系统操作界面,也就是同一组系统调用,对属于不同文件系统的文件进行操作。这样,就可以对用户程序隐藏各种不同文件系统的实现细节,为用户程序提供了一个统一的,抽象的,虚拟的文件系统界面,这就是所谓"VFS(VirtualFilesystemSwitch)"。这个抽象出来的接口就是一组函数操作。我们要实现一种文件系统就是要实现VFS所定义的一系列接口,file_operations,dentry_operations,inode_operations等,供上层调用。

file_operations是描述对每个具体文件的操作方法(如:读,写);

dentry_operations结构体指明了VFS所有目录的操作方法;

inode_operations提供所有结点的操作方法。

举个例子,我们写C程序,open(“hello.c”,O_RDONLY),它通过系统调用的流程是这样的

open()->系统调用->sys_open()->filp_open()->dentry_open()->file_operations->open()

不同的文件系统,调用不同的file_operations->open(),在sysfs下就是sysfs_open_file()。我们使用不同的文件系统,就是将它们各自的文件信息都抽象到dentry和inode中去。这样对于高层来说,我们就可以不关心底层的实现,我们使用的都是一系列标准的函数调用。这就是VFS的精髓,实际上就是面向对象。

注意sysfs是典型的特殊文件。它存储的信息都是由系统动态的生成的,它动态的包含了整个机器的硬件资源情况。从sysfs读写就相当于向kobject层次结构提取数据。

下面是详细分析:

a--sysfs_dirent是组成sysfs单元的基本数据结构,它是sysfs文件夹或文件在内存中的代表。sysfs_dirent只表示文件类型(文件夹/普通文件/二进制文件/链接文件)及层级关系,其它信息都保存在对应的inode中。我们创建或删除一个sysfs文件或文件夹事实上只是对以sysfs_dirent为节点的树的节点的添加或删除。sysfs_dirent数据结构如下:

b--inode(indexnode)中保存了设备的主从设备号、一组文件操作函数和一组inode操作函数。

文件操作比较常见:open、read、write等。inode操作在sysfs文件系统中只针对文件夹实现了两个函数一个是目录下查找inode函数(.lookup=sysfs_lookup),该函数在找不到inode时会创建一个,并用sysfs_init_inode为其赋值;另一个是设置inode属性函数(.setattr=sysfs_setattr),该函数用于修改用户的权限等。inode结构如下:

c--dentry(directoryentry)的中文名称是目录项,是Linux文件系统中某个索引节点(inode)的链接。

这个索引节点可以是文件,也可以是目录。引入dentry的目的是加快文件的访问。dentry数据结构如下:

sysfs_dirent、inode、dentry三者关系:

如上图sysfs超级块sysfs_sb、dentry根目录root、sysfs_direct根目录sysfs_root都是在sysfs初始化时创建。

sysfs_root下的子节点是添加设备对象或对象属性时调用sysfs_create_dir/sysfs_create_file创建的,同时会申请对应的inode的索引号s_ino。注意此时并未创建inode。

inode是在用到的时候调用sysfs_get_inode函数创建并依据sysfs_sb地址和申请到的s_ino索引计算散列表位置放入其中。

dentry的子节点也是需要用的时候才会创建。比如open文件时,会调用path_walk根据路径一层层的查找指定dentry,如果找不到,则创建一个,并调用父dentry的inodelookup函数(sysfs文件系统的为sysfs_lookup)查找对应的子inode填充指定的dentry。

这里有必要介绍一下sysfs_lookup的实现,以保证我们更加清晰地了解这个过程,函数主体如下:

四、实例分析

a--sysfs文件open流程

图3-2是从网上找到的,清晰地描述了file和dentry以及inode之间的关系

图3-2:file、dentry、inode关系

进程每打开一个文件,就会有一个file结构与之对应。同一个进程可以多次打开同一个文件而得到多个不同的file结构,file结构描述了被打开文件的属性,读写的偏移指针等等当前信息。

两个不同的file结构可以对应同一个dentry结构。进程多次打开同一个文件时,对应的只有一个dentry结构。dentry结构存储目录项和对应文件(inode)的信息。

在存储介质中,每个文件对应唯一的inode结点,但是,每个文件又可以有多个文件名。即可以通过不同的文件名访问同一个文件。这里多个文件名对应一个文件的关系在数据结构中表示就是dentry和inode的关系。

inode中不存储文件的名字,它只存储节点号;而dentry则保存有名字和与其对应的节点号,所以就可以通过不同的dentry访问同一个inode。

b--sysfs文件read/write流程

sysfs与普通文件系统的最大差异是,sysfs不会申请任何内存空间来保存文件的内容。事实上再不对文件操作时,文件是不存在的。只有用户读或写文件时,sysfs才会申请一页内存(只有一页),用于保存将要读取的文件信息。如果作读操作,sysfs就会调用文件的父对象(文件夹kobject)的属性处理函数kobject->ktype->sysfs_ops->show,然后通过show函数来调用包含该对象的外层设备(或驱动、总线等)的属性的show函数来获取硬件设备的对应属性值,然后将该值拷贝到用户空间的buff,这样就完成了读操作。写操作也类似,都要进行内核空间à用户空间内存的拷贝,以保护内核代码的安全运行。

图为用户空间程序读sysfs文件的处理流程,其他操作类似:

一、platform总线、设备与驱动

在Linux2.6的设备驱动模型中,关心总线、设备和驱动3个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设等确不依附于此类总线。基于这一背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动成为platform_driver。

注意,所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段,例如,在S3C6410处理器中,把内部集成的I2C、RTC、SPI、LCD、看门狗等控制器都归纳为platform_device,而它们本身就是字符设备。

基于Platform总线的驱动开发流程如下:

a--定义初始化platformbus

b--定义各种platformdevices

c--注册各种platformdevices

2、设备的驱动---platform_driver结构体

这个结构体中包含probe()、remove()、shutdown()、suspend()、resume()函数,通常也需要由驱动实现

3、系统中为platform总线定义了一个bus_type的实例---platform_bus_type

匹配platform_device和platform_driver主要看二者的name字段是否相同。对platform_device的定义通常在BSP的板文件中实现,在板文件中,将platform_device归纳为一个数组,最终通过platform_add_devices()函数统一注册。

platform_add_devices()函数可以将平台设备添加到系统中,这个函数的原型为:

该函数的第一个参数为平台设备数组的指针,第二个参数为平台设备的数量,它内部调用了platform_device_register()函数用于注册单个的平台设备。

a--platformbus总线先被kenrel注册。

b--系统初始化过程中调用platform_add_devices或者platform_device_register,将平台设备(platformdevices)注册到平台总线中(platformbus)

c--平台驱动(platformdriver)与平台设备(platformdevice)的关联是在platform_driver_register或者driver_register中实现,一般这个函数在驱动的初始化过程调用。通过这三步,就将平台总线,设备,驱动关联起来。

二.Platform初始化

系统启动时初始化时创建了platform_bus总线设备和platform_bus_type总线,platform总线是在内核初始化的时候就注册进了内核。

内核初始化函数kernel_init()中调用了do_basic_setup(),该函数中调用driver_init(),该函数中调用platform_bus_init(),我们看看platform_bus_init()函数:

总线类型match函数是在设备匹配驱动时调用,uevent函数在产生事件时调用。

platform_match函数在当属于platform的设备或者驱动注册到内核时就会调用,完成设备与驱动的匹配工作

不难看出,如果pdrv的id_table数组中包含了pdev->name,或者drv->name和pdev->name名字相同,都会认为是匹配成功。id_table数组是为了应对那些对应设备和驱动的drv->name和pdev->name名字不同的情况。

再看看platform_uevent()函数:platform_uevent热插拔操作函数

添加了MODALIAS环境变量,我们回顾一下:platform_bus.parent->kobj->kset->uevent_ops为device_uevent_ops,bus_uevent_ops的定义如下

当调用device_add()时会调用kobject_uevent(&dev->kobj,KOBJ_ADD)产生一个事件,这个函数中会调用相应的kset_uevent_ops的uevent函数

三.Platform设备的注册

我们在设备模型的分析中知道了把设备添加到系统要调用device_initialize()和platform_device_add(pdev)函数。

Platform设备的注册分两种方式:

a--对于platform设备的初注册,内核源码提供了platform_device_add()函数,输入参数platform_device可以是静态的全局设备,它是进行一系列的操作后调用device_add()将设备注册到相应的总线(platform总线)上,内核代码中platform设备的其他注册函数都是基于这个函数,如platform_device_register()、platform_device_register_simple()、platform_device_register_data()等。

无论哪一种platform_device,最终都将通过platform_device_add这册到platform总线上。区别在于第二步:其实platform_device_add()包括device_add(),不过要先注册resources,然后将设备挂接到特定的platform总线。

1、第一种平台设备注册方式

platform_device是静态的全局设备,即platform_device结构的成员已经初始化完成。直接将平台设备注册到platform总线上。platform_device_register和device_register的区别:

a--主要是有没有resource的区别,前者的结构体包含后面,并且增加了structresource结构体成员,后者没有。platform_device_register在device_register的基础上增加了structresource部分的注册。

由此。可以看出,platform_device---paltform_driver_register机制与device-driver的主要区别就在于resource。前者适合于具有独立资源设备的描述,后者则不是。

b--其实linux的各种其他驱动机制的基础都是device_driver。只不过是增加了部分功能,适合于不同的应用场合.

2、第二种平台设备注册方式

先分配一个platform_device结构,对其进行资源等的初始化;之后再对其进行注册,再调用platform_device_register()函数

一个更好的方法是,通过下面的函数platform_device_register_simple()动态创建一个设备,并把这个设备注册到系统中:

该函数就是调用了platform_device_alloc()和platform_device_add()函数来创建的注册platformdevice,函数也根据res参数分配资源,看看platform_device_add_resources()函数:

四.Platform设备驱动的注册

我们在设备驱动模型的分析中已经知道驱动在注册要调用driver_register(),platformdriver的注册函数platform_driver_register()同样也是进行其它的一些初始化后调用driver_register()将驱动注册到platform_bus_type总线上.

然后设定了platform_driver内嵌的driver的probe、remove、shutdown函数。

总结:

1、从这三个函数的代码可以看到,又找到了相应的platform_driver和platform_device,然后调用platform_driver的probe、remove、shutdown函数。这是一种高明的做法:

在不针对某个驱动具体的probe、remove、shutdown指向的函数,而通过上三个过度函数来找到platform_driver,然后调用probe、remove、shutdown接口。

如果设备和驱动都注册了,就可以通过bus->match、bus->probe或driver->probe进行设备驱动匹配了。2、驱动注册的时候platform_driver_register()->driver_register()->bus_add_driver()->driver_attach()->bus_for_each_dev(),

对每个挂在虚拟的platformbus的设备作__driver_attach()->driver_probe_device()->drv->bus->match()==platform_match()->比较strncmp(pdev->name,drv->name,BUS_ID_SIZE),如果相符就调用platform_drv_probe()->driver->probe(),如果probe成功则绑定该设备到该驱动。

一、platform驱动的工作过程

platform模型驱动编程,需要实现platform_device(设备)与platform_driver(驱动)在platform(虚拟总线)上的注册、匹配,相互绑定,然后再做为一个普通的字符设备进行相应的应用,总之如果编写的是基于字符设备的platform驱动,在遵循并实现platform总线上驱动与设备的特定接口的情况下,最核心的还是字符设备的核心结构:cdev、file_operations(他包含的操作函数接口)、dev_t(设备号)、设备文件(/dev)等,因为用platform机制编写的字符驱动,它的本质是字符驱动。

我们要记住,platform驱动只是在字符设备驱动外套一层platform_driver的外壳。

在一般情况下,2.6内核中已经初始化并挂载了一条platform总线在sysfs文件系统中。那么我们编写platform模型驱动时,需要完成两个工作:

a--实现platform驱动

b--实现platform设备

然而在实现这两个工作的过程中还需要实现其他的很多小工作,在后面介绍。platform模型驱动的实现过程核心架构就很简单,如下所示:

platform驱动模型三个对象:platform总线、platform设备、platform驱动。

platform总线对应的内核结构:structbus_type-->它包含的最关键的函数:match()(要注意的是,这块由内核完成,我们不参与)

platform设备对应的内核结构:structplatform_device-->注册:platform_device_register(unregister)

platform驱动对应的内核结构:structplatform_driver-->注册:platform_driver_register(unregister)

那具体platform驱动的工作过程是什么呢:

设备(或驱动)注册的时候,都会引发总线调用自己的match函数来寻找目前platform总线是否挂载有与该设备(或驱动)名字匹配的驱动(或设备),如果存在则将双方绑定;

如果先注册设备,驱动还没有注册,那么设备在被注册到总线上时,将不会匹配到与自己同名的驱动,然后在驱动注册到总线上时,因为设备已注册,那么总线会立即匹配与绑定这时的同名的设备与驱动,再调用驱动中的probe函数等;

如果是驱动先注册,同设备驱动一样先会匹配失败,匹配失败将导致它的probe函数暂不调用,而是要等到设备注册成功并与自己匹配绑定后才会调用。

二、实现platform驱动与设备的详细过程

1、思考问题?

在分析platform之前,可以先思考一下下面的问题:

a--为什么要用platform驱动?不用platform驱动可以吗?

b--设备驱动中引入platform概念有什么好处?

现在先不回答,看完下面的分析就明白了,后面会附上总结。

2、platform_device结构体VSplatform_driver结构体

这两个结构体分别描述了设备和驱动,二者有什么关系呢?先看一下具体结构体对比

前面提到,实现platform模型的过程就是总线对设备和驱动的匹配过程。打个比方,就好比相亲,总线是红娘,设备是男方,驱动是女方:

a--红娘(总线)负责男方(设备)和女方(驱动)的撮合;

b--男方(女方)找到红娘,说我来登记一下,看有没有合适的姑娘(汉子)——设备或驱动的注册;

c--红娘这时候就需要看看有没有八字(二者的name字段)匹配的姑娘(汉子)——match函数进行匹配,看name是否相同;

d--如果八字不合,就告诉男方(女方)没有合适的对象,先等着,别急着乱做事——设备和驱动会等待,直到匹配成功;

e--终于遇到八字匹配的了,那就结婚呗!接完婚,男方就向女方交代,我有多少存款,我的房子在哪,钱放在哪等等(structresource*resource),女方说好啊,于是去房子里拿钱,去给男方买菜啦,给自己买衣服、化妆品、首饰啊等等(int(*probe)(structplatform_device*)匹配成功后驱动执行的第一个函数),当然如果男的跟小三跑了(设备卸载),女方也不会继续待下去的(int(*remove)(structplatform_device*))。

3、设备资源结构体

在structplatform_device结构体中有一重要成员structresource*resource

flags指资源类型,我们常用的是IORESOURCE_MEM、IORESOURCE_IRQ这两种。start和end的含义会随着flags而变更,如

a--flags为IORESOURCE_MEM时,start、end分别表示该platform_device占据的内存的开始地址和结束值;

b--flags为IORESOURCE_IRQ时,start、end分别表示该platform_device使用的中断号的开始地址和结束值;

下面看一个实例:

引入platform模型符合Linux设备模型——总线、设备、驱动,设备模型中配套的sysfs节点都可以用,方便我们的开发;当然你也可以选择不用,不过就失去了一些platform带来的便利;

三、实例

1、device.c

2、driver.c

3、makefile

4、test.c

DeviceTree是一种描述硬件的数据结构,设备树源(DeviceTreeSource)文件(以.dts结尾)就是用来描述目标板硬件信息的。DeviceTree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在DeviceTree中,可描述的信息包括(原先这些信息大多被hardcode到kernel中)。

一、设备树基础概念

1、基本数据格式

devicetree是一个简单的节点和属性树,属性是键值对,节点可以包含属性和子节点。下面是一个.dts格式的简单设备树。

该树并未描述任何东西,也不具备任何实际意义,但它却揭示了节点和属性的结构。即:

a--一个的根节点:'/',两个子节点:node1和node2;node1的子节点:child-node1和child-node2,一些属性分散在树之间。

b--属性是一些简单的键值对(key-valuepairs):value可以为空也可以包含任意的字节流。而数据类型并没有编码成数据结构,有一些基本数据表示可以在devicetree源文件中表示。

c--文本字符串(null终止)用双引号来表示:string-property="astring"

d--“Cells”是由尖括号分隔的32位无符号整数:cell-property=<0xbeef1230xabcd1234>

e--二进制数据是用方括号分隔:binary-property=[0x010x230x450x67];

f--不同格式的数据可以用逗号连接在一起:mixed-property="astring",[0x010x230x450x67],<0x12345678>;

g--逗号也可以用来创建字符串列表:string-list="redfish","bluefish";

二、设备在devicetree中的描述

系统中的每个设备由devicetree的一个节点来表示;

1、节点命名

如果节点描述的设备有地址的话,就应该加上unit-address,unit-address通常是用来访问设备的主地址,并在节点的reg属性中被列出。后面我们将谈到reg属性。

2、设备

接下来将为设备树添加设备节点:

在上面的设备树中,系统中的设备节点已经添加进来,树的层次结构反映了设备如何连到系统中。外部总线上的设备就是外部总线节点的子节点,i2c设备是i2c总线控制节点的子节点。总的来说,层次结构表现的是从CPU视角来看的系统视图。在这里这棵树是依然是无效的。它缺少关于设备之间的连接信息。稍后将添加这些数据。

设备树中应当注意:每个设备节点有一个compatible属性。flash节点的compatible属性有两个字符串。请阅读下一节以了解更多内容。之前提到的,节点命名应当反映设备的类型,而不是特定型号。请参考ePAPR规范2.2.2节的通用节点命名,应优先使用这些命名。

3、compatible属性

树中的每一个代表了一个设备的节点都要有一个compatible属性。compatible是OS用来决定绑定到设备的设备驱动的关键。

compatible是字符串的列表。列表中的第一个字符串指定了","格式的节点代表的确切设备,第二个字符串代表了与该设备兼容的其他设备。例如,FreescaleMPC8349SoC有一个串口设备实现了NationalSemiconductorns16550寄存器接口。因此MPC8349串口设备的compatible属性为:compatible="fsl,mpc8349-uart","ns16550"。在这里,fsl,mpc8349-uart指定了确切的设备,ns16550表明它与NationalSemiconductor16550UART是寄存器级兼容的。

注:由于历史原因,ns16550没有制造商前缀,所有新的compatible值都应使用制造商的前缀。这种做法使得现有的设备驱动程序可以绑定到一个新设备上,同时仍能唯一准确的识别硬件。

4、编址

可编址的设备使用下列属性来将地址信息编码进设备树:

reg

#address-cells

#size-cells

每个可寻址的设备有一个reg属性,即以下面形式表示的元组列表:

reg=

每个元组,。每个地址值由一个或多个32位整数列表组成,被称做cells。同样地,长度值可以是cells列表,也可以为空。

既然address和length字段是大小可变的变量,父节点的#address-cells和#size-cells属性用来说明各个子节点有多少个cells。换句话说,正确解释一个子节点的reg属性需要父节点的#address-cells和#size-cells值。

5、内存映射设备

与CPU节点中的单一地址值不同,内存映射设备会被分配一个它能响应的地址范围。#size-cells用来说明每个子节点种reg元组的长度大小。

在下面的示例中,每个地址值是1cell(32位),并且每个的长度值也为1cell,这在32位系统中是非常典型的。64位计算机可以在设备树中使用2作为#address-cells和#size-cells的值来实现64位寻址。

每个设备都被分配了一个基地址及该区域大小。本例中的GPIO设备地址被分成两个地址范围:0x101f3000~0x101f3fff和0x101f4000~0x101f400f。

三、设备树在platform设备驱动开发中的使用解析

1、设备树对platform中platform_device的替换

其实我们可以看到,DeviceTree是用来描述设备信息的,每一个设备在设备树中是以节点的形式表现出来;而在上面的platform设备中,我们利用platform_device来描述一个设备,我们可以看一下二者的对比

可以看到设备树中的设备节点完全可以替代掉platform_device。

2、有了设备树,如何实现device与driver的匹配?

我们在上一篇还有platform_device中,是利用.name来实现device与driver的匹配的,但现在设备树替换掉了device,那我们将如何实现二者的匹配呢?有了设备树后,platform比较的名字存在哪?

我们先看一下原来是如何匹配的,platform_bus_type下有个match成员,platform_match定义如下

其中又调用了of_driver_match_device(dev,drv),其定义如下:

其调用of_match_device(drv->of_match_table,dev),继续追踪下去,注意这里的参数drv->of_match_table

又调用of_match_node(matches,dev->of_node),其中matches是structof_device_id类型的

找到match=__of_match_node(matches,node);注意着里的node是structdevice_node类型的

继续追踪下去

看这句prop=__of_find_property(device,"compatible",NULL);

可以发先追溯到底,是利用"compatible"来匹配的,即设备树加载之后,内核会自动把设备树节点转换成platform_device这种格式,同时把名字放到of_node这个地方。

platform_driver部分

匹配的方式发生了改变,那我们的platform_driver也要修改了

基于设备树的driver的结构体的填充:

原来的driver是这样的,可以对比一下

我们在arch/arm/boot/dts/exynos4412-fs4412.dts中添加

然后,将设备树下载到0x42000000处,并加载驱动insmoddriver.ko,测试下驱动。

Linux设备驱动中必须要解决的一个问题是多个进程对共享的资源的并发访问,并发的访问会导致竞态,即使是经验丰富的驱动工程师也常常设计出包含并发问题bug的驱动程序。

一、基础概念

a--并发(concurrency):并发指的是多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(racecondition);

b--竞态(racecondition):竞态简单的说就是两个或两个以上的进程同时访问一个资源,同时引起资源的错误;

c--临界区(CriticalSection):每个进程中访问临界资源的那段代码称为临界区;

d--临界资源:一次仅允许一个进程使用的资源称为临界资源;多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用;

2、并发产生的场合

a--对称多处理器(SMP)的多个CPU

SMP是一种共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和储存器,这里可以实现真正的并行;

b--单CPU内进程与抢占它的进程

一个进程在内核执行的时候有可能被另一个高优先级进程打断;

c--中断和进程之间

中断可以打断正在执行的进程,如果中断处理函数程序访问进程正在访问的资源,则竞态也会发生;

3、解决竞态问题的途径

解决竞态问题的途径最重要的是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。

Linux设备中提供了可采用的互斥途径来避免这种竞争。主要有原子操作,信号量,自旋锁。

那么这三种有什么相同的地方,有什么区别呢?适用什么不同的场合呢?会带来什么边际效应?要彻底弄清楚这些问题,要从其所处的环境来进行细化分类处理。是UP(单CPU)还是SMP(多CPU);是抢占式内核还是非抢占式内核;是在中断上下文不是进程上下文。似交通信号灯一样的措施来避免这种竞争。

先看一下三种并发机制的简单概念:

原子锁:原子操作不可能被其他的任务给调开,一切(包括中断),针对单个变量。

自旋锁:使用忙等待锁来确保互斥锁的一种特别方法,针对是临界区。

信号量:包括一个变量及对它进行的两个原语操作,此变量就称之为信号量,针对是临界区。

二、并发处理途径详解

1、中断屏蔽

在单CPU范围内避免静态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞争条件的发生。具体而言

a--中断屏蔽将使得中断和进程之间的并发不再发生;

b--由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免;

中断屏蔽的使用方法:

但是要注意:

b--宜与自旋锁联合使用。

所以,不建议使用中断屏蔽。

2、原子操作

在linux中,原子变量的定义如下:

typedefstruct{volatileintcounter;}atomic_t;关键字volatile用来暗示GCC不要对该类型做数据优化,所以对这个变量counte的访问都是基于内存的,不要将其缓冲到寄存器中。存储到寄存器中,可能导致内存中的数据已经改变,而寄存其中的数据没有改变。

原子整型操作:

1)定义atomic_t变量:

#defineATOMIC_INIT(i)((atomic_t){(i)})atomic_tv=ATOMIC_INIT(0);//定义原子变量v并初始化为0

2)设置原子变量的值:

#defineatomic_set(v,i)((v)->counter=(i))voidatomic_set(atomic_t*v,inti);//设置原子变量的值为i3)获取原子变量的值:

#defineatomic_read(v)((v)->counter+0)atomic_read(atomic_t*v);//返回原子变量的值4)原子变量加/减:

static__inline__voidatomic_add(inti,atomic_t*v);//原子变量增加istatic__inline__voidatomic_sub(inti,atomic_t*v);//原子变量减少i5)原子变量自增/自减:

#defineatomic_inc(v)atomic_add(1,v);//原子变量加1#defineatomic_dec(v)atomic_sub(1,v);//原子变量减16)操作并测试:

//这些操作对原子变量执行自增,自减,减操作后测试是否为0,是返回true,否则返回false#defineatomic_inc_and_test(v)(atomic_add_return(1,(v))==0)staticinlineintatomic_add_return(inti,atomic_t*v)原子操作的优点编写简单;缺点是功能太简单,只能做计数操作,保护的东西太少。下面看一个实例:

3、自旋锁

自旋锁是专为防止多处理器并发而引入的一种锁,它应用于中断处理等部分。对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用(忙等待,即当一个进程位于其临界区内,任何试图进入其临界区的进程都必须在进入代码连续循环)。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

1)自旋锁的使用:

spinlock_tspin;//定义自旋锁spin_lock_init(lock);//初始化自旋锁spin_lock(lock);//成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放spin_trylock(lock);//成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转"spin_unlock(lock);//释放自旋锁下面是一个实例:

自旋锁主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持的抢占的系统,自旋锁退化为空操作(因为自旋锁本身就需进行内核抢占)。在单CPU和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。由于内核可抢占的单CPU系统的行为实际很类似于SMP系统,因此,在这样的单CPU系统中使用自旋锁仍十分重要。

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部的影响。为了防止这种影响。为了防止影响,就需要用到自旋锁的衍生。

2)注意事项:

b--自旋锁不能递归使用。自旋锁被设计成在不同线程或者函数之间同步。这是因为,如果一个线程在已经持有自旋锁时,其处于忙等待状态,则已经没有机会释放自己持有的锁了。如果这时再调用自身,则自旋锁永远没有执行的机会了,即造成“死锁”。

【自旋锁导致死锁的实例】

1)a进程拥有自旋锁,在内核态阻塞的,内核调度进程b,b也要或得自旋锁,b只能自旋,而此时抢占已经关闭了,a进程就不会调度到了,b进程永远自旋。

2)进程a拥有自旋锁,中断来了,cpu执行中断,中断处理函数也要获得锁访问共享资源,此时也获得不到锁,只能死锁。

3)内核抢占

内核抢占是上面提到的一个概念,不管当前进程处于内核态还是用户态,都会调度优先级高的进程运行,停止当前进程;当我们使用自旋锁的时候,抢占是关闭的。

4)自旋锁有几个重要的特性:

a--被自旋锁保护的临界区代码执行时不能进入休眠。

b--被自旋锁保护的临界区代码执行时是不能被被其他中断中断。

c--被自旋锁保护的临界区代码执行时,内核不能被抢占。

从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器。

4、信号量

linux中,提供了两种信号量:一种用于内核程序中,一种用于应用程序中。这里只讲属前者

信号量和自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程时才能够进入临界区,执行临界代码。信号量和自旋锁的最大区别在于:当一个进程试图去获得一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待。

信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。

1)信号量的实现:

在linux中,信号量的定义如下:

structsemaphore{spinlock_tlock;//用来对count变量起保护作用。unsignedintcount;//大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源。structlist_headwait_list;//存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。};2)信号量的使用:

staticinlinevoidsema_init(structsemaphore*sem,intval);//设置sem为val#defineinit_MUTEX(sem)sema_init(sem,1)//初始化一个用户互斥的信号量sem设置为1#defineinit_MUTEX_LOCKED(sem)sema_init(sem,0)//初始化一个用户互斥的信号量sem设置为0定义和初始化可以一步完成:

DECLARE_MUTEX(name);//该宏定义信号量name并初始化1DECLARE_MUTEX_LOCKED(name);//该宏定义信号量name并初始化0当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutualexclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。

使用信号量,内核代码必须包含

3)获取(锁定)信号量:

voiddown(structsemaphore*sem);intdown_interruptible(structsemaphore*sem);intdown_killable(structsemaphore*sem);4)释放信号量

voidup(structsemaphore*sem);下面看一个实例:

三、自旋锁与信号量的比较

从以上的区别以及本身的定义可以推导出两都分别适应的场合。只考虑内核态

后记:除了上述几种广泛使用的的并发控制机制外,还有中断屏蔽、顺序锁(seqlock)、RCU(Read-Copy-Update)等等,做个简单总结如下图:

回顾一下在Unix/Linux下共有五种I/O模型,分别是:

a--阻塞I/Ob--非阻塞I/Oc--I/O复用(select和poll)d--信号驱动I/O(SIGIO)e--异步I/O(Posix.1的aio_系列函数)

下面我们先学习阻塞I/O、非阻塞I/O、I/O复用(select和poll),先学习一下基础概念

a--阻塞

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程知道满足可操作的条件后再进行操作;被挂起的进程进入休眠状态(放弃CPU),被从调度器的运行队列移走,直到等待的条件被满足;

b--非阻塞

非阻塞的进程在不能进行设备操作时,并不挂起(继续占用CPU),它或者放弃,或者不停地查询,直到可以操作为止;

二者的区别可以看应用程序的调用是否立即返回!

驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()才返回,整个过程仍然进行了正确的设备访问,用户并没感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作立刻返回,read()、write()等系统调用也随即被返回。

因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的挂了。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。

阻塞I/O通常由等待队列来实现,而非阻塞I/O由轮询来实现。

一、阻塞I/O实现——等待队列

1、基础概念

在Linux内核中使用等待队列的过程很简单,首先定义一个wait_queue_head,然后如果一个task想等待某种事件,那么调用wait_event(等待队列,事件)就可以了。

等待队列应用广泛,但是内核实现却十分简单。其涉及到两个比较重要的数据结构:__wait_queue_head,该结构描述了等待队列的链头,其包含一个链表和一个原子锁,结构定义如下:

struct__wait_queue_head{ spinlock_tlock;/*保护等待队列的原子锁*/ structlist_headtask_list;/*等待队列*/};typedefstruct__wait_queue_headwait_queue_head_t;

__wait_queue,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue,并且挂载到wait_queue_head上。该结构定义如下:

struct__wait_queue{ unsignedintflags; void*private;/*通常指向当前任务控制块*/ /*任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function*/ wait_queue_func_tfunc; structlist_headtask_list;/*挂入wait_queue_head的挂载点*/};

Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。

使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。当然,动态初始化的方法也很简单,初始化一下锁及队列就可以了。

一个任务需要等待某一事件的发生时,通常调用wait_event,该函数会定义一个wait_queue,描述等待任务,并且用当前的进程描述块初始化wait_queue,然后将wait_queue加入到wait_queue_head中。

函数实现流程说明如下:

a--用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。

b--在等待队列锁资源的保护下,将等待任务加入等待队列。

c--判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。

d--如果条件不满足,那么任务调度,将CPU资源交与其它任务。

e--当睡眠任务被唤醒之后,需要重复b、c步骤,如果确认条件满足,退出等待事件函数。

2、等待队列接口函数

1、定义并初始化

/*定义“等待队列头”*/wait_queue_head_tmy_queue;/*初始化“等待队列头”*/init_waitqueue_head(&my_queue);

直接定义并初始化。init_waitqueue_head()函数会将自旋锁初始化为未锁,等待队列初始化为空的双向循环链表。

DECLARE_WAIT_QUEUE_HEAD(my_queue);定义并初始化,可以作为定义并初始化等待队列头的快捷方式。

2、定义等待队列:

DECLARE_WAITQUEUE(name,tsk);

定义并初始化一个名为name的等待队列。

3、(从等待队列头中)添加/移出等待队列:

/*add_wait_queue()函数,设置等待的进程为非互斥进程,并将其添加进等待队列头(q)的队头中*/voidadd_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);/*该函数也和add_wait_queue()函数功能基本一样,只不过它是将等待的进程(wait)设置为互斥进程。*/voidadd_wait_queue_exclusive(wait_queue_head_t*q,wait_queue_t*wait);

4、等待事件:

(1)wait_event()宏:

在等待会列中睡眠直到condition为真。在等待的期间,进程会被置为TASK_UNINTERRUPTIBLE进入睡眠,直到condition变量变为真。每次进程被唤醒的时候都会检查condition的值.

(2)wait_event_interruptible()函数:

和wait_event()的区别是调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE状态.在每次被唤醒的时候,首先检查condition是否为真,如果为真则返回,否则检查如果进程是被信号唤醒,会返回-ERESTARTSYS错误码.如果是condition为真,则返回0.

(3)wait_event_timeout()宏:

5、唤醒队列

(1)wake_up()函数

唤醒等待队列.可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERUPTIBLE状态的进程,和wait_event/wait_event_timeout成对使用.(2)wake_up_interruptible()函数:

#definewake_up_interruptible(x)__wake_up(x,TASK_INTERRUPTIBLE,1,NULL)和wake_up()唯一的区别是它只能唤醒TASK_INTERRUPTIBLE状态的进程.,与wait_event_interruptible/wait_event_interruptible_timeout/wait_event_interruptible_exclusive成对使用。

注意两个概念:

a--疯狂兽群

wake_up的时候,所有阻塞在队列的进程都会被唤醒,但是因为condition的限制,只有一个进程得到资源,其他进程又会再次休眠,如果数量很大,称为疯狂兽群。

b--独占等待

等待队列的入口设置一个WQ_FLAG_EXCLUSIVE标志,就会添加到等待队列的尾部,没有设置设置的添加到头部,wakeup的时候遇到第一个具有WQ_FLAG_EXCLUSIVE这个标志的进程就停止唤醒其他进程。

二、非阻塞I/O实现方式——多路复用

1、轮询的概念和作用

2、应用程序中的轮询编程

在用户程序中,select()和poll()本质上是一样的,不同只是引入的方式不同,前者是在BSDUNIX中引入的,后者是在SystemV中引入的。用的比较广泛的是select系统调用。原型如下:

structtimeval{inttv_sec;//秒inttv_usec;//微秒}涉及到文件描述符集合的操作主要有以下几种:

1)清除一个文件描述符集FD_ZERO(fd_set*set);

2)将一个文件描述符加入文件描述符集中FD_SET(intfd,fd_set*set);

3)将一个文件描述符从文件描述符集中清除FD_CLR(intfd,fd_set*set);

4)判断文件描述符是否被置位FD_ISSET(intfd,fd_set*set);

3、设备驱动中的轮询编程

设备驱动中的poll()函数原型如下

unsignedint(*poll)(structfile*filp,structpoll_table*wait);第一个参数是file结构体指针,第二个参数是轮询表指针,poll设备方法完成两件事:

a--对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table,如果没有文件描述符可用来执行I/O,则内核使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。

b--返回表示是否能对设备进行无阻塞读、写访问的掩码。

位掩码:POLLRDNORM,POLLIN,POLLOUT,POLLWRNORM

设备可读,通常返回:(POLLIN|POLLRDNORM)

设备可写,通常返回:(POLLOUT|POLLWRNORM)

poll_wait()函数:用于向poll_table注册等待队列

voidpoll_wait(structfile*filp,wait_queue_head_t*queue,poll_table*wait)poll_wait()函数不会引起阻塞,它所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。

真正的阻塞动作是上层的select/poll函数中完成的。select/poll会在一个循环中对每个需要监听的设备调用它们自己的poll支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu进入阻塞状态,schedule返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪,select/poll都立即返回。

具体过程如下:

a--用户程序第一次调用select或者poll,驱动调用poll_wait并使两条队列都加入poll_table结构中作为下次调用驱动函数poll的条件,一个mask返回值指示设备是否可操作,0为未准备状态,如果文件描述符未准备好可读或可写,用户进程被会加入到写或读等待队列中进入睡眠状态。

b--当驱动执行了某些操作,例如,写缓冲或读缓冲,写缓冲使读队列被唤醒,读缓冲使写队列被唤醒,于是select或者poll系统调用在将要返回给用户进程时再次调用驱动函数poll,驱动依然调用poll_wait并使两条队列都加入poll_table结构中,并判断可写或可读条件是否满足,如果mask返回POLLIN|POLLRDNORM或POLLOUT|POLLWRNORM则指示可读或可写,这时select或poll真正返回给用户进程,如果mask还是返回0,则系统调用select或poll继续不返回

下面是一个典型模板:

4、调用过程:

Linux下select调用的过程:

1、用户层应用程序调用select(),底层调用poll())2、核心层调用sys_select()------>do_select()最终调用文件描述符fd对应的structfile类型变量的structfile_operations*f_op的poll函数。poll指向的函数返回当前可否读写的信息。1)如果当前可读写,返回读写信息。2)如果当前不可读写,则阻塞进程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。

3、驱动需要实现poll函数当驱动发现有数据可以读写时,通知核心层,核心层重新调用poll指向的函数查询信息。

poll_wait(filp,&wait_q,wait)//此处将当前进程加入到等待队列中,但并不阻塞在中断中使用wake_up_interruptible(&wait_q)唤醒等待队列。4、实例分析

1、memdev.h

/*mem设备描述结构体*/structmem_dev{char*data;unsignedlongsize;wait_queue_head_tinq;};#endif/*_MEMDEV_H_*/2、驱动程序memdev.c

3、应用程序app-write.c

4、应用程序app-read.c

阻塞和非阻塞访问、poll()函数提供了较多地解决设备访问的机制,但是如果有了异步通知整套机制就更加完善了。

异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

阻塞I/O意味着移植等待设备可访问后再访问,非阻塞I/O中使用poll()意味着查询设备是否可访问,而异步通知则意味着设备通知自身可访问,实现了异步I/O。由此可见,这种方式I/O可以互为补充。

1、异步通知的概念和作用

影响:阻塞–应用程序无需轮询设备是否可以访问

非阻塞–中断进行通知

即:由驱动发起,主动通知应用程序

2、linux异步通知编程

2.1linux信号

作用:linux系统中,异步通知使用信号来实现

函数原型为:

void(*signal(intsignum,void(*handler))(int)))(int)

原型比较难理解可以分解为

typedefvoid(*sighandler_t)(int);sighandler_tsignal(intsignum,sighandler_thandler);

第一个参数是指定信号的值,第二个参数是指定针对前面信号的处理函数2.2信号的处理函数(在应用程序端捕获信号)

signal()函数

2.3信号的释放(在设备驱动端释放信号)

为了是设备支持异步通知机制,驱动程序中涉及以下3项工作

(1)、支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应的进程ID。不过此项工作已由内核完成,设备驱动无须处理。(2)、支持F_SETFL命令处理,每当FASYNC标志改变时,驱动函数中的fasync()函数得以执行。因此,驱动中应该实现fasync()函数(3)、在设备资源中可获得,调用kill_fasync()函数激发相应的信号

设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。这个数据结构是fasync_struct结构体,两个函数分别是:

a--处理FASYNC标志变更

intfasync_helper(intfd,structfile*filp,intmode,structfasync_struct**fa);

b--释放信号用的函数

voidkill_fasync(structfasync_struct**fa,intsig,intband);

和其他结构体指针放到设备结构体中,模板如下

在设备驱动中的fasync()函数中,只需简单地将该函数的3个参数以及fasync_struct结构体指针的指针作为第四个参数传入fasync_helper()函数就可以了,模板如下

在设备资源可获得时应该调用kill_fasync()函数释放SIGIO信号,可读时第三个参数为POLL_IN,可写时第三个参数为POLL_OUT,模板如下

THE END
1.十四五国规教材《服装结构制图与样板——基础篇》全面地介绍服装专业基础知识,新文化式女子原型的结构制图、省位转移,女装上衣局部结构设计,同时介绍了男衬衫、女衬衫、裙子、裤子等的结构制图原理和样板制作方法,并根据企业的实际操作程序对衬衫、裙子、裤子等服种的制图与样板制作进行了案例分析。本教材图文并茂,br实用性强,其结构...http://www.ctapedu.com/mobile/front/specialsubject/index.php
2.基础理论论文范文10篇理论研究人才流失,甘坐“冷板凳”的学者越来越少;文学基本理论研究的边缘化——扎扎实实从学理本体和基本问题上做“功夫学问”的人在分化和锐减,而赶时髦、求新潮、扮“黑马”、标新立异走“捷径”的投机学人越来越多;还有文学理论研究学科目标的边缘化——即以所谓应用研究、交叉研究、跨学科研究来替代基础原理...https://www.gwyoo.com/jichulilunlunwen/
3.《世界经典服装设计与纸样1[General Information] 书名=世界经典服装设计与纸样 1 基础原理篇 上集 作者=熊能编著 页数=203 出版社=南昌市:江西美术出版社 出版日期=2009.01 SS号 DX号=000006734939 URL=/bookDetail.jsp?dxNumber=0000 =BD19A3C17FDD8507F6525286E09877B5 封面 版权 前言 目录 第一章 服装设计与纸样概论 第一节 服装...https://m.book118.com/html/2017/1007/136384361.shtm
1.自在梳理女孩的简单剪发秘诀通过这个简单又实用的方法,我们可以学会如何自己对待我们的长发,无论是去掉杂乱无章还是试验新的造型,每一次尝试都是学习和成长的一部分。而且,当你掌握了这种技能,你会发现原来如此简单的事情竟然如此让人满足!因此,不妨今天就开始尝试,为你的美丽打下坚实基础吧!https://www.zyaikupz.cn/xiu-fa-hu-li/257050.html
2.从经典到现代这些不同类型的层次剪为何成为当下最受青睐的20121年...首先,让我们回顾一下“层次剪”这一概念。它涉及到将头发分成不同的部分,并以这种方式进行裁剪,使得每个部分都有自己的长度和形状,从而营造出丰富多样的视觉效果。在设计过程中,可以根据个人偏好、脸型特征以及日常生活需要来调整这些部分,以确保既符合审美,又能满足实用需求。 https://www.poiys.cn/liu-xing-qu-shi/476646.html
3.初学立体裁剪必读立体裁剪的技术原理 1、立体裁剪所用布料的纱向: 立体裁剪所用的布料的丝道必须归正。许多坯布多存在着纵横丝道歪斜的问题,因此在操作之前要将布料用熨斗归烫,使纱向归正、布料平整,同时也要求坯布衣片与正式的面料复合时,应保持二者的纱向的一致,这样才能保证成品服装与人台上的服装造型一致 ...http://www.360doc.com/content/17/1121/07/35854156_705943133.shtml
4.版型解构"省道"女装结构里的灵魂,如何应用看这里!服装立体裁剪设计原理 服装设计从两点入手,一是人体是客观存在的,设计的服装首先要满足人体的基本要求,合体性,舒适性。二是从美的观点来看,设计的服装必须要美观,那么,立体裁剪也是从这两个方面着手: (一)合体性、舒适性 人体是客体,不能改变的,设计的服装首先要满足这一要求,在立体裁剪中,必须要了解人体的表面形...https://www.163.com/dy/article/E24TDDC40529A4IL.html
5.制版干货推板的基本原理和方法,附:男女上装的服装...一、推板的原理: 成衣推板是成衣制版的一部分,它是以中间规格(也可以用最大规格或最小规格)的标准,兼顾各个规格或号型系列之间的关系,进行科学的计算,正确合理的分配档差,绘制出各个规格和号型系列的裁剪用样板的方法,通称推板,也称放码、推挡或扩号。二、推板的https://www.19lou.com/wap/forum-464942-thread-64841629879267342-1-1.html
6.2022年山东省春季高考统一考试招生专业类别考试模块按照全国职业教育大会精神及《教育部 山东省人民政府关于整省推进提质培优建设职业教育创新发展高地的意见》(鲁政发〔2020〕3号)、《山东省教育厅关于进一步完善职业教育考试招生制度的意见》(鲁教学字〔2019〕7号)要求,现将我省2022年春季高考统一考试招生38个专业类别的考试模块公布,请结合实际抓好职业教育教学工作,...http://m.zk985.com/nd.jsp?id=2474
7.服装技术工作总结(通用20篇)调节具有美,功能又具有运动的袖子必须是由前面解阻,后面袖山弧度鼓起的曲线所构成的,袖子的安装角度和袖子的修正,正好运用这一原理。 牛仔裤的合体和运动量的解决 1.合体牛仔裤的材料选择需要微弹面料,才能塑造具有美和舒适的板型。 2.臀围尺寸比M码正常臀围-1-2cm(根据面料而定) ...https://www.cnfla.com/gongzuozongjie/2753657.html
8....值得你收藏,为您的面试助力目录1.一基础知识点2.二第...这篇文章是一份非常全面的 iOS 面试题汇总,涵盖了众多知识点,包括基础知识点(如设计模式、类的继承与扩展、属性关键字等)、线程创建与运行、数据存储与沙盒目录、算法、编码格式优化、第三方框架等。还涉及了一些高级概念如 Runtime、KVO、NSNotification 等,以及对各种技术的区别和用法的详细阐述。 关联问题: iOS如...https://article.juejin.cn/post/7233293144451317821
9.菏泽烟草发屋美容美发学校招生简章课程主要讲解系统裁剪的理论知识、剪发标准站位、发片的梳理方向与发型形成的关系。以点、线、面为基础元素,结合方、圆、三角的基本理念,对发型进行全方位阐述。 课程分为三大块,一、深入阐述发型设计原理,几何与头型,定位和分区,各种修剪手法及发片的提拉角度。传统发型制作:让学员对传统发型进一步理解,加强基本功的...http://www.cnsdjxw.com/school_brows.asp?id=4464%27
10.中等职业学校服装设计与工艺专业课程标准本课程是中等职业学校服装设计与工艺专业的一门专业核心课程,其功能在于通过学习,使学生了解各类服装与人体的比例关系,掌握大类产品服装结构设计及样板制作的专业技能,并为进一步学习各专门化方向课程打下基础。 1. 2设计思路 本课程的总体设计思路是打破传统学科课程,以知识为主线构建知识体系的传统课程模式。转变为以...http://qpzx.qpedu.cn/jhkx/kcgl/kcbz/249120.htm
11.服装工作计划(精选20篇)掌握计算机基本操作知识(前四周学习计算机操作知识);本课程的主要任务是要学生学会灵活的掌握服装电脑制图的工具;重点是掌握西裤、衬衫、西装的电脑制图;了解电脑制图的原理。通过本软件的操作,使学生能掌握一定的服装电脑基础知识和操作技能,能绘制出上装和下装的裁片;并作好衣片的编号,为今后到厂里操作服装电脑打下良...https://www.ruiwen.com/gongwen/gongzuojihua/496346.html
12.保定东方美容美发学校简章授课目的:课程通过深度分析讲解和模特头训练,使学生掌握科学的发型修剪理论和扎实的基本功,并使学生对发型基本层次精准的操作能力,为学习综合课程奠定坚实基础。 5.发型时尚创意课程:12天 课程设置对象:(具有一定的基础裁剪理论知识和实际操作能力)。 授课内容:剖析讲解发型立体裁剪构思与变化原理;示范训练发型多层次搭配...http://www.hebjxw.com/ShowInfo_zxzk.asp?id=1534
13.实用服装裁剪制板与成衣制作实例系列:居家一、装袖裁剪原理019 二、插肩袖裁剪原理021 三、其他袖型裁剪实例022 第三节 居家服衣身裁剪原理及变化024 一、连体式居家服裁剪原理025 二、两件式居家服裁剪原理026 三、居家服口袋及袋位的设计027 四、居家服省缝的变化原理028 第三章 经典居家服裁剪与缝制基础029 ...https://baike.sogou.com/v85175886.htm
14.服装工作计划范文(集合15篇)掌握计算机基本操作知识(前四周学习计算机操作知识);本课程的主要任务是要学生学会灵活的掌握服装电脑制图的工具;重点是掌握西裤、衬衫、西装的电脑制图;了解电脑制图的原理。通过本软件的操作,使学生能掌握一定的服装电脑基础知识和操作技能,能绘制出上装和下装的裁片;并作好衣片的编号,为今后到厂里操作服装电脑打下良...https://www.unjs.com/fanwenwang/gongzuojihuafanwen/gongzuojihuafanwen/20230102094428_6191514.html
15.国际时装设计基础教程3:服装制版原理与工艺基础epubpdfmobi...出版社: 中国青年出版社 ISBN:9787515320748 版次:1 商品编码:11394171 包装:平装 丛书名: 国际时装设计基础教程 开本:16开 出版时间:2014-03-01 用纸:铜版纸 页数:192 正文语种:中文 国际时装设计基础教程3:服装制版原理与工艺基础 epub pdf mobi txt 电子书 下载 2024 ...https://book.tinynews.org/books/11394171
16.气垫烫女教程?二、气垫烫裁剪教程? 第一步 洗头 将头发清洗一遍,用毛巾擦干水分。 不过,受损的发质,保留的水分要稍多一些,防止头发在烫发时受伤害;健康的发质可以稍稍少留一点水分。 第二步 修剪 用发剪分片修剪头发,确定发丝的长短。 用发剪继续修剪头发,分出片区,把头发打薄。修出整体发型的层次感。发量多的女生,发尾要...https://tool.a5.cn/article/show/65856.html
17.服装设计理论基本知识服装设计在满足实用功能的基础上应密切结合人体的型态特征,利用外型设计和内在结构的设计强调人体优美造型,扬长避短,充分体现人体美,展示服装与人体完美结合的整体魅力。那么,下面由小编为大家分享服装设计理论基本知识,欢迎大家阅读浏览。 服装设计理论基本知识 ...https://www.yjbys.com/edu/fuzhuang/243907.html
18.理论2021* 252 判断题 泡泡袖的肩宽在原规格的基础上应调大。 对 错 * 253 判断题 排料目的是使面料的利用率达到最高,同时给铺料、裁剪等工序提供 依据。 对 错 * 254 判断题 女西服装袖时,应先收好袖山吃势。 对 错 * 255 判断题 女短袖衬衫袖长长度一般为肘骨以上6~10厘米。 对 错 * 256 判断题 捻...https://www.wjx.cn/vj/m0ADDQH.aspx
19.毕业设计总结(通用20篇)总体情况毕业设计是学生在学习阶段的最后一个环节,是对所学基础知识和专业知识的一种综合应用,是一种综合的再学习、再提高的过程,这一过程对学生的学习能力和独立工作能力也是一个培养,同时毕业设计的水平也反映了本科教育的综合水平,因此学校十分重视毕业设计这一环节,加强了对毕业设计工作的指导和动员教育。在学期初...https://www.fwsir.com/fanwen/html/fanwen_20221206093723_2120824.html
20.模具实习报告15篇通过实习加深学生对模具设计基础知识和基本理论的理解,培养学生设计和实践能力的重要环 节和必要手段。其目的是使学生全面了解与掌握冲塑模具的组成、结构、动作原理及其设计、加工的方法与步骤等,并将所学的模具知识、零件设计、制图、工艺、刀具、公差与技术测量等知识有机地结合在一起,提高学生对模具零件设计制造、...https://www.oh100.com/a/202306/7020533.html
21.PADS原理图(Logic)电源符号接地符号怎么切换?日常做原理图的时候如果需要不同的接地符号或者不同的电源符号又或者页面连接符,我们怎么处理,PADS会有一个默认图形,如果我们需要切换就要按照如下步骤来处理: 放置接地符号,在连线状态下单击鼠标右键点击“接地”(或者组合快捷键ctrl+space这个组合键一般都会和别的软件冲突,可以的话可以自己改一下),放置电源符号,在...https://www.eet-china.com/mp/a328188.html
22.服装裁剪实用手册(下装篇)pdfepubmobitxt电子书下载2024...第二节 女裤廓形变化原理 第三节 女裤腰位、分割、褶裥的变化原理 第六章 裙裤的基本结构及其变化原理 第一节 裙裤的基本结构 第二节 裙裤廓形变化原理 第三节 裙裤腰位、分割、褶裥的变化原理 第七章 女裤、裙裤裁剪应用实例 附:服装裁剪基础知识 https://windowsfront.com/books/11015492
23.基础剪发剪发基本原理剪刀的裁剪手法时尚视频小狐狸89757639关注https://m.tv.sohu.com/v/dXMvODk3NTc2MzkvMzEzNDU5MTguc2h0bWw%3D.html