割り込みをつかう
なんらかのI/Oボードのデバイスドライバを作ろうと思ったとき、割り込みを使いたいことはよくあります。 ある程度遅いデバイスならポーリングをつかって、処理すればいいかと思いますが、それですと、最低間隔はLinuxのタイマ割り込み(標準 10ミリ秒)になってしまいます。 やっぱり、割り込みは便利です。

MS-DOSなどで割り込みを使った方がいらっしゃるかと思いますが、Linux の場合、あれより遥かに簡単です。 割り込みコントローラの処理などは全部 Linux がやってくれて、ドライバ側ですることは「この関数を呼んでください」と登録するだけです。

さらに、Linux では「割り込みの共有」がちゃんとできるので、拡張ボードを挿したら割り込みがたらなくなった、ということもありません(もちろん、独占欲の強いデバイスもありますが....)。

ここでは まずは、ちょっと不安ですが分かりやすい簡単な例で割り込みの実験をして、そのあと、ユーザプロセスで割り込みを受けるという荒業?に挑みます。

ここでは、まず、簡単な例を示します。 // gcc -c inttest.c -Wall -Wstrict-prototypes -O -pipe -m486 #define MODULE #define __KERNEL__ #include #include #include static char *devname="inttest"; static int irq=7; // 割り込み番号 static char *id="interrupt test"; // 割り込みの識別 #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devname, "s"); MODULE_PARM(irq, "i"); MODULE_PARM(id, "s"); #endif // 割り込みハンドル関数 static void inttest_interrupt(int irq, void *dev_id, struct pt_regs *regs) { printk("interrupted irq:%d dev_id:%s jiffies:%ld\n", irq,(char *)dev_id,jiffies); } int init_module(void) { printk("install '%s' into irq %d\n",devname,irq); if (request_irq(irq, inttest_interrupt, SA_INTERRUPT|SA_SHIRQ, devname, id)) return -EBUSY; // 割り込み登録失敗 return 0; } void cleanup_module(void) { printk("remove '%s' from irq %d\n",devname,irq); free_irq(irq,id); }; これだけです。 このプログラムは「指定された番号の割り込みを受けたら printk する」だけのプログラムです。 割り込みは何らかの方法で発生させてやらなければなりません。何らかの方法の例として
  1. Parallel I/O ボードなど手動で割り込み発生可能なボードがあれば、それをつかう。
  2. 「小判鮫」作戦。すでにある割り込みが共有可能なら、こっそり共有させてもらって、結果だけみる。
  3. パラレルポート(プリンタポート)に電線を一本さして実験する。
    (アイデア拝借: 「LINUX デバイスドライバ」)
が挙げられます。
PIOボードがPCIで割り込み番号が... という場合には /proc/pci を使うなりPCIの情報を直接読むようにするなりして、番号を得てください。

小判鮫作戦は、Linux 2.2 では出来るみたいですが、Linux 2.0 でやったら、みんなケチで share させてくれませんでした。

3つ目の方法はパラレルポートに電線(クリップでも可?)一本挿します。 メスコネクタに向って 13 12 11 10 9 8 7 6 5 4 3 2 1 \() () () ()=() () () () () () () () ()/ \() () () () () () () () () () () ()/ (左右対称) 25 24 23 22 21 20 19 18 17 16 15 14 とピンが並んでいて 9-10 に線をさしてつなぎます。 ただし、パラレルポートは電気的にもっとも弱い部類のコネクタなので、へたして壊しても責任持ちません。ご注意を(特に静電気とか)。 間違っても、4-5(対称位置) とか 9-8とかには挿さないようにしてください。 ここでは上の(2), (3) の例を示します。 #2の場合 Linux 2.2.10 (Vine 1.1) % gcc -c inttest.c -Wall -Wstrict-prototypes -O -pipe -m486 % cat /proc/interrupts <- 小判鮫する対象を探す CPU0 CPU1 0: 105356412 136968736 IO-APIC-edge timer 1: 95379 115758 IO-APIC-edge keyboard 2: 0 0 XT-PIC cascade 10: 5914946 6139915 IO-APIC-level eth0 12: 338961 727097 IO-APIC-edge PS/2 Mouse 13: 1 0 XT-PIC fpu 14: 54308 86433 IO-APIC-edge ide0 15: 179 134 IO-APIC-edge ide1 <- CDROM しかついてない NMI: 0 ERR: 0 # insmod inttest irq=15 ide1 に小判鮫 # mount /mnt/cdrom mount 実行 mount: No medium found ディスクはいれてません # rmmod inttest % dmesg install 'inttest' into irq 15 interrupted irq:15 dev_id:interrupt test jiffies:242598294 interrupted irq:15 dev_id:interrupt test jiffies:242598294 : interrupted irq:15 dev_id:interrupt test jiffies:242598296 cdrom: open failed. interrupted irq:15 dev_id:interrupt test jiffies:242598296 : interrupted irq:15 dev_id:interrupt test jiffies:242598297 VFS: Disk change detected on device ide1(22,0) remove 'inttest' from irq 15 ここは危険なことに IDEのスレーブを使いました。 もっとも、ここにはCD-ROM しかついていないので、それほど恐いとはおもいませんでしたが。 mount することで、IDEスレーブを活性化して、割り込みを発生させています(mount するたび、/proc/interrupts の数字が増えます)。

# insmod inttest irq=12 PS/2 Mouse 割り込み # nice --20 tail -f /proc/kmsg などとすると、マウスを動かす度にざ〜っと表示されます。

このプログラムは割り込みプログラムとしては実は問題がある(後述)ため、実行時には周囲に十分気をつけてください。


3つめのパラレルポートを使うためにはもうひとつ、割り込み起動のプログラムが必要です。 // lpint.c #include #define LPIO 0x378 int main(void) { iopl(3); // I/O アクセス許可 outb(0x80,LPIO); // /ACK 偽 outb(inb(LPIO+2) | 0x10, LPIO+2); // 割り込み有効 outb(0x00,LPIO); // /ACK 真 for(i=0;i<1000;i++); // すこしディレイ outb(0x80,LPIO); // /ACK 偽 outb(inb(LPIO+2) & ~0x10, LPIO+2); // 割り込み無効 return 0; } #2の場合 Linux 2.2.10 (Vine 1.1) % gcc -c inttest.c -Wall -Wstrict-prototypes -O -pipe -m486 % gcc -O -o lpint lpint.c # insmod inttest irq=7 # ./lpint # ./lpint # rmmod inttest % dmesg | tail install 'inttest' into irq 7 interrupted irq:7 dev_id:interrupt test jiffies:243310838 なぜか insmod 直後にも1回 interrupted irq:7 dev_id:interrupt test jiffies:243341182 lpint interrupted irq:7 dev_id:interrupt test jiffies:243342906 lpint remove 'inttest' from irq 7 insmod 直後にも1つ割り込みが来てしまうのが謎ですが...。 lpint の実行毎に割り込みが検出されます。 /proc/kmsg を見た方が動きは分かりやすいでしょう。 基本的に割り込みの登録は include/linux/sched.h ( 2.0/2.2 共通 ) int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long flags, const char *device, void *dev_id); void free_irq(unsigned int irq, void *dev_id); void my_handler(int irq, void *dev_id, struct pt_regs *regs){} を用います。IRQ 番号 irq に 関数 handler を登録します。 device は /proc/interrupt で表示される名前です。 dev_id は 割り込みを共有する場合などに識別子としても使われるポインタですが、固有の値なら何でも良く、割り込みハンドラにも渡される値なので、ドライバの情報を入れたポインタを渡すのもひとつの方法です。 今回の例ではとりあえず、文字列をいれておきました。

解放する場合には、IRQ番号と識別dev_idを渡します。 これまで同様、cleanup_module までには必ず解放しなければなりません。

割り込みが発生すると、割り込み番号、dev_id、割り込み時の保存情報(普通いらないでしょう)とともに、登録した関数が呼び出されます。 たったそれだけです。

さて、 request_irq の3つ目の引数, flag ですが、これは

  • SA_INTERRUPT: ハンドラ関数実行中は割り込み禁止=ハンドラは手短に終了しないといけない、が、いまどきのコンピュータ速いから...。
  • SA_SHIRQ: 割り込みを共有
を必要なら "|" で結合して渡します。 SA_INTERRUPT はつけてもつけなくとも良さそうです。いずれにせよ、割り込みハンドラからなるべく早く戻らなければなりません。 SA_SHIRQ は割り込みを共有する場合に使います。すでに、要求したいIRQを別のドライバが使っている場合でも、それがSA_SHIRQを指定していてくれれば&自分も指定すれば、共有になります。 ただ、そのときはdev_id を指定しないと、free_irq するときに面倒なことになります。

もちろん、複数の割り込みを1つのハンドラで受けることも可能です(ボードの2枚挿しなど)。 そのときは dev_id で、どのハードの割り込みかを区別するなどすべきでしょう。

上で「このプログラムは割り込みプログラムとして問題がある」と言ったのは他でもありません。 割り込みハンドラの中で printk なんか呼んでいるからです。 いままで、これで事故が起きたことはないので、おそらくカーネル側で、割り込みで使ってもいいようにしてくれているのだと思いますが、「割り込みハンドラの処理は最大限軽くすべし」の原則から、実験時以外はprintk などは入れないほうがいいでしょう。

いままでの内容をつかうと、ユーザプロセスで割り込み処理を行うことも可能になります。 題材としておもしろいので、ここで取り上げます。

具体的には、select を使います。selectで寝かせたところに、割り込みハンドル関数内で wakeup をしかけます。 たったこれだけで、ユーザプロセスで割り込みを拾うことが可能になってしまいます。

// モジュール側: intsel.c // gcc -c intsel.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/ocrtest c 60 0; chmod a+rw /dev/ocrtest #define MODULE #define __KERNEL__ #include #include #include #include #include #include #if LINUX_VERSION_CODE >= 0x020100 #include #include #else static inline unsigned long copy_to_user(void *to, const void *from, unsigned long n) { memcpy_tofs(to,from,n); return 0; } static inline unsigned long copy_from_user(void *to, const void *from, unsigned long n) { memcpy_fromfs(to,from,n); return 0; } #endif static int irq=7; // 割り込み番号 static int devmajor=60; static char *devname="intsel"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(irq, "i"); MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif // ファイルごとの管理領域 typedef struct { unsigned long f_version; // 識別用 f_version struct wait_queue *wait; // 休眠待機用領域 int sleep_mode; // 0: 起きてる 1: 待ち unsigned long long wtime; // 最後に起きた時間 unsigned long long rtime; // 最後にioctlした呼んだ時間=リセット } FileInfo; #define MAXFILE 3 static FileInfo fi[MAXFILE]; // 時間取得関数: unsigned long long get_time(void) { #ifdef USE_RDTSC // RDTSC 直読み unsigned int h,l; /* read Pentium cycle counter */ __asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h)); return ((unsigned long long int)h<<32)|l; #else // gettimeofday 使用 struct timeval tv; do_gettimeofday(&tv); return (unsigned long long)(tv.tv_sec)*1000000+tv.tv_usec; #endif } // Linux 2.0/2.2 共通 static int intsel_open(struct inode * inode, struct file * file) { int i; printk("intsel_open: (%ld)\n",file->f_version); for(i=0;if_version; fi[i].sleep_mode=0; fi[i].wait=NULL; file->private_data=fi+i; // プライベートデータに構造体ポインタ設定 MOD_INC_USE_COUNT; return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int intsel_close(struct inode * inode, struct file * file) #else static void intsel_close(struct inode * inode, struct file * file) #endif { printk("intsel_close: (%ld)\n",file->f_version); MOD_DEC_USE_COUNT; ((FileInfo *)(file->private_data))->f_version=0; ((FileInfo *)(file->private_data))->sleep_mode=0; ((FileInfo *)(file->private_data))->wtime=0; ((FileInfo *)(file->private_data))->rtime=0; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // poll / select : 2.2 では poll. 2.0 では select #if LINUX_VERSION_CODE >= 0x020100 static unsigned int intsel_poll(struct file *file, struct poll_table_struct *ptab) { if(((FileInfo *)(file->private_data))->rtime // リセット後 <((FileInfo *)(file->private_data))->wtime) // 起こされた return POLLIN|POLLRDNORM; // 読み込みOK poll_wait(file,&(((FileInfo *)(file->private_data))->wait),ptab); ((FileInfo *)(file->private_data))->sleep_mode=1; return 0; // おやすみなさい (^^; } #else // 2.0 系用 static int intsel_select(struct inode *inode, struct file *file, int sel_type, select_table *stab) { if(sel_type==SEL_IN) { if(((FileInfo *)(file->private_data))->rtime // リセット後 <((FileInfo *)(file->private_data))->wtime) // 起こされた return 1; // 読み込みOK ((FileInfo *)(file->private_data))->sleep_mode=1; select_wait(&(((FileInfo *)(file->private_data))->wait),stab); return 0; } return 0; } #endif // ioctl: Linux 2.0/2.2共通 static int intsel_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { ((FileInfo *)(file->private_data))->rtime=get_time(); // リセット switch(cmd) { case 1: // 起こされ時間の記録 copy_to_user((void *)arg, &(((FileInfo *)(file->private_data))->wtime),8); return 0; default: return 0; } } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations intsel_fops = { // Linux 2.2.10 より NULL, // loff_t llseek(struct file *, loff_t, int) NULL, // ssize_t read(struct file *, char *, size_t, loff_t *) NULL, // ssize_t write(struct file *, const char *, size_t, loff_t *) NULL, // int readdir(struct file *, void *, filldir_t) intsel_poll, // u. int poll(struct file *, struct poll_table_struct *) intsel_ioctl, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) intsel_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) intsel_close, // int release(struct inode *, struct file *) NULL, // int fsync(struct file *, struct dentry *) NULL, // int fasync(int, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) NULL, // int lock(struct file *, int, struct file_lock *) }; #else static struct file_operations intsel_fops = { // Linux 2.0.36 より NULL, // int lseek(struct inode *, struct file *, off_t, int) NULL, // int read(struct inode *, struct file *, char *, int) NULL, // int write(struct inode *, struct file *, const char *, int) NULL, // int readdir(struct inode *, struct file *, void *, filldir_t) intsel_select,// int select(struct inode *, struct file *, int, select_table *) intsel_ioctl, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) intsel_open, // int open(struct inode *, struct file *) intsel_close, // void release(struct inode *, struct file *) NULL, // int fsync(struct inode *, struct file *) NULL, // int fasync(struct inode *, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) }; #endif // 割り込みハンドル関数 static void intsel_interrupt(int irq, void *dev_id, struct pt_regs *regs) { int i; unsigned long long wtime=get_time(); //printk("interrupted irq:%d dev_id:%s jiffies:%ld\n", // irq,(char *)dev_id,jiffies); for(i=0;i // プロセス側: intsel_user.c #include #include #include #include #include #include #include // 時間取得関数: unsigned long long get_time(void) { #ifdef USE_RDTSC // RDTSC 直読み unsigned int h,l; /* read Pentium cycle counter */ __asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h)); return ((unsigned long long int)h<<32)|l; #else // gettimeofday 使用 struct timeval tv; gettimeofday(&tv,NULL); return (unsigned long long)(tv.tv_sec)*1000000+tv.tv_usec; #endif } #ifdef USE_RDTSC // 1ミリ秒あたりの数字 #define TickPerMSEC 366000.0 // CPU動作周波数 #else #define TickPerMSEC 1000.0 // 1ミリ/1マイクロ秒 #endif int main(void) { int fd,r; unsigned long long utime,wtime; struct timeval tv; fd_set fds; fd=open("/dev/intsel",O_RDWR); if(fd<0) { printf("cannnot open /dev/intsel\n"); return 1; } while(1) { FD_ZERO(&fds); // select の準備 FD_SET(fd,&fds); tv.tv_sec=1; tv.tv_usec=0; // タイムアウト1秒 r=select(fd+1,&fds,NULL,NULL,&tv); // 読みselect if(r<0) { printf("select returned with signal or error\n"); break; } if(r==0) { putchar('.'); fflush(stdout); continue; } if((r==1)&&(FD_ISSET(fd,&fds))) { utime=get_time(); ioctl(fd,1,&wtime); printf("\ninterrupt at %lld, selected at %lld\n",wtime,utime); printf("difference: %lf [ms] (%lld ticks)\n", (utime-wtime)/TickPerMSEC, utime-wtime); } } close(fd); return 0; } % gcc -O -o intsel_user intsel_user.c % gcc -c intsel.c -Wall -Wstrict-prototypes -O -pipe -m486 # ./insdev intsel /dev/intsel % ./intsel_user ...... <- べつのところで # ./lpint interrupt at 954497906049005, selected at 954497906049044 difference: 0.039000 [ms] (39 ticks) ..... interrupt at 954497911336106, selected at 954497911336150 difference: 0.044000 [ms] (44 ticks) .... 割り込みはなにを使ってもいいのですが、ここでは手頃なところでパラレルポートを使いました(なぜか、割り込みちゃんと発生しないことがありましたが)。

原理的には、上で述べたように、select で寝かせたものを 割り込みハンドラで wakeup します。 その判定には最後に ioctl したときとの時間で判断しています。 ioctl がリセットの役目です。 ロジックの組み方次第では、リセット動作を不要にできますが、ここでは単純化のためにこうしています。

ioctl はコマンドが1の場合に、unsigned long long 型で割り込み発生時刻を返すようにしています。 ユーザ空間のプロセスである、intsel_user では、selectから抜けたときに時刻とこの時刻から、割り込み発生からプロセスにCPUが移るまでの時間をおおまかに算出しています。 Linux 2.0.36 + PentiumII 233M で60マイクロ秒くらい、Linux 2.2.10 + Celeron 366MHz で30マイクロ秒くらいの性能が調子がいいときには出ます。 ただし、所詮はプロセスなので、他のプロセスの動作具合いや、カーネルの動作具合いによって1ミリ秒近くまで遅れることがありますし、最悪数十ミリ秒遅れることも有り得るでしょう。 (プロセスがスワップアウトされてしまっては、当然こんなものでは済みません)。 割り込みの性能にもよりますが、わりと応答性はいいとおもいます。いかがでしょうか?

時間の測定には gettimeofday の値を使用しています(ドライバでもdo_gettimeofdayを使用)。 もし、いちいちシステムコールなんて時間のかかるものつかえるか!という場合には、両プログラムを-DUSE_RDTSC オプションつきでコンパイルすれば、Pentium以降のCPUのみですが、CPUのクロックを数える命令を使用するようになります。 なお、その場合には TickPerMSEC の値をCPUの動作周波数をもとに正しく設定しないと、いい加減な時間を表示してしまいますのでご注意を。例では 366MHz にしてあります。

なお、この例のように、複数のものをカーネルに登録するとき(ここではドライバの関数テーブルと割り込み)、登録時にひとつでも転んだ場合にのこりを解放することを忘れてはいけません。

ここではLinuxの割り込みについてとりあげました。

割り込みは基本的にはカーネル内部で使うものですが、うまく連携させれば、ある程度は一般のプロセスに割り込み処理をまかせることもできます。

いずれにせよ、割り込みの利用は難しくないということがお分かり頂けたと思います。