曾經多少次想要在內核遊蕩?曾經多少次茫然不知方向?你不要再對着它迷惘,讓咱們指引你走向前方……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源代碼樹中 —— 若是真這樣,請聯繫咱們!