The Linux Kernel Module Programming Guide
The Linux Kernel Module Programming Guide
Peter Jay SalzmanMichael Burian
Ori Pomerantz
Copyright © 2001 Peter Jay Salzman
The Linux Kernel Module Programming Guide is a free book; you may reproduce and/or modify it under the terms of the Open Software License, version 1.1. You can obtain a copy of this license at https://opensource.org/licenses/osl.php.
This book is distributed in the hope it will be useful, but without any warranty, without even the implied warranty of merchantability or fitness for a particular purpose.
The author encourages wide distribution of this book for personal or commercial use, provided the above copyright notice remains intact and the method adheres to the provisions of the Open Software License. In summary, you may copy and distribute this book free of charge or for a profit. No explicit permission is required from the author for reproduction of this book in any medium, physical or electronic.
Derivative works and translations of this document must be placed under the Open Software License, and the original copyright notice must remain intact. If you have contributed new material to this book, you must make the material and source code available
for your revisions. Please make revisions and updates available directly to the document maintainer, Peter Jay Salzman <p@dirac.org>
. This will allow for the merging of updates and provide consistent
revisions to the Linux community.
If you publish or distribute this book commercially, donations, royalties, and/or printed copies are greatly appreciated by the author and the Linux Documentation Project (LDP). Contributing in this way shows your support for free software and the LDP. If you have questions or comments, please contact the address above.
- Table of Contents
- Foreword
- 1. Introduction
-
- 1.1. 什麼是內核模塊?
- 1.2. 內核模塊是如何被調入內核工作的?
-
- 1.2.1. 在開始前
-
- 1.2.1.1. 內核模塊和內核的版本問題
- 1.2.1.2. 使用 X帶來的問題
- 1.2.1.3. 編譯相關和內核版本相關的問題
- 2. Hello World
-
- 2.1. Hello, World (part 1): 最簡單的內核模塊
-
- 2.1.1. 介紹
printk()
- 2.1.1. 介紹
- 2.2. 編譯內核模塊
- 2.3. Hello World (part 2)
- 2.4. Hello World (part 3): 關於__init和__exit宏
- 2.5. Hello World (part 4): 內核模塊證書和內核模塊文檔說明
- 2.6. 從命令行傳遞參數給內核模塊
- 2.7. 由多個文件構成的內核模塊
- 2.8. 為已編譯的內核編譯模塊
- 3. Preliminaries
-
- 3.1. 內核模塊對比用戶程序
-
- 3.1.1. 內核模塊是如何開始和結束的
- 3.1.2. 模塊可調用的函數
- 3.1.3. 用戶空間和內核空間
- 3.1.4. 命名空間
- 3.1.5. 代碼空間
- 3.1.6. 設備驅動
-
- 3.1.6.1. Major and Minor Numbers
- 4. Character Device Files
-
- 4.1. 字符設備文件
-
- 4.1.1. 關於file_operations結構體
- 4.1.2. 關於file結構體
- 4.1.3. 注冊一個設備
- 4.1.4. 注銷一個設備
- 4.1.5. chardev.c
- 4.1.6. 為多個版本的內核編寫內核模塊
- 5. The /proc File System
-
- 5.1. 關於 /proc 文件係統
- 5.2. 讀寫 /proc 文件
- 5.3. 用標準文件係統管理 /proc 文件
- 5.4. 使用seq_file管理 /proc 文件
- 6. Using /proc For Input
-
- 6.1. TODO:編寫關於sysfs的一章
- 7. Talking To Device Files
- 8. System Calls
-
- 8.1. 係統調用
- 9. Blocking Processes
-
- 9.1. 阻塞進程
- 10. Replacing Printks
-
- 10.1. 替換
printk
- 10.2. 讓你的鍵盤指示燈閃起來
- 10.1. 替換
- 11. Scheduling Tasks
-
- 11.1. 任務調度
- 12. Interrupt Handlers
-
- 12.1. 中斷處理程序
-
- 12.1.1. Interrupt Handlers
- 12.1.2. Intel架構中的鍵盤
- 13. Symmetric Multi Processing
-
- 13.1. 對稱多處理
- 14. Common Pitfalls
-
- 14.1. 常見陷阱
- A. Changes: 2.4 To 2.6
-
- A.1. 從2.4到2.6的變化
-
- A.1.1. 從2.4到2.6的變化
- B. Where To Go From Here
-
- B.1. 從這裏如何起步?
- Index
- List of Examples
- 2-1. hello-1.c
- 2-2. 一個基本的Makefile
- 2-3. hello-2.c
- 2-4. 兩個內核模塊使用的Makefile
- 2-5. hello-3.c
- 2-6. hello-4.c
- 2-7. hello-5.c
- 2-8. start.c
- 2-9. stop.c
- 2-10. Makefile
- 4-1. chardev.c
- 5-1. procfs1.c
- 5-2. procfs2.c
- 5-3. procfs3.c
- 5-4. procfs4.c
- 7-1. chardev.c
- 7-2. chardev.h
- 7-3. ioctl.c
- 8-1. syscall.c
- 9-1. sleep.c
- 10-1. print_string.c
- 10-2. kbleds.c
- 11-1. sched.c
- 12-1. intrpt.c
Foreword
1. 作者聲明
《Linux內核驅動模塊編程指南》最初是由Ori Pomerantz為2.2版本的內核編寫的 ,後來,Ori將文檔維護的任務交給了Peter Jay Salzman,Peter完成了2.4內核版本文檔 的編寫,畢竟Linux內核驅動模塊是一個更新很快的內容。現在,Peter也無法騰出足夠的 時間來完成2.6內核版本文檔的編寫,目前該2.6內核版本的文檔由合作者Michael Burian 完成。
2. 版本和注意
Linux內核模塊是一塊不斷更新進步的內容,在LKMPG上總有關於是否保留還是曆史 版本的爭論。Michael和我最終是決定為每個新的穩定版本內核建立一個新的文檔分支。也 就是說LKMPG 2.4.x專注於2.4的內核,而LKMPG 2.6.x將專注於2.6的內核。我們不會在一 篇文檔中提供對舊版本內核的支持,對此感興趣的讀者應該尋找相關版本的文檔分支。
在文檔中的絕大部分源代碼和討論都應該適用於其它平台,但我無法提供任何保證。其中的一個例外就是 Chapter 12, 中斷處理該章的源代碼和討論就隻適用於x86平台。
4. 譯者注
我,譯者,名叫田競,目前是一名在北京郵電大學就讀的通信專業的大學生。 自高中起我就是Linux的愛好者並追隨至今。我喜歡Linux給我帶來的自由,我想大家也一樣。沒有人不向往自由。
我學習內核模塊編寫時最初閱讀的是Orelly出版社的使用2.0版本的內核的書籍,但如同我預料的一樣, 書中的許多事例由於內核代碼的變化而無法使用。這讓想親自體驗內核模塊的神秘的我非常苦惱。 我在Linux文檔計劃在上海的鏡像站ldp.linuxforum.net上找到了這篇文檔。
受原作者Ori的鼓勵,基於上次完成的LKMPG 2.4的,內容有稍許的改變和擴充。應該是目前最新的了。 翻譯的方式有所改變,在基於LDP認可的docbook格式上翻譯,通過docbook2html轉換為附件中的html文檔。 由於對docbook不是很熟悉,其中的一些標題尚未翻譯,而且可能破壞了原有的tag,導致html出現一些錯誤顯示, 但總體來說很少。修改了很多2.4中的錯別字。
學習並分享學習的過程是我翻譯的最終目的。
補注:不知為何,原譯者田競未能及時更新此文檔。我發電子郵件詢問並請求做一些維護工作,但他至今仍未給我答複。我冒 昧地做一下更新和維護工作,為大家盡微薄之力,希望原譯者看此文到後能聯係我。譯文的版權完全屬於原譯者田競,我不想保留任何版權, 但我會對翻譯質量負責。我的郵箱是xiyou [dot] wangcong [at] gmail [dot] com。總的來說,我對原文中的 一些技術錯誤做了訂正(我已經通知原文作者),對原譯文中的一些用詞和錯字做了更改,也使本文與最新的LKMPG版本2.6.3版一致。我也會繼續 做維護工作,如發現有任何錯誤可以通過上麵的郵箱聯係我,我會及時修訂。
Chapter 1. Introduction
1.1. 什麼是內核模塊?
現在,你是不是想編寫內核模塊。你應該懂得C語言,寫過一些用戶程序, 那麼現在你將要見識一些真實的東西。在這裏,你會看到一個野蠻的指針是如何 毀掉你的文件係統的,一次內核崩潰意味著重啟動。
什麼是內核模塊?內核模塊是一些可以讓操作係統內核在需要時載入和執 行的代碼,這同樣意味著它可以在不需要時由操作係統卸載。它們擴展了操作係 統內核的功能卻不需要重新啟動係統。舉例子來說,其中一種內核模塊時設備驅 動程序模塊,它們用來讓操作係統正確識別,使用安裝在係統上的硬件設備。如 果沒有內核模塊,我們不得不一次又一次重新編譯生成單內核操作係統的內核鏡 像來加入新的功能。這還意味著一個臃腫的內核。
1.2. 內核模塊是如何被調入內核工作的?
你可以通過執行lsmod命令來查看內核已經加載了哪 些內核模塊, 該命令通過讀取/proc/modules文件的內容 來獲得所需信息。
這些內核模塊是如何被調入內核的?當操作係統內核需要的擴展功能不存 在時,內核模塊管理守護進程kmod[1]執行modprobe去加載內核模 塊。兩種類型的參數被傳遞給modprobe:
-
一個內核模塊的名字像softdog或是ppp。
-
通用識別符像char-major-10-30。
當傳遞給modprobe是通用識別符時,modprobe首先在文件 /etc/modules.conf查找該字符串。如果它發現的一行別名像:
alias char-major-10-30 softdog |
它就明白通用識別符是指向內核模塊softdog.o。
然後,modprobe遍曆文件/lib/modules/version/modules.dep 來判斷是否有其它內核模塊需要在該模塊加載前被加載。該文件是由命令depmod -a 建立,保存著內核模塊的依賴關係。舉例來說,msdos.o依賴於模塊fat.o 內核模塊已經被內核載入。當要加載的內核模塊需要使用別的模塊提供的符號鏈接時(多半是變量或函數), 那麼那些提供這些所需符號鏈接的內核模塊就被該模塊所依賴。
最終,modprobe調用insmod先加載被依賴的模塊,然後加載該被內核要求的模塊。modprobe將insmod指向 /lib/modules/version/[2]目錄,該目錄為默認標準存放內核模塊的目錄。insmod對內核模塊存放位置 的處理相當呆板,所以modprobe應該很清楚的知道默認標準的內核模塊存放的位置。所以,當你想要載入一個內 核模塊時,你可以執行:
insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o |
或隻是執行"modprobe -a msdos"。
Linux提供modprobe, insmod and depmod在一個名為modutils 或 mod-utils的工具包內。
在結束本章前,讓我們來看一個 /etc/modules.conf文件:
#This file is automatically generated by update-modules path[misc]=/lib/modules/2.4.?/local keep path[net]=~p/mymodules options mydriver irq=10 alias eth0 eepro |
用'#'起始的行為注釋。空白行被忽略。
以 path[misc]起始的行告訴modprobe用 /lib/modules/2.4.?/local替代搜尋 misc內核模塊的路徑。正如你看到的,命令解釋器shell的元字符也可以使用。
以path[net]起始的行告訴modprobe 在目錄 ~p/mymodules搜索網絡方麵的內核模塊。但是,在path[net] 指令之前使用的"keep" 指令告訴modprobe隻是將該路徑添加到標準搜索路徑中,而不是像對待 misc前麵那樣進行替換。
以alias 起始的的行使modprobe加載eepro.o當kmod 以通用識別符'eth0' 要求加載相應內核模塊時。
你不會發現像"alias block-major-2 floppy"這樣的別名行在文件/etc/modules.conf 因為modprobe已經知道在絕大多數係統上安裝的標準的設備的驅動模塊。
現在你已經知道內核模塊是如何被調入的了。當你想寫你自己的依賴於其它模塊的內核模塊時, 還有一些內容沒有提供。這個相對高級的問題將在以後的章節中介紹,當我們已經完成前麵的學習後。
1.2.1. 在開始前
在我們介紹源代碼前,有一些事需要注意。係統彼此之間的不同會導致許多困難。 順利的編譯並且加載你的第一個"hello world"模塊有時就會比較困難。但是當你跨過 這道坎時,後麵會順利的多。
1.2.1.1. 內核模塊和內核的版本問題
為某個版本編譯的模塊將不能被另一個版本的內核加載如果內核中打開了 CONFIG_MODVERSIONS選項。我們暫時不會討論與此相關的 內容。在我們進入相關內容前,本文檔中的範例可能在該選項打開的情況下無法 工作。但是,目前絕大多數的發行版是將該選項打開的。所以如果你遇到和版本 相關的錯誤時,最好,重新編譯一個關閉該選項的內核。
1.2.1.2. 使用 X帶來的問題
強烈建議你在控製台下輸入文檔中的範例代碼,編譯然後加載模塊,而不是在X下。
模塊不能像printf()
那樣輸出到屏幕,但它們可以 記錄信息和警告,當且僅當你在使用控製台時這些信息才能最終顯示在屏幕上。 如果你從xterm中insmod一個模塊,這些日誌信息隻會記錄在你的日誌文件中。 除了查看日誌文件你將無法 得到輸出信息。想要及時的獲得這些日誌信息,建議 所有的工作都在控製台下進行。
1.2.1.3. 編譯相關和內核版本相關的問題
Linux的發行版經常給內核打一些非標準的補丁,這種情況回導致一些問題的發生。
一個更普遍的問題是一些Linux發行版提供的頭文件不完整。編譯模塊時你將需要非常多 的內核頭文件。墨菲法則之一就是那些缺少的頭文件恰恰是你最需要的。
我強烈建議從Linux鏡像站點下載源代碼包,編譯新內核並用新內核啟動係統來避免以上 的問題。參閱"Linux Kernel HOWTO"獲得詳細內容。
具有諷刺意味的是,這也會導致一些問題。gcc傾向於在缺省的內核源文件路徑(通常是/usr/src/)下尋找源代碼文件。這可以通過gcc的-I 選項來切換。
Chapter 2. Hello World
2.1. Hello, World (part 1): 最簡單的內核模塊
當第一個洞穴程序員在第一台洞穴計算機的牆上上鑿寫第一個程序時, 這是一個在羚羊皮上輸出`Hello, world'的字符串。羅馬的編程書籍上是以 `Salut, Mundi'這樣的程序開始的。 我不明白人們為什麼要破壞這個傳統, 但我認為還是不明白為好。我們將從編寫一係列的`Hello, world'模塊開始, 一步步展示編寫內核模塊的基礎的方方麵麵。
這可能是一個最簡單的模塊了。先別急著編譯它。我們將在下章模塊編譯的章節介紹相關內容。
Example 2-1. hello-1.c
/* * hello-1.c - The simplest kernel module. */ #include <linux/module.h> /* Needed by all modules */ #include <linux/kernel.h> /* Needed for KERN_ALERT */ int init_module(void) { printk(KERN_INFO "Hello world 1.\n"); /* * A non 0 return means init_module failed; module can't be loaded. */ return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye world 1.\n"); } |
一個內核模塊應該至少包含兩個函數。一個“開始”(初始化)的函數被稱為init_module()
還有一個“結束” (幹一些收尾清理的工作)的函數被稱為cleanup_module()
,當內核模塊被rmmod卸載時被執行。實際上,從內核版本2.3.13開始這種情況有些改變。 你可以為你的開始和結束函數起任意的名字。 你將在以後學習如何實現這一點Section
2.3。 實際上,這個新方法時推薦的實現方法。但是,許多人仍然使init_module()
和 cleanup_module()
作為他們的開始和結束函數。
一般,init_module()
要麼向內核注冊它可以處理的事物,要麼用自己的代碼 替代某個內核函數(代碼通常這樣做然後再去調用原先的函數代碼)。函數 cleanup_module()
應該撤消任何init_module()
做的事,從而 內核模塊可以被安全的卸載。
最後,任一個內核模塊需要包含linux/module.h。 我們僅僅需要包含 linux/kernel.h當需要使用 printk()
記錄級別的宏擴展時KERN_ALERT,相關內容將在Section
2.1.1中介紹。
2.1.1. 介紹printk()
不管你可能怎麼想,printk()
並不是設計用來同用戶交互的,雖然我們在 hello-1就是出於這樣的目的使用它!它實際上是為內核提供日誌功能, 記錄內核信息或用來給出警告。因此,每個printk()
聲明都會帶一個優先級,就像你看到的<1>和KERN_ALERT 那樣。內核總共定義了八個優先級的宏,
所以你不必使用晦澀的數字代碼,並且你可以從文件 linux/kernel.h查看這些宏和它們的意義。如果你 不指明優先級,默認的優先級DEFAULT_MESSAGE_LOGLEVEL將被采用。
閱讀一下這些優先級的宏。頭文件同時也描述了每個優先級的意義。在實際中, 使用宏而不要使用數字,就像<4>。總是使用宏,就像 KERN_WARNING。
當優先級低於int console_loglevel,信息將直接打印在你的終端上。如果同時 syslogd和klogd都在運行,信息也同時添加在文件 /var/log/messages,而不管是否顯示在控製台上與否。我們使用像 KERN_ALERT這樣的高優先級,來確保printk()
將信息輸出到
控製台而不是隻是添加到日誌文件中。 當你編寫真正的實用的模塊時,你應該針對可能遇到的情況使用合 適的優先級。
2.2. 編譯內核模塊
內核模塊在用gcc編譯時需要使用特定的參數。另外,一些宏同樣需要定義。 這是因為在編譯成可執行文件和內核模塊時, 內核頭文件起的作用是不同的。 以往的內核版本需要我們去在Makefile中手動設置這些設定。盡管這些Makefile是按目錄分層次 安排的,但是這其中有許多多餘的重複並導致代碼樹大而難以維護。 幸運的是,一種稱為kbuild的新方法被引入,現在外部的可加載內核模塊的編譯的方法已經同內核編譯統一起來。想了解更多的編 譯非內核代碼樹中的模塊(就像我們將要編寫的)請參考幫助文件linux/Documentation/kbuild/modules.txt。
現在讓我們看一個編譯名為hello-1.c的模塊的簡單的Makefile:
Example 2-2. 一個基本內核模塊的Makefile
obj-m += hello-1.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編譯模塊。 你應該得到同下麵類似的屏幕輸出:
hostname:~/lkmpg-examples/02-HelloWorld# make make -C /lib/modules/2.6.11/build M=/root/lkmpg-examples/02-HelloWorld modules make[1]: Entering directory `/usr/src/linux-2.6.11' CC [M] /root/lkmpg-examples/02-HelloWorld/hello-1.o Building modules, stage 2. MODPOST CC /root/lkmpg-examples/02-HelloWorld/hello-1.mod.o LD [M] /root/lkmpg-examples/02-HelloWorld/hello-1.ko make[1]: Leaving directory `/usr/src/linux-2.6.11' hostname:~/lkmpg-examples/02-HelloWorld# |
請注意2.6的內核現在引入一種新的內核模塊命名規範:內核模塊現在使用.ko的文件後綴(代替 以往的.o後綴),這樣內核模塊就可以同常規的目標文件區別開。這樣做的理由是它們包含一個附加的.modinfo段, 那裏存放著關於模塊的附加信息。我們將馬上看到這些信息的好處。
使用modinfo hello-*.ko來看看它是什麼樣的信息。
hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-1.ko filename: hello-1.ko vermagic: 2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3 depends: |
到目前為止,沒什麼驚人的。一旦我們對後麵的一個例子,hello-5.ko,使用modinfo,那將會改變。
hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-5.ko filename: hello-5.ko license: GPL author: Peter Jay Salzman vermagic: 2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3 depends: parm: myintArray:An array of integers (array of int) parm: mystring:A character string (charp) parm: mylong:A long integer (long) parm: myint:An integer (int) parm: myshort:A short integer (short) hostname:~/lkmpg-examples/02-HelloWorld# |
這裏有很多有用的信息去看。報告錯誤的作者信息,許可證信息,甚至對它接受參數的簡短描述。
更詳細的文檔請參考 linux/Documentation/kbuild/makefiles.txt。在研究Makefile之前請確認你已經參考了這些文檔。它很可能會節省你很多工作。
現在是使用insmod ./hello-1.ko命令加載該模塊的時候了(忽略任何你看到的關於內核汙染的輸出 顯示,我們將在以後介紹相關內容)。
所有已經被加載的內核模塊都羅列在文件/proc/modules中。cat一下這個文件看一下你的模塊是否真的 成為內核的一部分了。如果是,祝賀你!你現在已經是內核模塊的作者了。當你的新鮮勁過去後,使用命令 rmmod hello-1.卸載模塊。再看一下/var/log/messages文件的內容是否有相關的日誌內容。
這兒是另一個練習。看到了在聲明 init_module()
上的注釋嗎? 改變返回值非零,重新編譯再加載,發生了什麼?
2.3. Hello World (part 2)
在內核Linux 2.4中,你可以為你的模塊的“開始”和“結束”函數起任意的名字。它們不再必須使用 init_module()
和cleanup_module()
的名字。這可以通過宏 module_init()
和module_exit()
實現。這些宏在頭文件linux/init.h定義。唯一需要注意的地方是函數必須在宏的使用前定義,否則會有編譯
錯誤。下麵就是一個例子。
Example 2-3. hello-2.c
/* * hello-2.c - Demonstrating the module_init() and module_exit() macros. * This is preferred over using init_module() and cleanup_module(). */ #include <linux/module.h> /* Needed by all modules */ #include <linux/kernel.h> /* Needed for KERN_ALERT */ #include <linux/init.h> /* Needed for the macros */ static int __init hello_2_init(void) { printk(KERN_INFO "Hello, world 2\n"); return 0; } static void __exit hello_2_exit(void) { printk(KERN_INFO "Goodbye, world 2\n"); } module_init(hello_2_init); module_exit(hello_2_exit); |
現在我們已經寫過兩個真正的模塊了。添加編譯另一個模塊的選項十分簡單,如下:
Example 2-4. 兩個內核模塊使用的Makefile
obj-m += hello-1.o obj-m += hello-2.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 |
現在讓我們來研究一下linux/drivers/char/Makefile這個實際中的例子。就如同你看到的, 一些被編譯進內核 (obj-y),但是這些obj-m哪裏去了呢?對於熟悉shell腳本的人這不難理解。這些在Makefile中隨處可見 的obj-$(CONFIG_FOO)的指令將會在CONFIG_FOO被設置後擴展為你熟悉的obj-y或obj-m。這其實就是你在使用 make menuconfig編譯內核時生成的linux/.config中設置的東西。
2.4. Hello World (part 3): 關於__init和__exit宏
這裏展示了內核2.2以後引入的一個新特性。注意在負責“初始化”和“清理收尾”的函數定義處的變化。宏 __init
的使用會在初始化完成後丟棄該函數並收回所占內存,如果該模塊被編譯進內核,而不是動態加載。
也有一個宏__initdata
同__init
類似,隻不過對變量有效。
宏__exit
將忽略“清理收尾”的函數如果該模塊被編譯進內核。同宏 __exit
一樣,對動態加載模塊是無效的。這很容易理解。編譯進內核的模塊 是沒有清理收尾工作的, 而動態加載的卻需要自己完成這些工作。
這些宏在頭文件linux/init.h定義,用來釋放內核占用的內存。 當你在啟動時看到這樣的Freeing unused kernel memory: 236k freed內核輸出,上麵的 那些正是內核所釋放的。
Example 2-5. hello-3.c
/* * hello-3.c - Illustrating the __init, __initdata and __exit macros. */ #include <linux/module.h> /* Needed by all modules */ #include <linux/kernel.h> /* Needed for KERN_ALERT */ #include <linux/init.h> /* Needed for the macros */ static int hello3_data __initdata = 3; static int __init hello_3_init(void) { printk(KERN_INFO "Hello, world %d\n", hello3_data); return 0; } static void __exit hello_3_exit(void) { printk(KERN_INFO "Goodbye, world 3\n"); } module_init(hello_3_init); module_exit(hello_3_exit); |
2.5. Hello World (part 4): 內核模塊許可證和內核模塊文檔說明
如果你在使用2.4或更新的內核,當你加載你的模塊時,你也許注意到了這些輸出信息:
# insmod xxxxxx.o Warning: loading xxxxxx.o will taint the kernel: no license See https://www.tux.org/lkml/#export-tainted for information about tainted modules Hello, world 3 Module xxxxxx loaded, with warnings |
在2.4或更新的內核中,一種識別代碼是否在GPL許可下發布的機製被引入, 因此人們可以在使用非公開的源代碼產品時得到警告。這通過在下一章展示的宏 MODULE_LICENSE()
當你設置在GPL證書下發布你的代碼時, 你可以取消這些警告。這種證書機製在頭文件linux/module.h 實現,同時還有一些相關文檔信息。
/* * The following license idents are currently accepted as indicating free * software modules * * "GPL" [GNU Public License v2 or later] * "GPL v2" [GNU Public License v2] * "GPL and additional rights" [GNU Public License v2 rights and more] * "Dual BSD/GPL" [GNU Public License v2 * or BSD license choice] * "Dual MPL/GPL" [GNU Public License v2 * or Mozilla license choice] * * The following other idents are available * * "Proprietary" [Non free products] * * There are dual licensed components, but when running with Linux it is the * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL * is a GPL combined work. * * This exists for several reasons * 1. So modinfo can show license info for users wanting to vet their setup * is free * 2. So the community can ignore bug reports including proprietary modules * 3. So vendors can do likewise based on their own policies */ |
類似的,宏MODULE_DESCRIPTION()
用來描述模塊的用途。 宏MODULE_AUTHOR()
用來聲明模塊的作者。宏MODULE_SUPPORTED_DEVICE()
聲明模塊支持的設備。
這些宏都在頭文件linux/module.h定義, 並且內核本身並不使用這些宏。它們隻是用來提供識別信息,可用工具程序像objdump查看。 作為一個練習,使用grep從目錄linux/drivers看一看這些模塊的作者是如何 為他們的模塊提供識別信息和檔案的。
我推薦在/usr/src/linux-2.6.x/目錄下使用類似grep -inr MODULE_AUTHOR *的命令。不熟悉命令行工具的人可能喜歡網上那樣的方法, 搜索提供LXR做索引的內核源代碼樹的網站(或在自己的本地機器上安裝它)。
使用像emacs或vi那樣傳統的Unix編輯器的用戶將會發現tag文件很有用。它們能夠在/usr/src/linux-2.6.x/ 下用make tags或make TAGS生成。 一旦你在內核目錄樹中得到了這種tag文件,你就能把鼠標放到某個函數調用上使用一些組合鍵直接跳 到函數的定義處。
Example 2-6. hello-4.c
/* * hello-4.c - Demonstrates module documentation. */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #define DRIVER_AUTHOR "Peter Jay Salzman <p@dirac.org>" #define DRIVER_DESC "A sample driver" static int __init init_hello_4(void) { printk(KERN_INFO "Hello, world 4\n"); return 0; } static void __exit cleanup_hello_4(void) { printk(KERN_INFO "Goodbye, world 4\n"); } module_init(init_hello_4); module_exit(cleanup_hello_4); /* * You can use strings, like this: */ /* * Get rid of taint message by declaring code as GPL. */ MODULE_LICENSE("GPL"); /* * Or with defines, like this: */ MODULE_AUTHOR(DRIVER_AUTHOR); /* Who wrote this module? */ MODULE_DESCRIPTION(DRIVER_DESC); /* What does this module do */ /* * This module uses /dev/testdevice. The MODULE_SUPPORTED_DEVICE macro might * be used in the future to help automatic configuration of modules, but is * currently unused other than for documentation purposes. */ MODULE_SUPPORTED_DEVICE("testdevice"); |
2.6. 從命令行傳遞參數給內核模塊
模塊也可以從命令行獲取參數。但不是通過以前你習慣的argc/argv。
要傳遞參數給模塊,首先將獲取參數值的變量聲明為全局變量。然後使用宏MODULE_PARM()
(在頭文件linux/module.h)。運行時,insmod將給變量賦予命令行的參數,如同 ./insmod mymodule.ko myvariable=5。為使代碼清晰,變量的聲明和宏都應該放在
模塊代碼的開始部分。以下的代碼範例也許將比我公認差勁的解說更好。
宏module_param()
需要三個參數,變量的名字,其類型和在sysfs中關聯文件的權限。 整數型既可為通常的signed也可為unsigned。 如果你想使用整數數組或者字符串,請看module_param_array()和module_param_string()。
int myint = 3; module_param(myint, int, 0); |
數組同樣被支持。但是情況和2.4時代有點不一樣了。為了追蹤參數的個數,你需要傳遞一個指向數目變量的指針作為第三個參數。 在你自己,你也可以忽略數目並傳遞NULL。我們把兩種可能性都列出來:
int myintarray[2]; module_param_array(myintarray, int, NULL, 0); /* not interested in count */ int myshortarray[4]; int count; module_parm_array(myshortarray, short, & count, 0); /* put count into "count" variable */ |
將初始值設為缺省使用的IO端口或IO尋址是一個不錯的作法。如果這些變量有缺省值,則可以進行自動設備檢測, 否則保持當前設置的值。我們將在後續章節解釋清楚相關內容。在這裏我隻是演示如何向一個模塊傳遞參數。
最後,還有這樣一個宏,MODULE_PARM_DESC()
被用來注解該模塊可以接收的參數。該宏 兩個參數:變量名和一個格式自由的對該變量的描述。
Example 2-7. hello-5.c
/* * hello-5.c - Demonstrates command line argument passing to a module. */ #include <linux/module.h> #include <linux/moduleparam.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/stat.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Peter Jay Salzman"); static short int myshort = 1; static int myint = 420; static long int mylong = 9999; static char *mystring = "blah"; static int myintArray[2] = { -1, -1 }; static int arr_argc = 0; /* * module_param(foo, int, 0000) * The first param is the parameters name * The second param is it's data type * The final argument is the permissions bits, * for exposing parameters in sysfs (if non-zero) at a later stage. */ module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); MODULE_PARM_DESC(myshort, "A short integer"); module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); MODULE_PARM_DESC(myint, "An integer"); module_param(mylong, long, S_IRUSR); MODULE_PARM_DESC(mylong, "A long integer"); module_param(mystring, charp, 0000); MODULE_PARM_DESC(mystring, "A character string"); /* * module_param_array(name, type, num, perm); * The first param is the parameter's (in this case the array's) name * The second param is the data type of the elements of the array * The third argument is a pointer to the variable that will store the number * of elements of the array initialized by the user at module loading time * The fourth argument is the permission bits */ module_param_array(myintArray, int, &arr_argc, 0000); MODULE_PARM_DESC(myintArray, "An array of integers"); static int __init hello_5_init(void) { int i; printk(KERN_INFO "Hello, world 5\n=============\n"); printk(KERN_INFO "myshort is a short integer: %hd\n", myshort); printk(KERN_INFO "myint is an integer: %d\n", myint); printk(KERN_INFO "mylong is a long integer: %ld\n", mylong); printk(KERN_INFO "mystring is a string: %s\n", mystring); for (i = 0; i < (sizeof myintArray / sizeof (int)); i++) { printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]); } printk(KERN_INFO "got %d arguments for myintArray.\n", arr_argc); return 0; } static void __exit hello_5_exit(void) { printk(KERN_INFO "Goodbye, world 5\n"); } module_init(hello_5_init); module_exit(hello_5_exit); |
我建議用下麵的方法實驗你的模塊:
satan# insmod hello-5.ko mystring="bebop" mybyte=255 myintArray=-1 mybyte is an 8 bit integer: 255 myshort is a short integer: 1 myint is an integer: 20 mylong is a long integer: 9999 mystring is a string: bebop myintArray is -1 and 420 satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mystring="supercalifragilisticexpialidocious" \ > mybyte=256 myintArray=-1,-1 mybyte is an 8 bit integer: 0 myshort is a short integer: 1 myint is an integer: 20 mylong is a long integer: 9999 mystring is a string: supercalifragilisticexpialidocious myintArray is -1 and -1 satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mylong=hello hello-5.o: invalid argument syntax for mylong: 'h' |
2.7. 由多個文件構成的內核模塊
有時將模塊的源代碼分為幾個文件是一個明智的選擇。
這裏是這樣的一個模塊範例。
Example 2-8. start.c
/* * start.c - Illustration of multi filed modules */ #include <linux/kernel.h> /* We're doing kernel work */ #include <linux/module.h> /* Specifically, a module */ int init_module(void) { printk(KERN_INFO "Hello, world - this is the kernel speaking\n"); return 0; } |
另一個文件:
Example 2-9. stop.c
/* * stop.c - Illustration of multi filed modules */ #include <linux/kernel.h> /* We're doing kernel work */ #include <linux/module.h> /* Specifically, a module */ void cleanup_module() { printk(KERN_INFO "Short is the life of a kernel module\n"); } |
最後是該模塊的Makefile:
Example 2-10. Makefile
obj-m += hello-1.o obj-m += hello-2.o obj-m += hello-3.o obj-m += hello-4.o obj-m += hello-5.o obj-m += startstop.o startstop-objs := start.o stop.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 |
這是目前為止所有例子的完整的Makefile。前五行沒有什麼特別之處,但是最後一個例子需要兩行。 首先,我們為聯合的目標文件構造一個名字,其次,我們告訴make什麼目標文件是模塊的一部分。
2.8. 為已編譯的內核編譯模塊
很顯然,我們強烈推薦你編譯一個新的內核,這樣你就可以打開內核中一些有用的排錯功能,像強製卸載模塊(MODULE_FORCE_UNLOAD): 當該選項被打開時,你可以rmmod -f module強製內核卸載一個模塊,即使內核認為這是不安全的。該選項可以為你節省不少開發時間。
但是,你仍然有許多使用一個正在運行中的已編譯的內核的理由。例如,你沒有編譯和安裝新內核的權限,或者你不希望重啟你的機器來運行新內核。 如果你可以毫無阻礙的編譯和使用一個新的內核,你可以跳過剩下的內容,權當是一個腳注。
如果你僅僅是安裝了一個新的內核代碼樹並用它來編譯你的模塊,當你加載你的模塊時,你很可能會得到下麵的錯誤提示:
insmod: error inserting 'poet_atkm.ko': -1 Invalid module format |
一些不那麼神秘的信息被紀錄在文件/var/log/messages中;
Jun 4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686 REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3' |
換句話說,內核拒絕加載你的模塊因為記載版本號的字符串不符(更確切的說是版本印戳)。版本印戳作為一個靜態的字符串存在於內核模塊中,以 vermagic:。 版本信息是在連接階段從文件init/vermagic.o中獲得的。 查看版本印戳和其它在模塊中的一些字符信息,可以使用下麵的命令 modinfo module.ko:
[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko license: GPL author: Peter Jay Salzman <p@dirac.org> description: A sample driver vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3 depends: |
我們可以借助選項--force-vermagic解決該問題,但這種方法有潛在的危險,所以在成熟的模塊中也是不可接受的。 解決方法是我們構建一個同我們預先編譯好的內核完全相同的編譯環境。如何具體實現將是該章後麵的內容。
首先,準備同你目前的內核版本完全一致的內核代碼樹。然後,找到你的當前內核的編譯配置文件。通常它可以在路徑 /boot下找到,使用像config-2.6.x的文件名。你可以直接將它拷貝到內核代碼樹的路徑下: cp /boot/config-`uname -r` /usr/src/linux-`uname -r`/.config。
讓我們再次注意一下先前的錯誤信息:仔細看的話你會發現,即使使用完全相同的配置文件,版本印戳還是有細小的差異的,但這足以導致 模塊加載的失敗。這其中的差異就是在模塊中出現卻不在內核中出現的custom字符串,是由某些發行版提供的修改過的 makefile導致的。檢查/usr/src/linux/Makefile,確保下麵這些特定的版本信息同你使用的內核完全一致:
VERSION = 2 PATCHLEVEL = 6 SUBLEVEL = 5 EXTRAVERSION = -1.358custom ... |
像上麵的情況你就需要將EXTRAVERSION一項改為-1.358。我們的建議是將原始的makefile備份在 /lib/modules/2.6.5-1.358/build下。 一個簡單的命令cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname -r`即可。 另外,如果你已經在運行一個由上麵的錯誤的Makefile編譯的內核,你應該重新執行 make,或直接對應/lib/modules/2.6.x/build/include/linux/version.h從文件 /usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆蓋後者的。
現在,請執行make來更新設置和版本相關的頭文件,目標文件:
[root@pcsenonsrv linux-2.6.x]# make CHK include/linux/version.h UPD include/linux/version.h SYMLINK include/asm -> include/asm-i386 SPLIT include/linux/autoconf.h -> include/config/* HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/split-include HOSTCC scripts/basic/docproc HOSTCC scripts/conmakehash HOSTCC scripts/kallsyms CC scripts/empty.o ... |
如果你不是確實想編譯一個內核,你可以在SPLIT後通過按下CTRL-C中止編譯過程。因為此時你需要的文件 已經就緒了。現在你可以返回你的模塊目錄然後編譯加載它:此時模塊將完全針對你的當前內核編譯,加載時也不會由任何錯誤提示。
Chapter 3. Preliminaries
3.1. 內核模塊對比用戶程序
3.1.1. 內核模塊是如何開始和結束的
用戶程序通常從函數main()
開始,執行一係列的指令並且 當指令執行完成後結束程序。內核模塊有一點不同。內核模塊要麼從函數init_module
或是你用宏module_init
指定的函數調用開始。這就是內核模塊 的入口函數。它告訴內核模塊提供那些功能擴展並且讓內核準備好在需要時調用它。 當它完成這些後,該函數就執行結束了。模塊在被內核調用前也什麼都不做。
所有的模塊或是調用cleanup_module
或是你用宏 module_exit
指定的函數。這是模塊的退出函數。它撤消入口函數所做的一切。 例如注銷入口函數所注冊的功能。
所有的模塊都必須有入口函數和退出函數。既然我們有不隻一種方法去定義這兩個 函數,我將努力使用“入口函數”和“退出函數”來描述 它們。但是當我隻用init_module
和cleanup_module
時,我希望你明白我指的是什麼。
3.1.2. 模塊可調用的函數
程序員並不總是自己寫所有用到的函數。一個常見的基本的例子就是 printf()
你使用這些C標準庫,libc提供的庫函數。 這些函數(像printf()
) 實際上在連接之前並不進入你的程序。 在連接時這些函數調用才會指向 你調用的庫,從而使你的代碼最終可以執行。
內核模塊有所不同。在hello world模塊中你也許已經注意到了我們使用的函數 printk()
卻沒有包含標準I/O庫。這是因為模塊是在insmod加 載時才連接的目標文件。那些要用到的函數的符號鏈接是內核自己提供的。 也就是說, 你可以在內核模塊中使用的函數隻能來自內核本身。如果你對內核提供了哪些函數符號 鏈接感興趣,看一看文件/proc/kallsyms。
需要注意的一點是庫函數和係統調用的區別。庫函數是高層的,完全運行在用戶空間, 為程序員提供調用真正的在幕後 完成實際事務的係統調用的更方便的接口。係統調用在內核 態運行並且由內核自己提供。標準C庫函數printf()
可以被看做是一 個通用的輸出語句,但它實際做的是將數據轉化為符合格式的字符串並且調用係統調用 write()
輸出這些字符串。
是否想看一看printf()
究竟使用了哪些係統調用? 這很容易,編譯下麵的代碼。
#include <stdio.h> int main(void) { printf("hello"); return 0; } |
使用命令gcc -Wall -o hello hello.c編譯。用命令 strace hello行該可執行文件。是否很驚訝? 每一行都和一個係統調用相對應。 strace[3] 是一個非常有用的程序,它可以告訴你程序使用了哪些係統調用和這些係統調用的參數,返回值。
這是一個極有價值的查看程序在幹什麼的工具。在輸出的末尾,你應該看到這樣類似的一行 write(1, "hello", 5hello)
。這就是我們要找的。藏在麵具printf()
的真實麵目。既然絕大多數人使用庫函數來對文件I/O進行操作(像 fopen, fputs, fclose)。 你可以查看man說明的第二部分使用命令man
2 write. 。man說明的第二部分 專門介紹係統調用(像kill()
和read()
)。 man說明的第三部分則專門介紹你可能更熟悉的庫函數, (像cosh()
和random()
)。
你甚至可以編寫代碼去覆蓋係統調用,正如我們不久要做的。駭客常這樣做來為係統安裝後門或木馬。 但你可以用它來完成一些更有益的事,像讓內核在每次某人刪除文件時輸出 “ Tee hee, that tickles!” 的信息。
3.1.3. 用戶空間和內核空間
內核全權負責對硬件資源的訪問,不管被訪問的是顯示卡,硬盤,還是內存。 用戶程序常為這些資源競爭。就如同我在保存這 份文檔同時本地數據庫正在更新。 我的編輯器vim進程和數據庫更新進程同時要求訪問硬盤。內核必須使這些請求有條不紊的進行, 而不是隨用戶的意願提供計算機資源。 為方便實現這種機製, CPU 可以在不同的狀態運行。不同的狀態賦予不同的你對係統操作的自由。Intel 80836 架構有四種狀態。 Unix隻使用了其中 的兩種,最高級的狀態(操作狀態0,即“超級狀態”,可以執行任何操作)和最低級的狀態 (即“用戶狀態”)。
回憶以下我們對庫函數和係統調用的討論,一般庫函數在用戶態執行。 庫函數調用一個或幾個係統調用,而這些係統調用為庫函數完成工作,但是在超級狀態。 一旦係統調用完成工作後係統調用就返回同時程序也返回用戶態。
3.1.4. 命名空間
如果你隻是寫一些短小的C程序,你可為你的變量起一個方便的和易於理解的變量名。 但是,如果你寫的代碼隻是 許多其它人寫的代碼的一部分,你的全局一些就會與其中的全局變量發生衝突。 另一個情況是一個程序中有太多的 難以理解的變量名,這又會導致變量命名空間汙染 在大型項目中,必須努力記住保留的變量名,或為獨一無二的命名使用一種統一的方法。
當編寫內核代碼時,即使是最小的模塊也會同整個內核連接,所以這的確是個令人頭痛的問題。 最好的解決方法是聲明你的變量為static靜態的並且為你的符號使用一個定義的很好的前綴。 傳統中,使用小寫字母的內核前綴。如果你不想將所有的東西都聲明為static靜態的, 另一個選擇是聲明一個symbol table(符號表)並向內核注冊。我們將在以後討論。
文件/proc/kallsyms保存著內核知道的所有的符號,你可以訪問它們, 因為它們是內核代碼空間的一部分。
3.1.5. 代碼空間
內存管理是一個非常複雜的課題。O'Reilly的《Understanding The Linux Kernel》絕大部分都在 討論內存管理!我們並不準備專注於內存管理,但為了編寫真正的模塊有一些東西還是得知道的。
如果你沒有認真考慮過內存設計缺陷意味著什麼,你也許會驚訝的獲知一個指針並不指向一個確切 的內存區域。當一個進程建立時,內核為它分配一部分確切的實際內存空間並把它交給進程,被進程的 代碼,變量,堆棧和其它一些計算機學的專家才明白的東西使用[4]。這些內存從$0$ 開始並可以擴展到需要的地方。這些 內存空間並不重疊,所以即使進程訪問同一個內存地址,例如0xbffff978, 真實的物理內存地址其實是不同的。進程實際指向的是一塊被分配的內存中以0xbffff978 為偏移量的一塊內存區域。絕大多數情況下,一個進程像普通的"Hello, World"不可以訪問別的進程的 內存空間,盡管有實現這種機製的方法。 我們將在以後討論。
內核自己也有內存空間。既然一個內核模塊可以動態的從內核中加載和卸載,它其實是共享內核的 內存空間而不是自己擁有 獨立的內存空間。因此,一旦你的模塊具有內存設計缺陷,內核就是內存設計缺陷了。 如果你在錯誤的覆蓋數據,那麼你就在 破壞內核的代碼。這比現在聽起來的還糟。所以盡量小心謹慎。
順便提一下,以上我所指出的對於任何單整體內核的操作係統都是真實的[5]。 也存在模塊化微內核的操作係統,如 GNU Hurd 和 QNX Neutrino。
3.1.6. 設備驅動
一種內核模塊是設備驅動程序,為使用硬件設備像電視卡和串口而編寫。 在Unix中,任何設備都被當作路徑/dev 的設備文件處理,並通過這些設備文件提供訪問硬件的方法。 設備驅動為用戶程序 訪問硬件設備。舉例來說,聲卡設備驅動程序es1370.o將會把設備文件 /dev/sound同聲卡硬件Ensoniq IS1370聯係起來。 這樣用戶程序像 mp3blaster 就可以通過訪問設備文件/dev/sound 運行而不必知道那種聲卡硬件安裝在係統上。
3.1.6.1. Major and Minor Numbers
讓我們來研究幾個設備文件。這裏的幾個設備文件代表著一塊主IDE硬盤上的頭三個分區:
# ls -l /dev/hda[1-3] brw-rw---- 1 root disk 3, 1 Jul 5 2000 /dev/hda1 brw-rw---- 1 root disk 3, 2 Jul 5 2000 /dev/hda2 brw-rw---- 1 root disk 3, 3 Jul 5 2000 /dev/hda3 |
注意一下被逗號隔開的兩列。第一個數字被叫做主設備號,第二個被叫做從設備號。 主設備號決定使用何種設備驅動程序。 每種不同的設備都被分配了不同的主設備號; 所有具有相同主設備號的設備文件都是被同一個驅動程序控製。上麵例子中的 主設備號都為3, 表示它們都被同一個驅動程序控製。
從設備號用來區別驅動程序控製的多個設備。上麵例子中的從設備號不相同是因為它們被識別為幾個設備。
設備被大概的分為兩類:字符設備和塊設備。區別是塊設備有緩衝區,所以它們可以對請求進行優化排序。 這對存儲設備尤其 重要,因為讀寫相鄰的文件總比讀寫相隔很遠的文件要快。另一個區別是塊設備輸入和輸出 都是以數據塊為單位的,但是字符設備 就可以自由讀寫任意量的字節。大部分硬件設備為字符設備,因為它們 不需要緩衝區和數據不是按塊來傳輸的。你可以通過命令ls -l輸出的頭一個字母識別一個 設備為何種設備。如果是'b' 就是塊設備,如果是'c'就是字符設備。以上你看到的是塊設備。這兒還有一些 字符設備文件(串口):
crw-rw---- 1 root dial 4, 64 Feb 18 23:34 /dev/ttyS0 crw-r----- 1 root dial 4, 65 Nov 17 10:26 /dev/ttyS1 crw-rw---- 1 root dial 4, 66 Jul 5 2000 /dev/ttyS2 crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3 |
如果你想看一下已分配的主設備號都是些什麼設備可以看一下文件 /usr/src/linux/Documentation/devices.txt。
係統安裝時,所有的這些設備文件都是由命令mknod建立的。去建立一個新的名叫 coffee',主設備號為12和從設備號為2的設備文件,隻要簡單的 執行命令mknod /dev/coffee c 12
2 最後更新:2017-04-03 18:51:44 上一篇:
史蒂夫·喬布斯的打字技術很爛
下一篇:
linux內核空間與用戶空間信息交互方法