本記事では、UNIX系OSで主に使用されているシステムコール ioctl についてサンプルコードを用いて分かりやすく解説します。
特徴
ioctl (Input/Output Control) システムコールは、デバイス固有の入出力操作やカーネルインターフェースを提供するために使われる汎用的なシステムコールです。ioctlを使用することで、デバイスに対して、read や write などのシステムコールを用いた入出力操作では実現しにくい、様々な操作を実行することができます。
ioctlシステムコールには、以下のような特徴があります。
- ファイルディスクリプタを通じてデバイスを操作
- デバイス固有の操作が可能
- 様々なデータ型が使用可能
- UNIX系OSに対応
- 非標準のAPI
- 移植性が低い
ファイルディスクリプタを通じてデバイスを操作
ioctlは、ファイルディスクリプタを通じてデバイスを操作します。この時、ファイルディスクリプタは、デバイスファイルと呼ばれる特殊なファイルを指します。
ファイルディスクリプタに関しては、「ファイルディスクリプタについて解説」の記事で解説しています。また、デバイスファイルに関しても、「デバイスファイルについて解説」の記事で解説しています。
デバイス固有の操作が可能
ioctlは、デバイス固有の多様な操作を行うことができます。例えば、シリアルポートの設定やNIC1の情報取得などを行うことができます。
また、一つのデバイスに対しても、リクエストコードを使用して、様々な命令を行うことができます。例えば、設定変更や情報取得、機能の有効化・無効化など、一つのデバイスに対して実行したい命令ごとに、それぞれリクエストコードを定義することができます。
様々なデータ型が使用可能
ioctlでは、単純な数値から複雑な構造体まで、様々なデータ型のやり取りが可能です。
一方、read や write などのシステムコールでは、バイトストリーム (バイト配列) でのやり取りであり、特定のデータ型を指定することはできません。
UNIX系OSに対応
ioctlは、主にUNIX系OSで使用されています。ioctlが対応しているOSの例を以下に挙げます。
- Linux (Ubuntu, CentOS)
- BSD系OS (FreeBSD, OpenBSD)
- macOS
非標準のAPI
ioctlは様々なUNIX系OSで使用されていますが、 read や write などのシステムコールとは異なり、POSIXなどの標準規格に準拠していない非標準のAPIです。
ioctlの仕様は、OSごとに定義されています。そのため、OSごとに機能が少し異なる場合があります。
POSIXに関しては、「POSIXについて解説」の記事で解説しています。
移植性が低い
ioctlは、OSやデバイスに依存しているため、ioctlを使用しているコードを他のプラットフォームに移植することが難しいです。逆に、ioctlはOSやデバイスに合わせてカスタマイズできるため、柔軟性は高いといえます。
使い方
関数詳細
ioctl の関数詳細 (Linuxでの定義) を以下に示します。
[書式]
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
[引数]
引数名 | 意味 |
fd | 操作を行う対象のデバイスファイルのファイルディスクリプタを指定します。 ※デバイスファイル以外のファイルを扱う場合もあります。 |
request | 実行したいデバイス固有の操作を行うリクエストコードを指定します。 |
… | リクエストコードに応じて、ioctl操作に必要な追加情報を指定します。可変長引数です。 |
[戻り値]
戻り値は操作するデバイスに依存します。
成功した場合、基本的には0 を返します。0 より大きい場合もあります。
失敗した場合、負の値を返します。
サンプルコード
ioctlを使用したサンプルコード (カーネル用とユーザーアプリ用) を用意したので、内容について簡単に説明し、実行していきたいと思います。
環境は Ubuntu 22.04 LTS でカーネルバージョンは 6.5.0-17-generic です。
今回使用するファイルは以下の4つで、それぞれ同じディレクトリ階層に置きます。
- ioctl_kernel_sample.c (カーネル用のサンプルコード)
- ioctl_user_sample.c (ユーザーアプリ用のサンプルコード)
- ioctl_sample.h (共通ヘッダファイル)
- Makefile
Linuxのデバイスドライバ(カーネルモジュール)のプログラミングの基本的な内容に関しては、「デバイスファイルについて解説」と「カーネルモジュールについて解説」の記事で解説しています。
[ioctl_kernel_sample.c]
ioctl_kernel_sample.c は、カーネルモジュールのサンプルコードです。ユーザー空間でこのカーネルモジュールに対応するデバイスファイル対して ioctl が呼ばれたときに実行される処理を記述しています。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include "ioctl_sample.h"
// デバイスのメジャーナンバー
int major_number;
// デバイス名
#define DEVICE_NAME "ioctl_sample"
// 仮のデータストレージ
static ioctl_sample_data sample_data = {
.number = 0,
.string = "initial data",
};
static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd)
{
case IOCTL_SAMPLE_WRITE:
if (copy_from_user(&sample_data, (ioctl_sample_data *)arg, sizeof(ioctl_sample_data)))
{
return -EACCES;
}
printk(KERN_INFO "Received data from user space - number: %d, string: %s\n", sample_data.number, sample_data.string);
break;
case IOCTL_SAMPLE_READ:
if (copy_to_user((ioctl_sample_data *)arg, &sample_data, sizeof(ioctl_sample_data)))
{
return -EACCES;
}
printk(KERN_INFO "Sent data to user space - number: %d, string: %s\n", sample_data.number, sample_data.string);
break;
default:
return -EINVAL;
}
return 0;
}
// file_operations構造体
static struct file_operations fops = {
.unlocked_ioctl = device_ioctl,
};
// モジュール初期化
static int __init ioctl_sample_init(void)
{
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0)
{
printk(KERN_ALERT "Failed to register a major number\n");
return major_number;
}
printk(KERN_INFO "Registered correctly with major number %d\n", major_number);
return 0;
}
// モジュール終了
static void __exit ioctl_sample_exit(void)
{
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_INFO "Goodbye!\n");
}
module_init(ioctl_sample_init);
module_exit(ioctl_sample_exit);
MODULE_LICENSE("GPL");
ユーザー空間でioctl関数が呼ばれたときに、カーネル側で実行される関数が device_ioctl() です。
第2引数 cmd には、ioctl() を呼び出すときに第2引数に指定されるリクエストコードが渡されます。
第3引数 arg には、ioctl() を呼び出すときに第3引数に指定されるユーザー空間上のデータのアドレスが渡されます。アドレスのデータにアクセスするためには、ポインタ型にキャストし、ポインタを通じてアクセスすることが一般的です。
switch文の case の部分に指定されている IOCTL_SAMPLE_WRITE と IOCTL_SAMPLE_READ はリクエストコードです。リクエストコードはユーザー空間でも使用する必要があるので、カーネル空間とユーザー空間で共通のヘッダファイル (ioctl_sample.h) で定義しています。また、キャストで使用している型 (ioctl_sample_data型) も同様に共通のヘッダファイル (ioctl_sample.h) で定義しています。
IOCTL_SAMPLE_WRITE が指定されると、copy_from_user() を呼び出して、ユーザー空間から受け取った値をカーネル空間で定義している変数 sample_data にコピーしています。
IOCTL_SAMPLE_READ が指定されると、copy_to_user() を呼び出して、sample_data の値を arg が指すアドレスにコピーしています。
[ioctl_user_sample.c]
ユーザー空間で実行するアプリケーションのサンプルコードです。ioctl_kernel_sample.c から作成したカーネルモジュールに対して ioctl を実行します。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include "ioctl_sample.h"
int main()
{
int fd;
ioctl_sample_data data;
fd = open("/dev/ioctl_sample", O_RDWR);
if (fd < 0)
{
perror("Failed to open the device");
return errno;
}
data.number = 42;
strcpy(data.string, "Hello from user space");
// データをカーネルモジュールに書き込む
if (ioctl(fd, IOCTL_SAMPLE_WRITE, &data) < 0)
{
perror("Failed to write to the device");
return errno;
}
printf("Sent data to kernel space - number: %d, string: %s\n", data.number, data.string);
// データをカーネルモジュールから読み込む
if (ioctl(fd, IOCTL_SAMPLE_READ, &data) < 0)
{
perror("Failed to read from the device");
return errno;
}
printf("Received data from kernel space - number: %d, string: %s\n", data.number, data.string);
close(fd);
return 0;
}
ioctl_user_sample.c では、以下の流れで処理を行っています。
- デバイスファイルをオープン
- ioctl() (リクエストコードに IOCTL_SAMPLE_WRITE を指定) を呼び出し、カーネルモジュールにioctl_sample_data型のデータを書き込む
- ioctl() (リクエストコードに IOCTL_SAMPLE_READ を指定) を呼び出し、カーネルモジュールに書き込まれたioctl_sample_data型のデータを読み出す
- デバイスファイルをクローズ
[ioctl_sample.h]
ioctl_sample.h は、ユーザーアプリとカーネルモジュールで使用する共通のヘッダファイルです。ioctl_kernel_sample.c と ioctl_user_sample.c でインクルードします。
#ifndef IOCTL_SAMPLE_H
#define IOCTL_SAMPLE_H
#include <linux/ioctl.h>
typedef struct
{
int number;
char string[256];
} ioctl_sample_data;
#define IOCTL_SAMPLE_WRITE _IOW('k', 1, ioctl_sample_data *)
#define IOCTL_SAMPLE_READ _IOR('k', 2, ioctl_sample_data *)
#endif
ioctl_sample.h では、構造体 (ioctl_sample_data型) とリクエストコード (IOCTL_SAMPLE_WRITE, IOCTL_SAMPLE_READ) を定義しています。
リクエストコードの作成には、しばしばマクロが使用されます。マクロには以下の4種類が存在します。
※引数 type にはデバイス固有の識別子、引数 nr には操作を一意に識別する番号、引数 size にはカーネルに渡すデータ型をそれぞれ指定します。
マクロ | 説明 |
_IO(type, nr) | データの転送を伴わない操作に使用されます。 |
_IOW(type, nr, size) | ユーザー空間からカーネル空間へデータを書き込む操作に使用されます。 |
_IOR(type, nr, size) | ユーザー空間からカーネル空間のデータを読み取る操作に使用されます。 |
_IOWR(type, nr, size) | ユーザー空間とカーネル空間の間で双方向のデータ転送(読み書き両方)を行う際に使用されます。 |
[Makefile]
カーネルモジュールをビルドするための Makefile です。
obj-m += ioctl_kernel_sample.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
実行手順
[デバイスファイルの作成]
makeコマンドでカーネル用のソースコードをビルドします。
$ make
カーネルオブジェクト ioctl_kernel_sample.ko が生成されるので、insmodコマンドでカーネルにロードします。
$ sudo insmod ioctl_kernel_sample.ko
カーネルログにドライバのメジャー番号が出力されるので確認します。
$ sudo dmesg | tail
...
[ 151.755276] ioctl_kernel_sample: loading out-of-tree module taints kernel.
[ 151.755288] ioctl_kernel_sample: module verification failed: signature and/or required key missing - tainting kernel
[ 151.756081] Registered correctly with major number 234
mknodコマンドでキャラクタデバイスファイル (/dev/ioctl_sample) を作成します。メジャー番号はカーネルログで確認した値になります。
$ sudo mknod /dev/ioctl_sample c 234 0
「/dev/ioctl_sample」はデバイスファイル名、「c」はキャラクタデバイス、「234」はメジャー番号、「0」はマイナー番号を表します。
作成したデバイスファイルは、そのままでは読み書きできないので、アクセス権限 (666) を付与します。
$ sudo chmod 666 /dev/ioctl_sample
[ユーザーアプリの作成・実行]
gccコマンドでユーザーアプリ用のサンプルコードをビルドします。
gcc ioctl_user_sample.c -o ioctl_user_sample
サンプルコードを実行します。以下のようなログが標準出力されます。
$ ./ioctl_user_sample
Sent data to kernel space - number: 42, string: Hello from user space
Received data from kernel space - number: 42, string: Hello from user space
カーネル側のログも確認します。
$ sudo dmesg | tail
...
[ 278.723423] Received data from user space - number: 42, string: Hello from user space
[ 278.723531] Sent data to user space - number: 42, string: Hello from user space
参考資料
脚注
- Network Interface Cardの略で、コンピュータなどをネットワークに繋げるための拡張部品のこと。 ↩︎