Linux设备驱动Hello World程序介绍
如何编写一个简单的linux内核模块和设备驱动程序。我将学习到如何在内核模式下以三种不一样的方式来打印hello world,这三种方式分别是: printk(),/proc文件,/dev下的设备文件。linux
准备:安装内核模块的编译环境
一个内核模块kernel module是一段能被内核动态加载和卸载的内核代码,由于内核模块程序是内核的一个部分,而且和内核紧密的交互,因此内核模块不可能脱离内核编译环境, 至少,它须要内核的头文件和用于加载的配置信息。编译内核模块一样须要相关的开发工具,好比说编译器。为了简化,本文只简要讨论如何在Debian、 Fedora和其余以.tar.gz形式提供的原版linux内核下进行核模块的编译。在这种状况下,你必须根据你正在运行内核相对应的内核源代码来编译 你的内核模块kernel module(当你的内核模块一旦被装载到你内核中时,内核就将执行该模块的代码)android
必需要注意内核源代码的位置,权限:内核程序一般在/usr/src/linux目录下,而且属主是root。现在,推荐的方式是将内核程序放在一个非 root用户的home目录下。本文中全部命令都运行在非root的用户下,只有在必要的时候,才使用sudo来得到临时的root权限。配置和使用 sudo能够man sudo(8) visudo(8) 和sudoers(5)。或者切换到root用户下执行相关的命令。无论什么方式,你都须要root权限才能执行本文中的一些命令。程序员
在Debian下编译内核模块的准备web
使用以下的命令安装和配置用于在Debian编译内核模块的module-assitant包shell
1
|
$
sudo
apt-get
install
module-assistant
|
以此你就能够开始编译内核模块,你能够在《Debian Linux Kernel Handbook》这本书中找到对Debian内核相关任务的更深度的讨论。缓存
Fedora的kernel-devel包包含了你编译Fedora内核模块的全部必要内核头文件和工具。你能够经过以下命令获得这个包。安全
1
|
$
sudo
yum
install
kernel-devel
|
有了这个包,你就能够编译你的内核模块kernel modules。关于Fedora编译内核模块的相关文档你能够从Fedora release notes中找到。bash
通常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不多发生中断,可是若是你在下载过程当中发生了中断,这个命令将帮助你继续下载剩下的部分。
1
|
$ wget -c http:
//kernel
.org
/pub/linux/kernel/v2
.6
/linux-2
.6.21.5.
tar
.bz2
|
解包内核源代码
1
|
$
tar
xjvf linux-<version>.
tar
.bz2
|
如今你的内核源代码位于linux-/目录下。转到这个目录下,并配置它:
1
2
|
$
cd
linux-<version>
$
make
menuconfig
|
一些很是易用的编译目标make targets提供了多种编译安装内核的形式:Debian 包,RPM包,gzip后的tar文件 等等,使用以下命令查看全部能够编译的目标形式
1
|
$
make
help
|
一个能够工做在任何linux的目标是:(译者注:REHL AS4上没有tar-pkg这个目标,你能够任选一个rpm编译,编译完后再上层目录能够看到有一个linux-.tar.gz可使用)
1
|
$
make
tar
-pkg
|
当编译完成后,能够调用以下命令安装你的内核
1
|
$
sudo
tar
-C / -xvf linux-<version>.
tar
|
在标准位置创建的到内核源代码的连接
1
|
$
sudo
ln
-s <location of
top
-level
source
directory>
/lib/modules/
'uname -r'
/build
|
如今已经内核源代码已经能够用于编译内核模块了,重启你的机器以使得你根据新内核程序编译的内核能够被装载。
使用printk()函数打印”Hello World”
咱们的第一个内核模块,咱们将以一个在内核中使用函数printk()打印”Hello world”的内核模块为开始。printk是内核中的printf函数。printk的输出打印在内核的消息缓存kernel message buffer并拷贝到/var/log/messages(关于拷贝的变化依赖于如何配置syslogd)
下载hello_printk 模块的tar包 并解包:
1
|
$
tar
xzvf hello_printk.
tar
.gz
|
这个包中包含两个文件:Makefile,里面包含如何建立内核模块的指令和一个包含内核模块源代码的hello_printk.c文件。首先,咱们将简要的过一下这个Makefile 文件。
1
|
obj-m := hello_printk.o
|
obj-m指出将要编译成的内核模块列表。.o格式文件会自动地有相应的.c文件生成(不须要显示的罗列全部源代码文件)
1
|
KDIR :=
/lib/modules/
$(shell
uname
-r)
/build
|
KDIR表示是内核源代码的位置。在当前标准状况是连接到包含着正在使用内核对应源代码的目录树位置。
1
|
PWD := $(shell
pwd
)
|
PWD指示了当前工做目录而且是咱们本身内核模块的源代码位置
1
2
|
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
|
default是默认的编译链接目标;即,make将默认执行本条规则编译目标,除非程序员显示的指明编译其余目标。这里的的编译规则的意思是,在包含内 核源代码位置的地方进行make,而后之编译$(PWD)(当前)目录下的modules。这里容许咱们使用全部定义在内核源代码树下的全部规则来编译我 们的内核模块。
如今咱们来看看hello_printk.c这个文件
1
2
3
4
|
#include
<linux/init.h>
#include
<linux/module.h>
|
这里包含了内核提供的全部内核模块都须要的头文件。这个文件中包含了相似module_init()宏的定义,这个宏稍后咱们将用到
1
2
3
4
5
|
static
int
__init
hello_init(
void
){
printk(
"Hello, world!n"
);
return
0;
}
|
这是内核模块的初始化函数,这个函数在内核模块初始化被装载的时候调用。__init关键字告诉内核这个代码只会被运行一次,并且是在内核装载的时候。 printk()函数这一行将打印一个”Hello, world”到内核消息缓存。printk参数的形式在大多数状况和printf(3)如出一辙。
1
2
|
module_init(hello_init);
module_init()
|
宏告诉内核当内核模块第一次运行时哪个函数将被运行。任何在内核模块中其余部分都会受到内核模块初始化函数的影响。
1
2
3
4
5
|
static
void
__exit
hello_exit(
void
){
printk(
"Goodbye, world!n"
);
}
module_exit(hello_exit);
|
一样地,退出函数也只在内核模块被卸载的时候会运行一次,module_exit()宏标示了退出函数。__exit关键字告诉内核这段代码只在内核模块被卸载的时候运行一次。
1
2
3
4
5
|
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Valerie Henson val@nmt.edu"
);
MODULE_DESCRIPTION(
"Hello, world!"
minimal module");
MODULE_VERSION(
"printk"
);
MODULE_LICENSE()
|
宏告诉内核,内核模块代码在什么样的license之下,这将影响主那些符号(函数和变量,等等)能够访问主内核。GPLv2 下的模块(如同本例子中)能访问全部的符号。某些内核模块license将会损害内核开源的特性,这些license指示内核将装载一些非公开或不受信的 代码。若是内核模块不使用MODULE_LICENSE()宏,就被假定为非GPLv2的,这会损害内核的开源特性,而且大部分Linux内核开发人员都 会忽略来自受损内核的bug报告,由于他们没法访问全部的源代码,这使得调试变得更加困难。剩下的MODULE_*()这些宏以标准格式提供有用的标示该 内核模块的信息(译者注:这里意思是,你必须使用GPLv2的license,不然你的驱动程序颇有可能得不到Linux社区的开发者的支持 :))
如今,开始编译和运行代码。转到相应的目录下,编译内核模块
1
2
|
$
cd
hello_printk
$
make
|
接着,装载内核模块,使用insmod指令,而且经过dmesg来检查打印出的信息,dmesg是打印内核消息缓存的程序。
1
2
|
$
sudo
insmod .
/hello_printk
.ko
$ dmesg |
tail
|
你将从dmesg的屏幕输出中看见”Hello world!”信息。如今卸载使用rmmod卸载内核模块,并检查退出信息。
1
2
|
$
sudo
rmmod hello_printk
$ dmesg |
tail
|
到此,你就成功地完成了对内核模块的编译和安装!
使用/proc的Hello, World!
一种用户程序和内核通信最简单和流行的方式是经过使用/proc下文件系统进行通信。/proc是一个伪文件系统,从这里的文件读取的数据是由内核返回的 数据,而且写入到这里面的数据将会被内核读取和处理。在使用/proc方式以前,所用用户和内核之间的通信都不得不使用系统调用来完成。使用系统调用意味 着你将在要在查找已经具备你须要的行为方式的系统调用(通常不会出现这种状况),或者建立一种新的系统调用来知足你的需求(这样就要求对内核全局作修改, 并增长系统调用的数量,这是一般是很是很差的作法),或者使用ioctl这个万能系统调用,这就要求要建立一个新文件类型供ioctl操做(这也是很是复 杂并且bug比较多的方式,一样是很是繁琐的)。/proc提供了一个简单的,无需定义的方式在用户空间和内核之间传递数据,这种方式不只能够知足内核使 用,一样也提供足够的自由度给内核模块作他们须要作的事情。
为了知足咱们的要求,咱们须要当咱们读在/proc下的某一个文件时将会返回一个“Hello world!”。咱们将使用/proc/hello_world这个文件。下载并解开hello proc这个gzip的tar包后,咱们将首先来看一下hello_proc.c这个文件
1
2
3
|
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
|
此次,咱们将增长一个proc_fs头文件,这个头文件包括驱动注册到/proc文件系统的支持。当另一个进程调用read()时,下一个函数将会被调 用。这个函数的实现比一个完整的普通内核驱动的read系统调用实现要简单的多,由于咱们仅作了让”Hello world”这个字符串缓存被一次读完。
1
2
3
4
|
static
int
hello_read_proc(
char
*buffer,
char
**start,off_t offset,
int
size,
int
*eof,
void
*data)
{
|
这个函数的参数值得明确的解释一下。buffer是指向内核缓存的指针,咱们将把read输出的内容写到这个buffer中。start参数多用更复杂的 /proc文件;咱们在这里将忽略这个参数;而且我只明确的容许offset这个的值为0。size是指buffer中包含多字节数;咱们必须检查这个参 数已避免出现内存越界的状况,eof参数一个EOF的简写,用于返回文件是否已经读到结束,而不须要经过调用read返回0来判断文件是否结束。这里咱们 不讨论依靠更复杂的/proc文件传输数据的方法。这个函数方法体罗列以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
char
*hello_str =
"Hello, world!\n"
;
int
len =
strlen
(hello_str);
/* Don't include the null byte. */
/* * We only support reading the whole string at once. */
if
(size < len)
return
< -EINVAL;
/* * If file position is non-zero, then assume the string has
* been read and indicate there is no more data to be read.
*/
if
(offset != 0)
return
0;
/* * We know the buffer is big enough to hold the string. */
strcpy
(buffer, hello_str);
/* * Signal EOF. */
*eof = 1;
return
len;
}
|
下面,咱们需将内核模块在初始化函数注册在/proc 子系统中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static
int
__init
hello_init(
void
){
/*
* Create an entry in /proc named "hello_world" that calls
* hello_read_proc() when the file is read.
*/
if
(create_proc_read_entry(
"hello_world"
, 0,
NULL, hello_read_proc, NULL) == 0) {
printk(KERN_ERR
"Unable to register "
Hello, world!
" proc filen"
);
return
-ENOMEM;
}
return
0;
}
module_init(hello_init);
|
当内核模块卸载时,须要在/proc移出注册的信息(若是咱们不这样作的,当一个进程试图去访问/proc/hello_world,/proc文件系统将会试着执行一个已经不存在的功能,这样将会致使内核崩溃)
1
2
3
4
5
6
7
8
9
|
static
void
__exit
hello_exit(
void
){
remove_proc_entry(
"hello_world"
, NULL);
}
module_exit(hello_exit);
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Valerie Henson val@nmt.edu"
);
MODULE_DESCRIPTION(
""
Hello, world!
" minimal module"
);
MODULE_VERSION(
"proc"
);
|
下面咱们将准备编译和装载模组
1
2
3
|
$
cd
hello_proc
$
make
$
sudo
insmod .
/hello_proc
.ko
|
如今,将会有一个称为/proc/hello_world的文件,而且读这个文件的,将会返回一个”Hello world”字符串。
1
2
|
$
cat
/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
2
3
4
5
|
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <asm/uaccess.h>
|
正如咱们看到的必须的头文件外,建立一个新设备还须要更多的内核头文件支持。fs.sh包含全部文件操做的结构,这些结构将由设备驱动程序来填值,并关联 到咱们相关的/dev文件。miscdevice.h头文件包含了对通用miscellaneous设备文件注册的支持。 asm/uaccess.h包含了测试咱们是否违背访问权限读写用户内存空间的函数。hello_read将在其余进程在/dev/hello调用 read()函数被调用的是一个函数。他将输出”Hello world!”到由read()传入的缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
static
ssize_t hello_read(
struct
file * file,
char
* buf,
size_t
count, loff_t *ppos)
{
char
*hello_str =
"Hello, world!n"
;
int
len =
strlen
(hello_str);
/* Don't include the null byte. */
/* * We only support reading the whole string at once. */
if
(count < len)
return
-EINVAL;
/*
* If file position is non-zero, then assume the string has
* been read and indicate there is no more data to be read.
*/
if
(*ppos != 0)
return
0;
/*
* Besides copying the string to the user provided buffer,
* this function also checks that the user has permission to
* write to the buffer, that it is mapped, etc.
*/
if
(copy_to_user(buf, hello_str, len))
return
-EINVAL;
/*
* Tell the user how much data we wrote.
*/
*ppos = len;
return
len;
}
|
下一步,咱们建立一个文件操做结构file operations struct,并用这个结构来定义当文件被访问时执行什么动做。在咱们的例子中咱们惟一关注的文件操做就是read。
1
2
3
4
|
static
const
struct
file_operations hello_fops = {
.owner = THIS_MODULE,
.read = hello_read,
};
|
如今,咱们将建立一个结构,这个结构包含有用于在内核注册一个通用miscellaneous驱动程序的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static
struct
miscdevice hello_dev = {
/*
* We don't care what minor number we end up with, so tell the
* kernel to just pick one.
*/
MISC_DYNAMIC_MINOR,
/*
* Name ourselves /dev/hello.
*/
"hello"
,
/*
* What functions to call when a program performs file
* operations on the device.
*/
&hello_fops
};
|
在一般状况下,咱们在init中注册设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static
int
__init
hello_init(
void
){
int
ret;
/*
* Create the "hello" device in the /sys/class/misc directory.
* Udev will automatically create the /dev/hello device using
* the default rules.
*/
ret = misc_register(&hello_dev);
if
(ret)
printk(KERN_ERR
"Unable to register "
Hello, world!
" misc devicen"
);
return
ret;
}
module_init(hello_init);
|
接下是在卸载时的退出函数
1
2
3
4
5
6
7
8
9
|
static
void
__exit
hello_exit(
void
){
misc_deregister(&hello_dev);
}
module_exit(hello_exit);
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Valerie Henson val@nmt.edu>"
);
MODULE_DESCRIPTION(
""
Hello, world!
" minimal module"
);
MODULE_VERSION(
"dev"
);
|
编译并加载模块:
1
2
3
|
$
cd
hello_dev
$
make
$
sudo
insmod .
/hello_dev
.ko
|
如今咱们将有一个称为/dev/hello的设备文件,而且这个设备文件被root访问时将会产生一个”Hello, world!”
1
2
|
$
sudo
cat
/dev/hello
Hello, world!
|
可是咱们不能使用普通用户访问他:
1
2
3
4
5
|
$
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规则必须作两件事情:第一建立一个符号链接,第二修改设备的访问权限。
下面这条规则能够达到这个目的:
1
|
KERNEL==
"hello"
, SYMLINK+=
"hello_world"
, MODE=
"0444"
|
咱们将详细的分解这条规则,并解释每个部分。KERNEL==”hello” 标示下面的的规则将做用于/sys中设备名字”hello”的设备(==是比较符)。hello 设备是咱们经过调用misc_register()并传递了一个包含设备名为”hello”的文件操做结构file_operations为参数而达到 的。你能够本身经过以下的命令在/sys下查看
1
|
$
ls
-d
/sys/class/misc/hello//sys/class/misc/hello/
|
SYMLINK+=”hello_world” 的意思是在符号连接列表中增长 (+= 符号的意思着追加)一个hello_world ,这个符号链接在设备出现时建立。在咱们场景下,咱们知道咱们的列表的中的只有这个符号链接,可是其余设备驱动程序可能会存在多个不一样的符号链接,所以使 用将设备追加入到符号列表中,而不是覆盖列表将会是更好的实践中的作法。
MODE=”0444″的意思是原始的设备的访问权限是0444,这个权限容许用户,组,和其余用户能够访问。
一般,使用正确的操做符号(==, +=, or =)是很是重要的,不然将会出现不可预知的状况。
如今咱们理解这个规则是怎么工做的,让咱们将其安装在/etc/udev目录下。udev规则文件以和System V初始脚本目录命名的同种方式的目录下,/etc/udeve/rules.d这个目录,并以字母/数字的顺序。和System V的初始化脚本同样,/etc/udev/rules.d下的目录一般符号链接到真正的文件,经过使用符号链接名,将使得规则文件已正确的次序获得执行。
使用以下的命令,拷贝hello.rules文件从/hello_dev目录到/etc/udev目录下,并建立一一个最早被执行的规则文件连接在/etc/udev/rules.d目录下。
1
2
|
$
sudo
cp
hello.rules
/etc/udev/
$
sudo
ln
-s ..
/hello
.rules
/etc/udev/rules
.d
/010_hello
.rules
|
如今咱们从新装载驱动程序,并观察新的驱动程序项
1
2
3
4
5
|
$
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设备.
1
2
3
4
5
|
$
cat
/dev/hello_world
Hello, world!
$
cat
/dev/hello
Hello, world!
|
- 上一篇:听一首歌就会想起某我的
- 下一篇:如何在android 中获取Wifi设备的IP地址
暂无评论