独自のキャラクタデバイスの実装としては、PTYかFUSE/CUSEを使えば良いと思います。
PTY
- openptyのmasterを通信に使う(slaveはttyname用)
- cfmakerawしてtcsetattrするのが必須、しないとサーバー側で書き込んだ内容をそのまま読み込んでしまう
- cfmakeraw/tcsetattrはサーバーかクライアントのどちらかでやればいいらしいが、クライアントは簡単にしたいので、サーバーを変更できるならサーバーでやったほうが良いと思います
- cfmakerawとかは 情報が https://github.com/Greenzie/emuccan-b202/blob/main/utility/main.c にあった
- なおFIFO(名前付きパイプ)だとそのまま読み込んでしまう事象は解消できませんでした、server/clientが対等であるからかも。
- cfmakeraw/tcsetattrはサーバーかクライアントのどちらかでやればいいらしいが、クライアントは簡単にしたいので、サーバーを変更できるならサーバーでやったほうが良いと思います
- 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);
}
}