发信人: kevintz() 整理人: kevintz(2000-06-22 00:59:44), 站内信件 |
<<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