## 10.5.?中斷驅動 I/O
無論何時一個數據傳送到或自被管理的硬件可能因為任何原因而延遲, 驅動編寫者應當實現緩存. 數據緩存幫助來分離數據傳送和接收從寫和讀系統調用, 并且整個系統性能受益.
一個好的緩存機制產生了中斷驅動的 I/O, 一個輸入緩存在中斷時填充并且被讀取設備的進程清空; 一個輸出緩存由寫設備的進程填充并且在中斷時清空. 一個中斷驅動的輸出的例子是 /dev/shortprint 的實現.
為使中斷驅動的數據傳送成功發生, 硬件應當能夠產生中斷, 使用下列語義:
-
對于輸入, 設備中斷處理器, 當新數據到達時, 并且準備好被系統處理器獲取. 進行的實際動作依賴是否設備使用 I/O 端口, 內存映射, 或者 DMA.
- 對于輸出, 設備遞交一個中斷, 或者當它準備好接受新數據, 或者確認一個成功的數據傳送. 內存映射的和能DMA的設備常常產生中斷來告訴系統它們完成了這個緩存.
在一個讀或寫與實際數據到達之間的時間關系在第 6 章的"阻塞和非阻塞操作"一節中介紹.
### 10.5.1.?一個寫緩存例子
我們已經幾次提及 shortprint 驅動; 現在是時候真正看看. 這個模塊為并口實現一個非常簡單, 面向輸出的驅動; 它是足夠的, 但是, 來使能文件打印. 如果你選擇來測試這個驅動, 但是, 記住你必須傳遞給打印機一個文件以它理解的格式; 不是所有的打印機在給一個任意數據的流時很好響應.
shortprint 驅動維護一個一頁的環形輸出緩存. 當一個用戶空間進程寫數據到這個設備, 數據被填入緩存, 但是寫方法實際沒有進行任何 I/O. 相反, shortp_write 的核心看來如此:
~~~
while (written < count)
{
/* Hang out until some buffer space is available. */
space = shortp_out_space();
if (space <= 0) {
if (wait_event_interruptible(shortp_out_queue,
(space = shortp_out_space()) > 0))
goto out;
}
/* Move data into the buffer. */
if ((space + written) > count)
space = count - written;
if (copy_from_user((char *) shortp_out_head, buf, space)) {
up(&shortp_out_sem);
return -EFAULT;
}
shortp_incr_out_bp(&shortp_out_head, space);
buf += space;
written += space;
/* If no output is active, make it active. */
spin_lock_irqsave(&shortp_out_lock, flags);
if (! shortp_output_active)
shortp_start_output();
spin_unlock_irqrestore(&shortp_out_lock, flags);
}
out:
*f_pos += written;
~~~
一個旗標 ( shortp_out_sem ) 控制對這個環形緩存的存取; shortp_write 就在上面的代碼片段之前獲得這個旗標. 當持有這個旗標, 它試圖輸入數據到這個環形緩存. 函數 shortp_out_space 返回可用的連續空間的數量(因此, 沒有必要擔心緩存回繞); 如果這個量是 0, 驅動等到釋放一些空間. 它接著拷貝它能夠的數量的數據到緩存中.
一旦有數據輸出, shortp_write 必須確保數據被寫到設備. 數據的寫是通過一個工作隊列函數完成的; shortp_write 必須啟動這個函數如果它還未在運行. 在獲取了一個單獨的, 控制存取輸出緩存的消費者一側(包括 shortp_output_active)的數據的自旋鎖后, 它調用 shortp_start_output 如果需要. 接著只是注意多少數據被寫到緩存并且返回.
啟動輸出進程的函數看來如下:
~~~
static void shortp_start_output(void)
{
if (shortp_output_active) /* Should never happen */
return;
/* Set up our 'missed interrupt' timer */
shortp_output_active = 1;
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
/* And get the process going. */
queue_work(shortp_workqueue, &shortp_work);
}
~~~
處理硬件的事實是, 你可以, 偶爾, 丟失來自設備的中斷. 當發生這個, 你確實不想你的驅動一直停止直到系統重啟; 這不是一個用戶友好的做事方式. 最好是認識到一個中斷已經丟失, 收拾殘局, 繼續. 為此, shortprint 甚至一個內核定時器無論何時它輸出數據給設備. 如果時鐘超時, 我們可能丟失一個中斷. 我們很快會看到定時器函數, 但是, 暫時, 讓我們堅持在主輸出功能上. 那是在我們的工作隊列函數里實現的, 它, 如同你上面看到的, 在這里被調度. 那個函數的核心看來如下:
~~~
spin_lock_irqsave(&shortp_out_lock, flags);
/* Have we written everything? */
if (shortp_out_head == shortp_out_tail)
{ /* empty */
shortp_output_active = 0;
wake_up_interruptible(&shortp_empty_queue);
del_timer(&shortp_timer);
}
/* Nope, write another byte */
else
shortp_do_write();
/* If somebody's waiting, maybe wake them up. */
if (((PAGE_SIZE + shortp_out_tail -shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE)
{
wake_up_interruptible(&shortp_out_queue);
}
spin_unlock_irqrestore(&shortp_out_lock, flags);
~~~
因為我們在使用共享變量的輸出一側, 我們必須獲得自旋鎖. 接著我們看是否有更多的數據要發送; 如果無, 我們注意輸出不再激活, 刪除定時器, 并且喚醒任何在等待隊列全空的進程(這種等待當設備被關閉時結束). 如果, 相反, 有數據要寫, 我們調用 shortp_do_write 來實際發送一個字節到硬件.
接著, 因為我們可能在輸出緩存中有空閑空間, 我們考慮喚醒任何等待增加更多數據給那個緩存的進程. 但是我們不是無條件進行喚醒; 相反, 我們等到有一個最低數量的空間. 每次我們從緩存拿出一個字節就喚醒一個寫者是無意義的; 喚醒進程的代價, 調度它運行, 并且使它重回睡眠, 太高了. 相反, 我們應當等到進程能夠立刻移動相當數量的數據到緩存. 這個技術在緩存的, 中斷驅動的驅動中是普通的.
為完整起見, 這是實際寫數據到端口的代碼:
~~~
static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL);
/* Something happened; reset the timer */
mod_timer(&shortp_timer, jiffies + TIMEOUT);
/* Strobe a byte out to the device */
outb_p(*shortp_out_tail, shortp_base+SP_DATA);
shortp_incr_out_bp(&shortp_out_tail, 1);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr | SP_CR_STROBE, shortp_base+SP_CONTROL);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr & ~SP_CR_STROBE, shortp_base+SP_CONTROL);
}
~~~
這里, 我們復位定時器來反映一個事實, 我們已經作了一些處理, 輸送字節到設備, 并且更新了環形緩存指針.
工作隊列函數沒有直接重新提交它自己, 因此只有一個單個字節會被寫入設備. 在某一處, 打印機將, 以它的緩慢方式, 消耗這個字節并且準備好下一個; 它將接著中斷處理器. shortprint 中使用的中斷處理是簡短的:
~~~
static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
if (! shortp_output_active)
return IRQ_NONE;
/* Remember the time, and farm off the rest to the workqueue function */
do_gettimeofday(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}
~~~
因為并口不要求一個明顯的中斷確認, 中斷處理所有真正需要做的是告知內核來再次運行工作隊列函數.
如果中斷永遠不來如何? 至此我們已見到的驅動代碼將簡單地停止. 為避免發生這個, 我們設置了一個定時器在幾頁前. 當定時器超時運行的函數是:
~~~
static void shortp_timeout(unsigned long unused)
{
unsigned long flags;
unsigned char status;
if (! shortp_output_active)
return;
spin_lock_irqsave(&shortp_out_lock, flags);
status = inb(shortp_base + SP_STATUS);
/* If the printer is still busy we just reset the timer */
if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
spin_unlock_irqrestore(&shortp_out_lock, flags);
return;
}
/* Otherwise we must have dropped an interrupt. */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL, NULL);
}
~~~
如果沒有輸出要被激活, 定時器函數簡單地返回. 這避免了定時器重新提交自己, 當事情在被關閉時. 接著, 在獲得了鎖之后, 我們查詢端口的狀態; 如果它聲稱忙, 它完全還沒有時間來中斷我們, 因此我們復位定時器并且返回. 打印機能夠, 有時, 花很長時間來使自己準備; 考慮一下缺紙的打印機, 而每個人在一個長周末都不在. 在這種情況下, 只有耐心等待直到事情改變.
但是, 如果打印機聲稱準備好了, 我們一定丟失了它的中斷. 這個情況下, 我們簡單地手動調用我們的中斷處理來使輸出處理再動起來.
shortpirnt 驅動不支持從端口讀數據; 相反, 它象 shortint 并且返回中斷時間信息. 但是一個中斷驅動的讀方法的實現可能非常類似我們已經見到的. 從設備來的數據可能被讀入驅動緩存; 它可能被拷貝到用戶空間只在緩存中已經累積了相當數量的數據, 完整的讀請求已被滿足, 或者某種超時發生.
- 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. 快速參考