ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础

设备驱动概述

Linux设备分类

Linux系统的设备分为三类:字符设备、块设备和网络设备:    

(1)字符设备通常指像普通文件或字节流一样,以字节为单位输入输出数据的设备,如并口设备、虚拟控制台等。字符设备可以通过设备文件节点访问,它与普通文件之间的区别在于普通文件可以被随机访问(可以前后移动访问指针),而大多数字符设备只能提供顺序访问,因为对它们的访问不会被系统所缓存。但也有例外,例如帧缓存(FrameBuffer)是一个可以被随机访问的字符设备。

(2)块设备通常指一些需要以块为单位随机读写的设备,如IDE硬盘、SCSI硬盘、光驱等。块设备也是通过文件节点来访问,它不仅可以提供随机访问,而且可以容纳文件系统(例如硬盘、闪存等)。Linux可以使用户态程序像访问字符设备一样每次进行任意字节的操作,只是在内核态内部中的管理方式和内核提供的驱动接口不同。

(3)网络设备通常是指通过网络能够与其他主机进行数据通信的设备,如网卡等。内核和网络设备驱动程序之间的通信调用一套数据包处理函数,它们完全不同于内核和字符以及块设备驱动程序之间的通信(read(), write()等函数)。Linux网络设备不是面向流的设备,因此不会将网络设备的名字(例如eth0)映射到文件系统中去。

Linux的内核组成

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第1张图片

设备驱动简介及驱动模块

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

Linux内核中采用可加载的模块化设计(LKMs,Loadable Kernel Modules),一般情况下编译的Linux内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其他的代码可以编译到内核中,或者编译为内核的模块文件(在需要时动态加载)。

内核模块的主要相关命令

常见的驱动程序是作为内核模块动态加载的,比如声卡驱动和网卡驱动等,而Linux最基础的驱动,如CPU、PCI总线、TCP/IP协议、APM(高级电源管理)、VFS等驱动程序则直接编译在内核文件中。有时也把内核模块叫做驱动程序,只不过驱动的内容不一定是硬件罢了,比如ext3文件系统的驱动。因此,加载驱动就是加载内核模块。

lsmod命令可以列出当前系统中加载的模块,其中左边第一列是模块名,第二列是该模块大小,第三列则是使用该模块的对象数目。

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第2张图片

rmmod命令是用于将当前模块卸载。    

insmod和modprobe命令是用于加载当前模块,但insmod不会自动解决依存关系,即如果要加载的模块引用了当前内核符号表中不存在的符号,则无法加载,也不会去查在其他尚未加载的模块中是否定义了该符号;modprobe可以根据模块间依存关系以及/etc/modules.conf文件中的内容自动加载其他有依赖关系的模块。

设备号

设备号是一个数字,它是设备的标志。一个设备文件(也就是设备节点)可以通过mknod命令来创建,其中指定了主设备号和次设备号。主设备号表明设备的类型(例如串口设备、SCSI硬盘),与一个确定的驱动程序对应;次设备号通常用于标明同类型设备中的不同设备,它标志着某个具体的物理设备。32位设备号中的高12位为主设备号,低20位为次设备号。    

例如,在系统中的块设备IDE硬盘的主设备号是3,而多个IDE硬盘及其各个分区分别赋予次设备号1、2、3……

驱动层次结构

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第3张图片

设备驱动程序的特点

  1. (内核代码:设备驱动程序是内核的一部分,如果驱动程序出错,则可能导致系统崩溃。    
  2. (内核接口:设备驱动程序必须为内核或者其子系统提供一个标准接口。比如,一个终端驱动程序必须为内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件的I/O接口及缓冲区。    
  3. 内核机制和服务:设备驱动程序使用一些标准的内核服务,如内存分配等。
  4. 可装载:大多数的Linux操作系统设备驱动程序都可以在需要时装载进内核,在不需要时从内核中卸载。    
  5. 可设置:Linux操作系统设备驱动程序可以集成为内核的一部分,并可以根据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行相应的设置即可。    
  6. 动态性:在系统启动且各个设备驱动程序初始化后,驱动程序将维护其控制的设备。如果该设备驱动程序控制的设备不存在也不影响系统的运行,那么此时的设备驱动程序只是多占用了一点系统内存罢了。

字符设备驱动编程

设备驱动程序工作原理

模块在调用insmod命令时被加载,此时的入口点是init_module()函数,通常在该函数中完成设备的注册。同样,模块在调用rmmod命令时被卸载,此时的入口点是cleanup_module()函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作。

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第4张图片

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第5张图片

    #include 
    #include 
    static int __init hello_init()
    {
        printk(KERN_WARNING “(init)Hello World!\n”);
        return 0;
    }
    static void __exit hello_exit()
    {
        printk(KERN_INFO“(exit)Hello World!\n”);
    }
    module_init(hello_init);//设置模块初始化函数
    module_exit(hello_exit);//设置模块退出时清除函数

ARM9嵌入式Linux开发-Linux设备管理和驱动开发基础_第6张图片

重要数据结构(file_operaions结构)

用户应用程序调用设备的一些功能是在设备驱动程序中定义的,也就是设备驱动程序的入口点,它是一个在中定义的struct file_dperations结构,这是一个内核结构,不会出现在用户空间的程序中,它定义了常见文件I/O函数的入口。

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	…
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};
llseek:改变文件中的当前读/写位置,并且新位置作为(正的)返回值。
read:用来从设备中获取数据。
write:发送数据给设备。
readdir:对于设备文件这个成员应当为 NULL;它用来读取目录, 并且仅对文件系统有用。
poll:返回设备资源的可获取状态。
ioctl:提供了发出设备特定命令的方法。
mmap:用来请求将设备内存映射到进程的地址空间。
open:打开设备。
flush:在进程关闭设备文件描述符的拷贝时调用
release:释放设备。
sync:刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
fasync:异步通知。
lock:用来实现文件加锁

文件系统处理文件所需要的信息在inode(索引节点)数据结构体中。inode中保存了“页”结构,用于进行设备缓冲,当进行读写操作时,系统首先检查是否有inode存在,然后检查是否已经获得其缓冲内容。若没有,则请求;若已经存在,则把被写的“页”作上标记。     

inode结构提供了设备文件/dev/driver(这里假设设备名为driver)的信息。     

file结构主要与文件系统对应的设备驱动程序使用,其他设备驱动程序也可以使用。

设备号相关函数

在linux2.6的版本中,用dev_t类型来描述设备号(dev_t是32位数值类型,其中高12位表示主设备号,低20位表示次设备号)。用两个宏MAJOR和MINOR分别获得dev_t设备号的主设备号和次设备号,而且用MKDEV宏来实现逆过程,即组合主设备号和次设备号而获得dev_t类型设备号。 分配设备号有静态和动态的两种方法:    

  • 静态分配(register_chrdev_region()函数)是指在事先知道设备主设备号的情况下,通过参数函数指定第一个设备号(它的次设备号通常为0)而向系统申请分配一定数目的设备号。    
  • 动态分配(alloc_chrdev_region())是指通过参数仅设置第一个次设备号(通常为0,事先不会知道主设备号)和要分配的设备数目而系统动态分配所需的设备号。    

通过unregister_chrdev_region()函数释放已分配的(无论是静态的还是动态的)设备号。

字符设备注册

早期版本的设备注册使用函数register_chrdev(),调用该函数后就可以

向系统申请主设备号,如果register_chrdev()操作成功,设备名就会出现在/proc/devices文件里。在关闭设备时,通常需要解除原先的设备注册,此时可使用函数unregister_chrdev(),此后该设备就会从/proc/devices里消失。其中主设备号和次设备号不能大于255。

在Linux 2.6内核中使用struct cdev结构来描述字符设备,我们在驱动程序中必须将已分配到的设备号以及设备操作接口(即为struct file_operations结构)赋予struct cdev结构变量。    

字符设备的注册可分为如下3个步骤:    

  1. 使用cdev_alloc()函数向系统申请分配struct cdev结构;    
  2. 使用cdev_init()函数初始化已分配到的结构,并与file_operations结构关联起来;    
  3. 最后使用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册,这样新设备就可以被使用了。    

要从系统中删除一个设备,则要调用cdev_del()函数。

内核空间和用户空间的数据交换

内核空间地址和用户空间地址是有很大区别的,其中一个区别是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以不能使用诸如memcpy()之类的函数来完成这样的操作。在这里要使用copy_to_user()或copy_from_user()等函数,它们是用来实现用户空间和内核空间的数据交换的。

打印信息

在内核空间要用函数printk()而不能用平常的函数printf()。 printk()还可以定义打印消息的优先级。内核中共提供了八种不同的日志级别,在 linux/kernel.h 中有相应的宏对应。

#define KERN_EMERG    "<0>"    /* system is unusable */
#define KERN_ALERT    "<1>"    /* action must be taken immediately */
#define KERN_CRIT     "<2>"    /* critical conditions */
#define KERN_ERR      "<3>"    /* error conditions */
#define KERN_WARNING  "<4>"    /* warning conditions */
#define KERN_NOTICE   "<5>"    /* normal but significant */
#define KERN_INFO     "<6>"    /* informational */
#define KERN_DEBUG    "<7>"    /* debug-level messages */

所以 printk() 可以这样用:printk(KERN_INFO "Hello, world!\n");。

创建设备文件

创建设备文件有2种方法:

  1. 使用mknod命令手工创建
  2. 自动创建 手工创建:    

mknod用法:        

mknod  filename  type  major  minor    

参数:

  • filename:设备文件名          
  • type:设备文件类型          
  • major:主设备号          
  • minor:次设备号    

例:  # mknod  serial0  c  100  0

 

从Linux 2.6.13开始,利用udev(mdev)来实现设备文件的自动创建,在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。相关定义在device.h中。 例:

struct class *myclass = class_create(THIS_MODULE, “myclass”);
device_create(myclass, NULL, MKDEV(major_num, 0),  “my_device”);

当驱动被加载时,udev(mdev)就会自动在/dev下创建my_device设备文件。    

当驱动被卸载时,使用下面的函数。

device_destroy(myclass, MKDEV(major, 0));
class_destroy(myclass);

proc文件系统

/proc文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc存在于内存之中而不是在硬盘上。可以通过“ls”查看/proc文件系统的内容。

你可能感兴趣的:(ARM9嵌入式Linux开发)