主题:《Linux内核模块编程指南》(二)

来源:互联网 发布:知乎怎么取消赞同 编辑:程序博客网 时间:2024/04/27 16:12
主题:《Linux内核模块编程指南》(二)发信人: kevintz()
整理人: kevintz(2000-06-22 00:59:18), 站内信件<<Linux 内核模块编程指南>>
<<Linux Kernel Module Programming Guide>>
作者:Ori Pomerantz 中译者:谭志(lkmpg@21cn.com)
  
译者注:
1、LKMPG是一本免费的书,英文版的发行和修改遵从GPL version 2的许可。为了
节省时间,我只翻译了其中的大部分的大意,或者说这只是我学习中的一些中文
笔记吧,不能算是严格上的翻译,但我认为这已经足够了。本文也允许免费发布
,但发布前请和我联系,但不要把本文用于商业目的。鉴于本人的水平,文章中
难免有错误,请大家不吝指正。
2、本文中的例子在Linux(kernel version 2.2.10)上调试通过。你用的Linux必
须支持内核模块的加载,如果不支持,请在编译内核时选上内核模块的支持或升
级你的内核到一个支持内核模块的版本。
     


                          第二章 字符设备文件


    我们已经写了最简单的内核模块,并且它的确能正常运行。但我们好象还缺
少点什么,毕竟这么简单的内核模块不是很能引起我们的热情。

    内核模块有两个主要方法跟进程进行通信。一个是通过设备文件(象/dev目录
下的文件),另一个是使用proc文件系统。当我们写驱动程序来支持一些硬件时,
通常是使用设备文件,因此,我们从设备文件开始。
      
    设备文件原来的目的是允许进程和内核中的设备驱动程序通信,再通过驱动
程序和物理设备(modem,终端等)进行通信。这就是用来实现我们下面的字符内核
模块的方式。
    
    每个负责管理相应硬件的设备驱动程序都被赋予一个它自己的主设备号(maj
or number)。系统中可用驱动程序的列表和他们的主设备号被列出在/proc/devi
ces文件里。被设备驱动程序管理的每一个物理设备都被赋予一个次设备号,并在
/dev目录下有一个对应的文件,叫做设备文件,但不论真实物理设备是否安装,
它都会在/dev下有这个文件。

    例如,如果你用ls -l /dev/hd[ab]*,你会看到所有连到机器的IDE硬盘的分
区。它们都用同一个主设备号,但次设备号却不同(假设你是使用PC体系)。我不
清楚运行在其他体系的机器上的Linux的设备情况。

    系统安装时,所有这些设备文件通过mknod命令产生。并没有技术上的原因使
得这些文件一定要放在/dev目录下,仅仅是一个有用的惯例而已。当我们的例子
要建立一个设备文件时,放在编译内核模块的目录下比放在/dev下更方便些。
    
    设备分为两类:字符设备和块设备。块设备的不同之处在于对请求有缓冲机
制,所以能够以不同于请求的顺序来回应请求。这对存储设备这些快速的设备很
重要。另一个不同之处是块设备只能以块(块的大小随不同的设备而不同)为单位
来读写数据。字符设备则允许读写任意数量的字节,多少只由进程的需要来决定
。大多数的设备都是字符设备,因为他们不需要缓冲机制和操作不要求以块为限
制。你可以用ls -l命令来判断一个设备文件是块设备还是字符设备,第一个字符
是'b'表示是块设备,为'c'则表示字符设备。

    本章的内核模块例子分为两个部分:注册设备的模块和设备驱动模块。init
_module函数调用module_register_chrdev来把设备驱动程序加入到内核的字符设
备驱动程序表中,并返回驱动程序所使用的主设备号。cleanup_module函数注销
驱动程序。

    这(注册和注销驱动程序)是这两个函数最常用的功能。内核里的东西都不会
主动运行,通常都是被调用,例如进程通过系统调用、硬件通过中断或内核其他
部分的调用来运行内核中的函数。所以当你向内核里添加代码,你要作为一些事
件的处理函数来注册它,当你去掉它时,你要注销它。

    设备驱动程序部分由四个device_<action>函数组成,当用户尝试对属于我们
的主设备号的设备文件做一些动作时,这些函数将被调用。内核调用它们的方法
是通过file_operations结构体--Fops。它包含了四个函数的的指针,并在注册设
备驱动程序时用到。

    在这里我们有一点要记住,我们不能使内核模块被root用户在任何时候都能
rmmod。因为一个进程正打开一个设备文件的时侯我们移掉一个内核模块,对设备
文件的访问会引起对原来驱动程序中相应的读写函数的内存位置的访问。如果幸
运的话,没有其他代码被加载到那里,我们将会得到糟糕的错误信息。如果不幸
运的话,另一个内核模块被加载到相同的位置,这意味着跳到内核的另一个完全
不同的函数里执行。结果将是不可预知的,但这种错误却又是不明显的。

    通常,你不想允许某些事情时,你可以在函数中返回一个错误码(一个负数)
。但cleanup_module函数却不能,因为它是声明为void型的函数。一旦clean_mo
dule被调用,模块就死掉了。但是可以用一个称为参考计数器(文件/proc/modul
es里的最后一行的数字)来表示有多少个其他内核模块使用了当前模块。如果这个
数字不为0,rmmod将会失败。模块的参考计数器是用变量mod_use_count_表示。
有两个宏定义(MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT)来处理这个变量,我们
不应直接使用mod_use_count_,应该用宏来处理,这使我们的代码在以后实现改
变的时候,仍然是安全的。

例子chardev.c    


 
/* chardev.c 
 * Copyright (C) 1998 by Ori Pomerantz
 * 
 * Create a character device (read only)
 */

/* The necessary header files */

/* Standard in kernel modules */
#include <linux/kernel.h>   /* We're doing kernel work */
#include <linux/module.h>   /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

/* For character devices */
/* The character device definitions are here */
#include <linux/fs.h> 

/* A wrapper which does next to nothing at
 * at present, but may help for compatibility
 * with future versions of Linux */
#include <linux/wrapper.h>       

#define SUCCESS 0

/* The name for our device, as it will appear in /proc/devices */
#define DEVICE_NAME "char_dev"


/* The maximum length of the message from the device */
#define BUF_LEN 80

/* 设备是否被打开的标志?用来防止对同一设备的并发访问 */
static int Device_Open = 0;

static char Message[BUF_LEN];

/*该指针用于标识信息的位置,当用户进程读操作时,用户缓冲区比Message小

  就要用到*/
static char *Message_Ptr;


/* 本函数在用户进程打开设备文件时被调用*/
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值为0,而在增加
     该值时被停止调度,另一进程也打开设备文件,并增加了Device_Open的
     值,这时,第一个进程又再运行,则以为Device_Open还是为0,所以是
     错误的。
     但你不用担心,Linux的内核保证一个进程在运行内核的代码时是不会被
     抢占的,所以上面的情况可以避免。
     在SMP的情形,2.0内核通过加锁来保证在同一时候只有一个CPU在内核模块

     里运行。这影响了性能,这应该在以后的内核版本得以安全地修正。 */

  Device_Open++;

  /* Initialize the message. */
  sprintf(Message, 
    "If I told you once, I told you %d times - Hello, world/n",
    counter++);
  /* 这里要注意缓冲区溢出,特别是在内核模块里  */ 

  Message_Ptr = Message;

  /* 保证设备文件被打开时,内核模块不能被注销掉(通过增加计数器)
     如果计数器非零,rmmod将失败*/

  MOD_INC_USE_COUNT;

  return SUCCESS;
}


/* 当设备文件被关闭时,调用本函数。它不返回错误,因为你要保证通常
   都能关闭一个设备*/

static void device_release(struct inode *inode, struct file *file)
{
#ifdef DEBUG
  printk ("device_release(%p,%p)/n", inode, file);
#endif
 
  /* We're now ready for our next caller */
  Device_Open --;
  /*减少计数器*/
  MOD_DEC_USE_COUNT;
}


/* 进程读一个打开的设备文件时调用本函数*/
static int device_read(struct inode *inode,
                       struct file *file,
                       char *buffer,  
                       /* 接收数据的缓冲区和长度*/ 
                       int length) 
{
  /* Number of bytes actually written to the buffer */
  int bytes_read = 0;

#ifdef DEBUG
  printk("device_read(%p,%p,%p,%d)/n",
    inode, file, buffer, length);
#endif

  /* If we're at the end of the message, return 0 */

  if (*Message_Ptr == 0)
    return 0; /*it means end of file */

  /* Actually put the data into the buffer */
  while (length && *Message_Ptr)  {

    /*由于缓冲区在用户数据段,不在内核空间,所以不能通过赋值的方式
      来拷贝数据,应通过put_user调用来传输从内核到用户空间的数据*/ 
    put_user(*(Message_Ptr++), buffer++);
    length --;
    bytes_read ++;
  }

#ifdef DEBUG
   printk ("Read %d bytes, %d left/n",
     bytes_read, length);
#endif

   /* 返回所读的字节数*/
  return bytes_read;
}


/* 写设备文件时调用的函数,当前不支持,返回-EINVAL码*/
static int device_write(struct inode *inode,
                        struct file *file,
                        const char *buffer,
                        int length)
{
#ifdef DEBUG
  printk ("device_write(%p,%p,%s,%d)",
    inode, file, buffer, length);
#endif

  return -EINVAL;
}

/* 主设备号,声明为静态是因为注册和注销都要用到它*/

static int Major;

/* 设备文件操作的结构体*/

struct file_operations Fops = {
  NULL,   /* seek */
  device_read, 
  device_write,
  NULL,   /* readdir */
  NULL,   /* select */
  NULL,   /* ioctl */
  NULL,   /* mmap */
  device_open,
  device_release  /* a.k.a. close */
};


/* Initialize the module - Register the character device */
int init_module()
{
  /* Register the character device (at least try) */
  Major = module_register_chrdev(0, 
                                 DEVICE_NAME,
                                 &Fops);

  /* Negative values signify an error */
  if (Major < 0) {
printk ("Sorry, registering the character device failed with %d/n"
,
Major);
return Major;
}

printk ("Registeration is a success. The major device number is %d./
n",
Major);
printk ("If you want to talk to the device driver, you'll have to/n"
);
printk ("create a device file. We suggest you use:/n");
printk ("mknod <name> c %d <minor>/n", Major);
  printk ("You can try different minor numbes and see what happens./n"
);

  return 0;
}


/* Cleanup - unregister the appropriate file from /proc */
void cleanup_module()
{
  int ret;

  /* Unregister the device */
  ret = module_unregister_chrdev(Major, DEVICE_NAME);
 
  /* If there's an error, report it */ 
  if (ret < 0)
printk("Error in module_unregister_chrdev: %d/n", ret);
}


译者注:这个内核模块有很多错误和编译上要注意的问题,所以我自己特别写了
一章来解决这些问题。