linux下字符设备驱动详解

来源:互联网 发布:北京四达时代 知乎 编辑:程序博客网 时间:2024/05/16 08:01
LINUX系统下的设备驱动程序 


具体到LINUX系统里,设备驱动程序所提供的这组入口点由一个结构来向系统进行说明,此结构定义为: 
#include struct file_operations { int (*lseek)(struct inode *inode,struct file *filp,off_t off,int pos); int (*read)(struct inode *inode,struct file *filp,char *buf, int count); int (*write)(struct inode *inode,struct file *filp,char *buf,int count); int (*readdir)(struct inode *inode,struct file *filp,struct dirent *dirent,int count); int (*select)(struct inode *inode,struct file *filp,int sel_type,select_table *wait); int (*ioctl) (struct inode *inode,struct file *filp,unsigned int cmd,unsigned int arg); int (*mmap) (void); int (*open) (struct inode *inode, struct file *filp); void (*release) (struct inode *inode, struct file *filp); int (*fsync) (struct inode *inode, struct file *filp); }; 




其中,struct inode提供了关于特别设备文件/dev/driver(假设此设备名为driver)的信息,它的定义为: 
#include struct inode { dev_t i_dev; unsigned long i_ino; /* Inode number */ umode_t i_mode; /* Mode of the file */ nlink_t i_nlink; uid_t i_uid; gid_t i_gid; dev_t i_rdev; /* Device major and minor numbers*/ off_t i_size; time_t i_atime; time_t i_mtime; time_t i_ctime; unsigned long i_blksize; unsigned long i_blocks; struct inode_operations * i_op; struct super_block * i_sb; struct wait_queue * i_wait; struct file_lock * i_flock; struct vm_area_struct * i_mmap; struct inode * i_next, * i_prev; struct inode * i_hash_next, * i_hash_prev; struct inode * i_bound_to, * i_bound_by; unsigned short i_count; unsigned short i_flags; /* Mount flags (see fs.h) */ unsigned char i_lock; unsigned char i_dirt; unsigned char i_pipe; unsigned char i_mount; unsigned char i_seek; unsigned char i_update; Union{ struct pipe_inode_info pipe_i; struct minix_inode_info minix_i; struct ext_inode_info ext_i; struct msdos_inode_info msdos_i; struct iso_inode_info isofs_i; struct nfs_inode_info nfs_i; } u; }; 



struct file主要用于与文件系统对应的设备驱动程序使用。当然,其它设备驱动程序也可以使用它。它提供关于被打开的文件的信息,定义为:

#include struct file { mode_t f_mode; dev_t f_rdev; /* needed for /dev/tty */ off_t f_pos; /* Curr. posn in file */ unsigned short f_flags; /* The flags arg passed to open */ unsigned short f_count; /* Number of opens on this file */ unsigned short f_reada; struct inode *f_inode; /* pointer to the inode struct */ struct file_operations *f_op;/* pointer to the fops struct*/ };  



在结构file_operations里,指出了设备驱动程序所提供的入口点位置,分别是 
(1) lseek,移动文件指针的位置,显然只能用于可以随机存取的设备。 
(2) read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误,否则返回实际读取的字节数。
    对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode->i_blksize的的倍数。 
(3) write,进行写操作,与read类似。 
(4) readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才使用。 
(5) selec,进行选择操作,如果驱动程序没有提供select入口,select操作将会认为设备已经准备好进行任何的I/O操作。 
(6) ioctl,进行读、写以外的其它操作,参数cmd为自定义的的命令。 
(7) mmap,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。 
(8) open,打开设备准备进行I/O操作。返回0表示打开成功,返回负数表示失败。如果驱动程序没有提供open入口,则只要/dev/driver文件存在就认为打开成
    功。 
(9) release,即close操作。 




设备驱动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。LINUX系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。register_chrdev定义为: 

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); 

其中,major是为设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号。name是设备名。fops就是前面所说的对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。如果是动态分配主设备号成功,此函数将返回所分配的主设备号。如果register_chrdev操作成功,设备名就会出现在/proc/devices文件里。 
初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O端口等,这些资源也可以在open子程序或别的地方申请。在这些资源不用的时候,应该释放它们,以利于资源的共享。在UNIX系统里,对中断的处理是属于系统核心的部分,因此如果设备与系统之间以中断方式进行数据交换的话,就必须把该设备的驱动程序作为系统核心的一部分。



设备驱动程序通过调用request_irq函数来申请中断,通过free_irq来释放中断。它们的定义为: 

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是申请时的选项,它决定中断处理程序的一
些特性,其中最重要的是中断处理程序是快速处理程序(flag里设置了SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT),快速处理程序运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在处理的中断外,其它中断都没有被屏蔽。 
   




在LINUX系统中,中断可以被不同的中断处理程序共享,这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,这些处理程序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id可以为NULL。request_irq返回0表示成功,返回- INVAL表示irq>15或handler==NULL,返回-EBUSY表示中断已经被占用且不能共享。作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用malloc和free,而代之以调用kmalloc和kfree,它们被定义为: 

void * kmalloc(unsigned int len, int priority); void kfree(void * obj); 


参数len为希望申请的字节数,obj为要释放的内存指针。priority为分配内存操作的优先级,即在没有足够空闲内存时如何操作,一般用 GFP_KERNEL。与中断和内存不同,使用一个没有申请的I/O端口不会使CPU产生异常,也就不会导致诸如“segmentation fault"一类的错误发生。任何进程都可以访问任何一个I/O端口。此时系统无法保证对I/O端口的操作不会发生冲突,甚至会因此而使系统崩溃。因此,在使用I/O端口前,也应该检查此I/O端口是否已有别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。这样需要用到如下几个函数: 

int  check_region(unsigned int from, unsigned int extent); void request_region(unsigned int from, unsigned int extent, const char *name); void release_region(unsigned int from, unsigned int extent); 

调用这些函数时的参数为:from表示所申请的I/O端口的起始地址;extent为所要申请的从from开始的端口数;name为设备名,将会出现在/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在被使用。 
在申请了I/O端口之后,就可以如下几个函数来访问I/O端口: 

inline unsigned int inb(unsigned short port); inline unsigned int inb_p(unsigned short port); inline void outb(char value, unsigned short port); inline void outb_p(char value, unsigned short port);

其中inb_p和outb_p插入了一定的延时以适应某些慢的I/O端口。在设备驱动程序里,一般都需要用到计时机制。在LINUX系统中,时钟是由系统接管,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有: 

void add_timer(struct timer_list * timer); int  del_timer(struct timer_list * timer); inline void init_timer(struct timer_list * timer); 



struct timer_list的定义为: 
struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data; void (*function)(unsigned long d); }; 



其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES表示当前时间,一般在调用add_timer时 jiffies=JIFFIES+num,表示在num个系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ表示一秒内最小时间间隔的数目,则num*HZ表示num秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删除,因此如果想要每隔一定时间间隔执行一次的话,就必须在function里再一次调用add_timer。function的参数d即为timer里面的 data项。在设备驱动程序里,还可能会用到如下的一些系统函数: 

#define cli() __asm__ __volatile__ ("cli"::) #define sti() __asm__ __volatile__ ("sti"::) 


这两个函数负责打开和关闭中断允许。 


void memcpy_fromfs(void * to,const void * from,unsigned long n); void memcpy_tofs(void * to,const void * from,unsigned long n); 


在用户程序调用read 、write时,因为进程的运行状态由用户态变为核心态,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地址空间复制, memcpy_tofs则反之。参数to为复制的目的指针,from为源指针,n为要复制的字节数。在设备驱动程序里,可以调用printk来打印一些调试信息,用法与printf类似。printk打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。 

LINUX系统下的具体实现 
在LINUX里,除了直接修改系统核心的源代码,把设备驱动程序加进核心里以外,还可以把设备驱动程序作为可加载的模块,由系统管理员动态地加载它,使之成为核心地一部分。也可以由系统管理员把已加载地模块动态地卸载下来。 

LINUX中,模块可以用C语言编写,用gcc编译成目标文件(不进行链接,作为*.o文件存在),为此需要在gcc命令行里加上-c的参数。在编译时,还应该在gcc的命令行里加上这样的参数:-D__KERNEL__ -DMODULE。由于在不链接时,gcc只允许一个输入文件,因此一个模块的所有部分都必须在一个文件里实现。编译好的模块*.o放在 /lib/modules/xxxx/misc下(xxxx表示核心版本,如在核心版本为2.0.30时应该为 /lib/modules/2.0.30/misc),然后用depmod -a使此模块成为可加载模块。模块用insmod命令加载,用rmmod命令来卸载,并可以用lsmod命令来查看所有已加载的模块的状态。 

编写模块程序的时候,必须提供两个函数,一个是int init_module(void),供insmod在加载此模块的时候自动调用,负责进行设备驱动程序的初始化工作。init_module返回0以表示初始化成功,返回负数表示失败。另一个函数是voidcleanup_module (void),在模块被卸载时调用,负责进行设备驱动程序的清除工作。 

在成功的向系统注册了设备驱动程序后(调用register_chrdev成功后),就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时候,只要对此特别文件进行操作就行了。 



原创粉丝点击