1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

独自のキャラクタデバイスを作成する

Last updated at Posted at 2022-10-07

独自のキャラクタデバイスの実装としては、PTYかFUSE/CUSEを使えば良いと思います。

PTY

  • openptyのmasterを通信に使う(slaveはttyname用)
  • cfmakerawしてtcsetattrするのが必須、しないとサーバー側で書き込んだ内容をそのまま読み込んでしまう
    • cfmakeraw/tcsetattrはサーバーかクライアントのどちらかでやればいいらしいが、クライアントは簡単にしたいので、サーバーを変更できるならサーバーでやったほうが良いと思います
    • なおFIFO(名前付きパイプ)だとそのまま読み込んでしまう事象は解消できませんでした、server/clientが対等であるからかも。
  • REPサーバーでないなら(PUBとか)、recvとsendは別スレッドにする必要がある(サンプルでは未考慮)

FUSE/CUSE

  • サーバーを起動できるのはrootだけ
  • CUSEのオプションにパーミッション設定がないらしく、chmodを使う必要がある
  • 終了はCtrl+Cか<<<1 sudo dd of=/sys/devices/virtual/cuse/DEVNAME/abortによって行う
  • デバイス名はコンテナ間でユニークである必要がある
  • コンテナ内には/dev/DEVNAMEが自動で作成されないので、mknodで作ってあげる必要がある

Sample

Server

簡単どころで、memfrobに相当する操作を1バイト単位で行うサンプルを…

※安全性考慮されてませんしバッファもありません、blocked ioを投げると間違いなく死にます

PTY

server_pty.c
//usr/bin/env true; tmpfile=$(mktemp); gcc -O2 -std=gnu99 -xc -o $tmpfile $0 -lutil && $tmpfile "$@"; rm $tmpfile; exit
#include <stdio.h>
#include <unistd.h>
#include <pty.h>
#include <termios.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/select.h>

const char *devicename = "frobnicate";

int main(){
    int master = -1;
    int slave = -1;

    // open pty and make symlink
    if(1){
        char buf[256];
        openpty(&master, &slave, NULL, NULL, NULL);
        ttyname_r(slave, buf, sizeof(buf));
        symlink(buf, devicename);
    }

    // set raw io (otherwise server can read its own output)
    // btw it seems to be enough to set it on either server or client
    {
        struct termios t;
        tcgetattr(master, &t);
        cfmakeraw(&t);
        tcsetattr(master, TCSANOW, &t);
    }

    fcntl(0,F_SETFL,O_NONBLOCK);
    fcntl(master,F_SETFL,O_NONBLOCK);

    unsigned char b;
    struct timeval tv;
    fd_set nfds;
    puts("Press enter key to stop server.");
    for(;read(0, &b, 1)<=0;){
        FD_ZERO(&nfds);
        FD_SET(master, &nfds);
        tv.tv_sec = 0;
        tv.tv_usec = 1000;
        if(select(master+1, &nfds, NULL, NULL, &tv)<=0)continue;
        for(;read(master, &b, 1)<=0;);
        //putchar(b);
        b^=42;
        for(;write(master, &b, 1)<=0;);
    }

    unlink(devicename);
    return 0;
}

FUSE/CUSE

server_cuse.c
//usr/bin/env true; tmpfile=$(mktemp); gcc -O2 -std=gnu99 -xc -o $tmpfile $0 -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse -pthread -lfuse && sudo PATHNAME=frobnicate $tmpfile -f "$@"; rm $tmpfile; exit

#define FUSE_USE_VERSION 29

#include <cuse_lowlevel.h>
#include <fuse_opt.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <libgen.h> // basename

static int b = -1;

static void myopen(fuse_req_t req, struct fuse_file_info *fi){
	fuse_reply_open(req, fi);
}

static void myread(fuse_req_t req, size_t size, off_t off, struct fuse_file_info *fi){
	(void)fi;
    if(b<0){fuse_reply_err(req, EAGAIN);return;}
	fuse_reply_buf(req, (char*)&b, 1);
    b = -1;
}

static void mywrite(fuse_req_t req, const char *buf, size_t size, off_t off, struct fuse_file_info *fi){
	(void)fi;
    if(!size){fuse_reply_err(req, EINVAL);return;}
    b = (unsigned char)buf[0];
    b ^= 42;
	fuse_reply_write(req, 1);
}

static const struct cuse_lowlevel_ops operations = {
	.open  = myopen,
	.read  = myread,
	.write = mywrite,
};

static void *makeallwritable(void *p){
    char *dev_path = p;
    for(;access(dev_path, F_OK);)usleep(1000);
    if(chmod(dev_path, 0666)){printf("failed to chmod: %d\n", errno);}
}

int main(int argc, char **argv){
	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
	char dev_name_arg[128] = "DEVNAME=";
    char dev_path[128] = "/dev/";
	const char *dev_info_argv[] = { dev_name_arg };
	struct cuse_info ci;
	int ret = 1;

	if(fuse_opt_parse(&args, NULL, NULL, NULL))goto out;
    char *pathname = getenv("PATHNAME");
    char *dev_name = basename(pathname);
    if(!pathname)goto out;
    strncat(dev_name_arg, dev_name, sizeof(dev_name_arg) - sizeof("DEVNAME="));
    strncat(dev_path, dev_name, sizeof(dev_path) - sizeof("DEVNAME="));

	memset(&ci, 0, sizeof(ci));
	ci.dev_major = 231;
	ci.dev_minor = 1;
	ci.dev_info_argc = 1;
	ci.dev_info_argv = dev_info_argv;
	ci.flags = 0;

    // to allow client access from normal user, have to call chmod by creating a thread...
    pthread_t pthread;
    pthread_create(&pthread, NULL, &makeallwritable, dev_path);
    symlink(dev_path, pathname);
    printf("Press Ctrl+C or '<<<1 sudo dd of=/sys/devices/virtual/cuse/%s/abort' to stop server.\n", dev_name);
	ret = cuse_lowlevel_main(args.argc, args.argv, &ci, &operations, NULL);
    unlink(pathname);
    pthread_join(pthread, NULL);
out:
	fuse_opt_free_args(&args);
	return ret;
}

Client

参考までにクライアントはこんな感じです、終端検知がないのでcatとかはできないのですね、、

あ、当然ながら同じファイルに複数箇所から接続すると死ぬので注意です。デバイス通信では一般的に言えることだと思いますが。

client.c
//usr/bin/env true; tmpfile=$(mktemp); gcc -O2 -std=gnu99 -xc -o $tmpfile $0 && $tmpfile "$@"; rm $tmpfile; exit
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>

const char *devicename = "frobnicate";

int main(){
    int fd = open(devicename, O_RDWR|O_NONBLOCK/*|O_NOCTTY*/);

    fd_set nfds;
    int c;
    for(;(c=getchar())>=0;){
        for(;write(fd, &c, 1)<=0;);
        FD_ZERO(&nfds);
        FD_SET(fd, &nfds);
        select(fd+1, &nfds, NULL, NULL, NULL);
        for(;read(fd, &c, 1)<=0;);
        putchar(c);
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?