割り込み処理 (※マルチコア対応関連)
割り込みハンドラは別のコードと同時に実行されます。
つまり、並行処理と、データ構造体とハードウェアの競争の問題が必ず起こることになります。
多くの場合、モジュールは別のドライバと割り込み信号線を共有することになります。
1)、割り込みハンドラのインストール
①、関連関数
割り込みハンドラのインストールは、ドライバの初期化時、あるいはデバイスが始めてオープンされたときに行います。
・割り込み登録のIFとして、 (<linux/interrupt.h>)
int request_irq( // 割り込み番号 // 割り込みハンドラ
unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
// オプション
unsigned long flags,
// 割り込み所有者
const char *dev_name,
// 割り込み回線を共有する場合使われる。共有しない場合はNULL。
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
int can_request_irq(unsigned int irq, unsigned long flags);
※1 「request_irq」を呼び出す正しい位置は、デバイスが初めてオープンされたときで、
ハードウェアに「割り込みを生成しなさい」という指示を出す「前」です。
※2 「free_irq」を呼び出す位置は、デバイスが最後にクローズされたときで、
ハードウェアに「以後CPUに割り込みをかけないように」と指示を出した「後」です。
※3 「can_request_irq」を行ってから「request_irq」を呼び出す間に、様子が変わってしまう可能性があります。
②、/procインタフェース
・「/proc/interrupts」
割り込みを処理した回数が「/proc/interrupts」に示されます。
ハードウェア割り込みがCPUに通知されるたびに1増える内部カウンタ用意されています。
「/proc/interrupts」には、ハンドラがインストールされているものだけ記載します。
・「/proc/stat」
システムアクティビティに関する低レベルな統計を示します。起動以降に受信した割り込みの数も含まれています。
ドライバがオープン/クローズのたびに割り込み信号線を獲得/解放する場合には、「/proc/interrupts」よりも便利です。
③、IRQ番号の自動推定
ドライバの初期化時には、デバイスが使うIRQ信号線の決定方法が問題となります。
・カーネルが手伝う自動検出
unsigned long probe_irq_on(void);
int probe_irq_off(unsigned long);
※1 「probe_irq_off」を呼び出す前に、「udelay」が必要、CPUの処理速度に応じて、
実際に割り込みが発生して処理されるのを、短時間待つ必要があります。
※2 自動検出には、かなり時間かかるかもしれません。
・自前の自動検出
自動検出の仕組みは、すべての未使用の割り込みを有効にした後、何かが起きるのをじっと待つというものです。
関連関数は: request_irq と free_irq です。
事前に「ありえる」IRQが知っている場合、それらをテストして、何が起こるかをじっと待って、自動検出を行います。
事前に「ありえる」IRQが知らない場合は、IRQ0~IRQ NR_IRQS-1までを調べます。
④、高速ハンドラと低速ハンドラ
最新のカーネルは、高速割り込みと低速割り込みの違いはほとんどなくなっています。
ただ、高速割り込み(SA_INTERRUPT)が、カレントプロセッサ上で、
他の全ての割り込みを無効にして実行されるという点あるため、強力な理由がない限り、使うべきではありません。
2)、ハンドラを実装する
①、通常ルーチンとの違い
実は、割り込みハンドラといっても何も特別なことはありません、通常のCのコードです。
唯一の違いは、ハンドラーはプロセスのコンテキスト中で実行されていないため、
・ユーザ空間とのデータ転送ができません
・スリープができません
・GFP_ATOMIC以外を使ったメモリ割り当てが行えません
・セマフォのロックが行えません
・scheduleを呼び出すことができません
②、ハンドラ作成の注意点
・実行にかかる時間を最小限にする
・長時間にわたる計算を実行するなら、最もよい方法はタスクレットかタスク待ち列を用いてスケジュールし、
より安全な時間に計算を行うようにすることです。
③、ハンドラの引数と戻り値
/* 【引数】
irq: 割り込み番号
dev_id: クライアントデータの一種で、request_irqに渡されたものと同じポインタが、
割り込みが発生したときにハンドラーに戻されます。
通常は、dev_idにデバイスデータ構造体(ユーザデータ)を指すポインタを渡します。
regs: 通常のデバイスドライバの処理で必要になることはありません。
【戻り値】
デバイスに割り込みがあったことをハンドラが見つけた場合は、「IRQ_HANDLED」を、
それ以外の場合は、「IRQ_NONE」を返します。
戻り値は、マクロ「IRQ_RETVAL(handled)」で生成できます。
*/
irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
xxx xxx
wake_up_interruptible(&sample_queue); /* 処理プロセスを目覚めさせる */
return XXX;
}
④、割り込みの有効化と無効化
要因例:
・システムのデッドロックを防ぐために、スピンロックを保持している間は、割り込みをブロックする必要がある。
・処理速度の問題で割り込みをブロックする必要がある。
(例えば、受信側のIFが1バイト受信するたびに2つの割り込みを処理することになる場合。)
※ ドライバであっても、割り込みを無効化するのは比較的珍しい操作であることを指摘しておきます。
・単一の割り込みを無効にする
void disable_irq(int irq); // ①指定した割り込みを無効にする。
// ②実行中の割り込みハンドラがあればその完了を待つことになる。
void disable_irq_nosync(int irq); // no sync
void enable_irq(int irq);
※1 このAPIをドライバにおける相互排他の仕組みとして使ってはいけません。
※2 ほとんどのドライバではこの使用をお勧めしません。とりわけ、共有されている割り込み信号線を無効にしてはいけません。
※3 全てのCPUが対象。
・全ての割り込みを無効にする
void local_irq_save(unsigned long flags); // ①割り込み状態をflagsに保存。
// ②割り込みの配付を無効。
void local_irq_disable(void); // 割り込みの配付を無効にするのみ。
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
※1 カレントCPUにおける割り込みの配布を無効にします。
3)、前半部と後半部
割り込み処理における最大の問題は、ハンドラー内で時間のかかる処理をどう実行するかということです。
Linuxはこの問題を、割り込みハンドラーを2つの部分に分割して「半分」にすることで解決しています。
・「前半部(Top half)」は実際に割り込みに応答するルーチンであり、これをrequest_irqを使って登録します。 ※ 割り込みが無効
・「後半部(Bottom half)」は前半部によってスケジュールされるもので、後のもっと安全な時間に実行されます ※ 割り込みが有効
後半部の実装に使用するメカニズムが2つあります。
①タスクレット
タスクレットは
・大変速く処理されますが、コードはアトミックでないといけない。
・割り込みコンテキスト中で実行される特殊な関数である。
・何回も実行するようにスケジュールできますが、タスクレットのスケジュールは累積されない。
従って、タスクレット自身が並行して実行されることがない。
下記原因でタスクレット間にロック機能を求められます。
・SMPシステムでは、あるタスクレットと並行して別のタスクレットが実行されることがあります。
・タスクレットの実行中に、別の割り込みが発生することがありえます。
このため、タスクレットと割り込みハンドラーの間のロック機能がやはり必要になります。
②作業待ち列
作業待ち列は
・タスクレットよりも長時間待つことになりますが、スリープできる方法です。
・将来のある時点に、特殊なワーカプロセスのコンテキストの中で実行されます。
・ユーザ空間とのデータのやり取りをするのにDMA処理が必要です。
・ドライバに遅延が要求される場合、或いは作業関数内で長時間スリープする場合、作業待ち列が適当です。
4)、割り込みの共有
例: PCIデバイスは割り込み共有をサポートする。少ない例として、ISAデバイスは共有しない。
①、共有ハンドラーのインストール
割り込みを共有しないものと同様に「request_irq」で行うのですが、相違点は2つあります。
・引数flagsの中で「SA_SHIRQ」ビットを指定する
・引数dev_idは、ユニークでないといけない。 (カーネルはハンドラーの識別にdev_idを使います)
・共有ハンドラーのために利用できる自動検出機能はないです。
幸い、割り込みを共有できるハードウェアは、どの割り込み信号線を使っているかをCPUに通知できます。
・「enable_irq」、「disable_irq」は使えないです。
②、ハンドラの実行
カーネルが割り込み要求を受け取ると、登録された全てのハンドラーを起動します。
このため、共有ハンドラは自分が処理すべき割り込みと、他のデバイスが生成した割り込みを区別する必要があります。
・「dev_id」より区別
・割り込み保留ビットより区別
③、/procインタフェースと共有割り込み
共有ハンドラーは「/proc/interrupts」にの同じ割り込み番号に表示されます。「/proc/stat」には表示しません。
5)、割り込み駆動I/O
管理しているハードウェアとの間のデータ転送が何らかの理由で遅延されそうなときは、必ずバッファリングを実装しましょう。
・write & readシステムコールからデータの送受信処理を切り離すのを助ける。
・システム全体の処理能力を高めます。
時としてデバイスからの割り込みを失うことがあります。
このため、データをデバイスに出力するたびにカーネルタイムを設定します。タイマが満了したら、割り込みを失ったと見なすのです。