linux 驱动模块 内核编译环境
Linux设备驱动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包
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中找到。
一般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
|
$ //kernel .org /pub/linux/kernel/v2 .6 /linux-2 .6.21.5. tar .bz2 |
解包内核源代码
1
|
$ tar xjvf 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
tar |
在标准位置建立的到内核源代码的链接
1
|
$ sudo ln
top -level source directory> /lib/modules/ 'uname /build |
现在已经内核源代码已经可以用于编译内核模块了,重启你的机器以使得你根据新内核程序编译的内核可以被装载。
使用printk()函数打印”Hello World”
我们的第一个内核模块,我们将以一个在内核中使用函数printk()打印”Hello
world”的内核模块为开始。printk是内核中的printf函数。printk的输出打印在内核的消息缓存kernel message
buffer并拷贝到/var/log/messages(关于拷贝的变化依赖于如何配置syslogd)
下载hello_printk 模块的tar包 并解包:
1
|
$ tar xzvf tar .gz |
这个包中包含两个文件:Makefile,里面包含如何创建内核模块的指令和一个包含内核模块源代码的hello_printk.c文件。首先,我们将简要的过一下这个Makefile 文件。
1
|
obj-m |
obj-m指出将要编译成的内核模块列表。.o格式文件会自动地有相应的.c文件生成(不需要显示的罗列所有源代码文件)
1
|
KDIR /lib/modules/ $(shell uname -r) /build |
KDIR表示是内核源代码的位置。在当前标准情况是链接到包含着正在使用内核对应源代码的目录树位置。
1
|
PWD pwd ) |
PWD指示了当前工作目录并且是我们自己内核模块的源代码位置
1
2
|
default: $(MAKE) |
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
hello_init( void ){ printk( "Hello, ); return 0; } |
这是内核模块的初始化函数,这个函数在内核模块初始化被装载的时候调用。__init关键字告诉内核这个代码只会被运行一次,而且是在内核装载的时候。
printk()函数这一行将打印一个”Hello, world”到内核消息缓存。printk参数的形式在大多数情况和printf(3)一模一样。
1
2
|
module_init(hello_init); module_init() |
宏告诉内核当内核模块第一次运行时哪一个函数将被运行。任何在内核模块中其他部分都会受到内核模块初始化函数的影响。
1
2
3
4
5
|
static void
hello_exit( void ){ printk( "Goodbye, ); } module_exit(hello_exit); |
同样地,退出函数也只在内核模块被卸载的时候会运行一次,module_exit()宏标示了退出函数。__exit关键字告诉内核这段代码只在内核模块被卸载的时候运行一次。
1
2
3
4
5
|
MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "Hello,
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 $ tail |
你将从dmesg的屏幕输出中看见”Hello world!”信息。现在卸载使用rmmod卸载内核模块,并检查退出信息。
1
2
|
$ sudo rmmod $ 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 #include #include |
这次,我们将增加一个proc_fs头文件,这个头文件包括驱动注册到/proc文件系统的支持。当另外一个进程调用read()时,下一个函数将会被调
用。这个函数的实现比一个完整的普通内核驱动的read系统调用实现要简单的多,因为我们仅做了让”Hello world”这个字符串缓存被一次读完。
1
2
3
4
|
static int hello_read_proc( char *buffer, char **start,off_t 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, ; int len strlen (hello_str); /* /* if (size return < /* * */ if (offset return 0; /* strcpy (buffer, /* *eof return len; } |
下面,我们需将内核模块在初始化函数注册在/proc 子系统中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static int
hello_init( void ){ /* * * */ if (create_proc_read_entry( "hello_world" , NULL, printk(KERN_ERR "Unable Hello, " ); return -ENOMEM; } return 0; } module_init(hello_init); |
当内核模块卸载时,需要在/proc移出注册的信息(如果我们不这样做的,当一个进程试图去访问/proc/hello_world,/proc文件系统将会试着执行一个已经不存在的功能,这样将会导致内核崩溃)
1
2
3
4
5
6
7
8
9
|
static void
hello_exit( void ){ remove_proc_entry( "hello_world" , } module_exit(hello_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "" Hello, " ); 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, |
你可以为为同一个驱动程序创建多个/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 #include #include #include #include |
正如我们看到的必须的头文件外,创建一个新设备还需要更多的内核头文件支持。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 struct file char * size_t count, { char *hello_str "Hello, ; int len strlen (hello_str); /* /* if (count return -EINVAL; /* * * */ if (*ppos return 0; /* * * * */ if (copy_to_user(buf, return -EINVAL; /* * */ *ppos return len; } |
下一步,我们创建一个文件操作结构file operations struct,并用这个结构来定义当文件被访问时执行什么动作。在我们的例子中我们唯一关注的文件操作就是read。
1
2
3
4
|
static const
.owner .read }; |
现在,我们将创建一个结构,这个结构包含有用于在内核注册一个通用miscellaneous驱动程序的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static struct
/* * * */ MISC_DYNAMIC_MINOR, /* * */ "hello" , /* * * */ &hello_fops }; |
在通常情况下,我们在init中注册设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static int
hello_init( void ){ int ret; /* * * * */ ret if (ret) printk(KERN_ERR "Unable Hello, " ); return ret; } module_init(hello_init); |
接下是在卸载时的退出函数
1
2
3
4
5
6
7
8
9
|
static void
hello_exit( void ){ misc_deregister(&hello_dev); } module_exit(hello_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "" Hello, " ); MODULE_VERSION( "dev" ); |
编译并加载模块:
1
2
3
|
$ cd hello_dev $ make $ sudo insmod /hello_dev .ko |
现在我们将有一个称为/dev/hello的设备文件,并且这个设备文件被root访问时将会产生一个”Hello, world!”
1
2
|
$ sudo cat
Hello, |
但是我们不能使用普通用户访问他:
1
2
3
4
5
|
$ cat /dev/hello cat : /dev/hello : $ ls -l /dev/hello crw-rw---- /dev/hello |
这是有默认的udev规则导致的,这个条规将标明当一个普通设备出现时,他的名字将会是/dev/,并且默认的访问权限是0660(用户和组读写访问,其
他用户无法访问)。我们在真实情况中可能会希望创建一个被普通用户访问的设备驱动程序,并且给这个设备起一个相应的连接名。为达到这个目的,我们将编写一
条udev规则。
udev规则必须做两件事情:第一创建一个符号连接,第二修改设备的访问权限。
下面这条规则可以达到这个目的:
1
|
KERNEL== "hello" , "hello_world" , "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
/etc/udev/ $ sudo ln
/hello .rules /etc/udev/rules .d /010_hello .rules |
现在我们重新装载驱动程序,并观察新的驱动程序项
1
2
3
4
5
|
$ sudo rmmod $ sudo insmod /hello_dev .ko $ ls -l /dev/hello * cr--r--r-- /dev/hello lrwxrwxrwx /dev/hello_world -> |
最后,检查你可以使用普通用户访问/dev/hello_world设备.
1
2
3
4
5
|
$ cat /dev/hello_world Hello, $ cat /dev/hello Hello, |
本文转自陈皓( 酷壳 – CoolShell.cn )
- 上一篇:听一首歌就会想起某个人
- 下一篇:如何在android 中获取Wifi设备的IP地址

- 如何在android 中获取Wifi设备的IP地址(407)
- 如何编写一个简单的linux内核模块和设备驱动程序(112)
- 天空灰蒙蒙的,心情有点像这天气(46)
- 听一首歌就会想起某个人(43)
- Android broadcastReiver使用 步骤(24)
Linux设备驱动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包
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中找到。
一般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
|
$ //kernel .org /pub/linux/kernel/v2 .6 /linux-2 .6.21.5. tar .bz2 |
解包内核源代码
1
|
$ tar xjvf 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
tar |
在标准位置建立的到内核源代码的链接
1
|
$ sudo ln
top -level source directory> /lib/modules/ 'uname /build |
现在已经内核源代码已经可以用于编译内核模块了,重启你的机器以使得你根据新内核程序编译的内核可以被装载。
使用printk()函数打印”Hello World”
我们的第一个内核模块,我们将以一个在内核中使用函数printk()打印”Hello
world”的内核模块为开始。printk是内核中的printf函数。printk的输出打印在内核的消息缓存kernel message
buffer并拷贝到/var/log/messages(关于拷贝的变化依赖于如何配置syslogd)
下载hello_printk 模块的tar包 并解包:
1
|
$ tar xzvf tar .gz |
这个包中包含两个文件:Makefile,里面包含如何创建内核模块的指令和一个包含内核模块源代码的hello_printk.c文件。首先,我们将简要的过一下这个Makefile 文件。
1
|
obj-m |
obj-m指出将要编译成的内核模块列表。.o格式文件会自动地有相应的.c文件生成(不需要显示的罗列所有源代码文件)
1
|
KDIR /lib/modules/ $(shell uname -r) /build |
KDIR表示是内核源代码的位置。在当前标准情况是链接到包含着正在使用内核对应源代码的目录树位置。
1
|
PWD pwd ) |
PWD指示了当前工作目录并且是我们自己内核模块的源代码位置
1
2
|
default: $(MAKE) |
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
hello_init( void ){ printk( "Hello, ); return 0; } |
这是内核模块的初始化函数,这个函数在内核模块初始化被装载的时候调用。__init关键字告诉内核这个代码只会被运行一次,而且是在内核装载的时候。
printk()函数这一行将打印一个”Hello, world”到内核消息缓存。printk参数的形式在大多数情况和printf(3)一模一样。
1
2
|
module_init(hello_init); module_init() |
宏告诉内核当内核模块第一次运行时哪一个函数将被运行。任何在内核模块中其他部分都会受到内核模块初始化函数的影响。
1
2
3
4
5
|
static void
hello_exit( void ){ printk( "Goodbye, ); } module_exit(hello_exit); |
同样地,退出函数也只在内核模块被卸载的时候会运行一次,module_exit()宏标示了退出函数。__exit关键字告诉内核这段代码只在内核模块被卸载的时候运行一次。
1
2
3
4
5
|
MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "Hello,
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 $ tail |
你将从dmesg的屏幕输出中看见”Hello world!”信息。现在卸载使用rmmod卸载内核模块,并检查退出信息。
1
2
|
$ sudo rmmod $ 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 #include #include |
这次,我们将增加一个proc_fs头文件,这个头文件包括驱动注册到/proc文件系统的支持。当另外一个进程调用read()时,下一个函数将会被调
用。这个函数的实现比一个完整的普通内核驱动的read系统调用实现要简单的多,因为我们仅做了让”Hello world”这个字符串缓存被一次读完。
1
2
3
4
|
static int hello_read_proc( char *buffer, char **start,off_t 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, ; int len strlen (hello_str); /* /* if (size return < /* * */ if (offset return 0; /* strcpy (buffer, /* *eof return len; } |
下面,我们需将内核模块在初始化函数注册在/proc 子系统中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static int
hello_init( void ){ /* * * */ if (create_proc_read_entry( "hello_world" , NULL, printk(KERN_ERR "Unable Hello, " ); return -ENOMEM; } return 0; } module_init(hello_init); |
当内核模块卸载时,需要在/proc移出注册的信息(如果我们不这样做的,当一个进程试图去访问/proc/hello_world,/proc文件系统将会试着执行一个已经不存在的功能,这样将会导致内核崩溃)
1
2
3
4
5
6
7
8
9
|
static void
hello_exit( void ){ remove_proc_entry( "hello_world" , } module_exit(hello_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "" Hello, " ); 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, |
你可以为为同一个驱动程序创建多个/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 #include #include #include #include |
正如我们看到的必须的头文件外,创建一个新设备还需要更多的内核头文件支持。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 struct file char * size_t count, { char *hello_str "Hello, ; int len strlen (hello_str); /* /* if (count return -EINVAL; /* * * */ if (*ppos return 0; /* * * * */ if (copy_to_user(buf, return -EINVAL; /* * */ *ppos return len; } |
下一步,我们创建一个文件操作结构file operations struct,并用这个结构来定义当文件被访问时执行什么动作。在我们的例子中我们唯一关注的文件操作就是read。
1
2
3
4
|
static const
.owner .read }; |
现在,我们将创建一个结构,这个结构包含有用于在内核注册一个通用miscellaneous驱动程序的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static struct
/* * * */ MISC_DYNAMIC_MINOR, /* * */ "hello" , /* * * */ &hello_fops }; |
在通常情况下,我们在init中注册设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static int
hello_init( void ){ int ret; /* * * * */ ret if (ret) printk(KERN_ERR "Unable Hello, " ); return ret; } module_init(hello_init); |
接下是在卸载时的退出函数
1
2
3
4
5
6
7
8
9
|
static void
hello_exit( void ){ misc_deregister(&hello_dev); } module_exit(hello_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Valerie ); MODULE_DESCRIPTION( "" Hello, " ); MODULE_VERSION( "dev" ); |
编译并加载模块:
1
2
3
|
$ cd hello_dev $ make $ sudo insmod /hello_dev .ko |
现在我们将有一个称为/dev/hello的设备文件,并且这个设备文件被root访问时将会产生一个”Hello, world!”
1
2
|
$ sudo cat
Hello, |
但是我们不能使用普通用户访问他:
1
2
3
4
5
|
$ cat /dev/hello cat : /dev/hello : $ ls -l /dev/hello crw-rw---- /dev/hello |
这是有默认的udev规则导致的,这个条规将标明当一个普通设备出现时,他的名字将会是/dev/,并且默认的访问权限是0660(用户和组读写访问,其
他用户无法访问)。我们在真实情况中可能会希望创建一个被普通用户访问的设备驱动程序,并且给这个设备起一个相应的连接名。为达到这个目的,我们将编写一
条udev规则。
udev规则必须做两件事情:第一创建一个符号连接,第二修改设备的访问权限。
下面这条规则可以达到这个目的:
1
|
KERNEL== "hello" , "hello_world" , "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
/etc/udev/ $ sudo ln
/hello .rules /etc/udev/rules .d /010_hello .rules |
现在我们重新装载驱动程序,并观察新的驱动程序项
1
2
3
4
5
|
$ sudo rmmod $ sudo insmod /hello_dev .ko $ ls -l /dev/hello * cr--r--r-- /dev/hello lrwxrwxrwx /dev/hello_world -> |
最后,检查你可以使用普通用户访问/dev/hello_world设备.
1
2
3
4
5
|
$ cat /dev/hello_world Hello, $ cat /dev/hello Hello, |
linux 驱动模块 内核编译环境的更多相关文章
- linux内核编译环境配置
linux内核编译环境配置 如果不是编译内核,只需要安装与内核相匹配的kernel-devel开发包即可.即是/lib/modules/`uname -r`/build -> /usr/src/ ...
- 【转】Linux CentOS内核编译:下载CentOS源码、编译2.6.32-220的错误(apic.c:819 error 'numi_watchdog' undeclared)
一.下载CentOS源码 1.1 查看CentOS版本 cat /etc/issue 1.2 查看Linux内核版本 uname -r 1.3 下载 文件名:kernel-2.6.32-220.el6 ...
- 分析linux下的编译环境
不论是windows下的程序,还是linux下的程序,开发环境都离不开三个目录:include.lib.bin,分别是头文件目录.库文件目录.运行文件目录.或许目录不叫这个名字,但却必不可少,除非你的 ...
- Linux C/C++编译环境搭建
1. 配置GCC,LInux 在安装后已经有GCC了,但可能文件不全,我们可以利用 sudo apt-get install build-essential 安装 2. 安装文本编辑器 sudo ap ...
- 【linux】内核编译
原创,转载时请注明,谢谢.邮箱:tangzhongp@163.com 博客园地址:http://www.cnblogs.com/embedded-tzp Csdn博客地址:http://blog.cs ...
- 轻松学习Linux之内核编译
欢迎大家给我投票: http://2010blog.51cto.com/350944 650) this.width=650;" onclick='window.open("h ...
- 快速建立Linux c/c++编译环境
sudo apt-get install build-essential 省时又省心~
- Linux 安装ruby编译环境
1.输入:yum install ruby 1.1如果安装文件出错Error Downloading Packages: 输入:yum clean all 输入:yum makecache,此时如果出 ...
- 编译android源码官方教程(2)建立编译环境「linux & mac osx」
https://source.android.com/source/initializing.html Establishing a Build Environment IN THIS DOCUMEN ...
随机推荐
- Floyd算法(弗洛伊德算法)
算法描述: Floyd算法又称为弗洛伊德算法,插点法,是一种用于寻找给定的加权图中顶点间最短路径的算法.从图的带权邻接矩阵A=[a(i,j)] n×n开始,递归地进行n次更新,即由矩阵D(0)=A,按 ...
- AngularJS的指令(Directive) compile和link的区别及使用示例
如果我想实现这样一个功能,当一个input失去光标焦点时(blur),执行一些语句,比如当输入用户名后,向后台发ajax请求查询用户名是否已经存在,好有及时的页面相应. 输入 camnpr 失去焦点后 ...
- Python学习笔记(五)Python的切片和迭代
切片 Python提供了切片操作符,可以对list.tuple.字符串进行截取操作. list中的切片应用 语法如下: >>> L = ['Michael', 'Sarah', 'T ...
- python核心编程-第五章-个人笔记
1.用del删除对对象的引用 >>> a = 123 >>> a 123 >>> del a >>> a Traceback ( ...
- Leetcode算法刷题:第112题 Path Sum
Path Sum 题目 给予一个二叉树,和一个值su,寻找是否有一个从根节点到叶节点的和为su,有则返回True,没有为False.比如: 5 / \ 4 8 / / \ 11 13 4 / \ \ ...
- Jquery一个slideToggle搞定div的隐藏与显示
Jquery一个slideToggle搞定div的隐藏与显示 <!DOCTYPE html> <html> <head> <script src=" ...
- 【ZOJ】3785 What day is that day? ——浅谈KMP在ACM竞赛中的暴力打表找规律中的应用
转载请声明出处:http://www.cnblogs.com/kevince/p/3887827.html ——By Kevince 首先声明一下,这里的规律指的是循环,即找到最小循环周期. 这 ...
- Jam's math problem(思维)
Jam's math problem Submit Status Practice HDU 5615 Description Jam has a math problem. He just lea ...
- TagBuilder 性能如此低下?
本文来自:http://www.cnblogs.com/zhuisha/archive/2010/03/12/1684022.html 需要通过ASP.NET MVC生成一个列表,MVC里面根正苗红的 ...
- oracle后台进程2
oracle中的进程共分为三类:用户进程.服务进程.后台进程.其中后台进程伴随实例的启动而启动,他们主要是维护数据库的稳定,相当于一个企业中的管理者及内部服务人员.他们并不会直接给用户提供服务. 一: ...