摘要:计算机软件和集成电路技术的发展,为嵌入式产业带来了巨大的机遇和挑战,Linux以其稳定、高效、易定制、硬件广泛支持等特点,迅速崛起为当今计算机领域的一匹黑马。文章通过对与嵌入式Linux设备驱动程序相关内核源码进行分析,从设备驱动的体系结构和内核环境两方面入手,对嵌入式Linux设备驱动程序的工作原理进行剖析和阐述。
关键词: Linux;嵌入式系统;设备驱动;内核环境
0. 引 言
设备驱动程序在Linux内核中占有极其重要的位置,它是内核用于完成对物理设备的控制操作的功能模块。除了CPU、内存以及其他很少的几个部分以外,所有的设备控制操作都必须由与被控设备相关代码——驱动程序来完成。否则设备就无法在Linux下正常工作,这就是驱动程序开发成为Linux内核开发的主要工作的原因。
然而,在嵌入式Linux 系统中,内核提供保护机制,用户空间的进程一般不能直接访问硬件。进行嵌入式系统的开发,很大的工作量是为各种设备编写驱动程序,Linux 设备驱动程序在Linux 内核源代码中占有60%以上,从2.0、2.2 到2.4 版本的内核,源代码的长度日益增加,其实主要是设备驱动程序在增加。
文章通过对与设备驱动相关内核源码进行分析,从设备驱动的内部结构和内核环境两方面入手,对设备驱动程序的工作原理进行深入地剖析和阐述。
1. Linux I/O子系统的体系结构
1.1 I/O子系统的层次结构
Linux的I/O子系统分为上下两个层次:其下层是与设备有关的,即设备驱动程序,它直接控制设备完成具体的I/O操作,并且向上层提供一组访问接口;上层部分是与设备无关的,它根据进程的I/O请求设备驱动程序接口与设备进行通信。由于Linux把设备作为文件管理,所以I/O子系统的上层实际上就是实现文件管理功能的虚拟文件系统VFS,进程的I/O请求经过VFS的转换完成对设备的各种操作,而这些操作的具体实现是由设备驱动来完成的。(如图1所示)
1.2 I/O驱动软件的总体目标
I/O驱动软件的总体目标是将软件组织成一种层次结构,底层软件用来屏蔽具体设备硬件的细节;高层软件则为用户提供一个简洁的界面,从而实现I/O设计的设备无关性。(如图1所示)
2. 文件操作例程登记表
2.1 文件系统调用接口
struct file_operations IOdriver_fops =
{
read : IOdriver_read,
write : IOdriver_write,
};
2.2基本入口点函数结构
我们用以globar_read和read函数为例来对驱动底层软件结构是如何屏蔽设备硬件细节进行剖析。
static ssize_t globar_read(struct file *file, char *buf, size_t len, loff_t *off)
{
if(__copy_to_user(buf,&globar_var, sizeof(int)))
{
return –EFAULT;
}
return sizeof(int);
}
上述函数的任务是实现将数据从设备复制到用户空间,也就是将内核空间缓冲区里的数据复制其工作原理如图2所示:
用户程序读操作的入口点到用户空间的缓冲区中,
read和global_read函数之间的通信是靠字符设备驱动接口中的file_operations结构,基于Linux-2.4.20的的定义如下:
struct file_operations {
struct module *owner;
/* 指向拥有本结构的module,用于内核维护模块的引用技术 */
loff_t (*llseek) (struct file *, loff_t, int);
/* 改变当前文件的读/写位置并返回新位置,错误返回负数 */
ssize_t (*read) (struct file *, char *, size_t, loff_t);
/* 用来从设备接收数据,返回的非负数表示成功读取的字节数 */
ssize_t (*write) (struct file *,const char *, size_t, loff_t *);
/* 用来往设备发数据,返回的非负数表示成功写入的字节数 */
int(readdir))(struct inode *, struct file *, void *, filldir_t);
/* 仅用于文件系统的目录读取,不用于设备驱动 */
int (*select)(struct inode *, struct file *, int, select table *);
/* 用于查询设备是否可读、可写或处于特殊的状态 */
int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned int );
/* 用于给设备发送命令的接口函数。如驱动不提供,则所有调用失败,返回ENOTTY */
int (*mmap)(struct inode *, struct file *, struct vm_area_struct *);
/* 用于请求将设备内存映射到进程空间。如果驱动不提供,则所有调用失败,返回ENODEV */
int (*open)(struct inode *, struct file *);
/* 打开设备,通常是对设备的第一个操作函数 */
void (*release)(struct inode *, struct file *);
/* 关闭设备。只有当设备文件的所有备份都被释放时,才进行release调用,而不是每次调用close时都执行。 */
int (*fsync)(struct inode *, struct file *);
/* 是系统调用fsync的背后支撑,用户可调用fsync来刷新缓存数据 */
};
3. Linux驱动工作原理
3.1 驱动程序工作原理图(如图3所示)
3.2 驱动程序工作原理剖析
3.2.1注册驱动程序
设备驱动是通过insmod命令加载到系统内核中,在内核里由加载模块Init_module()调用注册函数register_chrdev()来完成(如图3所示)。
注册函数格式为:
int register_chrdev(unsigned int major, const char *name, struct file_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;
return major;}
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;
return 0;}
在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