## 10.3.?前和后半部
中斷處理的一個主要問題是如何在處理中進行長時間的任務. 常常大量的工作必須響應一個設備中斷來完成, 但是中斷處理需要很快完成并且不使中斷阻塞太長. 這 2 個需要(工作和速度)彼此沖突, 留給驅動編寫者一點困擾.
Linux (許多其他系統一起)解決這個問題通過將中斷處理分為 2 半. 所謂的前半部是實際響應中斷的函數 -- 你使用 request_irq 注冊的那個. 后半部是由前半部調度來延后執行的函數, 在一個更安全的時間. 最大的不同在前半部處理和后半部之間是所有的中斷在后半部執行時都使能 -- 這就是為什么它在一個更安全時間運行. 在典型的場景中, 前半部保存設備數據到一個設備特定的緩存, 調度它的后半部, 并且退出: 這個操作非常快. 后半部接著進行任何其他需要的工作, 例如喚醒進程, 啟動另一個 I/O 操作, 等等. 這種設置允許前半部來服務一個新中斷而同時后半部仍然在工作.
幾乎每個認真的中斷處理都這樣劃分. 例如, 當一個網絡接口報告有新報文到達, 處理者只是獲取數據并且上推給協議層; 報文的實際處理在后半部進行.
Linux 內核有 2 個不同的機制可用來實現后半部處理, 我們都在第 7 章介紹. tasklet 常常是后半部處理的首選機制; 它們非常快, 但是所有的 tasklet 代碼必須是原子的. tasklet 的可選項是工作隊列, 它可能有一個更高的運行周期但是允許睡眠.
下面的討論再次使用 short 驅動. 當使用一個模塊選項加載時, short 能夠被告知在前/后半部模式使用一個 tasklet 或者工作隊列處理者來進行中斷處理. 在這個情況下, 前半部快速地執行; 它簡單地記住當前時間并且調度后半部處理. 后半部接著負責將時間編碼并且喚醒任何可能在等待數據的用戶進程.
### 10.3.1.?Tasklet 實現
記住 tasklet 是一個特殊的函數, 可能被調度來運行, 在軟中斷上下文, 在一個系統決定的安全時間中. 它們可能被調度運行多次, 但是 tasklet 調度不累積; ; tasklet 只運行一次, 即便它在被投放前被重復請求. 沒有 tasklet 會和它自己并行運行, 因為它只運行一次, 但是 tasklet 可以與 SMP 系統上的其他 tasklet 并行運行. 因此, 如果你的驅動有多個 tasklet, 它們必須采取某類加鎖來避免彼此沖突.
tasklet 也保證作為函數運行在第一個調度它們的同一個 CPU 上. 因此, 一個中斷處理可以確保一個 tasklet 在處理者結束前不會開始執行. 但是, 另一個中斷當然可能在 tasklet 在運行時被遞交, 因此, tasklet 和中斷處理之間加鎖可能仍然需要.
tasklet 必須使用 DECLARE_TASKLET 宏來聲明:
~~~
DECLARE_TASKLET(name, function, data);
~~~
name 是給 tasklet 的名子, function 是調用來執行 tasklet (它帶一個 unsigned long 參數并且返回 void )的函數, 以及 data 是一個 unsigned long 值來傳遞給 tasklet 函數.
short 驅動聲明它的 tasklet 如下:
~~~
void short_do_tasklet(unsigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
~~~
函數 tasklet_schedule 用來調度一個 tasklet 運行. 如果 short 使用 tasklet=1 來加載, 它安裝一個不同的中斷處理來保存數據并且調度 tasklet 如下:
~~~
irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning
*/
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
~~~
實際的 tasklet 函數, short_do_tasklet, 將在系統方便時很快執行. 如同前面提過, 這個函數進行處理中斷的大量工作; 它看來如此:
~~~
void short_do_tasklet (unsigned long unused)
{
int savecount = short_wq_count, written;
short_wq_count = 0; /* we have already been removed from the queue */
/*
* The bottom half reads the tv array, filled by the top half,
* and prints it to the circular text buffer, which is then consumed
* by reading processes */
/* First write the number of interrupts that occurred before this bh */
written = sprintf((char *)short_head,"bh after %6i\n",savecount);
short_incr_bp(&short_head, written);
/*
* Then, write the time values. Write exactly 16 bytes at a time,
* so it aligns with PAGE_SIZE */
do {
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv_tail->tv_sec % 100000000),
(int)(tv_tail->tv_usec));
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
} while (tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* awake any reading process */
}
~~~
在別的東西中, 這個 tasklet 記錄了從它上次被調用以來有多少中斷到達. 一個如 short 一樣的設備能夠在短時間內產生大量中斷, 因此在后半部執行前有幾個中斷到達就不是不尋常的. 驅動必須一直準備這種可能性并且必須能夠從前半部留下的信息中決定有多少工作要做.
### 10.3.2.?工作隊列
回想, 工作隊列在將來某個時候調用一個函數, 在一個特殊工作者進程的上下文中. 因為這個工作隊列函數在進程上下文運行, 它在需要時能夠睡眠. 但是, 你不能從一個工作隊列拷貝數據到用戶空間, 除非你使用我們在 15 章演示的高級技術; 工作者進程不存取任何其他進程的地址空間.
short 驅動, 如果設置 wq 選項為一個非零值來加載, 為它的后半部處理使用一個工作隊列. 它使用系統缺省的工作隊列, 因此不要求特殊的設置代碼; 如果你的驅動有特別的運行周期要求(或者可能在工作隊列函數長時間睡眠), 你可能需要創建你自己的, 專用的工作隊列. 我們確實需要一個 work_struct 結構, 它聲明和初始化使用下列:
~~~
static struct work_struct short_wq;
/* this line is in short_init() */
INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);
~~~
我們的工作者函數是 short_do_tasklet, 我們已經在前面一節看到.
當使用一個工作隊列, short 還建立另一個中斷處理, 看來如此:
~~~
irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* Grab the current time information. */
do_gettimeofday((struct timeval *) tv_head);
short_incr_tv(&tv_head);
/* Queue the bh. Don't worry about multiple enqueueing */
schedule_work(&short_wq);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
~~~
如你所見, 中斷處理看來非常象這個 tasklet 版本, 除了它調用 schedule_work 來安排后半部處理.
- 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. 快速參考