注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

刺马的博客

 
 
 

日志

 
 

基于Linux平台PCI设备驱动程序设计(二)  

2009-10-25 16:54:27|  分类: LInux |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
第三章 可加载的内核模块:
  3.1 Linux内核与驱动程序
  在Linux系统中,若干并发进程执行着不同的任务。每个进程都可能有获得系统资源的要求。内核是一整块可执行代码,它负责处理所有这样的请求。内核可以被划分为以下这些部分:进程管理、内存管理、文件系统、设备控制、网络。
  Linux有一个很好的特性,即通过加载模块可以扩展内核代码,也就是说可以随时增加系统的功能。而本文所要讨论的PCI设备驱动程序其实也是加载到内核中的一个模块。
  世界各地钻研Linux内核的人群当中,大多是在写设备驱动程序。尽管每个驱动程序都不一样,而且还需知道自己设备的特殊细节,但是这些设备驱动程序的许多原则和基本技术技巧都是一样的。
   在编写驱动程序时,程序员应该注意以下问题:不同用户有不同的需求,程序员编写内核代码访问硬件时,不能强迫用户采用某种特定的策略。设备驱动程序应该 仅仅处理硬件,将如何使用硬件的问题留给应用程序。我们可以这样来看待我们所编写的驱动程序:它是位于应用层与实际设备之间的软件。程序员可以使不同的驱 动程序提供不同的能力,甚至相同的设备也可以提供不同的能力,只要使用不同的驱动程序即可。
  3.2 模块与应用程序
  3.2.1 内核模块与应用程序之间的区别
   一个应用程序从头到尾完成一个任务;而模块是可以在系统启动之后任何时刻动态连接到核心的代码块,它们可以在系统不再需要它们时从核心删除并卸载。 init_module( )(模块的入口点)在加载模块时被调用,其任务就是为以后调用模块的函数做准备;模块的第二个入口点,cleanup_module,仅当模块被下载前才 被调用。能够卸载是模块化的优良特性之一,这可以使程序开发者减少开发时间:无需每次都花很长的时间开关机就可以测试所编写的驱动程序。
  内核编程和应用程序编程还有一个区别,就是它们出错后所造成的后果不同:在应用程序开发期间,段违例是无害的,利用调试器可以轻松地跟踪到引起问题的错误之处;然而内核失效却是致命的,即使不至于使整个系统崩溃,那至少会使当前进程无法继续运行。
   在涉及到内核模块与应用程序之间的区别时,还得注意一下“名字空间污染”问题:即存在很多函数和全局变量时,它们的名字已不再富有足够的意义来很容易地 区分彼此的问题。在编写应用程序时,程序员就必须花大量的精力来记住某些“保留”名,并为新符号寻找新的唯一的名字。而在编写内核代码时如果出现“名字空 间污染”问题,那对程序员来说简直是无法容忍的,因为即便是最小的模块也要连接到整个内核中。防止此类问题出现的方法是把所有自己定义的符号都声明为 static。此外,也可以通过声明一个符号表来避免对所有符号都使用static声明。
  3.2.2 用户空间和内核空间
  操作系 统要为程序提供一个计算机硬件一致的视图;同时,操作系统有处理程序的独立操作,并防止对资源的未经授权的访问。这就要求CPU具有可以防止系统软件免受 应用软件干扰的保护机制,而每种现代处理器都能实现这种功能。实现的方案就是在CPU内部实现不同的操作模式(或级),不同的级有不同的权限,而且某些操 作不允许在最低级使用,程序代码只能从一个级切换到另一个级。在Linux系统中,执行态分最高级(也称为“管理员态”)和最低级(也称为“用户态”), 它们分别对应“内核空间”和“用户空间”。模块就是在“内核空间”运行的,而应用程序则是在“用户空间”中运行的。
  3.3 模块的基本结构
   内核模块至少必须包含两个函数:init_module和cleanup_module。第一个函数是在把模块加载入内核时调用的;第二个函数则是在删 除该模块时调用。一般说来,init_module向内核注册模块所能提供的所有新功能,即可以由应用程序使用的新功能。函数 cleanup_module的任务是清除掉init_module所做的一切,这样,这个模块就被安全地卸载了。
 .4 模块的编译和加载
  我们可以使用makefile来编译内核可以加载的目标代码(具体使用方法可参阅有关介绍makefile的资 料)。如果所编写的模块不是非常庞大,目标代码文件数量较少时,还有一种更为简单的方法可编译模块目标代码:直接使用gcc来编译目标代码,当然,在使用 gcc时必须包含编译内核模块所需的所有参数,如-DMODULE、-D_KERNEL_和-DLINUX等。
  模块编译好后,有两种方法可以载入模块:一种是使用命令insmod手工载入;另一种方法则更为灵活,是在需要时自动载入,当内核发现需要载入某个模块时,它会要求内核守护程序去载入相应的模块。
  内核守护程序是一个拥有超级用户权限的进程,它的主要工作是载入和卸载模块,它也做其他一些任务,如打开和关闭PPP连接。内核守护程序并非亲自做这些工作,而是调用相应的程序(如insmod)来完成,它只是一个内核代理,自动地安排调度各项工作。
  卸载模块的方法很简单,用rmmod命令即可。但对于在需要时载入的模块,当其不再需要时,会由kerneld自动将其从系统中删除。
  第四章 驱动程序框架
  在编写驱动程序之前,我们首先需要确定驱动程序能提供给用户程序何种能力。
  以下我们将以字符设备为主要介绍对象,这类设备的驱动程序适用于大多数简单的PCI设备。
  4.1 获得主设备号
  向系统增加一个驱动程序时,要赋予它一个主设备号。这一赋值过程应该在驱动程序的初始化过程中完成。调用如下函数可完成此过程,这个函数定义在:
  int register_chrdev(unsigned int major,
  const char *name,
  struct file_operatoins *fops);
  当出错时返回一个负值;成功时返回零或正值。参数major是所请求的主设备号,name是设备的名字,它将在/proc/devices中出现,fops是一个指向跳转表的指针,利用这个跳转表完成对设备函数的调用。
  接下来的问题就是如何给程序一个它们可以请求的设备驱动程序的名字。这个名字必须在/dev目录中,并与驱动程序的主设备号和次设备号相连。用mknod命令在文件系统上创建一个设备节点,如:
  mknod /dev/mydevice c 120 0
  创建了一个名字为“mydevice”的字符设备(c),主设备号是120,次设备号是0。
  上述是静态地分配主设备号的方法,事先为设备选取主设备号会出现个问题——可配置的设备要比主设备号多得多,主设备号可能会不够分配。
  我们还可以使用动态分配机制来获得主设备号。
  由于动态分配机制不能保证每次获得的主设备号总是一样的,似乎无法事先创建设备节点了。其实,一旦分配了设备号,我们总可以从/proc/devices读到,因此可以先从/proc/devices获得新分配的主设备号,再创建节点。上述过程需要编写脚本程序。
  4.2 释放主设备号
  当从系统中卸载一个模块时,应该释放该模块占用的主设备号。这一操作可以在cleanup_module中调用如下函数完成:
  int unregister_chrdev(unsigned int major,const char *name);
  参数major是要释放的主设备号,name是相应的设备名。
  还需要在卸载驱动程序时删除设备节点。如果设备节点是在加载时创建的,可以写一个简单的脚本在卸载时删除它们。
  4.3 文件操作
  Linux内核内部用file结构来识别设备,它代表了一个“打开的文件”,此结构定义在中。
  以下是我使用系统中的file结构的原型:
  struct file {
  struct file *f_next, **f_pprev;
  struct dentry *f_dentry;
  struct file_operations *f_op;
  mode_t f_mode;
  loff_t f_pos;
  unsigned int f_count, f_flags;
  unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
  struct fown_struct f_owner;
  unsigned int f_uid, f_gid;
  int f_error;
  unsigned long f_version;
  /* needed for tty driver, and maybe others */
  void *private_data;
  };
  其中的重要结构项罗列如下:
  mode_t f_mode;
  用户需要在ioctl函数中查看这个域来检查读/写权限,但由于内核在调用驱动程序的read和write前已经检查了权限,无需在这两个方法中检查权限。例如,一个不允许的写操作在驱动程序还不知道的情况下就被已经内核拒绝了。
  loff_t f_pos;
  为下一步读写操作设定当前文件的位置。loff_t是一个64位数值。如果驱动程序需要这个值,可以直接读取这个字段。如果定义了lseek方法,应该更新f_pos的值。当传输数据时,read和write也应该更新这个值。
  unsigned int f_flags;
  文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC。驱动程序为了支持非阻塞型操作需要检查这个标志。注意,检查读/写权限应该查看f_mode而不是f_flags。所有这些标志都定义在中。
  struct file_operations *f_op;
   与文件操作对应的指针。内核在完成open时对这个指针赋值,以后需要对文件进行操作时就访问此指针。f_op中的值并不保存,也就是说可以在需要的时 候修改文件所对应的操作,下一次再调用此打开文件的相应操作时就会调用新方法。这种技巧有助于在不增加系统调用负担的情况下方便地识别主设备号相同的设 备,这在面向对象编程技术中称为“方法重载”。
  void *private_data;
  系统在调用驱动程序的open方法前将这个指针置为NULL。驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。驱动程序可以用这个字段指向已分配的数据,但是一定要在内核释放file结构前的release方法中清除它。
  下面介绍驱动程序能够对它管理的设备完成哪些操作。
  我们可以设想驱动程序与操作系统内核之间存在一个接口,这个接口是通过数据结构file_operations来完成的。内核使用该结构访问驱动程序的函数。
  下面列举了应用程序能够对设备进行的部分操作,这些操作通常被称为“方法”,它们返回0时表示成功,发生错误时返回一个负的错误编码。
  loff_t (*llseek)(struct file *, off_t, int);
  方法llseek的功能是修改一个文件的当前读写位置,并将新位置做为(正的)返回值返回。出错时返回一个负值。
  ssize_t (*read) (struct file *, char *, size_t, loff_t *);
  用来从设备中读取数据。当其为NULL指针时,read系统调用返回-EINVAL(“非法参数”)。函数返回一个非负值时表示成功地读取了多少字节。
  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
  向设备发送数据。如果没有这个函数,write系统调用返回-EINVAL。如果返回值非负,表示成功写入的字节数。
  int (*readdir)(struct file *, void *, filldir_t);
  对于设备节点来说,这个字段应该为NULL,因为它仅用于目录。
  int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
   系统调用ioctl提供了一种调用设备相关命令的方法(比如软盘的格式化命令,既不是读操作也不是写操作)。另外,内核还识别一部分ioctl命令,而 不必调用结构file_operations中的ioctl。如果设备不提供ioctl入口点,对于任何内核没有定义的请求,ioctl系统调用将返回 -EINVAL。当调用成功时,返回给调用程序一个非负值。
  int (*mmap)(struct file *, struct vm_area_struct *);
  mmap用来将设备内存映射到进程内存中。如果设备不支持这个方法,mmap系统调用将返回-ENODEV。
  int (*open)(struct inode *, struct file *);
  尽管此方法总是操作设备节点所需的第一个步骤,然而并不要求驱动程序一定要声明这个方法。如果该项为NULL,设备的打开操作永远成功,但是系统不会通知驱动程序。
  void (*release)(struct inode *, struct file *);
  当节点被关闭时调用这个操作。与open相仿,也可以不用声明release。
  int (*fsync) (struct file *, struct dentry *);
  功能是刷新设备。如果驱动程序不支持,fsync系统调用返回-EINVAL。
  int (*fasync) (int, struct file *, int);
  这个操作用来通知设备FASYNC标志的变化。fasync调用在设备已经完全刷新数据后才返回。如果设备不支持异步触发,该字段可以是NULL。
  int (*check_media_change)(kdev_t dev);
  方法check_media_change只用于块设备。内核调用此方法来判断设备中的物理介质(如软盘)自最近一次操作以来发生了变化(返回1)或是没有(返回0)。而字符设备无需实现这个函数。
  int (*revalidate)(kdev_t dev);
  这一项与前面提到的那个方法一样,也只适用于块设备。revalidate与高速缓存区有关。
  下面简要介绍一下以上述的open、release、write、read和ioctl五项方法以及中断处理应该包含哪些内容。
  open方法
  open方法是驱动程序用来为以后的操作完成初始化准备工作的。此外,open还会增加设备计数值,以防止文件在关闭前模块被卸载出内核。
  open完成如下工作:
  检查设备相关错误(诸如设备未就绪或相似的硬件问题)。
  如果是首次打开,初始化设备。
  标别次设备号,如有必要更新f_op指针。
  分配和填写要放在filp->private_data里的数据结构。
  增加使用计数。
  release方法
  release方法的作用正好与open相反,这个方法有时也称为close。它完成如下工作:
  使用计数减1。
  释放open分配于private_data中的内存。
  做最后一次关闭操作时关闭设备。
  read和write 方法
  读写设备意味着要进行内核空间到用户进程空间的数据传输,以下函数可完成这些功能:
  void memcpy_fromfs(void *to,const void *from,unsigned long count);
  void memcpy_tofs(void *to,const void *from, unsigned long count);
  read方法的任务就是将数据从设备复制到用户空间,使用memcpy_tofs;write方法是将数据从用户空间复制到设备,使用memcpy_fromfs。
  调用read方法,返回值可能有如下的情况:
  如果返回值等于count参数传递给read系统调用的值,所请求的字节数传输成功完成了。
  如果返回值是正的,但是比count小,说明只有部分数据成功传送。这种情况下程序会重新读数据。
  返回值为0,表示已经到达了文件尾。
  write与read相似,返回值规则如下:
  如果返回值等于count,则表示完成了请求数目的字节传输。
  如果返回值是正的,但小于count,表示只传输了部分数据,程序很可能会再次读取余下的部分。
  如果返回值为0,表示什么也没写。
  负值表示发生了错误。
  ioctl 方法
  与read和其他方法不同,ioctl是设备相关的,它有允许应用程序访问被驱动硬件的特殊功能。
  在用户空间调用ioctl函数的原型为:
  int ioctl(int fd,int cmd,…);
  省略号(第三个参数)的具体情况与要完成的控制命令(第二个参数)有关,它可以是一个整数、一个指针,也可以不定义此参数。
  而在驱动程序内部实际起作用的是以下的函数:
  int (*ioctl)(struct inode *inode, struct file *filp,
  unsigned int cmd, unsigned long arg);
   inode和filp指针是根据应用程序传递的文件描述符fd计算而得的;cmd由用户空间调用ioctl的cmd参数传递而来;arg为可选参数,无 论此参数是指针还是整数,它都以unsigned long的形式传递给驱动程序,若调用程序没有传递第三个参数,驱动程序接收的arg没有任何意义。
  在ioctl中,可用switch语句来根据cmd参数选择正确的操作,不同的命令应该对应不同的数值。为了简化代码,通常使用符号名来代替数值,可在预处理过程中对这些符号名赋值。
  中断处理
  管理硬件最终就是要管理中断资源。
  Linux为中断处理提供了良好的接口,编写与安装中断处理程序的过程几乎和其他内核函数一样。但要注意,中断处理程序和系统的其他部分是异步运行的。
  内核维护着一个中断信号线注册表,一个模块可以申请一个中断通道号(IRQ),处理完后还可以释放掉。用定义在的以下函数可完成申请、释放工作:
  int request_irq( unsigned int irq,
  void (*handler)(int, void*, struct pt_regs *),
  unsigned long flags,
  const char *device, void *dev_id);
  void free_irq(unsigned int irq, void *dev_id);
   中断处理程序可以在驱动程序初始化时或者在设备第一次打开时安装。虽然可以在init_module函数中安装中断处理程序,但这并不是一种很好的做 法。因为中断信号线数量有限,用户的计算机拥有的设备通常要比中断信号线多。如果一个设备模块在初始化时就申请了一个中断,那么就会一直占用此中断,即便 这个设备根本不使用它占用的这个中断。调用request_irq的正确位置是在设备第一次打开,硬件被指示产生中断前的时候;而调用free_irq的 位置是设备最后关闭,硬件被通知不再需要中断处理的时候。
  因此,只要驱动程序设计得当,Linux就可使不同设备共享同一个中断信号线。
   如果在和硬件进行数据传输过程中因为某些原因会被延迟的话,那么驱动程序必须实现缓冲。数据缓冲可以将数据的发送和接收与write及read系统调用 分离开来,可提高系统的整体性能。一种好的缓冲机制就是“中断驱动的输入输出”,它在中断时间内填充一个输入缓冲区并由读设备的进程将其取空;或由写设备 的进程来填充一个输入缓冲区并在中断时间内将其取空。
  评论这张
 
阅读(290)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018