本文由赵锟翻译,转载于“酷壳”:http://cocre.com/?p=566
by Valerie Henson
07/05/2007
(译者注:本文的例子是只能在linux的2.6内核下使用的,2.6以上的内核,译者没有做过实验,2.4是要修改make文件才能运行。)
本文的出处:这里
自古以来,学习一门新编程语言的第一步就是写一个打印“hello world”的程序(可以看《hello world 集中营》这个帖子供罗列了300个“hello world”程序例子)在本文中,我们将用同样的方式学习如何编写一个简单的linux内核模块和设备驱动程序。我将学习到如何在内核模式下以三种不同的方式来打印hello world,这三种方式分别是: printk(),/proc文件,/dev下的设备文件。
准备:安装内核模块的编译环境
一个内核模块kernel module是一段能被内核动态加载和卸载的内核代码,因为内核模块程序是内核的一个部分,并且和内核紧密的交互,所以内核模块不可能脱离内核编译环境,至少,它需要内核的头文件和用于加载的配置信息。编译内核模块同样需要相关的开发工具,比如说编译器。为了简化,本文只简要讨论如何在Debian、Fedora和其他以.tar.gz形式提供的原版linux内核下进行核模块的编译。在这种情况下,你必须根据你正在运行内核相对应的内核源代码来编译你的内核模块kernel module(当你的内核模块一旦被装载到你内核中时,内核就将执行该模块的代码)
必须要注意内核源代码的位置,权限:内核程序通常在/usr/src/linux目录下,并且属主是root。如今,推荐的方式是将内核程序放在一个非root用户的home目录下。本文中所有命令都运行在非root的用户下,只有在必要的时候,才使用sudo来获得临时的root权限。配置和使用sudo可以man sudo(8) visudo(8) 和sudoers(5)。或者切换到root用户下执行相关的命令。不管什么方式,你都需要root权限才能执行本文中的一些命令。
在Debian下编译内核模块的准备
使用如下的命令安装和配置用于在Debian编译内核模块的module-assitant包
$
sudo
apt-get
install
module-assistant
以此你就可以开始编译内核模块,你可以在《Debian Linux Kernel Handbook》这本书中找到对Debian内核相关任务的更深度的讨论。
Fedora的kernel-devel包包含了你编译Fedora内核模块的所有必要内核头文件和工具。你可以通过如下命令得到这个包。
$
sudo
yum
install
kernel-devel
有了这个包,你就可以编译你的内核模块kernel modules。关于Fedora编译内核模块的相关文档你可以从Fedora release notes中找到。
一般Linux 内核源代码和配置
(译者注,下面的编译很复杂,如果你的Linux不是上面的系统,你可以使用REHL AS4系统,这个系统的内核就是2.6的内核,并且可以通过安装直接安装内核编译支持环境,从而就省下了如下的步骤。而且下面的步骤比较复杂,建议在虚拟机安装Linux进行实验。)
如果你选择使用一般的Linux内核源代吗,你必须,配置,编译,安装和重启的你编译内核。这个过程非常复杂,并且本文只会讨论使用一般内核源代码的基本概念。
linux的著名的内核源代码在http://kernel.org上都可以找到。最近新发布的稳定版本的代码在首页上。下载全版本的源代码,不要下载补丁代码。例如,当前发布稳定版本在url: http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2上。如果需要更快速的下载,从htpp://kernel.org/mirrors上找到最近的镜像进行下载。最简单获得源代码的方式是以断点续传的方式使用wget。如今的http很少发生中断,但是如果你在下载过程中发生了中断,这个命令将帮助你继续下载剩下的部分。
解包内核源代码
$
tar
xjvf linux-&
lt
;version&
gt
;.
tar
.bz2
现在你的内核源代码位于linux-/目录下。转到这个目录下,并配置它:
$
cd
linux-&
lt
;version&
gt
;
一些非常易用的编译目标make targets提供了多种编译安装内核的形式:Debian 包,RPM包,gzip后的tar文件 等等,使用如下命令查看所有可以编译的目标形式
一个可以工作在任何linux的目标是:(译者注:REHL AS4上没有tar-pkg这个目标,你可以任选一个rpm编译,编译完后再上层目录可以看到有一个linux-.tar.gz可以使用)
当编译完成后,可以调用如下命令安装你的内核
$
sudo
tar
-C / -xvf linux-&
lt
;version&
gt
;.
tar
在标准位置建立的到内核源代码的链接
$
sudo
ln
-s &
lt
;location of
top
-level
source
directory&
gt
; /lib/modules/'
uname
-r'/build
现在已经内核源代码已经可以用于编译内核模块了,重启你的机器以使得你根据新内核程序编译的内核可以被装载。
使用printk()函数打印”Hello World”
我们的第一个内核模块,我们将以一个在内核中使用函数printk()打印”Hello world”的内核模块为开始。printk是内核中的printf函数。printk的输出打印在内核的消息缓存kernel message buffer并拷贝到/var/log/messages(关于拷贝的变化依赖于如何配置syslogd)
下载hello_printk 模块的tar包 并解包:
$
tar
xzvf hello_printk.
tar
.gz
这个包中包含两个文件:Makefile,里面包含如何创建内核模块的指令和一个包含内核模块源代码的hello_printk.c文件。首先,我们将简要的过一下这个Makefile 文件。
obj-m指出将要编译成的内核模块列表。.o格式文件会自动地有相应的.c文件生成(不需要显示的罗列所有源代码文件)
KDIR := /lib/modules/$(shell
uname
-r)/build
KDIR表示是内核源代码的位置。在当前标准情况是链接到包含着正在使用内核对应源代码的目录树位置。
PWD指示了当前工作目录并且是我们自己内核模块的源代码位置
$(MAKE) -C $(KDIR) M=$(PWD) modules
default是默认的编译连接目标;即,make将默认执行本条规则编译目标,除非程序员显示的指明编译其他目标。这里的的编译规则的意思是,在包含内核源代码位置的地方进行make,然后之编译$(PWD)(当前)目录下的modules。这里允许我们使用所有定义在内核源代码树下的所有规则来编译我们的内核模块。
现在我们来看看hello_printk.c这个文件
2.
<linux/init.h>
3.
#include
4.
<linux/module.h>
这里包含了内核提供的所有内核模块都需要的头文件。这个文件中包含了类似module_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.
hello_exit(
void
){
3.
printk(
"Goodbye, world!n"
);
4.
}
5.
module_exit(hello_exit);
同样地,退出函数也只在内核模块被卸载的时候会运行一次,module_exit()宏标示了退出函数。__exit关键字告诉内核这段代码只在内核模块被卸载的时候运行一次。
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社区的开发者的支持 :))
现在,开始编译和运行代码。转到相应的目录下,编译内核模块
接着,装载内核模块,使用insmod指令,并且通过dmesg来检查打印出的信息,dmesg是打印内核消息缓存的程序。
$
sudo
insmod ./hello_printk.ko
你将从dmesg的屏幕输出中看见”Hello world!”信息。现在卸载使用rmmod卸载内核模块,并检查退出信息。
$
sudo
rmmod hello_printk
到此,你就成功地完成了对内核模块的编译和安装!
使用/proc的Hello, World!
一种用户程序和内核通讯最简单和流行的方式是通过使用/proc下文件系统进行通讯。/proc是一个伪文件系统,从这里的文件读取的数据是由内核返回的数据,并且写入到这里面的数据将会被内核读取和处理。在使用/proc方式之前,所用用户和内核之间的通讯都不得不使用系统调用来完成。使用系统调用意味着你将在要在查找已经具有你需要的行为方式的系统调用(一般不会出现这种情况),或者创建一种新的系统调用来满足你的需求(这样就要求对内核全局做修改,并增加系统调用的数量,这是通常是非常不好的做法),或者使用ioctl这个万能系统调用,这就要求要创建一个新文件类型供ioctl操作(这也是非常复杂而且bug比较多的方式,同样是非常繁琐的)。/proc提供了一个简单的,无需定义的方式在用户空间和内核之间传递数据,这种方式不仅可以满足内核使用,同样也提供足够的自由度给内核模块做他们需要做的事情。
为了满足我们的要求,我们需要当我们读在/proc下的某一个文件时将会返回一个“Hello world!”。我们将使用/proc/hello_world这个文件。下载并解开hello proc这个gzip的tar包后,我们将首先来看一下hello_proc.c这个文件
1.
#include <linux/init.h>
2.
#include <linux/module.h>
3.
#include <linux/proc_fs.h>
这次,我们将增加一个proc_fs头文件,这个头文件包括驱动注册到/proc文件系统的支持。当另外一个进程调用read()时,下一个函数将会被调用。这个函数的实现比一个完整的普通内核驱动的read系统调用实现要简单的多,因为我们仅做了让”Hello world”这个字符串缓存被一次读完。
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);
03.
04.
if
(size < len)
05.
return
< -EINVAL;
06.
07.
08.
09.
if
(offset != 0)
10.
return
0;
11.
12.
strcpy
(buffer, hello_str);
13.
14.
*eof = 1;
15.
return
len;
16.
}
下面,我们需将内核模块在初始化函数注册在/proc 子系统中。
02.
hello_init(
void
){
03.
04.
05.
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"
);
下面我们将准备编译和装载模组
$
make
$
sudo
insmod ./hello_proc.ko
现在,将会有一个称为/proc/hello_world的文件,并且读这个文件的,将会返回一个”Hello world”字符串。
你可以为为同一个驱动程序创建多个/proc文件,并增加相应写/proc文件的函数,创建包含多个/proc文件的目录,或者更多的其他操作。如果要写比这个更复杂的驱动程序,可以使用seq_file函数集来编写是更安全和容易的。关于这些更多的信息可以看《Driver porting: The seq_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 <linux/fs.h>
2.
#include <linux/init.h>
3.
#include <linux/miscdevice.h>
4.
#include <linux/module.h>
5.
#include <asm/uaccess.h>
正如我们看到的必须的头文件外,创建一个新设备还需要更多的内核头文件支持。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);
05.
06.
if
(count < len)
07.
return
-EINVAL;
08.
09.
10.
11.
12.
if
(*ppos != 0)
13.
return
0;
14.
15.
16.
17.
18.
19.
if
(copy_to_user(buf, hello_str, len))
20.
return
-EINVAL;
21.
22.
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.
04.
05.
06.
MISC_DYNAMIC_MINOR,
07.
08.
09.
10.
"hello"
,
11.
12.
13.
14.
15.
&hello_fops
16.
};
在通常情况下,我们在init中注册设备
01.
static
int
__init
02.
hello_init(
void
){
03.
int
ret;
04.
05.
06.
07.
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!”
分享到:
相关推荐
本篇将深入浅出地介绍Linux设备驱动程序的基本概念,并通过一个简单的“Hello, world!”模块示例,帮助初学者入门Linux驱动程序开发。 首先,我们需要理解Linux设备驱动程序的分类。在Linux中,设备可以分为字符...
本篇文章将通过一个简单的"Linux设备驱动Hello World"示例,来帮助初学者理解驱动程序的基本结构和编写方式。 首先,我们要知道驱动程序的主要任务是为硬件提供一个抽象层,它处理硬件特定的操作,同时提供标准的...
此外,还讨论了Linux驱动的安全问题,如防止驱动程序漏洞被利用造成系统安全风险。版本编号和版权条款也是驱动开发中不可忽视的部分,它们涉及到代码的管理和合规性。最后,作者鼓励读者加入内核开发社团,以获得更...
本文档是最基本的Linux设备驱动程序hello world的技术文档,hello world很简单,但如果没有高手指导,或者你的开发板提供的资料做得不够好,那是足够让你花上一个星期也不一定能够搞出来的。本文档是针对Linux设备...
·Linux设备驱动程序学习(0)-Hello, world!模块 ·Linux设备驱动程序学习(2)-调试技术 ·Linux设备驱动程序学习(3)-并发和竞态 ·Linux设备驱动程序学习(4)-高级字符驱动程序操作[(1)ioctl and llseek...
Linux 字符设备驱动程序...Linux 字符设备驱动程序学习笔记中还介绍了驱动程序的分类、字符设备驱动程序的设计、驱动程序的安装方式、设备文件的概念等知识点,为读者提供了系统的 Linux 字符设备驱动程序学习资源。
在Linux驱动程序中,"Hello World"通常是一个非常简单的字符设备驱动,它会在设备节点上输出一条欢迎信息。这个驱动的核心部分通常包括以下内容: 1. **驱动注册**:在内核中注册设备驱动,定义设备号(major和...
对于新手来说,理解OpenWrt的软件包系统是至关重要的,因为这允许你在OpenWrt设备上添加自定义功能,例如添加新的服务、脚本或者像这里的HelloWorld程序。一旦你熟悉了这个过程,就可以扩展到更复杂的项目,为...
"Linux驱动开发之旅(一)--helloworld"是一个针对初学者的教程,旨在帮助新手理解驱动开发的基本步骤和关键点。在这个教程中,我们将创建一个简单的字符设备驱动,它的主要功能就是向用户空间输出"Hello, World!...
本教程旨在通过创建一个简单的`HelloWorld`驱动程序来介绍如何将其编入Linux内核并在系统启动时加载,进而实现打印“Hello World”的功能。这不仅有助于理解驱动程序的基本结构,还能够为学习更复杂的内核编程打下...
Ubuntu下驱动开发HelloWorld
首先,"HelloWorld"驱动程序通常是学习任何新编程领域时的第一个实例,它旨在简单地打印出“Hello, World!”信息。在Linux驱动开发中,这个"Hello, World!"会被替换为向内核日志输出一条消息,通过`printk`函数实现...
本文介绍了Linux设备驱动程序开发的基本概念和技术要点,包括设备驱动的分类、开发环境的搭建以及具体的模块开发流程。掌握这些基础知识对于从事Linux系统开发工作的工程师来说是非常重要的。此外,通过实践编写不同...
### Linux 设备驱动程序 Edition 3 - 关键知识点解析 #### 标题解析 - **标题**: “Linux 设备驱动程序 Edition 3.pdf” - **解释**: 该书是关于Linux设备驱动程序的经典教材,已经出版到第三版。这本书旨在为读者...
Linux设备驱动程序的一个示例是HelloWorld模块。这个模块展示了内核驱动模块的基本结构,包括模块的初始化函数和退出函数。模块代码通常包含两个重要的头文件:linux/init.h和linux/module.h,这两个头文件分别提供...