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

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


                   第三章 编译的问题和错误修正
    
    可能是版本的问题,原作者在这里给的程序已经不能在我的机器上正确运行
。下面我会分析出来并改正他们。这里可以给出Makefile的,但编译这个程序很
简单,所以我用命令行来直接编译,Makefile可以留给大家自己写。

错误1:
先试一下如下编译:
cc -D__KERNEL__ -DLINUX -DMODULE -DDEBUG -c chardev.c 
结果是可以通过编译,但有3个警告,都是一些函数原型类型不附的问题。接着s
u到root,执行insmod chardev,会报错:put_user函数没有找到,不能连接到目
标文件。put_user函数的定义在/usr/include/linux里是没有的,它的定义在/u
sr/src/linux/include/asm/uaccess.h中有定义(在Intel平台,asm是目录asm-i
386的一个符号连接),所以应该加入包含头文件的一句:#include <asm/uacces
s.h>,重新编译通过。
译者注:注意!可能你的编译环境找不到uaccess.h文件,如果这样,你还要加入
一个编译参数-I/usr/src/linux/include。

错误2:
用root运行insmod chardev报错:unresolved symbol __put_user_X,这个错误
的原因可能是gcc的一个缺陷。请在编译时加入-On(n为1,2,3,4,5,6)
的参数,就可以了。重新用以下命令编译:
cc -D__KERNEL__ -DLINUX -DMODULE -DDEBUG -O6 -c chardev.c
编译通过,用root执行insmod chardev成功,在我的机器环境上返回的主设备号
为254。是我们写一个测试程序的时候了:
先用mknod建立我们的两个设备文件(root用户):
mknod mychardev c 254 0
mknod mychardev1 c 254 1
并修改属性:
chmod 666 mychardev
chmod 666 mychardev1

下面是我写的很简单的测试程序testchardev.c:

/* 版权所有(C) 2000 by 谭志 */
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    int fd;
    char buf[80+1];
    int readn;

    if( argc != 2)
    {
       printf("usage: %s devfile/n", argv[0]);
       exit(1);
    }
    fd=open(argv[1], O_RDWR);
    if( fd == -1)
    {
       perror("open");
       exit(1);
    }
    bzero( buf, 81);
    while( readn=read(fd,buf,80) )
    {
       if( readn>0)
       {
           buf[readn]=0;
           printf("%s",buf);
       }
       else if( readn== -1 && errno != EINTR) 
       {
           perror("read");
           exit(1);
       }
    }
    printf("/n");
    snprintf(buf,80,"This message is write to kernel!");
    if( write(fd, buf, strlen(buf)) == -1)
    {
       perror("write");
       exit(1);
    }
    close(fd);
    exit(0);
}

我们编译这个测试文件,生成可执行文件为testchardev。
下面开始测试:
testchardev mychardev

错误3:
我们可以在虚拟控制台上看到内核模块输出的信息。我们可以看到设备打开(进程
的open引起)device_open的确正常工作了。不过程序有致命错误,有可能segmen
t fault,是在device_read的函数里。如果这样的话,要重新启动才能移去内核
模块了(rmmod移不去),所以调试内核模块的确挺麻烦,建议在自己的机器上调试
。要找出错误的原因,我们要从file_operations结构查起。
下面是在/usr/include/linux/fs.h中定义的file_operations结构:

struct file_operations {
 loff_t (*llseek) (struct file *, loff_t, int);
 ssize_t (*read) (struct file *, char *, size_t, loff_t *);
 ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
 int (*readdir) (struct file *, void *, filldir_t);
 unsigned int (*poll) (struct file *, struct poll_table_struct *);
 int (*ioctl)(struct inode*,struct file*,unsigned int,unsigned long);

 int (*mmap) (struct file *, struct vm_area_struct *);
 int (*open) (struct inode *, struct file *);
 int (*flush) (struct file *);
 int (*release) (struct inode *, struct file *);
 int (*fsync) (struct file *, struct dentry *);
 int (*fasync) (int, struct file *, int);
 int (*check_media_change) (kdev_t dev);
 int (*revalidate) (kdev_t dev);
 int (*lock) (struct file *, int, struct file_lock *);
};
大家可以发现read、write等函数的原型和源文件的定义有出入。下面是我重新修
改正确的文件:

/* chardev.c 
 * Copyright (C) 1998 by Ori Pomerantz
 * 版权所有(C) 2000 by 谭志 
 * 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 */
#include <asm/uaccess.h>

/* 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 int 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;
  return 0;
}


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

#ifdef DEBUG
  printk("device_read(%p,%p,%d,%p)/n",
    /*inode,*/ file, buffer, length, offset);
#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 ssize_t device_write(/*struct inode *inode,*/
                        struct file *file,
                        const char *buffer,
                        size_t length,
                        loff_t *offset)
{
#ifdef DEBUG
  printk ("device_write(%p,%s,%d,%p)/n",
    /*inode,*/ file, buffer, length, offset);
#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,
  NULL,   /*flush*/
  device_release,  /* a.k.a. close */
  NULL,   /*fsync*/
  NULL,   /*fasync*/
  NULL,   /*check_media_change*/
  NULL,   /*revalidate*/
  NULL    /*lock*/
};


/* 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);
}

重新用下面的命令编译:
cc -D__KERNEL__ -DLINUX -DMODULE -DDEBUG -O6 -c chardev.c
然后你就可以开始用testchardev来测试了。仔细观察结果吧!

kevintz注:
1)原来的Fops是这样的,我从里面发现一个现象:
struct file_operations Fops = {
NULL, /* seek */
device_read,
device_write,
NULL, /* readdir */
NULL, /* select */
NULL, /* ioctl */
NULL, /* mmap */
device_open,
device_release /* 这里是flush的位置*/

};
程序还可以运行成功,device_release也运行成功。我认为是关闭设备文件时内
核自动执行一次flush,所以导致这里的device_release被执行。

2)关于device_write的实现,留给大家去实现。

3)鉴于原来的程序有那么多的错误,以后的章节里的程序都是我经过修改的正确
程序,我还会尽量给出测试程序。

4)大家如果编译上有什么问题,可以给我写email: lkmpg@21cn.com