逸周书的出土:Linux设备驱动Hello World程序

来源:百度文库 编辑:中财网 时间:2024/05/06 08:38:39

Linux设备驱动Hello World程序  

2009-10-21 11:22:30|  分类: 默认分类 |  标签: |字号大中小 订阅

转自:http://hi.baidu.com/shanyefeng/blog/item/936012b7d72c05fd30add11e.html

自古以来,学习一门新编程语言的第一步就是写一个打印“hello world”的程序(可以看《hello world集中营》这个帖子供罗列了300个“helloworld”程序例子)在本文中,我们将用同样的方式学习如何编写一个简单的linux内核模块和设备驱动程序。我将学习到如何在内核模式下以三种不同的方式来打印hello world,这三种方式分别是: printk(),/proc文件,/dev下的设备文件

一 、printk方式打印

现在我们来看看hello_printk.c这个文件

1.#include
2.   
3.#include
4.   
这里包含了内核提供的所有内核模块都需要的头文件。这个文件中包含了类似module_init()宏的定义,这个宏稍后我们将用到

1.static int __init
2.hello_init(void){
3.    printk("Hello, world!n");
4.    return 0;
5.}
这是内核模块的初始化函数,这个函数在内核模块初始化被装载的时候调用。__init关键字告诉内核这个代码只会被运行一次,而且是在内核装载的时候。printk()函数这一行将打印一个”Hello, world”到内核消息缓存。printk参数的形式在大多数情况和printf(3)一模一样。

1.module_init(hello_init);
2.module_init()
宏告诉内核当内核模块第一次运行时哪一个函数将被运行。任何在内核模块中其他部分都会受到内核模块初始化函数的影响。

1.static void __exit
2.hello_exit(void){
3.    printk("Goodbye, world!n");
4.}
5.module_exit(hello_exit);
同样地,退出函数也只在内核模块被卸载的时候会运行一次,module_exit()宏标示了退出函数。__exit关键字告诉内核这段代码只在内核模块被卸载的时候运行一次。

1.MODULE_LICENSE("GPL");
2.MODULE_AUTHOR("Valerie Henson val@nmt.edu");
3.MODULE_DESCRIPTION("Hello, world!" minimal module");
4.MODULE_VERSION("printk");
5.MODULE_LICENSE()
宏告诉内核,内核模块代码在什么样的license之下,这将影响主那些符号(函数和变量,等等)可以访问主内核。GPLv2下的模块(如同本例子中)能访问所有的符号。某些内核模块license将会损害内核开源的特性,这些license指示内核将装载一些非公开或不受信的代码。如果内核模块不使用MODULE_LICENSE()宏,就被假定为非GPLv2的,这会损害内核的开源特性,并且大部分Linux内核开发人员都会忽略来自受损内核的bug报告,因为他们无法访问所有的源代码,这使得调试变得更加困难。剩下的MODULE_*()这些宏以标准格式提供有用的标示该内核模块的信息(译者注:这里意思是,你必须使用GPLv2的license,否则你的驱动程序很有可能得不到Linux社区的开发者的支持 :))

现在,开始编译和运行代码。转到相应的目录下,编译内核模块

2 。编写Makefile 文件

Makefile 文件。

obj-m := hello_printk.o
obj-m指出将要编译成的内核模块列表。.o格式文件会自动地有相应的.c文件生成(不需要显示的罗列所有源代码文件)

KDIR := /lib/modules/$(shell uname -r)/build
KDIR表示是内核源代码的位置。在当前标准情况是链接到包含着正在使用内核对应源代码的目录树位置。

PWD := $(shell pwd)
PWD指示了当前工作目录并且是我们自己内核模块的源代码位置

default:
     $(MAKE) -C $(KDIR) M=$(PWD) modules
default是默认的编译连接目标;即,make将默认执行本条规则编译目标,除非程序员显示的指明编译其他目标。这里的的编译规则的意思是,在包含内核源代码位置的地方进行make,然后之编译$(PWD)(当前)目录下的modules。这里允许我们使用所有定义在内核源代码树下的所有规则来编译我们的内核模块。

 

3  编译

 

$ cd hello_printk
$ make
接着,装载内核模块,使用insmod指令,并且通过dmesg来检查打印出的信息,dmesg是打印内核消息缓存的程序。

$ sudo insmod ./hello_printk.ko
$ dmesg | tail
你将从dmesg的屏幕输出中看见”Hello world!”信息。现在卸载使用rmmod卸载内核模块,并检查退出信息。

$ sudo rmmod hello_printk
$ dmesg | tail
到此,你就成功地完成了对内核模块的编译和安装!

 

二、使用/proc的Hello, World!

一种用户程序和内核通讯最简单和流行的方式是通过使用/proc下文件系统进行通讯。/proc是一个伪文件系统,从这里的文件读取的数据是由内核返回的数据,并且写入到这里面的数据将会被内核读取和处理。在使用/proc方式之前,所用用户和内核之间的通讯都不得不使用系统调用来完成。使用系统调用意味着你将在要在查找已经具有你需要的行为方式的系统调用(一般不会出现这种情况),或者创建一种新的系统调用来满足你的需求(这样就要求对内核全局做修改,并增加系统调用的数量,这是通常是非常不好的做法),或者使用ioctl这个万能系统调用,这就要求要创建一个新文件类型供ioctl操作(这也是非常复杂而且bug比较多的方式,同样是非常繁琐的)。/proc提供了一个简单的,无需定义的方式在用户空间和内核之间传递数据,这种方式不仅可以满足内核使用,同样也提供足够的自由度给内核模块做他们需要做的事情。

为了满足我们的要求,我们需要当我们读在/proc下的某一个文件时将会返回一个“Hello world!”。我们将使用/proc/hello_world这个文件。下载并解开helloproc这个gzip的tar包后,我们将首先来看一下hello_proc.c这个文件

1.#include
2.#include
3.#include
这次,我们将增加一个proc_fs头文件,这个头文件包括驱动注册到/proc文件系统的支持。当另外一个进程调用read()时,下一个函数将会被调用。这个函数的实现比一个完整的普通内核驱动的read系统调用实现要简单的多,因为我们仅做了让”Hello world”这个字符串缓存被一次读完。

1.static int
2.hello_read_proc(char *buffer, char **start,off_t offset,
3.                int size, int *eof, void *data)
4.{
这个函数的参数值得明确的解释一下。buffer是指向内核缓存的指针,我们将把read输出的内容写到这个buffer中。start参数多用更复杂的/proc文件;我们在这里将忽略这个参数;并且我只明确的允许offset这个的值为0。size是指buffer中包含多字节数;我们必须检查这个参数已避免出现内存越界的情况,eof参数一个EOF的简写,用于返回文件是否已经读到结束,而不需要通过调用read返回0来判断文件是否结束。这里我们不讨论依靠更复杂的/proc文件传输数据的方法。这个函数方法体罗列如下:

01.    char *hello_str = "Hello, world!\n";
02.    int len = strlen(hello_str); /* Don't include the null byte. */
03.    /*     * We only support reading the whole string at once.     */
04.    if (size < len)
05.        return< -EINVAL;
06.    /*     * If file position is non-zero, then assume the string has
07.    * been read and indicate there is no more data to be read.
08.    */
09.    if (offset != 0)
10.        return 0;
11.    /*     * We know the buffer is big enough to hold the string.     */
12.    strcpy(buffer, hello_str);
13.    /*     * Signal EOF.     */
14.    *eof = 1;
15.    return len;
16.}
下面,我们需将内核模块在初始化函数注册在/proc 子系统中。

01.static int __init
02.hello_init(void){
03.    /*
04.    * Create an entry in /proc named "hello_world" that calls
05.    * hello_read_proc() when the file is read.
06.    */
07.    if (create_proc_read_entry("hello_world", 0,
08.                        NULL, hello_read_proc, NULL) == 0) {
09.        printk(KERN_ERR
10.        "Unable to register "Hello, world!" proc filen");
11.        return -ENOMEM;
12.    }
13.    return 0;
14.}
15.module_init(hello_init);
当内核模块卸载时,需要在/proc移出注册的信息(如果我们不这样做的,当一个进程试图去访问/proc/hello_world,/proc文件系统将会试着执行一个已经不存在的功能,这样将会导致内核崩溃)


01.static void __exit
02.hello_exit(void){
03.    remove_proc_entry("hello_world", NULL);
04.}
05.module_exit(hello_exit);
06.MODULE_LICENSE("GPL");
07.MODULE_AUTHOR("Valerie Henson val@nmt.edu");
08.MODULE_DESCRIPTION(""Hello, world!" minimal module");
09.MODULE_VERSION("proc");
下面我们将准备编译和装载模组

cd hello_proc
$ make
$ sudo insmod ./hello_proc.ko
现在,将会有一个称为/proc/hello_world的文件,并且读这个文件的,将会返回一个”Hello world”字符串。

$ cat /proc/hello_world
Hello, world!
你可以为为同一个驱动程序创建多个/proc文件,并增加相应写/proc文件的函数,创建包含多个/proc文件的目录,或者更多的其他操作。如果要写比这个更复杂的驱动程序,可以使用seq_file函数集来编写是更安全和容易的。关于这些更多的信息可以看《Driver porting: Theseq_file interface》

三、Hello, World! 使用 /dev/hello_world
现在我们将使用在/dev目录下的一个设备文件/dev/hello_world实现”Hello,world!”。追述以前的日子,设备文件是通过MAKEDEV脚本调用mknod命令在/dev目录下产生的一个特定的文件,这个文件和设备是否运行在改机器上无关。到后来设备文件使用了devfs,devfs在设备第一被访问的时候创建/dev文件,这样将会导致很多有趣的加锁问题和多次打开设备文件的检查设备是否存在的重试问题。当前的/dev版本支持被称为udev,因为他将在用户程序空间创建到/dev的符号连接。当内核模块注册设备时,他们将出现在sysfs文件系统中,并mount在/sys下。一个用户空间的程序,udev,注意到/sys下的改变将会根据在/etc/udev/下的一些规则在/dev下创建相关的文件项。

下载hello world内核模块的gzip的tar包,我们将开始先看一下hello_dev.c这个源文件。


1.#include
2.#include
3.#include
4.#include
5.#include
正如我们看到的必须的头文件外,创建一个新设备还需要更多的内核头文件支持。fs.sh包含所有文件操作的结构,这些结构将由设备驱动程序来填值,并关联到我们相关的/dev文件。miscdevice.h头文件包含了对通用miscellaneous设备文件注册的支持。asm/uaccess.h包含了测试我们是否违背访问权限读写用户内存空间的函数。hello_read将在其他进程在/dev/hello调用read()函数被调用的是一个函数。他将输出”Hello world!”到由read()传入的缓存。


01.static ssize_t hello_read(struct file * file, char * buf, size_t count, loff_t *ppos)
02.{
03.    char *hello_str = "Hello, world!n";
04.    int len = strlen(hello_str); /* Don't include the null byte. */
05.    /*     * We only support reading the whole string at once.     */
06.    if (count < len)
07.        return -EINVAL;
08.    /*
09.    * If file position is non-zero, then assume the string has
10.    * been read and indicate there is no more data to be read.
11.    */
12.    if (*ppos != 0)
13.        return 0;
14.    /*
15.    * Besides copying the string to the user provided buffer,
16.    * this function also checks that the user has permission to
17.    * write to the buffer, that it is mapped, etc.
18.    */
19.    if (copy_to_user(buf, hello_str, len))
20.        return -EINVAL;
21.    /*
22.    * Tell the user how much data we wrote.
23.    */
24.    *ppos = len;
25.    return len;
26.}
下一步,我们创建一个文件操作结构file operations struct,并用这个结构来定义当文件被访问时执行什么动作。在我们的例子中我们唯一关注的文件操作就是read。


1.static const struct file_operations hello_fops = {
2.    .owner        = THIS_MODULE,
3.    .read        = hello_read,
4.};
现在,我们将创建一个结构,这个结构包含有用于在内核注册一个通用miscellaneous驱动程序的信息。


01.static struct miscdevice hello_dev = {
02.    /*
03.    * We don't care what minor number we end up with, so tell the
04.    * kernel to just pick one.
05.    */
06.    MISC_DYNAMIC_MINOR,
07.    /*
08.    * Name ourselves /dev/hello.
09.    */
10.    "hello",
11.    /*
12.    * What functions to call when a program performs file
13.    * operations on the device.
14.    */
15.    &hello_fops
16.};
在通常情况下,我们在init中注册设备

01.static int __init
02.hello_init(void){
03.    int ret;
04.    /*
05.    * Create the "hello" device in the /sys/class/misc directory.
06.    * Udev will automatically create the /dev/hello device using
07.    * the default rules.
08.    */
09.    ret = misc_register(&hello_dev);
10.    if (ret)
11.        printk(KERN_ERR
12.            "Unable to register "Hello, world!" misc devicen");
13.    return ret;
14.}
15.module_init(hello_init);
接下是在卸载时的退出函数


01.static void __exit
02.hello_exit(void){
03.    misc_deregister(&hello_dev);
04.}
05.module_exit(hello_exit);
06.MODULE_LICENSE("GPL");
07.MODULE_AUTHOR("Valerie Henson val@nmt.edu>");
08.MODULE_DESCRIPTION(""Hello, world!" minimal module");
09.MODULE_VERSION("dev");
编译并加载模块:


$ cd hello_dev
$ make
$ sudo insmod ./hello_dev.ko
现在我们将有一个称为/dev/hello的设备文件,并且这个设备文件被root访问时将会产生一个”Hello, world!”


$ sudo cat /dev/hello
Hello, world!
但是我们不能使用普通用户访问他:


$ cat /dev/hello
cat:/dev/hello: Permission denied  

$ ls -l
/dev/hello crw-rw---- 1 root root 10, 61 2007-06-20 14:31 /dev/hello
这是有默认的udev规则导致的,这个条规将标明当一个普通设备出现时,他的名字将会是/dev/,并且默认的访问权限是0660(用户和组读写访问,其他用户无法访问)。我们在真实情况中可能会希望创建一个被普通用户访问的设备驱动程序,并且给这个设备起一个相应的连接名。为达到这个目的,我们将编写一条udev规则。

udev规则必须做两件事情:第一创建一个符号连接,第二修改设备的访问权限。

下面这条规则可以达到这个目的:

KERNEL=="hello", SYMLINK+="hello_world", MODE="0444"

我们将详细的分解这条规则,并解释每一个部分。KERNEL==”hello”标示下面的的规则将作用于/sys中设备名字”hello”的设备(==是比较符)。hello设备是我们通过调用misc_register()并传递了一个包含设备名为”hello”的文件操作结构file_operations为参数而达到的。你可以自己通过如下的命令在/sys下查看


$ ls -d /sys/class/misc/hello//sys/class/misc/hello/
SYMLINK+=”hello_world”的意思是在符号链接列表中增加 (+= 符号的意思着追加)一个hello_world,这个符号连接在设备出现时创建。在我们场景下,我们知道我们的列表的中的只有这个符号连接,但是其他设备驱动程序可能会存在多个不同的符号连接,因此使用将设备追加入到符号列表中,而不是覆盖列表将会是更好的实践中的做法。

MODE=”0444″的意思是原始的设备的访问权限是0444,这个权限允许用户,组,和其他用户可以访问。

通常,使用正确的操作符号(==, +=, or =)是非常重要的,否则将会出现不可预知的情况。

现在我们理解这个规则是怎么工作的,让我们将其安装在/etc/udev目录下。udev规则文件以和SystemV初始脚本目录命名的同种方式的目录下,/etc/udeve/rules.d这个目录,并以字母/数字的顺序。和SystemV的初始化脚本一样,/etc/udev/rules.d下的目录通常符号连接到真正的文件,通过使用符号连接名,将使得规则文件已正确的次序得到执行。
使用如下的命令,拷贝hello.rules文件从/hello_dev目录到/etc/udev目录下,并创建一一个最先被执行的规则文件链接在/etc/udev/rules.d目录下。


$ sudo cp hello.rules /etc/udev/
$ sudo ln -s ../hello.rules /etc/udev/rules.d/010_hello.rules
现在我们重新装载驱动程序,并观察新的驱动程序项


$ sudo rmmod hello_dev
$ sudo insmod ./hello_dev.ko
$ ls -l /dev/hello*
cr--r--r-- 1 root root 10, 61 2007-06-19 21:21 /dev/hello
lrwxrwxrwx 1 root root      5 2007-06-19 21:21 /dev/hello_world -> hello

最后,检查你可以使用普通用户访问/dev/hello_world设备.


$ cat /dev/hello_world
Hello, world!  

$ cat /dev/hello
Hello, world!
更多编写udev规则的信息可以在Daniel Drake的文章Writing udev rules中找到。

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/haoel/archive/2009/04/29/4134807