IOCTLをつかう

[| ]  最終更新: 2023/02/14 18:33:00

ioctl

ioctl は、なにやらデバイスを操作するプログラムを書いたことがあれば目にしたことがあるかも知れませんし、ifconfig のようなデバイス操作をするプログラムのエラーで見覚えがあるかも知れません。

私自身、見よう見まねでいろいろなプログラムを書いて(打ち込んで)いるときに、時々 ioctl に出会っていました。 得たいの知れない定数やら得たいの知れない構造体やらを渡すので、私にとっては不気味な存在でした。 C の関数なら、普通はマニュアルを読めば引数と機能は書いてあるのに、ioctl だけは、使い方だけで中身については一切なしなのです。

ですが、デバイスドライバをつくるようになってから、一気に謎が解けました。 ioctl は read や write とは別系統のドライバとのやり取りの口です。 read, write が基本的には連続したデータのやりとりをするように使用されることが多いのに対して、ioctl はどちらかというとデバイスの状態を制御する単発データ用といった趣きです。 ここのページでは、どちらも同じような使い方をしているので、区別はつきにくいですが。

ioctl は man ioctl してみると、ファイル記述子の他に、request とか cmd とか書いてある整数引数(unsigned int) がひとつあり、もうひとつ何か引数ととる(取らないこともあり)ようになってます。 デバイスドライバ内部の ioctl を受け持つ関数にはこの 整数引数と何らかの引数が unsigned long の形で渡されます。 この関数の帰り値が負の数値の場合、ユーザプロセスの ioctl()には-1が返り、errno にその絶対値が設定されます。

さて、ioctl を受け持つ関数には なんの機能を実装すればいいのでしょうか? それを決めるのはドライバを設計するみなさんです。一応、例を挙げます。

などなど。つまり、ioctlの最後の引数の使い方次第でかわる、ということです。

使い方&デバイスによったら、コマンドがある分だけ read/write で実装するより簡単だと思います。


ioctl を使った例

さすがに ioctl は cat では使えないので、専用のアクセステストプログラムもつくります。 また、両者で共有する情報はヘッダファイルにまとめました。
ファイル ocitest.h (ヘッダ)

// header file of module ocitest.o 

#define OCI_NOP 1         // ioctl 用のコマンド数値
#define OCI_INT 2         // 一応、カッコ良く定義しておく
#define OCI_STRING 3
#define OCI_STRUCT 4

struct oci_struct 
{
  char *p;
  int len;
};
ファイル ocitest.c (モジュール)
// gcc -c ocitest.c -Wall -Wstrict-prototypes -O -pipe -m486
// mknod /dev/ocitest c 60 0;  chmod a+rw /dev/ocitest  

#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 "ocitest.h"

static int devmajor=60;
static char *devname="ocitest";
#if LINUX_VERSION_CODE > 0x20115
MODULE_PARM(devmajor, "i");
MODULE_PARM(devname, "s");
#endif

#if LINUX_VERSION_CODE >= 0x020100
#include <asm/uaccess.h>
#else
// Linux 2.0.x では memcpy_tofs/_fromfs だった関数が
// Linux 2.1.x 以降で copy_to/from_user に 変更。なので合わせる
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

// Linux 2.0/2.2 共通
static int ocitest_open(struct inode * inode, struct file * file)
{
  MOD_INC_USE_COUNT;
  return 0;
}

// Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず)
#if LINUX_VERSION_CODE >= 0x020100
static int ocitest_close(struct inode * inode, struct file * file)
#else
static void ocitest_close(struct inode * inode, struct file * file)
#endif
{
  MOD_DEC_USE_COUNT;
#if LINUX_VERSION_CODE >= 0x020100
  return 0;
#endif
}

// ioctl: Linux 2.0/2.2共通
static int ocitest_ioctl(struct inode *inode, struct file *file, 
	      unsigned int cmd, unsigned long arg)
{
  char buff[257];
  struct oci_struct ocis;
  int i;
  printk("ocitest: ioctl: cmd=%04X, arg=%08lX\n",cmd,arg);
  switch(cmd)
    {
    case OCI_NOP: printk("    Cmd: No operation\n");     return 0;
    case OCI_INT: printk("    Cmd: Integer: %ld\n",arg); return 0;
    case OCI_STRING:
      for(i=0;i<256;i++)    // arg をポインタと見なしてユーザデータ取得
	{  // get_user はバージョン依存
#if LINUX_VERSION_CODE >= 0x020100
	  if(get_user(buff[i],(char *)(arg+i))) return -EFAULT;
#else
	  buff[i]=get_user((char *)(arg+i));
#endif
	  if(buff[i]=='\0') break;
	}
      buff[i]='\0';         // 安全止
      printk("    Cmd: String: %s\n",buff);
      for(i=0;buff[i];i++)  // to lower
	{ if((buff[i]>='A')&&(buff[i]<='Z')) buff[i]+=('a'-'A'); }
      copy_to_user((void *)arg,buff,i);  // データ返還
      return 0;
    case OCI_STRUCT:
      copy_from_user(&ocis,(void *)arg,sizeof(ocis));
      for(i=0;;i++)  // 文字列長さカウント
	{
#if LINUX_VERSION_CODE >= 0x020100
	  if(get_user(buff[0],ocis.p+i)) return -EFAULT;
#else
	  buff[0]=get_user(ocis.p+i);
#endif
	  if(buff[0]=='\0') break;
	}
      ocis.len=i;
      copy_to_user((void *)arg,&ocis,sizeof(ocis));  // データ返還
      return 0;
    }
  return -EINVAL;
}


#if LINUX_VERSION_CODE >= 0x020100
static struct file_operations ocitest_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)
  NULL,         // u. int  poll(struct file *, struct poll_table_struct *)
  ocitest_ioctl,// int     ioctl(struct inode *, struct file *, u.int, u.long)
  NULL,         // int     mmap(struct file *, struct vm_area_struct *)
  ocitest_open, // int     open(struct inode *, struct file *)
  NULL,         // int     flush(struct file *)
  ocitest_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 ocitest_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)
  NULL,         // int  select(struct inode *, struct file *, int, select_table *)
  ocitest_ioctl,// int  ioctl(struct inode *, struct file *, u.int, u.long)
  NULL,         // int  mmap(struct inode *, struct file *, struct vm_area_struct *)
  ocitest_open,  // int  open(struct inode *, struct file *)
  ocitest_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


int init_module(void)
{
  printk("install '%s' into major %d\n",devname,devmajor);
  if(register_chrdev(devmajor,devname,&ocitest_fops))  // 登録
    {
      printk("device registration error\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");
    }
};
ファイル ocitest_p.c (アクセス試験用プログラム)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <unistd.h>

#include "ocitest.h"

int main(void)
{
  int fd;
  char buff[100];
  struct oci_struct ocis;

  fd=open("/dev/ocitest",O_RDWR);
  if(fd<0)
    { fprintf(stderr,"cannot open device\n"); return 1; }
  
  ioctl(fd,OCI_NOP);
  ioctl(fd,OCI_INT,12345);

  strcpy(buff,"ABCDEFGhijklmnOPQRSTUvwxyz0123456789");
  printf("ioctl(fd,OCI_STRING,\"%s\"); ->\n",buff);
  ioctl(fd,OCI_STRING,buff);
  printf("       -> %s\n",buff);

  ocis.p=buff;
  ioctl(fd,OCI_STRUCT,&ocis);
  printf("length of \"%s\"=%d\n",ocis.p,ocis.len);
  close(fd);
  return 0;
}
実行方法を以下に示します。 insdev をつかってみたりしてますが、基本的にはこれまでと同じ実行方法です。 ただ、cat ではなく 自前のocitest_p を実行します。
% gcc -c ocitest.c -Wall -Wstrict-prototypes -O -pipe -m486
% gcc -o ocitest_p ocitest_p.c

# insdev ocitest /dev/ocitest   さっそく使ってみたりする
auto-selected devcie major(c)=60
device 60:0: /dev/ocitest

% ./ocitest_p
ioctl(fd,OCI_STRING,"ABCDEFGhijklmnOPQRSTUvwxyz0123456789"); ->
       -> abcdefghijklmnopqrstuvwxyz0123456789
length of "abcdefghijklmnopqrstuvwxyz0123456789"=36

# rmdev ocitest
unlink '/dev/ocitest'

% dmesg | tail 
install 'ocitest' into major 60
ocitest: ioctl: cmd=0001, arg=4000A3C8
    Cmd: No operation
ocitest: ioctl: cmd=0002, arg=00003039
    Cmd: Integer: 12345
ocitest: ioctl: cmd=0003, arg=BFFFF874
    Cmd: String: ABCDEFGhijklmnOPQRSTUvwxyz0123456789
ocitest: ioctl: cmd=0004, arg=BFFFF86C
remove 'ocitest' from major 60
このデバイスでは コマンドとして1〜4の整数を受け付けます。
  1. OCI_NOP: なにもしません
  2. OCI_INT: ioctl のコマンドのあとの引数を整数として表示します。
  3. OCI_STRING: ioctl のコマンドのあとの引数を文字列へのポインタと して認識し、画面に出力するとともに、アルファベットを小文字に変換して ioctl の呼出元に返します。
  4. OCI_STRUCT: 文字列へのポインタを含む構造体へのポインタを受取り、文字列の長さ('\0'まで)を算出し、構造体に代入して返します。

長ったらしいプログラムに見えますが、実際のところ、ioctl の動作を受け持つ関数は、コマンド数値による switch の嵐で、やってることは単純です。 ただし、readやwriteのところでもやりましたが、ユーザプロセスから渡されたポインタにアクセスするためには copy_to_user(), copy_from_user()が必須です。 特に OCI_STRUCT では構造体を得るのに1回、返すのに1回つかっています。

と、文字列を得る部分には copy_from_user()を使わず、get_user()を使っています。 copy_??_user()がある程度の長さのデータを一度にやり取りするのに対して、1,2,4バイト(char,short,long)のデータをやり取りするための「ちょっとだけ」用の関数がget_user()です。

 Linux2.0:asm/segment.h 
(unsigned) char  get_user(const (unsigned) char  *user);
(unsigned) short get_user(const (unsigned) short *user);
(unsigned) long  get_user(const (unsigned) long  *user);
        *user の値が帰ります

 Linux2.2:asm/uaccess.h 
int get_user((unsigned) char  val, (unsigned) char  *user);
int get_user((unsigned) short val, (unsigned) short *user);
int get_user((unsigned) long  val, (unsigned) long  *user);
        変数 val に *user の値が代入されます。
        (valはポインタではありません。マクロで処理されます)
        定義上は void ですが、実際にはこんな形で呼び出されます。
        帰り値は転送完了で0、不正な転送で -14 が帰ります。
"unsigned" が () 書きなのは、これらはサイズのみをチェックするマクロであるため、unsigned はあってもなくともいいからです。
こういう根本的な関数が変ると、互換性のあるプログラムを書くのが面倒になります。 いっそのこと、get_user をさらにもう一段マクロで包むといいかもしれません。

get があるからには put もあります。

 Linux2.0:asm/segment.h 
void put_user(char value, char *user);
 Linux2.2:asm/uaccess.h 
int put_user(char value, char *user);
        (unsigned) short, long も同形式。
        不正な転送でやはり -14が帰ります。
補足
Linux 2.0 と 2.2 で変った点は、ユーザ空間の領域チェックをするようになったことです。 その点 2.0 は完全にアセンブラへマクロ展開だったのでおそらく速度はあったはずですが、2.2では安全性が上がったようです。


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