2017年1月10日火曜日

Cのselect、epollを使ったI/O多重化


複数のファイルディスクリプタを監視し、その中のいずれが入出力可能な状態であるかを確認するCのシステムコールとしては、select、epoll、kqueue(BSD系)が有名だろう。
ここでは、selectとepollを使い、1プロセス1スレッドでのイベント駆動によるI/O多重化コードを備忘録として残しておく。

(参考)
最近ではselectではなく、epoll、kqueueが使われることが多い。
selectは待ち受けられるファイルディスクリプタの数に上限があり、またパフォーマンス問題も存在するためである。
I/Oからの入力に応じて発生するイベントを処理するライブラリとしてはlibevent, libev, libuvなどがあるがこの内部でもepollやkqueueが利用されている。
※libevはlibeventの速度改善、FDの制限撤廃の対応がされた改良版である。
※libuvはnode.js用のためにlibevをベースに開発されたライブラリである。



(select.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#define oops(m,x) { perror(m); exit(x); }
void showdata(char *, int);
int main(int ac, char *av[]) {
int fd1, fd2;
struct timeval timeout;
fd_set readfds;
int maxfd;
int retval;
if ( ac != 4 ){
fprintf(stderr, "usage: %s [file] [file] [timeout]\n", *av);
exit(1);
}
if ((fd1 = open(av[1], O_RDONLY)) == -1)
oops(av[1], 1);
if ((fd2 = open(av[2], O_RDONLY)) == -1)
oops(av[2], 1);
maxfd = 1 + (fd1 > fd2 ? fd1 : fd2);
while(1) {
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
timeout.tv_sec = atoi(av[3]);
timeout.tv_usec = 0;
printf("before wait\n");
retval = select(maxfd, &readfds, NULL, NULL, &timeout);
printf("after wait\n");
if( retval == -1 )
oops("select", 1);
if ( retval > 0 ){
if (FD_ISSET(fd1, &readfds))
showdata(av[1], fd1);
if (FD_ISSET(fd2, &readfds))
showdata(av[2], fd2);
}
else
printf("no input for %d seconds\n", atoi(av[3]));
}
}
void showdata(char *fname, int fd) {
char buf[BUFSIZ];
int n;
printf("%s: ", fname);
fflush(stdout);
n = read(fd, buf, BUFSIZ);
if ( n == -1 ) oops(fname, 1);
write(1, buf, n);
write(1, "\n", 1);
}
view raw select.c hosted with ❤ by GitHub


(epoll.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define NEVENTS 16
#define oops(m,x) { perror(m); exit(x); }
void showdata(char *, int);
int main(int ac, char *av[]) {
int fd1, fd2;
int epfd;
struct epoll_event ev, ev_ret[NEVENTS];
int nfds;
int timeout;
if ( ac != 4 ){
fprintf(stderr, "usage: %s [file] [file] [timeout]\n", *av);
exit(1);
}
if ((fd1 = open(av[1], O_RDONLY)) == -1)
oops(av[1], 2);
if ((fd2 = open(av[2], O_RDONLY)) == -1)
oops(av[2], 3);
timeout = atoi(av[3]);
epfd = epoll_create(NEVENTS);
if (epfd < 0) {
oops("epoll_create", 1);
}
memset(&ev, 0, sizeof(ev));
ev.events = EPOLLIN;
ev.data.fd = fd1;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev) != 0) {
oops("epoll_ctl", 1);
}
memset(&ev, 0, sizeof(ev));
ev.events = EPOLLIN;
ev.data.fd = fd2;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev) != 0) {
oops("epoll_ctl", 1);
return 1;
}
while (1) {
printf("before wait\n");
nfds = epoll_wait(epfd, ev_ret, NEVENTS, timeout*1000);
if (nfds < 0) {
oops("epoll_wait", 1);
return 1;
}
printf("after wait\n");
if (nfds == 0) {
printf("no input for %d seconds\n", atoi(av[3]));
}
for (int i=0; i<nfds; i++) {
if (ev_ret[i].data.fd == fd1) {
showdata(av[1], fd1);
} else if (ev_ret[i].data.fd == fd2) {
showdata(av[2], fd1);
}
}
}
return 0;
}
void showdata(char *fname, int fd) {
char buf[BUFSIZ];
int n;
printf("%s: ", fname);
fflush(stdout);
n = read(fd, buf, BUFSIZ);
if ( n == -1 ) oops(fname, 5);
write(1, buf, n);
write(1, "\n", 1);
}
view raw epoll.c hosted with ❤ by GitHub


$ gcc ファイル名

$ ./a.out /dev/tty /dev/input/mouse0 10
hello
/dev/tty: hello

no input after 10 seconds

マウス操作
/dev/input/mouse0: (


(補足)
readで指定したバッファサイズ以上のデータがあった場合、ループでepoll_waitの処理へ戻るとはどのような動作をするか分かるだろうか。
残りのデータを引き続き読み込めると判断されるた、待ち状態にはならない。
しかし、次のようにイベント条件を指定した場合は一度しかイベントは発火しない。
ev.events = EPOLLIN | EPOLLONESHOT

イベントを継続させる場合には、再度epoll_ctlを同じepfdとEPOLL_CTL_MOD値を渡して呼べばよい。
epoll_ctl(epfd, EPOLL_CTL_MOD, fd1, &ev)