linux 基础复习(9)设备驱动入门
扫描二维码
随时随地手机看文章
Linux 内核中采用可加载的模块化设计(LKMs,Loadable Kernel Modules),一般情况下编译的Linux 内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其他的代码可以选择在内核中,或者编译为内核的模块文件。常见的驱动程序也是作为内核模块动态加载的。
模块相关命令
lsmod 列出当前系统加载的模块
rmmod 将当前模块卸载
insmod、modprobe 用于加载当前模块。但insmod不会自动解决依存关系,而modprobe可以根据模块间的依存关系以及 /etc/modules.conf 文件中的内容自动插入模块
mknod 创建相关模块
Linux 系统的设备文件分为三类:块设备文件、字符设备文件和网络设备文件。
· 块设备文件通常指一些需要以块(如512 字节)的方式写入的设备,如IDE 硬盘、SCSI硬盘、光驱等。
· 字符型设备文件通常指可以直接读写,没有缓冲区的设备,如并口、虚拟控制台等。
· 网络设备文件通常是指网络设备访问的BSD socket接口,如网卡等。
设备号设备号是一个数字,它是设备的标志。就如前面所述,一个设备文件(也就是设备节点)可以通过mknod命令来创建,其中指定了主设备号和次设备号。主设备号表明某一类设备,
一般对应着确定的驱动程序;次设备号一般是用于区分标明不同属性,例如不同的使用方法,不同的位置,不同的操作等,它标志着某个具体的物理设备。高字节为主设备号和底字节为次设备号。例如,在系统中的块设备IDE 硬盘的主设备号是3,而多个IDE 硬盘及其各个分区分别赋予次设备号1、2、3……
Linux 设备驱动程序包含中断处理程序和设备服务子程序两部分
设备服务子程序包含了所有与设备操作相关的处理代码。它从面向用户进程的设备文件系统中接受用户命令,并对设备控制器执行操作。这样,设备驱动程序屏蔽了设备的特殊性,使用户可以像对待文件一样操作设备。
设备控制器需要获得系统服务时有两种方式:查询和中断。因为Linux 下的设备驱动程序是内核的一部分,在设备查询期间系统不能运行其他代码,查询方式的工作效率比较低,所以只有少数设备如软盘驱动程序采取这种方式,大多设备以中断方式向设备驱动程序发出输入/输出请求。
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://blogimg.chinaunix.net/blog/upfile2/080413193842.jpg" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
Linux 中的设备驱动程序有如下特点。
(1)内核代码:设备驱动程序是内核的一部分,如果驱动程序出错,则可能导致系统崩溃。
(2)内核接口:设备驱动程序必须为内核或者其子系统提供一个标准接口。比如,一个终端驱动程序必须为内核提供一个文件I/O 接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件的I/O 接口及缓冲区。
(3)内核机制和服务:设备驱动程序使用一些标准的内核服务,如内存分配等。
(4)可装载:大多数的Linux 操作系统设备驱动程序都可以在需要时装载进内核,在不需要时从内核中卸载。
(5)可设置:Linux 操作系统设备驱动程序可以集成为内核的一部分,并可以根据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行相应的设置即可。
(6)动态性:在系统启动且各个设备驱动程序初始化后,驱动程序将维护其控制的设备。
如果该设备驱动程序控制的设备不存在也不影响系统的运行,那么此时的设备驱动程序只是多占用了一点系统内存罢了。
驱动开发时却没有main 函数,模块在调用insmod命令时被加载,此时的入口点是init_module函数,通常在该函数中完成设备的注册。同样,模块在调rmmod
函数时被卸载,此时的入口点是cleanup_module函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如read、write等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作,init_module入口点函数则不需要完成其他如read、write之类功能。
设备驱动程序的入口点,它是一个在中定义的struct file结构,这是一个内核结构,不会出现在用户空间的程序中,它定义了常见文件I/O 函数的入口。
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *filp, char *buff, size_t count, loff_t *offp);
ssize_t (*write) (struct file *filp, const char *buff, size_t count, loff_t *offp);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned
long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};
每个设备的驱动程序不一定要实现其中所有的函数操作,若不需要定义实现时,则只需将其设为NULL即可。[!--empirenews.page--]
struct inode提供了关于设备文件/dev/driver(假设此设备名为driver)的信息。struct file 提供关于被打开的文件信息,主要用于与文件系统对应的设备驱动程序使用。struct file 较为重要,这里列出了它的定义:
struct file {
mode_t f_mode;/*标识文件是否可读或可写,FMODE_READ或FMODE_WRITE*/
dev_t f_rdev; /* 用于/dev/tty */
off_t f_pos; /* 当前文件位移 */
unsigned short f_flags; /* 文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC */
unsigned short f_count; /* 打开的文件数目 */
unsigned short f_reada;
struct inode *f_inode; /*指向inode的结构指针 */
struct file_operations *f_op;/* 文件索引指针 */
};
设备驱动程序主要组成
(1)设备注册
设备注册使用函数register_chrdev,调用该函数后就可以向系统申请主设备号,如果register_chrdev操作成功,设备名就会出现在/proc/devices 文件里。
register_chrdev等函数语法要点所需头文件 #i nclude 函数原型 int register_chrdev(unsigned int major, const char *name,struct file_operations *fops) major:设备驱动程序向系统申请的主设备号 如果为0 则系统为此驱动程序动态地分配一个主设备号函数传入值 name:设备名 fops:对各个调用的入口点函数返回值 成功:如果是动态分配主设备号,此返回所分配的主设备号 且设备名就会出现在/proc/devices文函数返回值 件里
出错:-1
(2)设备解除注册
在关闭设备时,通常需要解除原先的设备注册,此时可使用函数unregister_chrdev,此后该设备就会从/proc/devices 里消失。
unregister_chrdev等函数语法要点
所需头文件 #i nclude
函数原型 int unregister_chrdev(unsigned int major, const char *name)
major:设备的主设备号,必须和注册时的主设备号相同。
函数传入值 name:设备名
函数返回值 成功:0,且设备名从/proc/devices文件里消失。
出错:-1
(3)打开设备
打开设备的接口函数是open,根据设备的不同,open函数完成的功能也有所不同,但通常情况下在open函数中要完成如下工作。
· 递增计数器。
· 检查特定设备的特殊情况。
· 初始化设备。
· 识别次设备号
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开较多次数,也可以由不同的进程所使用,所以若有一进程想要关闭该设备,则必须保证其他设备没有使用该设备。因此使用计数器就可以很好地完成这项功能。
这里,实现计数器操作的是用在中定义的3 个宏如下。
· MOD_INC_USE_COUNT:计数器加一。
· MOD_DEC_USE_COUNT:计数器减一。
· MOD_IN_USE:计数器非零时返回真。
另外,当有多个物理设备时,就需要识别次设备号来对各个不同的设备进行不同的操作,在有些驱动程序中并不需要用到。
虽然这是对设备文件执行的第一个操作,但却不是驱动程序一定要声明的操作。若这个函数的入口为NULL,那么设备的打开操作将永远成功,但系统不会通知驱动程序。
(4)释放设备
释放设备的接口函数是release。要注意释放设备和关闭设备是完全不同的。当一个进程释放设备时,其他进程还能继续使用该设备,只是该进程暂时停止对该设备的使用;而当一个进程关闭设备时,其他进程必须重新打开此设备才能使用。
释放设备时要完成的工作如下。
· 递减计数器MOD_DEC_USE_COUNT。
· 在最后一次释放设备操作时关闭设备。
(5)读写设备
读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将内核空间缓冲区里的数据复制到用户空间的缓冲区中或者相反。这里首先解释一个read和write函数的入口函数,如下表所示。
read、write函数语法要点
所需头文件 #i nclude
函数原型 ssize_t (*read) (struct file *filp, char *buff, size_t count, loff_t *offp)
ssize_t (*write) (struct file *filp, const char *buff, size_t count, loff_t *offp)
filp:文件指针
函数传入值 buff:指向用户缓冲区
count:传入的数据长度
offp:用户在文件中的位置
函数返回值 成功:写入的数据长度
虽然这个过程看起来很简单,但是内核空间地址和应用空间地址是有很大区别的,其中之一就是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以就不能使用诸如memcpy 之类的函数来完成这样的操作。在这里就要使用copy_to_user 或copy_from_user 函数,它们就是用来实现用户空间和内核空间的数据交换的。copy_to_user 和copy_from_user 的格式如下表
所需头文件 #i nclude
函数原型 Unsigned long copy_to_user(void *to, const void *from, unsigned long count)
Unsigned long copy_from_user(void *to, const void *from, unsigned long count)
To:数据目的缓冲区
函数传入值 From:数据源函数传入值 缓冲区
count:数据长度
函数返回值 成功:写入的数据长度
失败:-EFAULT
这两个函数不仅实现了用户空间和内核空间的数据转换,而且还会检查用户空间指针的有效性。如果指针无效,那么就不进行复制
(6)获取内存
在应用程序中获取内存通常使用函数malloc,但在设备驱动程序中动态开辟内存可以有基于内存地址和基于页面为单位两类。其中,基于内存地址的函数有kmalloc,注意的是,kmalloc函数返回的是物理地址,而malloc 等返回的是线性地址,因此在驱动程序中不能使用malloc函数。与malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方,并且不会对所获取的内存空间清零。
基于页为单位的内存有函数族有如下。
· get_zeroed_page:获得一个已清零页面。[!--empirenews.page--]
· get_free_page:获得一个或几个连续页面。
· get_dma_pages:获得用于DMA传输的页面。
与之相对应的释放内存用也有kfree或free_pages 族。
kmalloc 函数语法要点
所需头文件 #i nclude
函数原型 void *kmalloc(unsigned int len,int flags)
Len:希望申请的字节数
GFP_KERNEL:内核内存的通常分配方法,可能引起睡眠
GFP_BUFFER:用于管理缓冲区高速缓存
函数传入值 flags GFP_ATOMIC:为中断处理程序或其他运行于进程上下文之外的代码分
配内存,且不会引起睡眠
GFP_USER:用户分配内存,可能引起睡眠
GFP_HIGHUSER:优先高端内存分配
_GFP_DMA:DMA数据传输请求内存
_GFP_HIGHMEN:请求高端内存
函数返回值 成功:写入的数据长度
失败:-EFAULT
kfree函数的语法格式
所需头文件 #i nclude
函数原型 void kfree(void * obj)
函数传入值 obj:要释放的内存指针
函数返回值 成功:写入的数据长度
失败:-EFAULT
get_free_ page类函数语法要点
unsigned long get_zeroed_page(int flags)
unsigned long __get_free_page(int flags)
函数原型 unsigned long __get_free_page(int flags,unsigned long order)
unsigned long __get_dma_page(int flags,unsigned long order)
函数传入值 flags:同kmalloc
order:要请求的页面数,以2为底的对数
函数返回值 成功:写入的数据长度
失败:-EFAULT
free_page类函数语法要点
所需头文件 #i nclude
函数原型 unsigned long free_page(unsigned long addr)
unsigned long free_page(unsigned long addr)
函数传入值 flags:同kmalloc
order:要请求的页面数,以2为底的对数
函数返回值 成功:写入的数据长度
失败:-EFAULT
printk类函数语法要点
所需头文件 #i nclude
函数原型 int printk(const char * fmt,…)
KERN_EMERG:紧急时间消息
KERN_ALERT:需要立即采取动作的情况
KERN_CRIT:临界状态,通常涉及严重的硬件或软件操作失败
KERN_ERR:错误报告
函数传入值 fmt: KERN_WARNING:对可能出现的问题提出警告
日志级别 KERN_NOTICE:有必要进行提示的正常情况
KERN_INFO:提示性信息
KERN_DEBUG:调试信息
…:如printf一样的格式说明
函数返回值 成功:0
失败:-1
这些不同优先级的信息可以输出到控制台上、/var/log/messages 里。其中,对输出给控制台的信息有一个特定的优先级console_loglevel。若优先级小于这个整数值时,则消息才能显示到控制台上,否则,消息会显示在/var/log/messages 里。若不加任何优先级选项,则消息默认输出到/var/log/messages 文件中。
要开启klogd和syslogd服务,消息才能正常输出。
proc 文件系统
/proc 文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关进程的有用信息,在运行时通过改变内核参数改变设置。与其他文件系统不同,/proc存在于内存之中而不是硬盘上。读者可以通过“ls”查看/proc文件系统的内容。
/proc文件系统主要目录内容
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://blogimg.chinaunix.net/blog/upfile2/080413215654.jpg" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
除此之外,还有一些是以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID 号为目录名,它们是读取进程信息的接口。进程目录的结构如下
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://blogimg.chinaunix.net/blog/upfile2/080413215720.jpg" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
用户可以使用cat命令来查看其中的内容。
可以看到,/proc文件系统体现了内核及进程运行的内容,在加载模块成功后,读者可以使用查看/proc/device文件获得相关设备的主设备号。