11.4.3 一个字符设备驱动程序的实例

下面我们通过一个实例对字符设备以及编写驱动程序的方法进行说明,通过下面的分析我们可以了解一个设备驱动程序的编写过程以及注意事项。虽然这个驱动程序没有什么实用价值,但是我们也可以通过它对一个驱动程序的编写特别是字符设备驱动程序有一定的认识。

一个设备驱动程序在结构上是非常相似的,在 Linux , 驱动程序一般用C语言编写,有时也支持一些汇编和C++语言。

1.头文件、宏定义和全局变量

一个典型的设备驱动程序一般都包含有一个专用头文件,这个头文件中包含一些系统函数的声明、设备寄存器的地址、寄存器状态位和控制位的定义以及用于此设备驱动程序的全局变量的定义,另外大多数驱动程序还使用一些标准的头文件:

param.h    包含一些内核参数。 

dir.h      包含一些目录参数。 

user.h      用户区域的定义 。

tty.h       终端和命令列表的定义。

fs.h        其中包括 Buffer header 信息。

 

 下面是一些必要的头文件

 

#include <linux/kernel.h>  

#include <linux/module.h>

#if CONFIG_MODVERSIONS==1  /* 处理 CONFIG_MODVERSIONS */

#define MODVERSIONS

#include <linux/modversions.h>

#endif       

 

/* 下面是针对字符设备的头文件 */

#include <linux/fs.h>      

#include <linux/wrapper.h> 

 

/* 对于不同的版本我们需要做一些必要的事情*/

#ifndef KERNEL_VERSION

#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))

#endif

#if LINUX_VERSION_CODE > KERNEL_VERSION(2,4,0)

#include <asm/uaccess.h>  /* for copy_to_user */

#endif

  

#define SUCCESS 0

 

/* 声明设备 */

/* 这是本设备的名字,它将会出现在 /proc/devices */

#define DEVICE_NAME "char_dev"

 

/* 定义此设备消息缓冲的最大长度 */

#define BUF_LEN  100

 

/* 为了防止不同的进程在同一个时间使用此设备,定义此静态变量跟踪其状态 */

static int Device_Open = 0

 

/* 当提出请求的时候,设备将读写的内容放在下面的数组中 */

static char Message[BUF_LEN]

 

/* 在进程读取这个内容的时候,这个指针是指向读取的位置*/

static char *Message_Ptr

 

/* 在这个文件中,主设备号作为全局变量以便于这个设备在注册和释放的时候使用。*/

static int Major;

 

2open ()函数

功能:无论一个进程何时试图去打开这个设备都会调用这个函数。

 

static int device_open(struct inode *inode,

              struct file *file)

{

  static int counter = 0;

 

#ifdef DEBUG

  printk ("device_open(%p,%p)\n", inode, file);

#endif

 

  printk("Device: %d.%d\n",

    inode->i_rdev >> 8, inode->i_rdev & 0xFF);

 

/* 这个设备是一个独占设备,为了避免同时有两个进程使用这一个设备我们需要采取一定的措施*/

  if (Device_Open)

    return -EBUSY;

 

  Device_Open++;

 

/* 下面是初始化消息 , 注意不要使读写内容的长度超出缓冲区的长度,特别是运行在内核模式时,否则如果出现缓冲上溢则可能导致系统的崩溃。*/

  sprintf(Message,

 "If I told you once, I told you %d times - %s",

   counter++,

    "Hello, world\n");

 

  Message_Ptr = Message;

 

/*当这个文件被打开的时候,我们必须确认该模块还没有被移走并且增加此模块的用户数目。(在移走一个模块的时候会根据这个数字去决定可否移去,如果不是 0 则表明还有进程正在使用这个模块,不能移走。)*/

  MOD_INC_USE_COUNT;  

 

  return SUCCESS;

}

 

3release ( ) 函数

功能: 当一个进程试图关闭这个设备特殊文件的时候调用这个函数。

 

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)

static int device_release(struct inode *inode,

             struct file *file)

#else

static void device_release(struct inode *inode,

              struct file *file)

#endif

{

#ifdef DEBUG

  printk ("device_release(%p,%p)\n", inode, file);

#endif

 

  /* 为下一个使用这个设备的进程做准备。*/

  Device_Open --;

 

/* 减少这个模块使用者的数目,否则一旦你打开这个模块以后,你永远都不能释放掉它。*/

  MOD_DEC_USE_COUNT;

 

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)

  return 0;

#endif

}

 

 4.read ( ) 函数

功能:当一个进程已经打开此设备文件以后并且试图去读它的时候调用这个函数。

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)

static ssize_t device_read(struct file *file,

    char *buffer,    /* 把读出的数据放到这个缓冲区*/

    size_t length,   /* 缓冲区的长度*/

    loff_t *offset)  /* 文件中的偏移 */

#else

static int device_read(struct inode *inode,

                       struct file *file,

    char *buffer,       int length)

#endif

{

  /* 实际上读出的字节数 */

  int bytes_read = 0;

  /* 如果读到缓冲区的末尾,则返回0 ,类似文件的结束。*/

  if (*Message_Ptr == 0)

    return 0;

  /* 将数据放入缓冲区中*/

  while (length && *Message_Ptr)  {

 /* 由于缓冲区是在用户空间而不是内核空间,所以我们必须使用 copu_to_user()函数将内核空间中的数据拷贝到用户空间。*/

    copy_to_user(buffer++,*(Message_Ptr++), length--);

    bytes_read ++;

  }

 

#ifdef DEBUG

   printk ("Read %d bytes, %d left\n",

     bytes_read, length);

#endif

 

   /* Read 函数返回一个真正读出的字节数*/

  return bytes_read;

}

 

5. write ( )  函数

功能:当试图将数据写入这个设备文件的时侯,这个函数被调用

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)

static ssize_t device_write(struct file *file,

    const char *buffer,   

    size_t length,  

    loff_t *offset) 

#else

static int device_write(struct inode *inode,

                        struct file *file,

                        const char *buffer,

                        int length)

#endif

{

  int i;

 

#ifdef DEBUG

  printk ("device_write(%p,%s,%d)", file, buffer, length);

#endif

 

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)

    copy_from_user(Message, bufferlength);

 

  Message_Ptr = Message;

 

  /* 返回写入的字节数 */

  return i;

}

6.这个设备驱动程序提供给文件系统的接口

 当一个进程试图对我们生成的设备进行操作的时候就利用下面这个结构,这个结构就是我们提供给操作系统的接口,它的指针保存在设备表中,在init_module()中被传递给操作系统。

 

struct file_operations Fops = {

  read:    device_read,

  write:    device_write,

  open:     device_open,

  release: device_release

  };

 

 7. 模块的初始化和模块的卸载

这个函数用来初始化这个模块 注册该字符设备。init_module ()函数调用module_register_chrdev,把设备驱动程序添加到内核的字符设备驱动程序表中,它返回这个驱动程序所使用的主设备号。

 

int init_module()

{

  /* 试图注册设备*/

  Major = module_register_chrdev(0,

                                 DEVICE_NAME,

                                 &Fops);

 

  /* 失败的时候返回负值*/

  if (Major < 0) {

    printk ("%s device failed with %d\n",

       "Sorry, registering the character",

       Major);

    return Major;

  }

 

  printk ("%s The major device number is %d.\n",

          "Registeration is a success.",

          Major);

  printk ("If you want to talk to the device driver,\n");

  printk ("you'll have to create a device file. \n");

  printk ("We suggest you use:\n");

  printk ("mknod <name> c %d <minor>\n", Major);

  printk ("You can try different minor numbers %s",

          "and see what happens.\n");

 

  return 0;

}

 

 

这个函数的功能是卸载模块,主要是从  /proc 中 取消注册的设备特殊文件。

void cleanup_module()

{

  int ret;

 

  /* 取消注册的设备*/

  ret = module_unregister_chrdev(Major, DEVICE_NAME);

 

  /* 如果出错则显示出错信息 */

  if (ret < 0)

    printk("Error in unregister_chrdev: %d\n", ret);

}