## 15.3.?進行直接 I/O
大部分 I/O 操作是通過內核緩沖的. 一個內核空間緩沖區的使用允許一定程度的用戶空間和實際設備的分離; 這種分離能夠使編程容易并且還可以在許多情況下有性能的好處. 但是, 有這樣的情況它對于進行 I/O 直接到或從一個用戶空間緩沖區是有好處的. 如果正被傳輸的數據量大, 不使用一個額外的拷貝直接通過內核空間傳輸數據可以加快事情進展.
2.6 內核中一個直接 I/O 使用的例子是 SCSI 磁帶驅動. 流動的磁帶能夠傳送大量數據通過系統, 并且磁帶傳送常常是面向記錄的, 因此在內核中緩沖數據沒有好處. 因此, 當條件正確(用戶空間緩沖區是頁對齊的, 例如), SCSI 磁帶驅動進行它的 I/O 而不拷貝數據.
就是說, 重要的是認識到直接 I/O 不是一直提供人們期望的性能提高. 設置直接 I/O (它調用出錯換入并且除下相關的用戶空間)的開銷可能是不小的, 并且被緩沖的 I/O 的好處丟失了. 例如, 直接 I/O 的使用要求 write 系統調用同步操作; 否則應用程序不能知道什么時間它可以重新使用它的 I/O 緩沖. 停止應用程序直到每個 write 完成可能拖慢事情, 這是為什么使用直接 I/O 的應用程序也常常使用異步 I/O 操作的原因.
事情的真正內涵是, 在任何情況下, 在一個字符驅動實現直接 I/O 常常是不必要并且可能是有害的. 你應當只在你確定緩沖的 I/O 的開銷確實拖慢了系統的情況下采取這個步驟. 還要注意, 塊和網絡驅動不必擔心實現直接 I/O; 這 2 種情況下, 內核中的高級的代碼在需要時建立和使用直接 I/O, 并且驅動級別的代碼甚至不需要知道直接 I/O 在被進行中.
實現直接 I/O 的關鍵是一個稱為 get_user_pages 的函數, 它在 <linux/mm.h> 中定義使用下列原型:
~~~
int get_user_pages(struct task_struct *tsk,
struct mm_struct *mm,
unsigned long start,
int len,
int write,
int force,
struct page **pages,
struct vm_area_struct **vmas);
~~~
這個函數有幾個參數:
tsk
一個指向進行 I/O 的任務的指針; 它的主要目的是告知內核誰應當負責任何一個當設置緩沖時導致的頁錯. 這個參數幾乎一直作為 current 傳遞.
mm
一個內存管理結構的指針, 描述被映射的地址空間. mm_struct 結構是捆綁一個進程的虛擬地址空間所有的部分在一起的. 對于驅動的使用, 這個參數應當一直是 current->mm.
start len
start 是(頁對齊的)用戶空間緩沖的地址, 并且 len 是緩沖的長度以頁計.
write force
如果 write 是非零, 這些頁被映射來寫(當然, 隱含著用戶空間在進行一個讀操作). force 標志告知 get_user_pages 來覆蓋在給定頁上的保護, 來提供要求的權限; 驅動應當一直傳遞 0 在這里.
pages vmas
輸出參數. 在成功完成后, 頁包含一系列指向 struct page 結構的指針來描述用戶空間緩沖, 并且 vmas 包含指向被關聯的 VMA 的指針. 這些參數應當, 顯然, 指向能夠持有至少 len 個指針的數組. 任一個參數可能是 NULL, 但是你需要, 至少, struct page 指針來實際對緩沖操作.
get_user_pages 是一個低級內存管理函數, 帶一個相稱的復雜的接口. 它還要求給這個地址空間的 mmap 讀者/寫者 旗標在調用前被以讀模式獲得. 結果是, 對 get_user_pages 常常看來象:
~~~
down_read(¤t->mm->mmap_sem);
result = get_user_pages(current, current->mm, ...);
up_read(¤t->mm->mmap_sem);
~~~
返回值是實際映射的頁數, 它可能小于請求的數目(但是大于 0).
一旦成功完成, 調用者有一個頁數組指向用戶空間緩沖, 它被鎖入內存. 為直接在緩沖上操作, 內核空間代碼必須將每個 struct page 指針轉換為一個內核虛擬地址, 使用 kmap 或者 kmap_atomic. 常常地, 但是, 對于可以使用直接 I/O 的設備在使用 DMA 操作, 因此你的驅動將可能想從 struct page 指針數組創建一個發散/匯聚列表. 我們在 "發散/匯聚映射"一節中討論如何做這個.
一旦你的直接 I/O 操作完成了, 你必須釋放用戶頁. 在這樣做之前, 但是, 你必須通知內核如果你改變了這些頁的內容. 否則, 內核可能認為這些頁是"干凈"的, 意味著它們匹配一個在交換設備中發現的一個拷貝, 并且釋放它們不寫出它們到備份存儲. 因此, 如果你已改變了這些頁(響應一個用戶空間寫請求), 你必須標志每個被影響到的頁為臟, 使用一個調用:
~~~
void SetPageDirty(struct page *page);
~~~
(這個宏定義在 <linux/page-flags.h>). 進行這個操作的代碼首先檢查來保證頁不在內存映射的保留部分, 這部分從不被換出. 因此, 代碼常常看來如此:
~~~
if (! PageReserved(page))
SetPageDirty(page);
~~~
因為用戶空間內存正常地不置為保留的, 這個檢查嚴格地不應當是必要的, 但是當你深入內存管理子系統時, 最好全面并且仔細.
不管這些頁是否已被改變, 它們必須從頁緩存中釋放, 或者它們一直留在那里. 這個調用是:
~~~
void page_cache_release(struct page *page);
~~~
這個調用應當, 當然, 在頁已被標識為臟之后進行, 如果需要.
### 15.3.1.?異步 I/O
增加到 2.6 內核的一個新的特性是異步 I/O 能力. 異步 I/O 允許用戶空間來初始化操作而不必等待它們的完成; 因此, 一個應用程序可以在它的 I/O 在進行中時做其他的處理. 一個復雜的, 高性能的應用程序還可使用異步 I/O 來使多個操作在同一個時間進行.
異步 I/O 的實現是可選的, 并且很少幾個驅動作者關心; 大部分設備不會從這個能力中受益. 如同我們將在接下來的章節中見到的, 塊和網絡驅動在整個時間是完全異步的, 因此只有字符驅動對于明確的異步 I/O 支持是候選的. 一個字符設備能夠從這個支持中受益, 如果有好的理由來使多個 I/O 操作在任一給定時間同時進行. 一個好例子是流化磁帶驅動, 這里這個驅動可停止并且明顯慢下來如果 I/O 操作沒有盡快到達. 一個應用程序試圖從一個流驅動中獲得最好的性能, 可以使用異步 I/O 來使多個操作在任何時間準備好進行.
對于少見的需要實現異步 I/O 的驅動作者, 我們提供一個快速的關于它如何工作的概觀. 我們涉及異步 I/O 在本章, 因為它的實現幾乎一直也包括直接 I/O 操作(如果你在內核中緩沖數據, 你可能常常實現異步動作而不必在用戶空間出現不必要的復雜性).
支持異步 I/O 的驅動應當包含 <linux/aio.h>. 有 3 個 file_operation 方法給異步 I/O 實現:
~~~
ssize_t (*aio_read) (struct kiocb *iocb, char *buffer,
size_t count, loff_t offset);
ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer,
size_t count, loff_t offset);
int (*aio_fsync) (struct kiocb *iocb, int datasync);
~~~
aio_fsync 操作只對文件系統代碼感興趣, 因此我們在此不必討論它. 其他 2 個, aio_read 和 aio_write, 看起來非常象常規的 read 和 write 方法, 但是有幾個例外. 一個是 offset 參數由值傳遞; 異步操作從不改變文件位置, 因此沒有理由傳一個指針給它. 這些方法還使用 iocb ("I/O 控制塊")參數, 這個我們一會兒就到.
aio_read 和 aio_write 方法的目的是初始化一個讀或寫操作, 在它們返回時可能完成或者可能沒完成. 如果有可能立刻完成操作, 這個方法應當這樣做并且返回通常的狀態: 被傳輸的字節數或者一個負的錯誤碼. 因此, 如果你的驅動有一個稱為 my_read 的讀方法, 下面的 aio_read 方法是全都正確的(盡管特別無意義):
~~~
static ssize_t my_aio_read(struct kiocb *iocb, char *buffer, ssize_t count, loff_t offset)
{
return my_read(iocb->ki_filp, buffer, count, &offset);
}
~~~
注意, struct file 指針在 kocb 結構的 ki_filp 成員中.
如果你支持異步 I/O, 你必須知道這個事實, 內核可能, 偶爾, 創建"異步 IOCB". 它們是, 本質上, 必須實際上被同步執行的異步操作. 有人可能非常奇怪為什么要這樣做, 但是最好只做內核要求做的. 同步操作在 IOCB 中標識; 你的驅動應當詢問狀態, 使用:
~~~
int is_sync_kiocb(struct kiocb *iocb);
~~~
如果這個函數返回一個非零值, 你的驅動必須同步執行這個操作.
但是, 最后, 所有這個結構的意義在于使能異步操作. 如果你的驅動能夠初始化這個操作(或者, 簡單地, 將它排隊到它能夠被執行時), 它必須做兩件事情: 記住它需要知道的關于這個操作的所有東西, 并且返回 -EIOCBQUEUED 給調用者. 記住操作信息包括安排對用戶空間緩沖的存取; 一旦你返回, 你將不再有機會來存取緩沖, 當再調用進程的上下文運行時. 通常, 那意味著你將可能不得不建立一個直接內核映射( 使用 get_user_pages ) 或者一個 DMA 映射. -EIOCBQUEUED 錯誤碼指示操作還沒有完成, 并且它最終的狀態將之后傳遞.
當"之后"到來時, 你的驅動必須通知內核操作已經完成. 那通過調用 aio_complete 來完成:
~~~
int aio_complete(struct kiocb *iocb, long res, long res2);
~~~
這里, iocb 是起初傳遞給你的同一個 IOCB, 并且 res 是這個操作的通常的結果狀態. res2 是將被返回給用戶空間的第 2 個結果碼; 大部分的異步 I/O 實現作為 0 傳遞 res2. 一旦你調用 aio_complete, 你不應當再碰 IOCB 或者用戶緩沖.
#### 15.3.1.1.?一個異步 I/O 例子
例子代碼中的面向頁的 scullp 驅動實現異步 I/O. 實現是簡單的, 但是足夠來展示異步操作應當如何被構造.
aio_read 和 aio_write 方法實際上不做太多:
~~~
static ssize_t scullp_aio_read(struct kiocb *iocb, char *buf, size_t count, loff_t pos)
{
return scullp_defer_op(0, iocb, buf, count, pos);
}
static ssize_t scullp_aio_write(struct kiocb *iocb, const char *buf, size_t count, loff_t pos)
{
return scullp_defer_op(1, iocb, (char *) buf, count, pos);
}
~~~
這些方法僅僅調用一個普通的函數:
~~~
struct async_work
{
struct kiocb *iocb;
int result;
struct work_struct work;
};
static int scullp_defer_op(int write, struct kiocb *iocb, char *buf, size_t count, loff_t pos)
{
struct async_work *stuff;
int result;
/* Copy now while we can access the buffer */
if (write)
result = scullp_write(iocb->ki_filp, buf, count, &pos);
else
result = scullp_read(iocb->ki_filp, buf, count, &pos);
/* If this is a synchronous IOCB, we return our status now. */
if (is_sync_kiocb(iocb))
return result;
/* Otherwise defer the completion for a few milliseconds. */
stuff = kmalloc (sizeof (*stuff), GFP_KERNEL);
if (stuff == NULL)
return result; /* No memory, just complete now */
stuff->iocb = iocb;
stuff->result = result;
INIT_WORK(&stuff->work, scullp_do_deferred_op, stuff);
schedule_delayed_work(&stuff->work, HZ/100);
return -EIOCBQUEUED;
}
~~~
一個更加完整的實現應當使用 get_user_pages 來映射用戶緩沖到內核空間. 我們選擇來使生活簡單些, 通過只拷貝在 outset 的數據. 接著調用 is_sync_kiocb 來看是否這個操作必須同步完成; 如果是, 結果狀態被返回, 并且我們完成了. 否則我們記住相關的信息在一個小結構, 通過一個工作隊列來為"完成"而安排, 并且返回 -EIOCBQUEUED. 在這點上, 控制返回到用戶空間.
之后, 工作隊列執行我們的完成函數:
~~~
static void scullp_do_deferred_op(void *p)
{
struct async_work *stuff = (struct async_work *) p;
aio_complete(stuff->iocb, stuff->result, 0);
kfree(stuff);
}
~~~
這里, 只是用我們保存的信息調用 aio_complete 的事情. 一個真正的驅動的異步 I/O 實現是有些復雜, 當然, 但是它遵循這類結構.
- 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. 快速參考