曾经多少次想要在内核游荡?曾经多少次茫然不知方向?你不要再对着它迷惘,让咱们指引你走向前方……css
内核编程经常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同的:抛开漫不经心,你必须当心翼翼,由于你编程中的一个bug就会影响到整个系统。浮点运算作起来可不容易,堆栈固定而狭小,而你写的代码老是异步的,所以你须要想一想并发会致使什么。而除了全部这一切以外,Linux内核只是一个很大的、很复杂的C程序,它对每一个人开放,任何人都去读它、学习它并改进它,而你也能够是其中之一。html
学习内核编程的最简单的方式也许就是写个内核模块:一段能够动态加载进内核的代码。模块所能作的事是有限的——例如,他们不能在相似进程描述符这样的公共数据结构中增减字段(LCTT译注:可能会破坏整个内核及系统的功能)。可是,在其它方面,他们是成熟的内核级的代码,能够在须要时随时编译进内核(这样就能够摒弃全部的限制了)。彻底能够在Linux源代码树之外来开发并编译一个模块(这并不奇怪,它称为树外开发),若是你只是想稍微玩玩,而并不想提交修改以包含到主线内核中去,这样的方式是很方便的。node
在本教程中,咱们将开发一个简单的内核模块用以建立一个/dev/reverse设备。写入该设备的字符串将以相反字序的方式读回(“Hello World”读成“World Hello”)。这是一个很受欢迎的程序员面试难题,当你利用本身的能力在内核级别实现这个功能时,可使你获得一些加分。在开始前,有一句忠告:你的模块中的一个bug就会致使系统崩溃(虽然可能性不大,但仍是有可能的)和数据丢失。在开始前,请确保你已经将重要数据备份,或者,采用一种更好的方式,在虚拟机中进行试验。linux
默认状况下,/dev/reverse只有root可使用,所以你只能使用sudo来运行你的测试程序。要解决该限制,能够建立一个包含如下内容的/lib/udev/rules.d/99-reverse.rules文件:git
1 SUBSYSTEM == & quot ; misc & quot ; , KERNEL == & quot ; reverse & quot ; , MODE =& quot ; 0666 & quot ;别忘了从新插入模块。让非root用户访问设备节点每每不是一个好主意,可是在开发其间倒是十分有用的。这并非说以root身份运行二进制测试文件也不是个好主意。程序员
因为大多数的Linux内核模块是用C写的(除了底层的特定于体系结构的部分),因此推荐你将你的模块以单一文件形式保存(例如,reverse.c)。咱们已经把完整的源代码放在GitHub上——这里咱们将看其中的一些片断。开始时,咱们先要包含一些常见的文件头,并用预约义的宏来描述模块:github
1
2
3
4
5
6
7
|
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE
(
"GPL"
)
;
MODULE_AUTHOR
(
"Valentine Sinitsyn <valentine.sinitsyn@gmail.com>"
)
;
MODULE_DESCRIPTION
(
"In-kernel phrase reverser"
)
;
|
这里一切都直接明了,除了MODULE_LICENSE():它不只仅是一个标记。内核坚决地支持GPL兼容代码,所以若是你把许可证设置为其它非GPL兼容的(如,“Proprietary”[专利]),某些特定的内核功能将在你的模块中不可用。web
内核编程颇有趣,可是在现实项目中写(尤为是调试)内核代码要求特定的技巧。一般来说,在没有其它方式能够解决你的问题时,你才应该在内核级别解决它。如下情形中,可能你在用户空间中解决它更好:面试
- 你要开发一个USB驱动 —— 请查看libusb。
- 你要开发一个文件系统 —— 试试FUSE。
- 你在扩展Netfilter —— 那么libnetfilter_queue对你有所帮助。
一般,内核里面代码的性能会更好,可是对于许多项目而言,这点性能丢失并不严重。shell
因为内核编程老是异步的,没有一个main()函数来让Linux顺序执行你的模块。取而代之的是,你要为各类事件提供回调函数,像这个:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static
int
__init
reverse_init
(
void
)
{
printk
(
KERN
_INFO
"reverse device has been registered\n"
)
;
return
0
;
}
static
void
__exit
reverse_exit
(
void
)
{
printk
(
KERN
_INFO
"reverse device has been unregistered\n"
)
;
}
module_init
(
reverse_init
)
;
module_exit
(
reverse_exit
)
;
|
这里,咱们定义的函数被称为模块的插入和删除。只有第一个的插入函数是必要的。目前,它们只是打印消息到内核环缓冲区(能够在用户空间经过dmesg命令访问);KERN_INFO是日志级别(注意,没有逗号)。__init和__exit是属性 —— 联结到函数(或者变量)的元数据片。属性在用户空间的C代码中是很罕见的,可是内核中却很广泛。全部标记为__init的,会在初始化后释放内存以供重用(还记得那条过去内核的那条“Freeing unused kernel memory…[释放未使用的内核内存……]”信息吗?)。__exit代表,当代码被静态构建进内核时,该函数能够安全地优化了,不须要清理收尾。最后,module_init()和module_exit()这两个宏将reverse_init()和reverse_exit()函数设置成为咱们模块的生命周期回调函数。实际的函数名称并不重要,你能够称它们为init()和exit(),或者start()和stop(),你想叫什么就叫什么吧。他们都是静态声明,你在外部模块是看不到的。事实上,内核中的任何函数都是不可见的,除非明确地被导出。然而,在内核程序员中,给你的函数加上模块名前缀是约定俗成的。
这些都是些基本概念 – 让咱们来作更多有趣的事情吧。模块能够接收参数,就像这样:
1
|
# modprobe foo bar=1
|
modinfo命令显示了模块接受的全部参数,而这些也能够在/sys/module//parameters下做为文件使用。咱们的模块须要一个缓冲区来存储参数 —— 让咱们把这大小设置为用户可配置。在MODULE_DESCRIPTION()下添加以下三行:
1
2
3
|
static
unsigned
long
buffer_size
=
8192
;
module_param
(
buffer_size
,
ulong
,
(
S_IRUSR
|
S_IRGRP
|
S_IROTH
)
)
;
MODULE_PARM_DESC
(
buffer_size
,
"Internal buffer size"
)
;
|
这儿,咱们定义了一个变量来存储该值,封装成一个参数,并经过sysfs来让全部人可读。这个参数的描述(最后一行)出如今modinfo的输出中。
因为用户能够直接设置buffer_size,咱们须要在reverse_init()来清除无效取值。你总该检查来自内核以外的数据 —— 若是你不这么作,你就是将本身置身于内核异常或安全漏洞之中。
1
2
3
4
5
6
7
8
9
|
static
int
__init
reverse_init
(
)
{
if
(
!
buffer_size
)
return
-
1
;
printk
(
KERN
_INFO
"reverse device has been registered, buffer size is %lu bytes\n"
,
buffer_size
)
;
return
0
;
}
|
来自模块初始化函数的非0返回值意味着模块执行失败。
但你开发模块时,Linux内核就是你所需一切的源头。然而,它至关大,你可能在查找你所要的内容时会有困难。幸运的是,在庞大的代码库面前,有许多工具使这个过程变得简单。首先,是Cscope —— 在终端中运行的一个比较经典的工具。你所要作的,就是在内核源代码的顶级目录中运行make cscope && cscope。Cscope和Vim以及Emacs整合得很好,所以你能够在你最喜好的编辑器中使用它。
若是基于终端的工具不是你的最爱,那么就访问http://lxr.free-electrons.com吧。它是一个基于web的内核导航工具,即便它的功能没有Cscope来得多(例如,你不能方便地找到函数的用法),但它仍然提供了足够多的快速查询功能。
如今是时候来编译模块了。你须要你正在运行的内核版本头文件(linux-headers,或者等同的软件包)和build-essential(或者相似的包)。接下来,该建立一个标准的Makefile模板:
1
2
3
4
5
|
obj
-
m
+=
reverse
.
o
all
:
make
-
C
/
lib
/
modules
/
$
(
shell
uname
-
r
)
/
build
M
=
$
(
PWD
)
modules
clean
:
make
-
C
/
lib
/
modules
/
$
(
shell
uname
-
r
)
/
build
M
=
$
(
PWD
)
clean
|
如今,调用make来构建你的第一个模块。若是你输入的都正确,在当前目录内会找到reverse.ko文件。使用sudo insmod reverse.ko插入内核模块,而后运行以下命令:
1
2
|
$
dmesg
|
tail
-
1
[
5905.042081
]
reverse
device
has
been
registered
,
buffer
size
is
8192
bytes
|
恭喜了!然而,目前这一行还只是假象而已 —— 尚未设备节点呢。让咱们来搞定它。
在Linux中,有一种特殊的字符设备类型,叫作“混杂设备”(或者简称为“misc”)。它是专为单一接入点的小型设备驱动而设计的,而这正是咱们所须要的。全部混杂设备共享同一个主设备号(10),所以一个驱动(drivers/char/misc.c)就能够查看它们全部设备了,而这些设备用次设备号来区分。从其余意义来讲,它们只是普通字符设备。
要为该设备注册一个次设备号(以及一个接入点),你须要声明struct misc_device,填上全部字段(注意语法),而后使用指向该结构的指针做为参数来调用misc_register()。为此,你也须要包含linux/miscdevice.h头文件:
1
2
3
4
5
6
7
8
9
10
11
|
static
struct
miscdevice
reverse_misc_device
=
{
.
minor
=
MISC_DYNAMIC_MINOR
,
.
name
=
"reverse"
,
.
fops
=
&
reverse
_fops
}
;
static
int
__init
reverse_init
(
)
{
.
.
.
misc_register
(
&
reverse_misc_device
)
;
printk
(
KERN
_INFO
.
.
.
}
|
这儿,咱们为名为“reverse”的设备请求一个第一个可用的(动态的)次设备号;省略号代表咱们以前已经见过的省略的代码。别忘了在模块卸下后注销掉该设备。
1
2
3
4
5
|
static
void
__exit
reverse_exit
(
void
)
{
misc_deregister
(
&
reverse_misc_device
)
;
.
.
.
}
|
‘fops’字段存储了一个指针,指向一个file_operations结构(在Linux/fs.h中声明),而这正是咱们模块的接入点。reverse_fops定义以下:
1
2
3
4
5
6
|
static
struct
file_operations
reverse_fops
=
{
.
owner
=
THIS_MODULE
,
.
open
=
reverse_open
,
.
.
.
.
llseek
=
noop
_llseek
}
;
|
另外,reverse_fops包含了一系列回调函数(也称之为方法),当用户空间代码打开一个设备,读写或者关闭文件描述符时,就会执行。若是你要忽略这些回调,能够指定一个明确的回调函数来替代。这就是为何咱们将llseek设置为noop_llseek(),(顾名思义)它什么都不干。这个默认实现改变了一个文件指针,并且咱们如今并不须要咱们的设备能够寻址(这是今天留给大家的家庭做业)。
让咱们来实现该方法。咱们将给每一个打开的文件描述符分配一个新的缓冲区,并在它关闭时释放。这实际上并不安全:若是一个用户空间应用程序泄漏了描述符(也许是故意的),它就会霸占RAM,并致使系统不可用。在现实世界中,你总得考虑到这些可能性。但在本教程中,这种方法没关系。
咱们须要一个结构函数来描述缓冲区。内核提供了许多常规的数据结构:连接列表(双联的),哈希表,树等等之类。不过,缓冲区经常从头设计。咱们将调用咱们的“struct buffer”:
1
2
3
4
|
struct
buffer
{
char
*
data
,
*
end
,
*
read_ptr
;
unsigned
long
size
;
}
;
|
data是该缓冲区存储的一个指向字符串的指针,而end指向字符串结尾后的第一个字节。read_ptr是read()开始读取数据的地方。缓冲区的size是为了保证完整性而存储的 —— 目前,咱们尚未使用该区域。你不能假设使用你结构体的用户会正确地初始化全部这些东西,因此最好在函数中封装缓冲区的分配和收回。它们一般命名为buffer_alloc()和buffer_free()。
static struct buffer buffer_alloc(unsigned long size) { struct buffer *buf; buf = kzalloc(sizeof(buf), GFP_KERNEL); if (unlikely(!buf)) goto out; … out: return buf; }
内核内存使用kmalloc()来分配,并使用kfree()来释放;kzalloc()的风格是将内存设置为全零。不一样于标准的malloc(),它的内核对应部分收到的标志指定了第二个参数中请求的内存类型。这里,GFP_KERNEL是说咱们须要一个普通的内核内存(不是在DMA或高内存区中)以及若是须要的话函数能够睡眠(从新调度进程)。sizeof(*buf)是一种常见的方式,它用来获取可经过指针访问的结构体的大小。
你应该随时检查kmalloc()的返回值:访问NULL指针将致使内核异常。同时也须要注意unlikely()宏的使用。它(及其相对宏likely())被普遍用于内核中,用于代表条件几乎老是真的(或假的)。它不会影响到控制流程,可是能帮助现代处理器经过分支预测技术来提高性能。
最后,注意goto语句。它们经常为认为是邪恶的,可是,Linux内核(以及一些其它系统软件)采用它们来实施集中式的函数退出。这样的结果是减小嵌套深度,使代码更具可读性,并且很是像更高级语言中的try-catch区块。
有了buffer_alloc()和buffer_free(),open和close方法就变得很简单了。
1
2
3
4
5
6
7
|
static
int
reverse_open
(
struct
inode
*
inode
,
struct
file
*
file
)
{
int
err
=
0
;
file
->
private_data
=
buffer_alloc
(
buffer_size
)
;
.
.
.
return
err
;
}
|
struct file是一个标准的内核数据结构,用以存储打开的文件的信息,如当前文件位置(file->f_pos)、标志(file->f_flags),或者打开模式(file->f_mode)等。另一个字段file->privatedata用于关联文件到一些专有数据,它的类型是void *,并且它在文件拥有者之外,对内核不透明。咱们将一个缓冲区存储在那里。
若是缓冲区分配失败,咱们经过返回否认值(-ENOMEM)来为调用的用户空间代码标明。一个C库中调用的open(2)系统调用(如glibc)将会检测这个并适当地设置errno 。
“read”和“write”方法是真正完成工做的地方。当数据写入到缓冲区时,咱们放弃以前的内容和反向地存储该字段,不须要任何临时存储。read方法仅仅是从内核缓冲区复制数据到用户空间。可是若是缓冲区尚未数据,revers_eread()会作什么呢?在用户空间中,read()调用会在有可用数据前阻塞它。在内核中,你就必须等待。幸运的是,有一项机制用于处理这种状况,就是‘wait queues’。
想法很简单。若是当前进程须要等待某个事件,它的描述符(struct task_struct存储‘current’信息)被放进非可运行(睡眠中)状态,并添加到一个队列中。而后schedule()就被调用来选择另外一个进程运行。生成事件的代码经过使用队列将等待进程放回TASK_RUNNING状态来唤醒它们。调度程序将在之后在某个地方选择它们之一。Linux有多种非可运行状态,最值得注意的是TASK_INTERRUPTIBLE(一个能够经过信号中断的睡眠)和TASK_KILLABLE(一个可被杀死的睡眠中的进程)。全部这些都应该正确处理,并等待队列为你作这些事。
一个用以存储读取等待队列头的自然场所就是结构缓冲区,因此从为它添加wait_queue_headt read\queue字段开始。你也应该包含linux/sched.h头文件。可使用DECLARE_WAITQUEUE()宏来静态声明一个等待队列。在咱们的状况下,须要动态初始化,所以添加下面这行到buffer_alloc():
1
|
init_waitqueue_head
(
&
buf
->
read_queue
)
;
|
咱们等待可用数据;或者等待read_ptr != end条件成立。咱们也想要让等待操做能够被中断(如,经过Ctrl+C)。所以,“read”方法应该像这样开始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static
ssize_t
reverse_read
(
struct
file
*
file
,
char
__user
*
out
,
size_t
size
,
loff_t
*
off
)
{
struct
buffer
*
buf
=
file
->
private_data
;
ssize_t
result
;
while
(
buf
->
read_ptr
==
buf
->
end
)
{
if
(
file
->
f_flags
&
O_NONBLOCK
)
{
result
=
-
EAGAIN
;
goto
out
;
}
if
(
wait_event_interruptible
(
buf
->
read_queue
,
buf
->
read_ptr
!=
buf
->
end
)
)
{
result
=
-
ERESTARTSYS
;
goto
out
;
}
}
.
.
.
|
咱们让它循环,直到有可用数据,若是没有则使用wait_event_interruptible()(它是一个宏,不是函数,这就是为何要经过值的方式给队列传递)来等待。好吧,若是wait_event_interruptible()被中断,它返回一个非0值,这个值表明-ERESTARTSYS。这段代码意味着系统调用应该从新启动。file->f_flags检查以非阻塞模式打开的文件数:若是没有数据,返回-EAGAIN。
咱们不能使用if()来替代while(),由于可能有许多进程正等待数据。当write方法唤醒它们时,调度程序以不可预知的方式选择一个来运行,所以,在这段代码有机会执行的时候,缓冲区可能再次空出。如今,咱们须要将数据从buf->data 复制到用户空间。copy_to_user()内核函数就干了此事:
1
2
3
4
5
|
size
=
min
(
size
,
(
size_t
)
(
buf
->
end
-
buf
->
read_ptr
)
)
;
if
(
copy_to_user
(
out
,
buf
->
read_ptr
,
size
)
)
{
result
=
-
EFAULT
;
goto
out
;
}
|
若是用户空间指针错误,那么调用可能会失败;若是发生了此事,咱们就返回-EFAULT。记住,不要相信任何来自内核外的事物!
1
2
3
4
5
|
buf
->
read_ptr
+=
size
;
result
=
size
;
out
:
return
result
;
}
|
为了使数据在任意块可读,须要进行简单运算。该方法返回读入的字节数,或者一个错误代码。
写方法更简短。首先,咱们检查缓冲区是否有足够的空间,而后咱们使用copy_from_userspace()函数来获取数据。再而后read_ptr和结束指针会被重置,而且反转存储缓冲区内容:
1
2
3
4
|
buf
->
end
=
buf
->
data
+
size
;
buf
->
read_ptr
=
buf
->
data
;
if
(
buf
->
end
>
buf
->
data
)
reverse_phrase
(
buf
->
data
,
buf
->
end
-
1
)
;
|
这里, reverse_phrase()干了全部吃力的工做。它依赖于reverse_word()函数,该函数至关简短而且标记为内联。这是另一个常见的优化;可是,你不能过分使用。由于过多的内联会致使内核映像徒然增大。
最后,咱们须要唤醒read_queue中等待数据的进程,就跟先前讲过的那样。wake_up_interruptible()就是用来干此事的:
1
|
wake_up_interruptible
(
&
buf
->
read_queue
)
;
|
耶!你如今已经有了一个内核模块,它至少已经编译成功了。如今,是时候来测试了。
或许,内核中最多见的调试方法就是打印。若是你愿意,你可使用普通的printk() (假定使用KERN_DEBUG日志等级)。然而,那儿还有更好的办法。若是你正在写一个设备驱动,这个设备驱动有它本身的“struct device”,可使用pr_debug()或者dev_dbg():它们支持动态调试(dyndbg)特性,并能够根据须要启用或者禁用(请查阅Documentation/dynamic-debug-howto.txt)。对于单纯的开发消息,使用pr_devel(),除非设置了DEBUG,不然什么都不会作。要为咱们的模块启用DEBUG,请添加如下行到Makefile中:
1 CFLAGS_reverse . o : = - DDEBUG完了以后,使用dmesg来查看pr_debug()或pr_devel()生成的调试信息。 或者,你能够直接发送调试信息到控制台。要想这么干,你能够设置console_loglevel内核变量为8或者更大的值(echo 8 /proc/sys/kernel/printk),或者在高日志等级,如KERN_ERR,来临时打印要查询的调试信息。很天然,在发布代码前,你应该移除这样的调试声明。
注意内核消息出如今控制台,不要在Xterm这样的终端模拟器窗口中去查看;这也是在内核开发时,建议你不在X环境下进行的缘由。
编译模块,而后加载进内核:
1
2
3
4
5
6
|
$
make
$
sudo
insmod
reverse
.
ko
buffer_size
=
2048
$
lsmod
reverse
2419
0
$
ls
-
l
/
dev
/
reverse
crw
-
rw
-
rw
-
1
root
root
10
,
58
Feb
22
15
:
53
/
dev
/
reverse
|
一切彷佛就位。如今,要测试模块是否正常工做,咱们将写一段小程序来翻转它的第一个命令行参数。main()(再三检查错误)可能看上去像这样:
1
2
3
4
|
int
fd
=
open
(
"/dev/reverse"
,
O_RDWR
)
;
write
(
fd
,
argv
[
1
]
,
strlen
(
argv
[
1
]
)
)
;
read
(
fd
,
argv
[
1
]
,
strlen
(
argv
[
1
]
)
)
;
printf
(
"Read: %s\n"
,
argv
[
1
]
)
;
|
像这样运行:
1
2
|
$
.
/
test
'A quick brown fox jumped over the lazy dog'
Read
:
dog
lazy
the
over
jumped
fox
brown
quick
A
|
它工做正常!玩得更逗一点:试试传递单个单词或者单个字母的短语,空的字符串或者是非英语字符串(若是你有这样的键盘布局设置),以及其它任何东西。
如今,让咱们让事情变得更好玩一点。咱们将建立两个进程,它们共享一个文件描述符(及其内核缓冲区)。其中一个会持续写入字符串到设备,而另外一个将读取这些字符串。在下例中,咱们使用了fork(2)系统调用,而pthreads也很好用。我也省略打开和关闭设备的代码,并在此检查代码错误(又来了):
1
2
3
4
5
6
7
8
9
10
11
|
char
*
phrase
=
"A quick brown fox jumped over the lazy dog"
;
if
(
fork
(
)
)
/* Parent is the writer */
while
(
1
)
write
(
fd
,
phrase
,
len
)
;
else
/* child is the reader */
while
(
1
)
{
read
(
fd
,
buf
,
len
)
;
printf
(
"Read: %s\n"
,
buf
)
;
}
|
你但愿这个程序会输出什么呢?下面就是在个人笔记本上获得的东西:
1
2
3
4
5
|
Read
:
dog
lazy
the
over
jumped
fox
brown
quick
A
Read
:
A
kcicq
brown
fox
jumped
over
the
lazy
dog
Read
:
A
kciuq
nworb
xor
jumped
fox
brown
quick
A
Read
:
A
kciuq
nworb
xor
jumped
fox
brown
quick
A
.
.
.
|
这里发生了什么呢?就像举行了一场比赛。咱们认为read和write是原子操做,或者从头至尾一次执行一个指令。然而,内核确实无序并发的,随便就从新调度了reverse_phrase()函数内部某个地方运行着的写入操做的内核部分。若是在写入操做结束前就调度了read()操做呢?就会产生数据不完整的状态。这样的bug很是难以找到。可是,怎样来处理这个问题呢?
基本上,咱们须要确保在写方法返回前没有read方法能被执行。若是你曾经编写过一个多线程的应用程序,你可能见过同步原语(锁),如互斥锁或者信号。Linux也有这些,但有些细微的差异。内核代码能够运行进程上下文(用户空间代码的“表明”工做,就像咱们使用的方法)和终端上下文(例如,一个IRQ处理线程)。若是你已经在进程上下文中和而且你已经获得了所需的锁,你只须要简单地睡眠和重试直到成功为止。在中断上下文时你不能处于休眠状态,所以代码会在一个循环中运行直到锁可用。关联原语被称为自旋锁,但在咱们的环境中,一个简单的互斥锁 —— 在特定时间内只有惟一一个进程能“占有”的对象 —— 就足够了。处于性能方面的考虑,现实的代码可能也会使用读-写信号。
锁老是保护某些数据(在咱们的环境中,是一个“struct buffer”实例),并且也经常会把它们嵌入到它们所保护的结构体中。所以,咱们添加一个互斥锁(‘struct mutex lock’)到“struct buffer”中。咱们也必须用mutex_init()来初始化互斥锁;buffer_alloc是用来处理这件事的好地方。使用互斥锁的代码也必须包含linux/mutex.h。
互斥锁很像交通讯号灯 —— 要是司机不看它和不听它的,它就没什么用。所以,在对缓冲区作操做并在操做完成时释放它以前,咱们须要更新reverse_read()和reverse_write()来获取互斥锁。让咱们来看看read方法 —— write的工做原理相同:
1
2
3
4
5
6
7
8
9
|
static
ssize_t
reverse_read
(
struct
file
*
file
,
char
__user
*
out
,
size_t
size
,
loff_t
*
off
)
{
struct
buffer
*
buf
=
file
->
private_data
;
ssize_t
result
;
if
(
mutex_lock_interruptible
(
&
buf
->
lock
)
)
{
result
=
-
ERESTARTSYS
;
goto
out
;
}
|
咱们在函数一开始就获取锁。mutex_lock_interruptible()要么获得互斥锁而后返回,要么让进程睡眠,直到有可用的互斥锁。就像前面同样,_interruptible后缀意味着睡眠能够由信号来中断。
1
2
3
4
5
6
7
8
|
while
(
buf
->
read_ptr
==
buf
->
end
)
{
mutex_unlock
(
&
buf
->
lock
)
;
/* ... wait_event_interruptible() here ... */
if
(
mutex_lock_interruptible
(
&
buf
->
lock
)
)
{
result
=
-
ERESTARTSYS
;
goto
out
;
}
}
|
下面是咱们的“等待数据”循环。当获取互斥锁时,或者发生称之为“死锁”的情境时,不该该让进程睡眠。所以,若是没有数据,咱们释放互斥锁并调用wait_event_interruptible()。当它返回时,咱们从新获取互斥锁并像往常同样继续:
1
2
3
4
5
6
7
8
9
|
if
(
copy_to_user
(
out
,
buf
->
read_ptr
,
size
)
)
{
result
=
-
EFAULT
;
goto
out_unlock
;
}
.
.
.
out_unlock
:
mutex_unlock
(
&
buf
->
lock
)
;
out
:
return
result
;
|
最后,当函数结束,或者在互斥锁被获取过程当中发生错误时,互斥锁被解锁。从新编译模块(别忘了从新加载),而后再次进行测试。如今你应该没发现毁坏的数据了。
如今你已经尝试了一次内核黑客。咱们刚刚为你揭开了这个话题的外衣,里面还有更多东西供你探索。咱们的第一个模块有意识地写得简单一点,在从中学到的概念在更复杂的环境中也同样。并发、方法表、注册回调函数、使进程睡眠以及唤醒进程,这些都是内核黑客们耳熟能详的东西,而如今你已经看过了它们的运做。或许某天,你的内核代码也将被加入到主线Linux源代码树中 —— 若是真这样,请联系咱们!