发信人: kevintz@GZ() 
整理人: qiaoqian(2001-12-31 00:01:56), 站内信件
 | 
 
 
 标  题: [转载] 《Linux内核模块编程指南》(三) 发信站: 网易虚拟社区 (Thu Jun  8 18:59:59 2000), 站内信件
  【 以下文字转载自 CLanguage 讨论区 】 【 原文由 我思故我不在 所发表 】 <<Linux 内核模块编程指南>>
 <<Linux Kernel Module Programming Guide>>
 作者:Ori Pomerantz 中译者:谭志([email protected])
   
 译者注:
 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: [email protected]
 
  -- 那一刹那,我开始用心去看这个世界,所有的事物真的可以看得前
 所未有的那么清楚…… 
  ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.105.37.129] -- ※ 转载:.月光软件站 http://www.moon-soft.com.[FROM: 202.105.37.129]
  | 
 
 
 |