内容发布更新时间 : 2024/12/29 12:35:34星期一 下面是文章的全部内容请认真阅读。
Linux操作系统将所有的设备(而不仅是存储器里的文件)全部都看成文件,都纳入文件系统的范畴,都通过文件的操作界面进行操作。这意味着:
(l)每一个设备都至少由文件系统的一个文件代表,因而都有一个“文件名”。每个这样的“设备文件”都唯一地确定了系统中地一项设备。应用程序通过设备地文件寻找访问具体地设备,而设备则象普通文件一样受到文件系统访问权限控制机制地保护。
(2)应用程序通常可以通过系统调用open()“打开”这个设备文件,建立起与目标设备的连接。代表着该设备的文件节点中记载着建立这种连接所需的信息。对于执行该应用程序的进程而言,建立起的连接就表现为一个已经打开的文件。 (3)打开了代表着目标设备的文件,即建立起与设备的连接后,就可以通过
read()、write()、ioctl()等常规的文件操作对目标设备进行操作。Linux将设备分成两大类。一类是像磁盘那样以记录块或“扇区”为单位,成块进行输入/输出设备,称为“块设备”;另一类是像键盘那样以字符(字节)为单位,逐个进行输入/输出的设备,称为“字符设备”、文件系统通常都建立在块设备上。网路设备是介于块设备和字符设备之间的一种特殊设备。设备文件的属性由三部分信息组成:第一部分是文件的类型(c/b),第二部分是一个“主设备号”,第三部分是一个“次设备号”。其中设备类型和主设备号结合在一起唯一地确定了设备文件地驱动程序及其界面,而次设备号则说明目标设备是同类设备中的第几个。应用程序通过Linux的系统调用与内核通信。由于Linux中将设备当作文件处理,所以对设备进行操作的调用和对文件操作的操作类似,主要包括open()、read()、write()、ioctl()、close()等。应用程序发出系统调用命令后,会从用户态转到内核态,通过内核将open()这样的系统调用转换成对物理设备的操作。在Linux中通过分层实现对物理设备的调用,并使得内核的结构清晰,提高了模块化的独立性。
2驱动程序的结构
一般Linux设备驱动程序可以分为3个主要组成部分:
(1)自动配置和初始化子程序,负责检测所要驱动的硬件设备是否存在和能否正常工作。如果设备正常则对这个设备及其相关的设备驱动程序需要的软件状态进行初始化。这部分驱动程序仅在初始化时被调用一次。
(2)服务于I/O请求的子程序,又称为驱动程序的上半部。调用这部分程序是由于系统调用的结果。这部分程序在执行时,系统仍认为是与进行调用的进程属于同一个进程,只是由用户态变成了核心态,具有进行此系统调用的用户程序的运行环境,因而可以在其中调用sleep()等与进程运行环境有关的函 数。
(3)中断服务程序,又称为驱动程序的下半部。在Linux系统中并不是直接从中断向量表调用设备驱动程序的中断服务子程序,而是由Linux系统来接收硬件中断,再由系统调用中断服务子程序。中断可以在任何一个进程运行时产生,因而在中断服务程序被调用时,不能依赖于任何进程的状态,也就不能调用任何与进程运行环境有关的函数。因为设备驱动程序一般支持同一类型的若干设备,所以一般在系统调用中断服务子程序时,都带有一个或多个参数,以唯一标志请求服务的设备。在系统内部,I/O设备的存/取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。具体到Linux系统,设备驱动程序所提
供的这组入口点由一个文件操作结构来向系统进行说明。file_operations结构
定义于linux/fs.h文件中,随着内核的不断升级,file_operations结构也越来越大,不同版本的内核会稍有不同。 struct file_operations{ struct module*owner;
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 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
datasync);
int(*fasync)(int,struct file*,int);
int(*lock)(struct file*,int,struct file_lock*);
ssize_t(*readv)(struct file*,const struct iovec*,unsigned long, loff_t*);
ssize_t(*writev)(struct file*,const struct iovec*,unsigned long, loff_t*);
ssize_t(*sendpage)(struct file*,struct page*,int,size_t,loff_t *,int);
unsigned long(*get_unmapped_area)(struct file*,unsigned long, unsigned long,unsigned long,unsigned long); };
file_operations结构中的成员全部是函数指针,所以实质上就是函数跳转表。每个进程对设备的操作,都会根据major、minor设备号,转换成对
file_operations结构的访问。常用的操作包括以下几种:lseek,移动文件指针的位置,只能用于可以随机存取的设备。read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误;否则,返回实际读取的字
节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是
inode-I_blksize的倍数。write,进行写操作,与read类似。select,进行选择操作。如果驱动程序没有提供select入口,select操作将会认为已经准备好进行任何的I/O操作。ioctl,进行读、写以外的其他操作,参数cmd为自定义的命令。mmap,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。open,打开设备进行I/O操作。返回0表示成功,返回负数表示失败。如果驱动程序没有提供open入口,则只要/dev/device文件存在就认为打开成功。release,即close操作。在用户自己的驱动程序中,首先要根据驱动程序的功能,完成file_operations结构中函数实现。不需要的函数接口可以直接在file_operations结构中初始化为NULL。file_operations变量会在驱动程序初
始化时,注册到系统内部。当操作系统对设备进行操作时,会调用驱动程序注册的file_operations结构中的函数指针。3 Linux对中断的处理在Linux系统中,对中断的处理是属于系统核心部分,因而如果设备与系统之间以中断方式进行数据交换,就必须把该设备的驱动程序作为系统核心的一部分。设备驱动程序通过调用request_irq函数来申请中断,通过free_irq来释放中断,它们被定义为: #include
int request_irq(unsigned int irq,
void(*handler)(int irq,void dev_id,struct pt_regs*regs), unsigned long flags, const char*device, void*dev_id);
void free_irq(unsigned int irq,void*dev_id); 参数irq表示所要申请的硬件中断号;handler为向系统登记的中断处理子程序,中断产生时由系统来调用,调用时所带参数irq为中断号;dev_id为申请时告诉系统的设备标识;regs为中断产生时的寄存器内容;device为设备名,将会出现在/proc/interrupts文件里;flag是申请时的选项,它决定中断处理程序的一些特性,其中最重要的是中断处理程序是快速处理程序还是慢速处理程序。快速处理程序运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在运行的中断外,其他中断都没有被屏蔽。在Linux系统中,中断 可以被不同的中断处理程序共享。作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用malloc和free,而是kmalloc和kfree,它们被定义为: #include
void*kmalloc(unsigned int len,int priority); void kfree(void*obj);
参数len为希望申请的字节数;obj为要释放的内存指针;priority为分配内存操作的优先级,即在没有空闲内存时如何操作,一般用GFP_KERNEL。与中断和内存不同,使用一个没有申请的I/O端口不会使系统产生异常,也就不会导致诸如“segmentation fault”一类的错误发生。任何进程都可以访问任何一个I/O端口,此时系统无法保证对I/O端口的操作不会发生冲突,甚至因此而使系统崩溃,因此,在使用I/O端口前,也应该检查此I/O端口是否已经有别的程序在使用。若没有,再把此端口标识为正在使用,在使用完以后释放
它。在设备驱动程序中,可以调用printk来打印一些调试信息,用法与printf类似。Printf打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。4设备驱动的初始化设备驱动程序所提供的入口点,在设备驱动程序初始化时向系统进行登记,以便系统在适当的时候调用。Linux系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。 register_chrdev定义为: #include
int register_chrdev(unsigned int major,const char*name,struct file_operations*fops);
其中,major是为设备驱动程序向系统申请的主设备号,如果为0,则系统为此驱动程序动态分配一个主设备号。Name是设备名。Fops即上述对各个调用的入口点说明。此函数返回0时表示成功。返回-EINVAL表示申请的主设备号非法,
一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其他设备程序使用。如果动态分配主设备号成功,此函数将返回所分配的主设备号。如果register_chrdev操作成功,设备名就会出现在/proc/dvices文件中。Linux为每个设备在/dev目录中建立一个文件,若用ls–l命令列出函数返回值,则小于0表示注册失败;返回0或者大于0的值表示注册成功。Linux kernel 2.0支持128个主设备号Linux kernel 2.2和2.4支持256个主设备号(0和255保留)。注册以后,Linux把设备名和主/次设备号联系起来。当有对此设备名的访问时,Linux通过请求访问的设备名
得到主/次设备号,然后把此访问分发到对应的设备驱动,设备驱动再根据次设备号调用不同的函数。当设备驱动模块从Linux内核中卸载,对应的主设备号必须被释放。在模块卸载调用cleanup_module()函数时,应该调用下面的函数卸载设备驱动:
int unregister_chrdev(unsigned int major,const char*name);
此函数的参数为主设备号major和设备名name。Linux内核把name和major在内核注册的名称对比,如果不相等,卸载失败,并返回-EINVAL;如果major大于最大的设备号,也返回-EINVAL。初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O端口等,这些资源也可以在open子程序或者其他地方申请。这些资源不用时,应该释放,以利于资源的共享。 设备驱动的初始化函数只要完成的功能是: (1)对驱动程序管理的硬件进行必要的初始化
对硬件寄存器进行设置。比如设置中断掩码,设置串口的工作方式、并口的数据方向等。
(2)初始化设备驱动相关的参数一般说来,每个设备都要定义一个设备变量,用以保存设备相关的参数。在
这里可以对设置变量中的项进行初始化。 (3)在内核注册设备
Linux内核通过设备的主设备号和从设备号来访问设备驱动,每个驱动程序都有唯一的主设备号。设备号可以自动获取,内核会分配一个独一无二的主设备号,但这样每次获得的主设备号可能不一样,设备文件必须重新建立,所有最好手工给设备分配一个主设备号。可以查看Linux文件系统中/proc下的devices文件,该文件记录内核中已经使用的主设备号和相应的设备名,选择一个没有被使用的主设备号,调用下面的函数来注册设备:
int register_chrdev(unsigned int,const char*,struct filr_operations*)
其中三个参数分别表示主设备号、设备名称和上面定义的filr_operation结构地址。该函数是在/linux/include/linux/fs.h中定义的。 (4)注册中断
如果设备需要IRQ支持,则要注册中断。注册中断使用函数: in request_irq(unsigned int irq,
void(*handler)(int,void*,struct pt_regs*), unsigned long flags, const char*device, void*dev_id); (5)其他初始化工作
比如给设备分配I/O。申请DMA通道等。若驱动程序是内核的一部分,则要按如下方式:
int__init chr_driver_init(void);
声明,注意不能缺少__init。在系统启动时会由内核调用chr_driver_init,完成驱动程序的初始化。
当驱动程序是以模块的形式编写时,则要按照如下方式: int init_module(void) 注:当运行insmod命令插入模块时,会调用init_module函数完成初始化工作。