让我们也从HelloWorld开始吧。从这里我们了解一个内核模块的基本框架,还有如何生成,如何加载。
废话少说,Coding吧:
//hello.c
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk()在这个文件里 */
static int init_module()
{
printk("Hello,World!/n");
return 0; /* 如果初始工作失败,就返回非0 */
}
static void cleanup_module()
{
printk("Bye!/n");
}
编译吧,当然是 gcc了:
gcc -c -o hello.o -I/usr/src/linux/include -D__KERNEL__ -DMODULE hello.c
说明: -I指定了内核模块工作环境的内核的头文件路径,如不指定,通常会遇到与kernel版本不符的警告,并且会经常出现问题。
-D定义宏,内核模块要使用两个宏Macro:__KERNEL__ 和 MODULE
现在我们有了模块文件,加载吧:
使用命令: insmod hello.o
如果没有问题,现在就加载成功了。检查一下,看看内核模块是否进入内核中了,使用命令: lsmod ,看看输出列表中有没有hello模块。
好吧,现在我们有了一个模块,它会在加载时输出"Hello,World!",退出时输出”Bye!"。没有看到,使用命令:dmesg,看输出消息中会有"Hello,World!"。
现在退出吧,把这个模块从内核中去掉,使用命令: rmmod hello 现在模块hello就从内核中走了,再用命令 dmesg,可以看到 ”Bye!"。
好了,这也是一个内核模块。但是要多说一点:
static int init_module()是模块加载时,系统要进行调用的函数,Oh,man,看样子是构造函数。如果成功,其需要返回 0,否则就是错误号码。
static void cleanup_module() 是模块卸载时,要调用的函数,和析构函数想像。
内核编程时不能使用标准库,在内核模块中只能使用内核函数,也就是可以在/proc/ksyms 中找到的函数。如printf-->printk
ok,我们还是来个Makefile吧,免得每次都常常的命令:
######## Makefile
CC = gcc
KERNELSRC = /usr/src/linux-2.4 if different, change it.
CFLAGS = -O2 -Wall -I$(KERNELSRC)/include -D__KERNEL__ -DMODULE
object = test.o
source = test.c
$(object):
$(CC) $(CFLAGS) -c $(source)
.PHONY: clean
clean:
rm -f *.o
2.复杂一点的吧
"Hello World"太简单,我们从这里知道了如何生成模块文件,如何加载,如何卸载。也知道了模块加载和卸载时的函数。
但我们还想知道更多。来点复杂的吧。
好吧,来个稍微复杂的。
/// TestModule.c
#define _NO__VERSION__
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/errno.h>
#include <asm/segment.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#define DATA_LENGTH 10
/* Module parameters */
MODULE_AUTHOR("sss <stp111@sina.com>");
MODULE_DESCRIPTION("test driver");
MODULE_LICENSE("GPL");
char kernel_version[]=UTS_RELEASE;
unsigned int test_major=0; 用来保存申请到的主设备号
ssize_t read_test(struct file *file, char *buf, size_t count, loff_t *f_ops)
{
int left,i,*p;
int data[DATA_LENGTH];
p=data;
for(i=0;i<10;i++)
data[i]=61; // ”= “符号。
for(left=count;left>0;left--)
{
放入用户空间
__copy_to_user(buf,p,1);
buf++;
p++;
}
return 0; must reture 0;
}
ssize_t write_test(struct file *file, const char *buf, size_t count, loff_t *f_ops)
{
return 0; must reture 0;
}
static int open_test(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return 0; must reture 0;
}
static int release_test(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return 0; must reture 0;
}
// 申请主设备号时用的结构, 在linux/fs.h里定义
struct file_operations test_fops ={
read: read_test,
write: write_test,
open: open_test,
release:release_test
};
/* insmod, call it */
int init_module(void)
{
int result;
result=register_chrdev(0,"test",&test_fops);
if(result<0)
{
/*printf("can't get major number");*/
printk(KERN_INFO "test:Can't get major number/n");
return result;
}
if(test_major==0)
test_major=result;
printk("test major:%d/r/n",result);
return 0;模块正常初始化
}
/* rmmod, call it */
void cleanup_module(void)
{
unregister_chrdev(test_major,"test");注销以后,设备就不存在了
}
///
这个内核注册了一个字符设备,并通过一个file_operations结构,指定了其各种操作。
好了,make吧,编译生成内核文件。再加载吧。
用命令: dmesg,看看得到的主设备号。(我的系统上是268)
再用命令:lsmod检查一下内核是否加载了我们的模块。
3.测试一下
好了,让我们的测试一下吧。
测试前,先看看是否已经建立了设备文件。 检查 /dev下是否有test设备文件,如果没有,那我们就手工建立一个。用前面看到的主设备号,使用命令: “mknod test c 268 0”
建立的设备文件,那就开始我的测试程序吧。
// TestApp.c
#include "stdio.h"
#include "sys/stat.h"
#include "fcntl.h"
int main()
{
char buf[20]={0,};
int i=0;
int p = open("/dev/test",O_RDWR);
if (p == -1)
{
printf("Can't open /dec/test /r/n");
exit(0);
}
printf("buf addr:%ui/r/n",buf);
//
read(p,buf,10);
for (i=0;i<10;i++)
{
printf("%s/r/n",buf+i);
}
close(p);
return 1;
}
再写个Makefile,然后make得到一个执行文件,开始我们的测试。看到了吗,从内核得到了正确的结果了。
4.一点小总结
这里展示了一个简单的字符驱动程序,并使用了一个程序进行了测试。看到了,我们主要的工作就是实现open,read,write,ioctl...系统调用的处理。这些函数指针被赋值进了fileoperations结构。当然,这些函数是有固定的形式的,你要遵循它们。
还有一点的就是,内核2.4和 2.6对内核的处理有了很大的变化。这里所说的都是2.4版。
还有一点就是版本问题,《Linux Device Driver》中有介绍。这本书也许每个要写内核的人都要学习。
5.驱动有什么不同呢嗯,这个不好说。驱动可以是内核模块,但内核模块不是驱动。所以驱动是内核模块的子集。
这要看内核模块的作用。内核模块主要是为方便地加入内核,并在内核空间运行。
而驱动呢,驱动是内核模块,有内核模块的属性,但其最主要的目的是“使某个硬件响应一个定义良好的的内部编程接口,同时要完全隐藏设备的工作细节”。主要是解决硬件的可用性问题。一般而言,它要和硬件发生直接的关系。