## 2.3.?內核模塊相比于應用程序
在我們深入之前, 有必要強調一下內核模塊和應用程序之間的各種不同.
不同于大部分的小的和中型的應用程序從頭至尾處理一個單個任務, 每個內核模塊只注冊自己以便來服務將來的請求, 并且它的初始化函數立刻終止. 換句話說, 模塊初始化函數的任務是為以后調用模塊的函數做準備; 好像是模塊說, " 我在這里, 這是我能做的."模塊的退出函數( 例子里是 hello_exit )就在模塊被卸載時調用. 它好像告訴內核, "我不再在那里了, 不要要求我做任何事了."這種編程的方法類似于事件驅動的編程, 但是雖然不是所有的應用程序都是事件驅動的, 每個內核模塊都是. 另外一個主要的不同, 在事件驅動的應用程序和內核代碼之間, 是退出函數: 一個終止的應用程序可以在釋放資源方面懶惰, 或者完全不做清理工作, 但是模塊的退出函數必須小心恢復每個由初始化函數建立的東西, 否則會保留一些東西直到系統重啟.
偶然地, 卸載模塊的能力是你將最欣賞的模塊化的其中一個特色, 因為它有助于減少開發時間; 你可測試你的新驅動的連續的版本, 而不用每次經歷漫長的關機/重啟周期.
作為一個程序員, 你知道一個應用程序可以調用它沒有定義的函數: 連接階段使用合適的函數庫解決了外部引用. printf 是一個這種可調用的函數并且在 libc 里面定義. 一個模塊, 在另一方面, 只連接到內核, 它能夠調用的唯一的函數是內核輸出的那些; 沒有庫來連接.在 hello.c 中使用的 printk 函數, 例如, 是在內核中定義的 printf 版本并且輸出給模塊. 它表現類似于原始的函數, 只有幾個小的不同, 首要的一個是缺乏浮點的支持.
圖 [連接一個模塊到內核](# "圖?2.1.?連接一個模塊到內核") 展示了函數調用和函數指針在模塊中如何使用來增加新功能到一個運行中的內核.
**圖?2.1.?連接一個模塊到內核**

因為沒有庫連接到模塊中, 源文件不應當包含通常的頭文件, <stdarg.h>和非常特殊的情況是僅有的例外. 只有實際上是內核的一部分的函數才可以在內核模塊里使用. 內核相關的任何東西都在頭文件里聲明, 這些頭文件在你已建立和配置的內核源碼樹里; 大部分相關的頭文件位于 include/linux 和 include/asm, 但是別的 include 的子目錄已經添加到關聯特定內核子系統的材料里了.
單個內核頭文件的作用在書中需要它們的時候進行介紹.
另外一個在內核編程和應用程序編程之間的重要不同是每一個環境是如何處理錯誤: 在應用程序開發中段錯誤是無害的, 一個調試器常常用來追蹤錯誤到源碼中的問題, 而一個內核錯誤至少會殺掉當前進程, 如果不終止整個系統. 我們會在第 4 章看到如何跟蹤內核錯誤.
### 2.3.1.?用戶空間和內核空間
A module runs in kernel space, whereas applications run in user space. This concept is at the base of operating systems theory. 一個模塊在內核空間運行, 而應用程序在用戶空間運行. 這個概念是操作系統理論的基礎.
操作系統的角色, 實際上, 是給程序提供一個一致的計算機硬件的視角. 另外, 操作系統必須承擔程序的獨立操作和保護對于非授權的資源存取. 這一不平凡的任務只有 CPU 增強系統軟件對應用程序的保護才有可能.
每種現代處理器都能夠加強這種行為. 選中的方法是 CPU 自己實現不同的操作形態(或者級別). 這些級別有不同的角色, 一些操作在低些級別中不允許; 程序代碼只能通過有限的幾個門從一種級別切換到另一個. Unix 系統設計成利用了這種硬件特性, 使用了兩個這樣的級別. 所有當今的處理器至少有兩個保護級別, 并且某些, 例如 x86 家族, 有更多級別; 當幾個級別存在時, 使用最高和最低級別. 在 Unix 下, 內核在最高級運行( 也稱之為超級模式 ), 這里任何事情都允許, 而應用程序在最低級運行(所謂的用戶模式), 這里處理器控制了對硬件的直接存取以及對內存的非法存取.
我們常常提到運行模式作為內核空間和用戶空間. 這些術語不僅包含存在于這兩個模式中不同特權級別, 還包含有這樣的事實, 即每個模式有它自己的內存映射 -- 它自己的地址空間.
Unix 從用戶空間轉換執行到內核空間, 無論何時一個應用程序發出一個系統調用或者被硬件中斷掛起時. 執行系統調用的內核代碼在進程的上下文中工作 -- 它代表調用進程并且可以存取該進程的地址空間. 換句話說, 處理中斷的代碼對進程來說是異步的, 不和任何特別的進程有關.
模塊的角色是擴展內核的功能; 模塊化的代碼在內核空間運行. 經常地一個驅動進行之前提到的兩種任務: 模塊中一些的函數作為系統調用的一部分執行, 一些負責中斷處理.
### 2.3.2.?內核的并發
內核編程與傳統應用程序編程方式很大不同的是并發問題. 大部分應用程序, 多線程的應用程序是一個明顯的例外, 典型地是順序運行的, 從頭至尾, 不必要擔心其他事情會發生而改變它們的環境. 內核代碼沒有運行在這樣的簡單世界中, 即便最簡單的內核模塊必須在這樣的概念下編寫, 很多事情可能馬上發生.
內核編程中有幾個并發的來源. 自然的, Linux 系統運行多個進程, 在同一時間, 不止一個進程能夠試圖使用你的驅動. 大部分設備能夠中斷處理器; 中斷處理異步運行, 并且可能在你的驅動試圖做其他事情的同一時間被調用. 幾個軟件抽象( 例如內核定時器, 第 7 章介紹 )也異步運行. 而且, 當然, Linux 可以在對稱多處理器系統( SMP )上運行, 結果是你的驅動可能在多個 CPU 上并發執行. 最后, 在 2.6, 內核代碼已經是可搶占的了; 這個變化使得即便是單處理器會有許多與多處理器系統同樣的并發問題.
結果, Linux 內核代碼, 包括驅動代碼, 必須是可重入的 -- 它必須能夠同時在多個上下文中運行. 數據結構必須小心設計以保持多個執行線程分開, 并且代碼必須小心存取共享數據, 避免數據的破壞. 編寫處理并發和避免競爭情況( 一個不幸的執行順序導致不希望的行為的情形 )的代碼需要仔細考慮并可能是微妙的. 正確的并發管理在編寫正確的內核代碼時是必須的; 由于這個理由, 本書的每一個例子驅動都是考慮了并發下編寫的. 用到的技術在我們遇到它們時再講解; 第 5 章也專門講述這個問題, 以及并發管理的可用的內核原語.
驅動程序員的一個通常的錯誤是假定并發不是一個問題, 只要一段特別的代碼沒有進入睡眠( 或者 "阻塞" ). 即便在之前的內核( 不可搶占), 這種假設在多處理器系統中也不成立. 在 2.6, 內核代碼不能(極少)假定它能在一段給定代碼上持有處理器. 如果你不考慮并發來編寫你的代碼, 就極有可能導致嚴重失效, 以至于非常難于調試.
### 2.3.3.?當前進程
盡管內核模塊不象應用程序一樣順序執行, 內核做的大部分動作是代表一個特定進程的. 內核代碼可以引用當前進程, 通過存取全局項 current, 它在 <asm/current.h> 中定義, 它產生一個指針指向結構 task_struct, 在 <linux/sched.h> 定義. current 指針指向當前在運行的進程. 在一個系統調用執行期間, 例如 open 或者 read, 當前進程是發出調用的進程. 內核代碼可以通過使用 current 來使用進程特定的信息, 如果它需要這樣. 這種技術的一個例子在第 6 章展示.
實際上, current 不真正地是一個全局變量. 支持 SMP 系統的需要強迫內核開發者去開發一種機制, 在相關的 CPU 上來找到當前進程. 這種機制也必須快速, 因為對 current 的引用非常頻繁地發生. 結果就是一個依賴體系的機制, 常常, 隱藏了一個指向 task_struct 的指針在內核堆棧內. 實現的細節對別的內核子系統保持隱藏, 一個設備驅動可以只包含 <linux/sched.h> 并且引用當前進程. 例如, 下面的語句打印了當前進程的進程 ID 和命令名稱, 通過存取結構 task_struct 中的某些字段.
~~~
printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);
~~~
存于 current->comm 的命令名稱是由當前進程執行的程序文件的基本名稱( 截短到 15 個字符, 如果需要 ).
### 2.3.4.?幾個別的細節
內核編程與用戶空間編程在許多方面不同. 我們將在本書的過程中指出它們, 但是有幾個基礎性的問題, 盡管沒有保證它們自己有一節內容, 也值得一提. 因此, 當你深入內核時, 下面的事項應當牢記.
應用程序存在于虛擬內存中, 有一個非常大的堆棧區. 堆棧, 當然, 是用來保存函數調用歷史以及所有的由當前活躍的函數創建的自動變量. 內核, 相反, 有一個非常小的堆棧; 它可能小到一個, 4096 字節的頁. 你的函數必須與這個內核空間調用鏈共享這個堆棧. 因此, 聲明一個巨大的自動變量從來就不是一個好主意; 如果你需要大的結構, 你應當在調用時間內動態分配.
常常, 當你查看內核 API 時, 你會遇到以雙下劃線(__)開始的函數名. 這樣標志的函數名通常是一個低層的接口組件, 應當小心使用. 本質上講, 雙下劃線告訴程序員:" 如果你調用這個函數, 確信你知道你在做什么."
內核代碼不能做浮點算術. 使能浮點將要求內核在每次進出內核空間的時候保存和恢復浮點處理器的狀態 -- 至少, 在某些體系上. 在這種情況下, 內核代碼真的沒有必要包含浮點, 額外的負擔不值得.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考