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

发表于:2007-07-04来源:作者:点击数: 标签:
.4 模块的编译和加载 我们可以使用makefile来编译内核可以加载的目标代码(具体使用方法可参阅有关介绍makefile的资料)。如果所编写的模块不是非常庞大,目标代码文件数量较少时,还有一种更为简单的方法可编译模块目标代码:直接使用gcc来编译目标代码,

  .4 模块的编译和加载
  我们可以使用makefile来编译内核可以加载的目标代码(具体使用方法可参阅有关介绍makefile的资料)。如果所编写的模块不是非常庞大,目标代码文件数量较少时,还有一种更为简单的方法可编译模块目标代码:直接使用gclearcase/" target="_blank" >cc来编译目标代码,当然,在使用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系统调用分离开来,可提高系统的整体性能。一种好的缓冲机制就是“中断驱动的输入输出”,它在中断时间内填充一个输入缓冲区并由读设备的进程将其取空;或由写设备的进程来填充一个输入缓冲区并在中断时间内将其取空。
  

原文转自:http://www.ltesting.net