嵌入式Linux设备驱动工作原理的研究

(整期优先)网络出版时间:2008-12-22
/ 2

嵌入式Linux设备驱动工作原理的研究

黄兆志

关键词:Linux;嵌入式系统;设备驱动;内核环境

0引言

设备驱动程序在Linux内核中占有极其重要的位置,它是内核用于完成对物理设备的控制操作的功能模块。除了CPU、内存以及其他很少的几个部分以外,所有的设备控制操作都必须由与被控设备相关代码——驱动程序来完成。否则设备就无法在Linux下正常工作,这就是驱动程序开发成为Linux内核开发的主要工作的原因。

然而,在嵌入式Linux系统中,内核提供保护机制,用户空间的进程一般不能直接访问硬件。进行嵌入式系统的开发,很大的工作量是为各种设备编写驱动程序,Linux设备驱动程序在Linux内核源代码中占有60%以上,从2.0、2.2到2.4版本的内核,源代码的长度日益增加,其实主要是设备驱动程序在增加。

文章通过对与设备驱动相关内核源码进行分析,从设备驱动的内部结构和内核环境两方面入手,对设备驱动程序的工作原理进行深入地剖析和阐述。

1.LinuxI/O子系统的体系结构

1.1I/O子系统的层次结构

Linux的I/O子系统分为上下两个层次:其下层是与设备有关的,即设备驱动程序,它直接控制设备完成具体的I/O操作,并且向上层提供一组访问接口;上层部分是与设备无关的,它根据进程的I/O请求设备驱动程序接口与设备进行通信。由于Linux把设备作为文件管理,所以I/O子系统的上层实际上就是实现文件管理功能的虚拟文件系统VFS,进程的I/O请求经过VFS的转换完成对设备的各种操作,而这些操作的具体实现是由设备驱动来完成的。(如图1所示)

1.2I/O驱动软件的总体目标

I/O驱动软件的总体目标是将软件组织成一种层次结构,底层软件用来屏蔽具体设备硬件的细节;高层软件则为用户提供一个简洁的界面,从而实现I/O设计的设备无关性。(如图1所示)

2.文件操作例程登记表

2.1文件系统调用接口

structfile_operationsIOdriver_fops=

{

read:IOdriver_read,

write:IOdriver_write,

};

2.2基本入口点函数结构

我们用以globar_read和read函数为例来对驱动底层软件结构是如何屏蔽设备硬件细节进行剖析。

staticssize_tglobar_read(structfile*file,char*buf,size_tlen,loff_t*off)

{

if(__copy_to_user(buf,&globar_var,sizeof(int)))

{

return–EFAULT;

}

returnsizeof(int);

}

上述函数的任务是实现将数据从设备复制到用户空间,也就是将内核空间缓冲区里的数据复制其工作原理如图2所示:

用户程序读操作的入口点到用户空间的缓冲区中,

read和global_read函数之间的通信是靠字符设备驱动接口中的file_operations结构,基于Linux-2.4.20的<linux/fs.h>的定义如下:

structfile_operations{

structmodule*owner;

/*指向拥有本结构的module,用于内核维护模块的引用技术*/

loff_t(*llseek)(structfile*,loff_t,int);

/*改变当前文件的读/写位置并返回新位置,错误返回负数*/

ssize_t(*read)(structfile*,char*,size_t,loff_t);

/*用来从设备接收数据,返回的非负数表示成功读取的字节数*/

ssize_t(*write)(structfile*,constchar*,size_t,loff_t*);

/*用来往设备发数据,返回的非负数表示成功写入的字节数*/

int(readdir))(structinode*,structfile*,void*,filldir_t);

/*仅用于文件系统的目录读取,不用于设备驱动*/

int(*select)(structinode*,structfile*,int,selecttable*);

/*用于查询设备是否可读、可写或处于特殊的状态*/

int(*ioctl)(structinode*,structfile*,unsignedint,unsignedint);

/*用于给设备发送命令的接口函数。如驱动不提供,则所有调用失败,返回ENOTTY*/

int(*mmap)(structinode*,structfile*,structvm_area_struct*);

/*用于请求将设备内存映射到进程空间。如果驱动不提供,则所有调用失败,返回ENODEV*/

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

/*打开设备,通常是对设备的第一个操作函数*/

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

/*关闭设备。只有当设备文件的所有备份都被释放时,才进行release调用,而不是每次调用close时都执行。*/

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

/*是系统调用fsync的背后支撑,用户可调用fsync来刷新缓存数据*/

};

3.Linux驱动工作原理

3.1驱动程序工作原理图(如图3所示)

3.2驱动程序工作原理剖析

3.2.1注册驱动程序

设备驱动是通过insmod命令加载到系统内核中,在内核里由加载模块Init_module()调用注册函数register_chrdev()来完成(如图3所示)。

注册函数格式为:

intregister_chrdev(unsignedintmajor,constchar*name,structfile_operation*fops);

major:主设备号

name:设备名

fops:驱动程序结构体的地址

当注册函数在执行时,首先从字符设备注册表chrdev[]的底部(实际是倒数第二个表项)开始向上依次查询各个表项(如图4所示),查到表项的成员项fops为null时,说明它是一个空表项。这时把参数给出的设备名和驱动程序指针集合分别赋予该表项device_struct结构体(也被称为设备描述符,在fs/device.c中)的成员项name和fops,如下所示:

if(major==0)

for(major=MAX_CHRDEV-1;major>0;major--)

{if(chrdev[major].fops==NULL)

{chrdev[major].name=name;

chrdev[major].fops=fops;

returnmajor;}

return–EBUSY;

}

3.2.2注销驱动程序

驱动程序的注销是由rmmod命令调用cleanup_module卸载模块,此时注销函数unregister_chrdev()函数就会被调用执行(如图3所示)。当注销函数的参数给出了要注销设备的主设备号major和设备名name后,根据参数major检查注册表项中注册的设备名与参数给出的名字是否一致,若不同则不能注销并返回错误值;如果一致则把该表项的两个成员项都置为null然后返回0值,如下所示:

if(strcmp(chrdev[major].name,name))

reture–EINVAL;

else

{chrdev[major].name=NULL;

chrdev[major].fops=NULL;

return0;}

在Linux中由于设备管理是由内核实现的,所以设备管理的数据结构在内存的内核区,设备管理使用的函数都是内核函数。

4.结束语

操作系统是通过各种驱动程序来驾驭硬件设备的,它为用户屏蔽了各种各样的设备,驱动硬件是操作系统最基本的功能,并且提供统一的操作方式。设备驱动程序是内核的一部分,硬件驱动程序是操作系统最基本的组成部分,因此熟悉驱动的编写是很重要的。

参考文献:

[1]刘淼.嵌入式系统接口设计与Linux驱动程序开发.北京:北京航空航天大学出版社,2006.5

[2]孙纪坤,张小全.嵌入式Linux系统开发技术详解——基于ARM.北京:人民邮电出版社,2007.6

[3]倪继利.Linux内核分析及编程[M].北京:电子工业出版社,2005.9

[4]王岩,王子牛.嵌入式Linux设备驱动程序开发.贵州工业大学学报(第一期37卷),2008.2

[5]贺刚,吴灏.Linux字符设备驱动程序工作原理.信息工程大学学报,2001.6