割り込みをつかう

[| ]  最終更新: 2023/02/14 18:32:44

Linuxの割り込み

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

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

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

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


簡単な例

ソースリスト

ここでは、まず、簡単な例を示します。

// gcc -c inttest.c -Wall -Wstrict-prototypes -O -pipe -m486

#define MODULE
#define __KERNEL__

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>

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 <asm/io.h>

#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 は割り込みを共有する場合に使います。すでに、要求したい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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/time.h>

#if LINUX_VERSION_CODE >= 0x020100
#include <asm/uaccess.h>
#include <linux/poll.h>            
#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;i<MAXFILE;i++)
    {
      if(fi[i].f_version==0) break;
    }
  if(i==MAXFILE)
    {
      printk("intsel_open: busy\n");
      return -EBUSY;
    }
  fi[i].f_version=file->f_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<MAXFILE;i++)
    if((fi[i].f_version)&&(fi[i].sleep_mode))        // 待ちを
      {
	wake_up_interruptible(&(fi[i].wait));        // 起こす
	//printk("wakeup %ld\n",fi[i].f_version);
	fi[i].sleep_mode=0;
	fi[i].wtime=wtime;
      }
}

int init_module(void)
{
  int i;
  for(i=0;i<MAXFILE;i++) fi[i].f_version=0;            // 未使用マーク

  printk("install '%s' into major %d\n",devname,devmajor);
  if(register_chrdev(devmajor,devname,&intsel_fops))  // 登録
    {
      printk("device registration error\n");
      return -EBUSY;
    }

  if (request_irq(irq, intsel_interrupt, SA_INTERRUPT|SA_SHIRQ, 
		  devname, devname))
    {
      printk("request_irq failed: unregister driver\n");
      if(unregister_chrdev(devmajor,devname))          // 忘れずに
	printk ("unregister_chrdev failed\n");
      return -EBUSY;                    // 割り込み登録失敗
    }
  return 0;
}

void cleanup_module(void)
{
  printk("remove '%s' from major %d\n",devname,devmajor);
  if(unregister_chrdev(devmajor,devname)) 
    printk ("unregister_chrdev failed\n");
  free_irq(irq,devname);
};
// プロセス側: intsel_user.c
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>


// 時間取得関数: 
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の割り込みについてとりあげました。

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

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



熊谷正朗 [→連絡]
東北学院大学 工学部 機械知能工学科 RDE
[| ]