こんにちはAnyTechの岩井です。今回は簡単なデバイスドライバをC言語で作成し、それをC++で呼び出してみるところまでやってみようと思います。
背景
AIが製造現場で普及していく中で、AIを動かすオンプレサーバからアナログ/デジタル入出力を行って外部機器と連携をしてほしいという話が出てくるようになってきました。PCから信号の入出力を行うAD/DAボードやDIOボードは昔から様々な製品が出ているので、サーバにボードを取り付けることでハードウェア的には外部機器との連携はできますが、ソフト側ではボードのデバイスドライバが必要です。最近はボードのメーカからUbuntu用のドライバも用意されてますが、少し昔はUbuntuのバージョンが対応してなくて自分で書くしかないということもありました。今でもUbuntuのバージョンが上がったらドライバが対応してないということも可能性としてはありますし、個人的にもデバイスドライバなんて書く機会が少なくて忘れていっちゃうので備忘録兼ねて記事を書きました。
実行環境
- OS : Ubuntu22.04
- カーネル : 6.5.0-15-generic
デバイスドライバ
デバイスドライバはC言語で書きますが、デバイスドライバはカーネルモジュールとして作成するのでint main()
から始まらない等少し特殊なところがあります。
今回使うコードは以下の通りです。
カーネルバージョンによって書き方が若干変わったりすることがあるのでもしかしたら動かない環境があるかもしれないです。
ソースコード
test_device.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> MODULE_LICENSE("GPL"); static struct class *test_device_class; static int major; // major番号 static const int MINOR_START = 0; // minor番号の始めの番号 static const int NUM_MINOR = 3; // 同じmajor番号でいくつのデバイスを使うかの数 static dev_t dev_id; static const char* DEVICE_NAME = "TEST_DEVICE"; static struct cdev c_dev; /* openで呼ばれる関数 今回はprintkするだけ DIOボード等ではデバイスオープンを行う */ int test_open(struct inode *inode, struct file *file){ printk("Device Open! major = %d, name = %s\n", major, DEVICE_NAME); return 0; } /* closeで呼ばれる関数 今回はprintkするだけ DIOボード等ではデバイスクローズを行う */ int test_close(struct inode *inode, struct file *file){ printk("Device Close! major = %d, name = %s\n", major, DEVICE_NAME); return 0; } /* readで呼ばれる関数 今回は{66, 75, 45, 2, 95}という配列をC++に返す DIOボード等では信号を読み取り、その結果をC++に返す */ ssize_t test_read(struct file *, char *array, size_t size, loff_t *){ char read_array[5] = {66, 75, 45, 2, 95}; // カーネル空間からユーザ空間に配列をコピー copy_to_user(array, read_array, size); printk("Device Read! major = %d, name = %s\n", major, DEVICE_NAME); return 0; } /* writeで呼ばれる関数 配列の中身をC++から受け取り、それをprintkする DIOボード等では出力信号の命令をC++から受け取り、その信号をデバイスから出力する */ ssize_t test_write(struct file *, const char *array, size_t size, loff_t *){ char write_array[5]; // ユーザ空間からカーネル空間に配列をコピー copy_from_user(write_array, array, size); printk("Device Write! major = %d, name = %s\n", major, DEVICE_NAME); for (int i=0; i < 5; i++){ printk(" write array[%d] = %d", i, write_array[i]); } return 0; } /* 呼び出し側では/dev/test_deviceをopen/close read/writeしてこのドライバにアクセスする openやreadでどの関数に対応付けるかをここに書く */ struct file_operations fops = { .open = test_open, .release = test_close, .read = test_read, .write = test_write, }; /* insmodで一番最初に呼ばれる関数 デバイスドライバの登録を行う */ int init_module(void) { int ret; // 自動的にmajor番号を決めてもらう ret = alloc_chrdev_region(&dev_id, MINOR_START, NUM_MINOR, DEVICE_NAME); if(ret<0) { printk(KERN_ERR "Failed alloc_chrdev_region!\n"); return -1; } major = MAJOR(dev_id); fops.owner = THIS_MODULE; cdev_init(&c_dev, &fops); ret = cdev_add(&c_dev, dev_id, NUM_MINOR); if(ret!=0) { printk(KERN_ERR "Failed cdev_add!\n"); return -1; } //ここからはinsmod時に自動的に/dev/test_device_0や/dev/test_device_1を作ってもらうための処理 // /sys/class/test_deviceを作ってもらう test_device_class = class_create("test_device"); if (IS_ERR(test_device_class)) { printk(KERN_ERR "Failed class_create!\n"); cdev_del(&c_dev); unregister_chrdev_region(dev_id, NUM_MINOR); return -1; } // /sys/class/test_device/test_device_0や/sys/class/test_device/test_device_1を作ってもらう for(int minor; minor < NUM_MINOR; minor++){ device_create(test_device_class, NULL, MKDEV(major, minor), NULL, "test_device_%d", minor); } printk("Device Init! major = %d, name = %s\n", major, DEVICE_NAME); return 0; } /* rmmodで呼ばれる関数 デバイスドライバの登録を解除する */ void cleanup_module(void) { cdev_del(&c_dev); unregister_chrdev_region(dev_id, NUM_MINOR); printk("Device Release! major = %d, name = %s\n", major, DEVICE_NAME); }
Makefile
obj-m := test_device.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: rm -rf *.ko *.o *.order *.symvers *.cmd *.mod.c *.mod
解説
test_device.cとMakefileを同じディレクトリに置いてmake
を実行するとtest_device.koというカーネルモジュールが出来上がります。
実際に動かすためにはinsmod
でカーネルモジュールとして登録する必要があるのでinsmod test_device.ko
を実行します。
dmesg
でprintkの結果をみるとDevice Init!が表示されてます。このようにカーネルモジュールとして登録すると一番最初にint main()
ではなくinit_module
が呼ばれます。
init_module
では主にデバイスドライバを登録する処理を行います。
デバイスドライバはその種類毎にmajor番号が付き、デバイス毎にminor番号が付きます。例えば同じ製品のDIOボードを2枚搭載したとき、majorでDIOボード製品自体を示しminorで1枚目か2枚目か識別できます。
C++からはmajorとminorを指定してドライバにアクセスするといったことはできず、先に/dev/test_device_0や/dev/test_device_1を作成してそれらにmajorとminorを紐付けておく必要があります。
紐付け作業は手動で頑張ることもできるのですが、init_module
内で書いてある通りclass_create
とdevice_create
を実行するとmajor番号の割り振り、紐付け及び/dev/test_device_0等の作成まで自動で行ってくれます。そのため、insmod
を実行したら/dev/test_device_0の権限を666にしてあげるだけでC++からアクセス可能になります。
これでデバイスドライバを使用する準備は整いましたが、長くなったので今回はここまでにして次回C++からアクセスしてみようと思います。