0.1秒周期で特定の関数を実行させたくて, signal(3)を使うことにしました。
main() {
int arg1[2], arg2[2];
double arg3[2], arg4[2], arg5;
/* foo */
signal(SIGALRM, function(arg1, arg2, arg3, arg4, arg5));
/* bar */
ualarm(100000, 100000);
for(;;) {
/* hoge */
}
}
しかし, signal(3)の第2引数は関数へのポインタですから,
このように引数を渡すことができません。
実際, コンパイルしても
main.c: In function `main':
main.c:25: invalid use of void expression
となります。
/usr/src/usr.bin/more や /usr/ports/mail/fetchmailなど,
signal(3)を使っているソースを覗いてみましたが,
どうもよくわかりません。
実行したい関数に引数を渡すには, どのようにすればよいのでしょうか。
まさか, 全部大域変数にしなければいけないのでしょうか。
--
大友紘之
wbha...@tky3.3web.ne.jp
> 大友です。
> FreeBSD(98) 3.5-STABLEでプログラムを組んでいます。
>
> 0.1秒周期で特定の関数を実行させたくて, signal(3)を使うことにしました。
signal の第二引数には、signal から第一引数としてシグナル番号を
うける関数へのポインタを渡します。よーするに使い方が根本的にまちがえてます。
> 実行したい関数に引数を渡すには, どのようにすればよいのでしょうか。
> まさか, 全部大域変数にしなければいけないのでしょうか。
ご明察。signal 自体がグローバルでプロセスにつき一つしかなく、
追加の引数を渡してくれるような親切な機構は備えていません。
一度汎用で擬似的に複数のタイマを扱える + 引数を渡せるハンドラを
書いてしまえばのちのち流用が効いて便利かもしれません。
# エントリされたタイマをリスト管理して、一番早いもので
# ualarm して時間経過したものを順次呼び出せばよい
# ちなみに Xt などのツールキットは、タイマがたくさん欲しいので、
# 全部自前でこの手のハンドラを持ってます。
# ユーザは勝手に SIG_ALARM を処理してはいけません
--
渡邊剛 (Watanabe, Go) goc...@astec.co.jp
ASTEC e-commerce, Inc.
> FreeBSD(98) 3.5-STABLEでプログラムを組んでいます。
>
> 0.1秒周期で特定の関数を実行させたくて, signal(3)を使うことにしました。
>
(snip)
> /usr/src/usr.bin/more や /usr/ports/mail/fetchmailなど,
> signal(3)を使っているソースを覗いてみましたが,
> どうもよくわかりません。
>
> 実行したい関数に引数を渡すには, どのようにすればよいのでしょうか。
> まさか, 全部大域変数にしなければいけないのでしょうか。
singal(3) を使うとすれば、そうです。
単に「 0.1秒周期で特定の関数を実行させたい」
ということにのみ注目するなら、代案はいろいろあります。
いうまでもないことですが 状況によって どれを選択するかが変わります。
大友さん、こんにちは。
> 実行したい関数に引数を渡すには, どのようにすればよいのでしょうか。
> まさか, 全部大域変数にしなければいけないのでしょうか。
「まさか」と申しますが、割り込み関数では大域変数からしか変数を参照できな
いのでは(^_^;)、たとえ少しのお化粧でかくしたとしてもですよ(^○^)(ファイルレ
ベルでstaticにする等)。
しかもsignalは「例外状態に対処する方法」を設定するものですからそれに必要
な変数しか渡されません。
signal関数がコンパイル済みでライブラリ化されているのと(コンパイラの宿
命)、引数の並びをsignal関数に渡すルールが無い(仕様)ので自由に引数を設定する
ことはできません。Cではよく構造体などのアドレスを一つ渡すなどして自由度を確
保しますが、signalにはこの機能も無いですね(^○^)。
それと、signalは個々のシグナルに設定できる処理関数は一つだけですよね。た
とえば同じプログラムの中で2秒毎に表示を更新するプログラムを作ったりすると、
もうどちらのタイミングで入ってくるかはualarmの実行順序に依存したりしてどちら
かは無視されてしまいます。多分もっとインターバルタイマー向けの関数があると思
うので探して見てください(^○^)。
From: wata...@gta.amita.co.jp (watanabe)
Date: Thu, 13 Jul 2000 10:15:52 JST
Newsgroups: fj.comp.lang.c
Message-ID: <8kj4vu$h0d$1...@nozomi.amico.co.jp>
Subject: Re: signal(3)に引数のある関数を渡す
> 単に「 0.1秒周期で特定の関数を実行させたい」
> ということにのみ注目するなら、代案はいろいろあります。
これは, どういったものが考えられるのでしょうか。
man -k {interval | time} などしても,
私には setitimer(2)や ualarm(3)を使うことくらいしか思い付きませんでした。
--
大友紘之
wbha...@tky3.3web.ne.jp
In <<8knf9t$dh$1...@news.tky.3web.ne.jp>>, Ootomo Hiroyuki wrote:
>
> > 単に「 0.1秒周期で特定の関数を実行させたい」
> > ということにのみ注目するなら、代案はいろいろあります。
>
> これは, どういったものが考えられるのでしょうか。
> man -k {interval | time} などしても,
> 私には setitimer(2)や ualarm(3)を使うことくらいしか思い付きませんでした。
>
少なくとも あと、select(2)を使う手があると思いますが。。。
--
SHIROYAMA Takayuki : p...@fortune.nest.or.jp
私の知っている範囲では他にpoll, select, usleep とか、
termiosをカノニカル モードにしておいて
制御文字を適当に設定して(0.1秒の精度で設定できます)readするとか、
Xt関係なら XtAppAddTimeOut とかあります。
例:
for(;;) {
usleep(100000L); /* 0.1秒停止 */
a();
}
for(;;) {
struct timeval wait_time;
wait_time.tv_sec = 0;
wait_time.tv_usec = 100000; /* 0.1秒停止 */
select(0,0,0,0,&wait_time);
a();
}
もともと、FreeBSDのタイマーは精度は期待できないので
(他のプロセスが動いていて、負荷がかかったときなど特に)
たいていの場合、この利用法で十分だと思います。
リクエストした秒数が経過したら呼び出される程度に
考えていたほうがよいかもしれません。
0.1秒間隔でよばれるようにする場合、fork か pthreadを使って
待つプロセス(スレッド)と、動作するプロセス(スレッド)を作る
とかすればよいのですが、動作する部分が
0.1秒以内に処理できなかったらどう振る舞うか
(例えば次の0.1秒のタイミングまで待つか、
あるいは終了後に速やかに次のターンを実行するか)
によってプログラムの書き方は異なってきます。
あれこれ考え出すと、意外と複雑なコーディングになります。
☆
X版は一般には あまり使われないでしょうね。
#include <X11/Intrinsic.h>
XtAppContext app_context;
unsigned long interval = 100000UL;
void b(XtPointer closure,XtIntervalId *id) /* 呼び出される関数 */
{
:
/* 次の呼び出しのために、再登録 */
XtAppAddTimeOut(app_context, interval, b, closure);
}
main () {
:
/* Xtに呼び出してもらう関数の登録 */
XtAppAddTimeOut(app_context, interval, b, closure);
/* Xtのメインループ */
XtAppMainLoop(app_context);
}
ちなみにXtの場合,XtAppMainLoop の内部で
select あるいは poll が利用されています。
(コンパイル時のオプション指定による)
From: wata...@gta.amita.co.jp (watanabe)
Date: Sat, 15 Jul 2000 10:16:12 JST
Newsgroups: fj.comp.lang.c
Message-ID: <8kodod$ar3$1...@nozomi.amico.co.jp>
Subject: Re: signal(3)に引数のある関数を渡す
> 私の知っている範囲では他にpoll, select, usleep とか、
> termiosをカノニカル モードにしておいて
> 制御文字を適当に設定して(0.1秒の精度で設定できます)readするとか、
> Xt関係なら XtAppAddTimeOut とかあります。
# 日本語マニュアルがないときつい……
説明不足でしたけど, Xlib使ってます。Xtは使ってませんでした。
「キー入力等をリアルタイムで処理しつつ」0.1秒間隔で……なので,
usleepは使えません。
具体的に言うと,
(main) 射出角や初速を設定
(呼び出す関数) 射出された放物線を描画
ということを *同時に* しています。
> 例:
>
> for(;;) {
> usleep(100000L); /* 0.1秒停止 */
> a();
> }
>
> for(;;) {
> struct timeval wait_time;
>
> wait_time.tv_sec = 0;
> wait_time.tv_usec = 100000; /* 0.1秒停止 */
> select(0,0,0,0,&wait_time);
>
> a();
> }
これだと, どちらも止まりますよね……
> もともと、FreeBSDのタイマーは精度は期待できないので
> (他のプロセスが動いていて、負荷がかかったときなど特に)
> たいていの場合、この利用法で十分だと思います。
はい, 遅くなる分には構わないんです。
実行速度の統一というか, ウェイトとして使いたかったもので。
--
大友紘之
wbha...@tky3.3web.ne.jp
> > 私の知っている範囲では他にpoll, select, usleep とか、
> > termiosをカノニカル モードにしておいて
> > 制御文字を適当に設定して(0.1秒の精度で設定できます)readするとか、
> > Xt関係なら XtAppAddTimeOut とかあります。
>
> # 日本語マニュアルがないときつい……
>
> 説明不足でしたけど, Xlib使ってます。Xtは使ってませんでした。
> 「キー入力等をリアルタイムで処理しつつ」0.1秒間隔で……なので,
> usleepは使えません。
>
> 具体的に言うと,
> (main) 射出角や初速を設定
> (呼び出す関数) 射出された放物線を描画
> ということを *同時に* しています。
>
(snip)
この手のことはXtを使ったほうがコーディングが楽だと思います。
勉強してみませんか?
設定値の入力などもGUIでかっこよくするとかも
Xlibだけより簡単にできる場合が多いような気がします。
…でXlibということなら、《thread対応のXlib》 でusleepを使うか
そうでないなら select あるいは poll を組み合わせて使うかでしょう。
前者は利用者と対話するメインスレッドと、
一定間隔でsleepしつつ描画するサブスレッドを組み合わせればよいでしょう。
後者は Xのリプライ キューの具合を確認しながら、
ConnectionNumberでソケットのディスクリプタをとって監視しつつ、
時々お休みという具合になるでしょうか。
私はXアプリケーションでthreadを使うのはまだ苦手なので、
select版を少し考えてみました。
このプログラムを動作させると2つのウィンドウが表示されます。
それぞれキー入力を受け付けます。
確認のために、どのキーが押されたか、簡単に表示します。
Xサーバーから応答がないと、一定時間sleepします。
sleep状態に入る前に繰り返し行いたい処置をすればよいでしょうか。
(ちなみにXtでは work procedure として これを記述します。)
応答性をよくするには、この処置はなるべく短時間で
終わるようにしなくてはなりません。
あ、プログラムは自力で終了するようにコーディングしていませんので、
Ctrl-C等で割り込みをかけるなどしてください。
では
------------------------ ここから最後まで
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
char default_font_name[] = "fixed";
Font load_font(Display *display, const char *font_name)
{
char **font_list = NULL;
Font font_id;
/* フォント名にワイルド カードが含まれる場合の処置 */
if (strchr(font_name,'*') || strchr(font_name, '?')) {
int max_font_count, actual_count;
font_list = XListFonts(display,font_name,max_font_count = 5,
&actual_count);
if (!font_list)
font_name = default_font_name;
else
font_name = *font_list;
}
font_id = XLoadFont(display,font_name);
if (font_list)
XFreeFontNames(font_list);
return font_id;
}
Window create_top_level_window(Display *display)
{
Window window;
int wx,wy;
unsigned width,height,border_width;
unsigned long background,border, event_mask;
XSelectInput(display,window =
XCreateSimpleWindow(display,DefaultRootWindow(display),
wx = 0, wy = 0,
width = 300, height = 200,
border_width = 2,
background = WhitePixel(display,DefaultScreen(display)),
border = WhitePixel(display,DefaultScreen(display))),
event_mask = KeyPressMask);
{
XWMHints hints;
hints.flags = InputHint;
hints.input = True;
XSetWMHints(display,window,&hints);
}
XMapWindow(display,window);
printf("Window: %#lx\n",window);
return window;
}
main(int argc,char **argv)
{
Display *display;
char *server_name = NULL;
int opt;
long lvalue, interval = 800; /* msec */
char *font_name = default_font_name;
GC gc;
unsigned long foreground, background;
int cx,cy,font_height;
Font font_id;
while ((opt = getopt(argc,argv,"d:i:f:")) > 0)
switch(opt) {
case 'd':
server_name = optarg;
break;
case 'f':
font_name = optarg;
break;
case 'i':
if (sscanf(optarg,"%li",&lvalue) == 1)
interval = lvalue;
break;
}
if ((display = XOpenDisplay(server_name)) == NULL) {
fprintf(stderr,"XOpenDisplay %s に失敗しました。\n"
"Xサーバーに接続できません。\n",
XDisplayName(server_name));
return 1;
}
XSetForeground(display, gc = DefaultGC(display,DefaultScreen(display)),
foreground = BlackPixel(display,DefaultScreen(display)));
XSetBackground(display, gc,
background = WhitePixel(display,DefaultScreen(display)));
XSetFont(display,gc,font_id = load_font(display,font_name));
{
static char test_pattern[] = "M";
int direction, ascent, descent;
XCharStruct over_all;
XQueryTextExtents(display,font_id,
test_pattern,sizeof test_pattern,
&direction, &ascent, &descent, &over_all);
cx = over_all.width, cy = ascent,
font_height = ascent + descent;
}
create_top_level_window(display);
create_top_level_window(display);
{
fd_set rfds;
XEvent event;
int width, flag;
struct timeval wait_time;
char buf[512];
FD_ZERO(&rfds);
for (;;) {
if (XEventsQueued(display,QueuedAfterFlush)) {
XNextEvent(display,&event);
if (event.type == KeyPress) {
KeySym key_symbol = XLookupKeysym(&event.xkey, 0);
XClearArea(display, event.xkey.window, 0,0,0,font_height,False);
XDrawImageString(display, event.xkey.window, gc, cx,cy, buf,
snprintf(buf,sizeof buf,"KeyPress: keycode:%#x, key_symbol:%#x %s ",
event.xkey.keycode, key_symbol, XKeysymToString(key_symbol)));
}
continue;
}
wait_time.tv_sec = interval / 1000;
wait_time.tv_usec = (interval % 1000) * 1000L;
FD_SET(ConnectionNumber(display),&rfds);
width = ConnectionNumber(display) + 1;
if ((flag = select(width,&rfds,0,0,&wait_time)) < 0)
fprintf(stderr,"select %d: %s\n",width,strerror(errno));
else if (flag == 0) {
time_t now;
char *t,*u;
if (time(&now) == -1)
perror("time");
if (!(u = strchr(t = ctime(&now),'\n')))
u = t + strlen(t);
fprintf(stderr,"\rtime out:%.*s %ld ",u - t,t, (long)now);
}
}
}
}
カノニカルモード → 非カノニカルモード
ですね。
UNIXでキー入力をチェックしながら別の処理も進めたい
ということなら、キー入力イベントをシグナルで受け取
るという方法もあります。FreeBSDやLinuxで使えるかど
うかは調べていませんが...。
8年近く前にUNIX USER誌に書いた記事をつけておきます。
現在のUNIX系OSでそのまま使えるかどうかはわかりませ
んが、参考になるかもしれません。
============================================================================
実践 UNIX C
第7回 未読データの有無を調べる
太田 純
■はじめに
対話型プログラムを開発していると、
まだ読んでいない入力データがあるかどうか調べたいことがよくある。
たとえば、次のような場合である。
・その時点でまだ先行入力が行われていなければ、
画面にプロンプトを表示してユーザーに入力を促したい。
・長時間かかる処理の間に入力があれば、
先に入力を処理してから元の処理の続きを行いたい。
・一定の周期で入力を監視し、その間は別の処理をしていたい。
通常の入力システム・コールは、データがなければ
入力が発生するまで待つことになり、この目的には利用できない。
したがって、何か別の方法で入力を調べることになる。
パソコン上でこのような処理を行うのは難しくない。
BASIC処理系の多くは、現在何かキーが押されているかどうかを調べる
INKEY$という関数を用意している。
MS-DOS上でC言語を使う場合でも、未読データの有無を調べるために
kbhit()などの関数が用意されていることが多い。
残念ながら、UNIXには未読データの有無をチェックする
共通のシステム・コールやライブラリ関数は存在しない。
プログラムの移植性を高めるためには、UNIXのバージョンごとに
異なった方法を用いてこの機能を実現する必要がある。
今回はそのために利用できるシステム・コールを分類し、
実際に未読データの有無を調べる方法について解説しよう。
■チェック方法の分類
未読のデータがあるかどうかを調べる方法はいくつかあり、
いずれの方法にも長所と短所がある。
処理の全貌をつかむために、ここではまずその方法を分類しておく。
●未読データのバイト数を調べる
UNIXによっては、未読データのバイト数を直接調べる
システム・コールを持つものがある。
このシステム・コールをフルに利用すれば柔軟な処理が可能になるが、
そのようなプログラムは他のUNIXへの移植が簡単ではない。
●未読データの有無を調べる
未読データが存在するかどうかだけを調べるシステム・コールはいくつかある。
これらを利用できるUNIXは多いので、移植性はそれほど失わずにすむ。
ただし、何バイトの未読データが存在するかまでは知ることができない。
●リード・タイマー機能を利用する
端末ドライバがリード・タイマーを持っていれば、
タイムアウト時間を0にすることによってread()はただちに返るようになる。
このことを利用しても未読データの有無を調べることができる。
ただし、未読データが存在すれば読まれてしまうという欠点がある。
●ノンブロッキングI/Oを利用する
read()がブロック(入力を待つこと)しないように設定できるUNIXはかなり多い。
これを利用すれば、リード・タイマーを用いた場合と同様の処理が可能になる。
また、この方法は一般に端末以外のファイルに対しても有効である。
●シグナルを利用する
比較的新しいUNIXでは、データが入力されたとき
プロセスにシグナルを送るように設定できるものもある。
この方法は単体では使いにくいが、
他の方法と組み合わせることによって威力を発揮する場合がある。
さて、以下ではそれぞれの方法について詳細に眺め、
同時に実際のコードを示していくことにしよう。
応用例としては、MS-DOSの処理系にならって
kbhit()という関数を組み立てることにする。
kbhit()は引数を取らず、未読のデータがなければ0、
未読のデータがあれば0以外の値(実際には1以上の整数)を返す関数である。
■未読データのバイト数を調べる
何バイトの未読データがあるかを直接調べる方法は1つしかない。
それは4.0BSDのFIONREADを使う方法である。
●FIONREADを使う (4.0BSD)
4.0BSDで用意されたioctl()のFIONREADリクエストは、
特定のファイル記述子の先にある未読データのバイト数を返すものである。
このリクエストは、端末だけでなく通常ファイルやパイプに対しても
利用できるので、汎用性は高い。
FIONREADを利用すれば、kbhit()は次のように記述できる。
関数の返り値としては、バイト数をそのまま返せばよい。
#include <sgtty.h>
...
int
kbhit()
{
int ret, n;
ret = ioctl(0, FIONREAD, &n);
if (ret != -1) {
/* 未読データのバイト数はn */
return n;
}
return 0;
}
FIONREADは4.0BSD以降のすべてのBSD UNIXで利用できるほか、
System V系UNIXのBSD拡張機能として用意されていることもある。
BSD UNIX以外では、マクロFIONREADはほとんどの場合
<sys/ioctl.h>で定義されているが、そうでない処理系もまれに存在する。
なお、SunOS4やHP-UXのマニュアルには
FIONREADリクエストがlongへのポインタを引数に取ると書かれているが、
ヘッダ・ファイルを見るかぎりは、
intへのポインタを引数に取るものとして実装されているようである。
■未読データの有無を調べる
前回説明したシステムコールselect()やpoll()を使えば、
特定のファイル記述子が入出力可能かどうかを調べられる。
入力が可能というのは、つまり未読データが存在するということである。
また、System Vリリース3のストリームI/Oでは、
FIONREADに対応するI_NREADというリクエストを用意している。
これらを利用すれば、未読データの存在を読まずに調べられる。
ただし、未読データのバイト数まで知ることはできない。
●select()を使う (4.2BSD)
select()は複数のファイル記述子を同時に監視して、
最初に入出力可能になったファイル記述子を知らせる機能を持つ。
select()は本来、ソケットを使ったプロセス間通信機能の一部として
4.2BSDで用意されたシステム・コールだが、
さまざまなバージョンのUNIXで採り入れられており、移植性はかなり高い。
select()の仕様はすでに説明してあるので、ここでは省略する。
詳細については前回の記事やマニュアルを参照していただきたい。
select()を利用してkbhit()を実現すると次のようになる。
#include <sys/types.h>
#include <sys/time.h>
...
int
kbhit()
{
int rfd, ret;
struct timeval timeout;
timeout.tv_sec = 0; /* 0秒 */
timeout.tv_usec = 0; /* 0マイクロ秒 */
rfd = (1 << (0));
ret = select(1, &rfd, NULL, NULL, &timeout);
if (ret == 1) {
/* 標準入力に未読データがある */
return 1;
}
return 0;
}
ここではタイム・アウトに0秒を設定しているので、
select()が入力の有無を検査したのち、ただちに戻る。
検査するイベントは入力の有無だけなので、
イベント指定としては第2引数のrfdだけを指定している。
検査対象のファイル記述子は標準入力(0)なので、
rfdの第0ビットを1にしておく。
select()の返り値はイベントの発生したファイル記述子の数であるが、
ここでは1つのファイル記述子しか検査していないので、
返り値が1であれば標準入力に未読データがあることがわかる。
●poll()を使う (SVR3)
poll()はSVR3で採用されたシステム・コールである。
本来ストリームI/Oのために用意されたものだが、
select()とほぼ同等の機能を持つ。
poll()についてもすでに紹介してあるので、ここでは説明を省く。
poll()を利用すると、kbhit()は次のように実現できる。
#include <poll.h>
...
int
kbhit()
{
struct pollfd fds[1];
fds[0].fd = 0;
fds[0].events = POLLIN;
poll(fds, 1L, 0); /* 0ミリ秒 */
if (fds[0].revents == POLLIN) {
/* 標準入力に未読データがある */
return 1;
}
return 0;
}
上と同様に、ここでもタイム・アウトを0秒に設定しているので、
poll()は入力の有無を検査するとただちに戻る。
検査対象は標準入力(0)だけなので、
struct pollfd構造体の配列要素数は1である。
メンバーfdには標準入力の0、
メンバーeventsには入力の有無を調べるPOLLINを指定する。
poll()から戻ってきたときにメンバーreventsに
POLLINが立っていれば、未読データがあることになる。
●I_NREADを使う (SVR3)
SVR3のストリームI/Oには、読み出しを待つメッセージの数を調べるための
I_NREADというリクエストが用意されている。
I_NREADはFIONREADに近い機能を持っており、
これを利用すれば、入力を読まずに未読データの有無を検査できる。
ただし、I_NREADは通常ファイルに対しては利用できない。
I_NREADによってkbhit()を実現する方法を次に示す。
#include <stropts.h>
...
int
kbhit()
{
int ret, n;
n = 0;
ret = ioctl(0, I_NREAD, &n);
if (ret > 0 && n > 0) {
/* 標準入力に未読データがある */
return n;
}
return 0;
}
ここでioctl()が返す値はストリーム・ヘッドにあるメッセージの数であり、
nには最初のメッセージに含まれるデータのバイト数が書き込まれる。
1つのメッセージには任意のバイト数(0以上)のデータが含まれるため、
未読データの総バイト数を知ることはできないが、
少なくとも最初のメッセージに含まれるデータの
バイト数かそれ以上の未読データが存在することはわかる。
上のコードを実行したときretが7、nが3であれば、
少なくとも3バイトの未読データがあると判断できる。
どういうわけか、I_NREADリクエストはあまり利用されていないようだ。
筆者の知るかぎり、フリー・ソフトウェアの対話型アプリケーションで、
I_NREADを使ってkbhit()の機能を実現している例は見当たらなかった。
■端末ドライバのリード・タイマー機能を利用する
こちらも前回説明したが、System VやPOSIXでは、
端末ドライバのtermio/termiosインタフェースが
リード・タイマー(入力の時間待ち)機能を提供している。
リード・タイマー機能は、端末ドライバが文字単位入力のモード
(ICANONフラグを落とした状態)になっているときだけ有効である。
リード・タイマーの最小入力文字数(VMIN)を0、入力待ち時間(VTIME)を
0に設定してread()を実行すれば、入力の有無によらずread()はただちに終了する。
このとき、入力があればそれが読み込まれ、入力がなければ0が返る。
したがって、実際にread()を実行することによって未読データの有無を検査できる。
ただし、未読データがあると読み込まれてしまうので、
それを読み戻したり、次回の入力のときには読み戻されたデータを
先に利用するなどの配慮が必要になる。
●System Vのtermioインタフェース
System Vのtermioインタフェースが提供する
リード・タイマー機能を利用すると、kbhit()は次のように書ける。
#include <termio.h>
#include <errno.h>
extern int errno;
...
unsigned char pbbuf[10];
int nchars = 0;
void
pushback(c)
int c;
{
pbbuf[nchars++] = c;
}
int
kbhit()
{
unsigned char c;
int n;
struct termio old, new;
if (nchars > 0) {
/* 読み戻された入力がある */
return 1;
}
/* リード・タイマーを設定する */
ioctl(0, TCGETA, &old);
new = old;
new.c_lflag &= ~ICANON;
new.c_cc[VMIN] = 0;
new.c_cc[VTIME] = 0; /* 0秒 */
ioctl(0, TCSETAW, &new);
/* 入力を読む */
while ((n = read(0, &c, 1)) < 0
&& errno == EINTR)
;
/* リード・タイマーを復元する */
ioctl(0, TCSETAW, &old);
if (n == 1) {
/* 入力を読み戻す */
pushback(c);
return 1;
}
return 0;
}
pushback()は前回と同じ、入力を読み戻す関数である。
当然ながら、読み戻された文字があればそれを先に読むように、
プログラム中の他の部分も変更しておく必要がある。
kbhit()では、最初に読み戻された入力があるかどうかを調べ、
あればそれ以降を実行することなく、未読データがあるものとして返る。
読み戻された入力がなければ、端末ドライバの現在の状態を取得して
行内編集(ICANON)を無効にし、リード・タイマーを適切に設定してから
端末ドライバに書き戻す。
次に実際に入力を行うが、入力中に割り込みが発生したときに
read()を再実行させるため、read()をループ内に置いている
(BSD UNIXの「信頼できるシグナル」を利用する場合はループする必要はない)。
入力が終了したら、端末ドライバを元の状態に戻し、
入力があればそれを読み戻してから結果を返す。
ここではkbhit()を実行するたびに端末ドライバの状態を取得しているが、
kbhit()の実行に際して端末ドライバが特定の状態にあることが明らかであれば、
あらかじめグローバル変数に端末ドライバの状態を設定しておき、
それを利用してもよい。
また、ここでは毎回ICANONフラグを落としているが、
kbhit()を利用したい場面では普通は行内編集が無効になっているため、
この処理も必要ないはずである。
●POSIXのtermiosインタフェース
POSIXのtermiosインタフェースを利用した場合、kbhit()は次のようになる。
termioインタフェースとの違いは、インクルードすべきヘッダ・ファイルと、
端末ドライバの状態を取得・設定するシステム・コールだけである。
pushback()に関する部分はtermioインタフェースと同じなので、ここでは省略する。
#include <termios.h>
#include <unistd.h>
#include <errno.h>
extern int errno;
...
int
kbhit()
{
unsigned char c;
int n;
struct termios old, new;
if (nchars > 0) {
/* 読み戻された入力がある */
return 1;
}
/* リード・タイマーを設定する */
tcgetattr(0, &old);
new = old;
new.c_lflag &= ~ICANON;
new.c_cc[VMIN] = 0;
new.c_cc[VTIME] = 0; /* 0秒 */
tcsetattr(0, TCSADRAIN, &new);
/* 入力を読む */
while ((n = read(0, &c, 1)) < 0
&& errno == EINTR)
;
/* リード・タイマーを復元する */
tcsetattr(0, TCSADRAIN, &old);
if (n == 1) {
/* 入力を読み戻す */
pushback(c);
return 1;
}
return 0;
}
■ノンブロッキングI/Oを利用する
System IIIでは、オープンされているファイルを制御するために、
ioctl()によく似たfcntl()というシステム・コールが用意された。
BSD UNIXの流儀では、入出力に関するさまざまな処理はすべてioctl()で行うが、
System IIIやSystem Vでは、追加モード、同期書き込みなどの属性設定や
ファイル・ロックなどの処理など、オープン・ファイルに関する処理は
すべてfcntl()で行うことになっている。
fcntl()の仕様を次に示す。
#include <fcntl.h>
int fcntl(fd, cmd, arg);
int fd, cmd;
fdは処理の対象となるファイル記述子である。
cmdは処理内容を示すコマンドで、<fcntl.h>(4.2BSDでは<sys/file.h>)で
定義されたマクロ名のうちいずれかを指定する。
argはコマンドの引数であり、コマンドの種類によって
int型であることも構造体へのポインタであることもある。
fcntl()に用意されたコマンドには、ファイル記述子に関する
ステータス・フラグを取得・設定するF_GETFL、F_SETFLがある。
これを用いることにより、特定のファイル記述子に対して
入出力がブロックしないように設定することが可能である。
たとえば、入力のためのファイル記述子をブロックしないよう設定すれば、
未読データがないときにはread()が入力を待たずに返るようになる。
これは一般に「ノンブロッキングI/O」と呼ばれる。
fcntl()とノンブロッキングI/Oは4.2BSDやPOSIXでも採用されているため、
現在の市場にあるほとんどのUNIXで利用できる。
ただし、どのUNIXでもまったく同じ方法が使えるわけではない。
ノンブロッキングI/Oにはわずかに異なる3種類の仕様がある。
●System VのO_NDELAY
System Vのfcntl()では、ノンブロッキングI/Oを
設定・解除するフラグはO_NDELAYである。
利用するときは、<fcntl.h>をインクルードする。
未読データがない場合、read()は0を返すだけであり、
ユーザーが^Dなどのファイル終了文字を入力した場合と区別がつかない。
このため、利用にあたっては注意が必要である。
ただし、この連載で扱っているような文字単位の入力を行う場合には、
この点は問題とはならないはずである。
O_NDELAYを利用したノンブロッキングI/Oの例を示す。
#include <fcntl.h>
...
unsigned char c;
int fdflags, ret;
/* ノンブロッキングI/Oを設定する */
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags | O_NDELAY));
/* 入力を行う */
ret = read(0, &c, 1);
if (ret == -1) {
/* エラーが発生した */
} else if (ret == 0) {
/* EOFまたは未読データがない */
} else /* if (ret > 0) */ {
/* データが読まれた */
}
/* ノンブロッキングI/Oを解除する */
fcntl(0, F_SETFL, fdflags);
●4.2BSDのFNDELAY
4.2BSDのfcntl()では、ノンブロッキングI/Oを
設定・解除するフラグはFNDELAYである。
利用するときは、<sys/file.h>をインクルードする。
未読データがない場合、read()は-1を返し、errnoにはEWOULDBLOCKが設定される。
また、4.3BSDでは<fcntl.h>が用意されており、
この中で定義されているO_NDELAYを利用しても同じ結果が得られる。
FNDELAYを利用したノンブロッキングI/Oの例を示す。
#include <sys/file.h>
#include <errno.h>
extern int errno;
...
unsigned char c;
int fdflags, ret;
/* ノンブロッキングI/Oを設定する */
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags | FNDELAY));
/* 入力を行う */
ret = read(0, &c, 1);
if (ret == -1 && errno == EWOULDBLOCK) {
/* 未読データがない */
} else if (ret == -1) {
/* エラーが発生した */
} else if (ret == 0) {
/* EOF */
} else /* if (ret > 0) */ {
/* データが読まれた */
}
/* ノンブロッキングI/Oを解除する */
fcntl(0, F_SETFL, fdflags);
●POSIXのO_NONBLOCK
POSIXのfcntl()では、ノンブロッキングI/Oを
設定・解除するフラグはO_NONBLOCKである。
利用するときは、<sys/types.h>、<unistd.h>、<fcntl.h>をインクルードする。
未読データがない場合、read()は-1を返し、errnoにはEAGAINが設定される。
O_NONBLOCKを利用したノンブロッキングI/Oの例を示す。
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
extern int errno;
...
unsigned char c;
int fdflags, ret;
/* ノンブロッキングI/Oを設定する */
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags | O_NONBLOCK));
/* 入力を行う */
ret = read(0, &c, 1);
if (ret == -1 && errno == EAGAIN) {
/* 未読データがない */
} else if (ret == -1) {
/* エラーが発生した */
} else if (ret == 0) {
/* EOF */
} else /* if (ret > 0) */ {
/* データが読まれた */
}
/* ノンブロッキングI/Oを解除する */
fcntl(0, F_SETFL, fdflags);
★コラム「EWOULDBLOCKとEAGAIN」
●UNIXのバージョンとマクロ名の混乱
現在のUNIXの多くはBSD UNIXとSystem Vの双方の機能を取り込んでいる。
ある機能がBSD UNIXとSystem Vのいずれにも存在し、しかも仕様が異なる場合、
その機能をどちらの仕様で実装するかはベンダーの意向次第であり、
これがさまざまな混乱のもととなっている。
最近ではこれにPOSIXなどの新たな標準が加わり、
混乱にさらに拍車がかかっている。
ノンブロッキングI/Oを設定するマクロ名は、
その中でももっとも悲惨な例のひとつである。
たとえば、4.3BSDのO_NDELAYはSystem VのO_NDELAYとは異なり、
4.2BSDのFNDELAYと同じものである。
その影響で、System VとBSD UNIXを統合した各社のUNIXでは、
O_NDELAYの仕様がSystem VのO_NDELAYであるか
4.2BSDのNDELAYであるかまったく予測不能である。
SunOSのようにコンパイル環境によっていずれかが決定する例もある。
また、本来はBSD系でないUNIXが<sys/file.h>を提供している場合、
その中で定義されているFNDELAYの仕様は、はなはだしいことに4.2BSDのFNDELAY、
System VのO_NDELAY、POSIXのO_NONBLOCKのいずれでもあり得る。
さらに、ベンダーによってはFNBIO、FNONBIO、FNONBLOCK、FNBLOCKなど、
独自のマクロ名を<sys/file.h>に追加している場合がある。
各種のUNIXでどのようなマクロ名が存在し、
どの仕様で実装されているかを表にしておいたので、参照していただきたい。
★表「ノンブロッキングI/Oのマクロ名」
これを見れば、<fcntl.h>で定義されているO_NONBLOCKを除き、
あるマクロ名が定義されているすべてのUNIXで仕様が統一された
マクロ名が存在しないことが理解できるはずである。
●ノンブロッキングI/Oで未読データをチェックする
ノンブロッキングI/Oのread()で未読データの有無をチェックする場合、
データが存在するときにはそれがread()で読まれてしまうことになる。
このため、リード・タイマーを使った場合と同様の配慮が必要である。
ここでは、文字単位入力の状態で利用されることを前提として、
3種類のノンブロッキングI/Oを統一して扱うことにする。
read()が返すエラーは無視しているが、
プログラムの信頼性にはそれほど影響しないはずである。
ノンブロッキングI/Oを利用すると、kbhit()は次のように実現できる。
pushback()に関する部分はこれまでどおりである。
#ifdef SYSVNBIO
#include <fcntl.h>
#define NONBLOCK O_NDELAY
#endif
#ifdef BSDNBIO
#include <sys/file.h>
#define NONBLOCK FNDELAY
#endif
#ifdef POSIXNBIO
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#define NONBLOCK O_NONBLOCK
#endif
#include <errno.h>
extern int errno;
...
int
kbhit()
{
unsigned char c;
int fdflags, n;
if (nchars > 0) {
/* 読み戻された入力がある */
return 1;
}
/* ノンブロッキングI/Oを設定する */
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags | NONBLOCK));
/* 入力を読む */
while ((n = read(0, &c, 1)) < 0
&& errno == EINTR)
;
/* ノンブロッキングI/Oを解除する */
fcntl(0, F_SETFL, fdflags);
if (n == 1) {
/* 入力を読み戻す */
pushback(c);
return 1;
}
return 0;
}
ところで、端末ドライバのリード・タイマー機能は
ノンブロッキングI/Oより優先されるので注意していただきたい。
つまり、ノンブロッキングI/Oが有効になっていても、リード・タイマーで
時間が指定されるとread()はその時間だけ入力を待つようになる。
また、ノンブロッキングI/Oが無効であっても、
リード・タイマーのタイム・アウトが0であればread()はただちに返る。
●FIONBIO (4.2BSD)
4.2BSD以降では、ioctl()のFIONBIOリクエストでも
ノンブロッキングI/Oを設定できる。
動作の仕様は4.2BSDのFNDELAYと同じである。
FIONBIOの利用例を次に示しておく。
#include <sys/ioctl.h>
...
int n;
/* ノンブロッキングI/Oを設定する */
n = 1;
ioctl(0, FIONBIO, &n);
...
/* ノンブロッキングI/Oを解除する */
n = 0;
ioctl(0, FIONBIO, &n);
なお、HP-UXではFIOSNBIO、FIOGNBIOという
少し仕様の異なるリクエストも提供されている。
■シグナルを利用する
これまで説明してきた方法は、いずれもプログラム中から
ある時点での未読データの有無を調べようとするものである。
それ以外にも、入力が用意されたときにプロセスに対して
シグナルが送られるように設定する方法がある。
しかし、シグナルを利用する方法は実際の入力処理と
タイミングを合わせるのが面倒であること、
またシグナル処理に付きまとう不確実性などから、
一般的な対話型アプリケーションではあまり使われないようだ。
したがって、ここではアウトラインだけを示し、
詳細については触れないことにする。
興味があれば読者自身でマニュアルを参照していただきたい。
●SIGIOを利用する (4.2BSD)
4.2BSDでは非同期入出力(asynchronous I/O)の機能が用意された。
fcntl()を利用して端末入力にFASYNCを設定すると
(ioctl()のFIOASYNCリクエストを利用してもよい)、
入力が用意されたときにプロセスに対してSIGIOシグナルが送られる。
プロセスではSIGIOシグナルに対するシグナル・ハンドラを設定しておき、
その中で何らかの入力処理を行えばよい。
実際の処理は概略次のようになる。
#include <signal.h>
#include <sys/file.h>
...
int ready;
...
sig_t
onio()
{
ready = 1;
}
...
void
kbhit_init()
{
int fdflags;
signal(SIGIO, onio);
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags | FASYNC));
ready = 0;
}
...
void
kbhit_term()
{
int fdflags;
fdflags = fcntl(0, F_GETFL, 0);
fcntl(0, F_SETFL, (fdflags & ~FASYNC));
signal(SIGIO, SIG_DFL);
}
...
int
kbhit()
{
return ready;
}
onio()はシグナル・ハンドラであり、
sig_tは前回までの記事で示したようにintあるいはvoidのいずれかである。
kbhit()の利用を開始するときは、kbhit_init()を呼び出して
シグナル・ハンドラを設定し、非同期入出力を有効にする。
kbhit()の利用を終了するときは、反対にkbhit_term()を呼び出して、
シグナル・ハンドラの復元し、非同期入出力を無効にする。
プロセスが活動状態にあるとき、ファイル記述子0に入力データが用意されると、
その時点でカーネルはこのプロセスにSIGIOシグナルを送る。
ここではシグナル・ハンドラの中で変数readyに適当な値を設定することにより、
入力があったことを記録している。
kbhit()はこの変数を参照して未読データの有無を返すことになる。
注意しなければならないのは、未読データが尽きたことを知る方法がない点である。
このため、実際の入力を行うときにはその時点での未読データをすべて読み込み、
変数readyの値を0に戻しておかなければならない。
さもなければ、次回kbhit()を実行したときには
未読データの有無を正しく認識できなくなってしまう。
●SIGPOLLを利用する (SVR3)
SVR3のストリームI/Oでも、SIGIOと同様の機能が提供されている。
ioctl()のI_SETSIGリクエストを利用すると、
入力ストリームにデータが用意されたときに
プロセスに対してSIGPOLLシグナルが送られるように設定できる。
シグナル・ハンドラの取り扱いはSIGIOのときと同様である。
処理の概略を次に示す。
#include <signal.h>
#include <stropts.h>
...
int ready;
...
sig_t
onpoll()
{
signal(SIGPOLL, onpoll);
ready = 1;
}
...
void
kbhit_init()
{
signal(SIGPOLL, onpoll);
ioctl(0, I_SETSIG, S_INPUT);
ready = 0;
}
...
void
kbhit_term()
{
ioctl(0, I_SETSIG, 0);
signal(SIGPOLL, SIG_DFL);
}
...
int
kbhit()
{
return ready;
}
処理内容や注意点については、SIGIOを使う場合とほぼ同じである。
ただし、System Vのシグナル・パッケージの利用を前提としているため、
ここではシグナル・ハンドラの先頭でシグナル・ハンドラを再設定している。
SIGIOとの大きな違いは、この方法がストリームに対してのみ有効な点である。
したがって、入力がストリームかどうかの検査をしっかり行っておく必要がある。
また、SIGPOLLはpause()やsleep()で休眠しているプロセスを起こしてしまうので、
sleep()で定期的に入力を検査するような目的には利用できない。
■どの方法を選択すべきか?
これまで見てきたように、kbhit()を実現するためには
実にさまざまな方法が利用できることになる。
しかし、どの方法で実現するのがもっともよいのだろうか?
未読データのバイト数まで調べられることを考えれば
4.0BSDのFIONREADがもっとも便利だが、
どの標準にも含まれていないため移植性は低い。
また、今後UNIXの主流がSVR4系に移行していくと、
利用可能なプラットフォームは減少することになる。
それに次ぐのはread()を行わずに未読データの有無を調べられる
select()とpoll()であるが、いずれもPOSIXに含まれていないため、
UNIX以外のプラットフォームで利用できることは期待できない。
ただし、select()は業界標準になった感もあり、
またpoll()はSVID2(System V Interface Definition Issue 2)に含まれるため、
新しいUNIXのほとんどはこれらのいずれかを提供しているはずである。
これらを利用することで、かなりの移植性を確保できる。
SVR3のI_NREADを利用する方法はこれに準ずるが、
世間であまり使われていないという欠点がある。
これは思ったよりも重大な問題だ。
使われない機能はいずれすたれていく運命にあるからである。
端末ドライバのリード・タイマー機能を利用する方法は、
未読データがある場合にそれを読んでしまうという使いにくさがある。
しかし、これはプログラミングでかなりの程度までカバーできる。
リード・タイマーはSVIDやPOSIXで定義されているため、
現在でもある程度の移植性は得られるだろう。
fcntl()によるノンブロッキングI/Oは現在のほとんどのUNIXに存在し、
それなりの機能は得られるが、これまで見てきたように
同じマクロ名でも機種によって微妙な違いが存在する。
通常はこれらの違いを無視しても実害はないはずだが、
POSIXで定義されているO_NONBLOCKを利用するのでなければ、
これに頼らない方がよいかもしれない。
最後にSIGIOやSIGPOLLを使う方法だが、
これらはkbhit()を実現する目的には向いていない。
対話型アプリケーションで利用することは避けた方がよい。
結局のところ、POSIX準拠のシステムでは端末ドライバの
リード・タイマー機能かfcntl()のO_NONBLOCKフラグを利用し、
それ以外のシステムではselect()かpoll()の
いずれかを利用するのが、現状では最善の選択だろう。
POSIX準拠のシステムでリード・タイマー機能とfcntl()の
いずれを利用するかは、アプリケーションのその他の部分で
どちらを利用しているかによって決めればよい。
■応用のための考察
ここでは端末上で実行されるスクリーン・エディタを例にとり、
今回実現したkbhit()をどのように応用できるか考えてみよう。
スクリーン・エディタは、ユーザーの入力に応じて
画面を頻繁かつ広範囲に書き換えるアプリケーションである。
しかし、ユーザーの入力と入力の間には何回もの書き換えが必要であり、
そのたびに変更を画面に反映していては効率がよくない。
このため、ユーザーからの入力を待つ直前に一気に画面を書き換えることが多い。
また、端末出力のコストは高いので、
通常は直前の画面との差分だけを出力するという方法をとることになる。
このためには、現在の端末画面に対応する2次元配列と、
次に書き換わる画面に対応する2次元配列の2つを用意しておき、
次画面配列から現画面配列に内容をコピーしながら、
異なる部分だけを端末に出力するのが常套手段である。
これをプログラムにすると、およそ次のように書ける。
ただし、簡略化のためにスクロールの処理は考えない。
これは過去に連載で取り上げた画面更新の方法そのものでもある。
/* 画面を更新する */
update()
{
for (1行ずつ) {
if (行が変更されていない)
continue;
/*
* 行をコピーしながら
* 異なる部分を出力する
*/
}
}
ここで、ユーザーが画面の次のページに進むコマンドを
連続して入力したとしよう。
画面のほとんどの部分はページごとに異なるはずなので、
update()ではすべての行が律義に書き換えられ、
さらにその処理がコマンドの回数だけ繰り返される。
画面全体の書き換えにはかなりの時間を要するため、
ユーザーは処理が終わるまでしばらく待たされることになる。
ところが、ユーザーが実際に得たいのは何ページか後の画面であるから、
実際には途中の書き換えはすべて無駄である
(とは一概には言えないが、ここではあえて目をつぶる)。
そこで、各行の処理を行う前にkbhit()で入力の有無を調べ、
ユーザーが次のコマンドを入力していたら
書き換えを中止して戻るように変更する。
/* 画面を更新する */
update()
{
for (1行ずつ) {
if (kbhit())
return;
if (行が変更されていない)
continue;
/*
* 行をコピーしながら
* 異なる部分を出力する
*/
}
}
これにより、画面を書き換えている途中でコマンドが入力されると
update()は直ちに終了し、またその後連続して呼ばれるupdate()は
画面をまったく書き換えずに返るようになる。
画面の一部だけが書き換えられた状態でupdate()が終了しても、
実際の端末画面と配列の内容が一致しているかぎり、
次回にupdate()が呼ばれたときに正しく画面を更新できることに注意してほしい。
この変更でとりあえず目的は達成できたわけだが、
ここではkbhit()がかなりの回数呼び出されているため、
kbhit()の実現方法によっては必要以上に処理に負担がかかる場合がある。
これを改善する方法を考えてみよう。
よく見てみると、update()の中ではまったく入力を読んでいないことがわかる。
このため、リード・タイマーやノンブロッキングI/Oを用いて
kbhit()を実現している場合、リード・タイマーや
ノンブロッキングI/Oを毎回設定・解除する必要はない。
update()が呼び出された直後に設定を行い、
update()から返る直前に解除すればじゅうぶんである。
そこで、これらの設定・解除を行うコード部分をkbhit()から分離し、
kbhit_enable()、kbhit_disable()という関数を作ることにする。
(シグナルを利用する場合のkbhit_init()、kbhit_term()とは
処理の意味がやや異なるため、名前を変えてある。
また、FIONREADやselect()、poll()でkbhit()を実現している場合には、
これらの関数の内容は空でよい。)
kbhit_enable()、kbhit_disable()を用いると、
プログラムは次のように書ける。
(ただし、同じプログラムでシグナルの処理を行っている場合には
これ以外にも変更を要する可能性がある。)
/* 画面を更新する */
update()
{
kbhit_enable();
for (1行ずつ) {
if (kbhit()) {
kbhit_disable();
return;
}
if (行が変更されていない)
continue;
/*
* 行をコピーしながら
* 異なる部分を出力する
*/
}
kbhit_disable();
}
この変更により、不要な処理の大部分を省くことができ、
プログラムの処理効率を改善できたはずである。
このように、kbhit()は対話型アプリケーションの
さまざまな局面で役立つ可能性を持っているが、
効率よく利用するためにはそれなりの配慮が必要である。
読者もまた、身近なアプリケーションを題材にして、
kbhit()がどのように応用できるか、
また効率向上のためにはどのような変更が必要かについて考えていただきたい。
これまでの連載で扱ってきた画面更新のコードなどは、
実際にkbhit()を組み込んでみる練習台としてちょうどよいはずである。
■サンプル・プログラムについて
今回の付録ディスクには、さまざまな方法でkbhit()を実現する
サンプル・プログラムを収録した(シグナルによる実現は除く)。
Makefileから適当な選択肢を選んでmakeを実行すれば、
kbhit()のテスト・プログラムが生成できる。
テスト・プログラムを実行すると、1秒間隔で「.」が表示される。
キーボードから適当に文字を入力すると、次に「.」が表示される
タイミングで、代わりに入力された文字がすべて表示される。
入力された文字の中に「q」があると、プログラムは終了する。
プログラムのメイン・ループは次のとおりである。
効率についてはとくに考慮していないので、改善は読者への課題とする。
for (;;) {
sleep(1);
if (!kbhit()) {
putch('.');
continue;
}
while (kbhit()) {
c = getch();
putch(c);
if (c == 'q')
goto out;
}
}
out:
さて、次回は少し力を抜いてcursesライブラリを取り上げることにしよう。
これを利用すれば、この連載で扱ってきた処理の多くが簡単に実現できる。
cursesの特徴などを眺めながら、どのようなアプリケーションで
利用すべきかについて考えてみたい。
★コラム「EWOULDBLOCKとEAGAIN」
============================================================================
EWOULDBLOCKはBSD UNIXで追加されたエラーであり、
「本来ならば処理が完了するまでブロックするはずであったが、
ノンブロッキングI/Oが設定されていたためにシステム・コールに失敗した」
という意味を持つ。
これに対して、EAGAINはシステム・コールの再実行を促すためのものであり、
「システム資源が一時的に使い尽くされていたためにシステム・コールに失敗した」
という意味を持っている。
EAGAINは古くからあったエラーで、通常は「No more processes」、
すなわちプロセス・テーブルに空きがないために
fork()に失敗した場合のエラーとして知られている。
つまり、POSIX標準では「未読データ」をシステム資源と見て、
EWOULDBLOCKのような新しいエラーを導入するかわりに、
既存のエラーを流用することにしたわけである。
============================================================================
★表「ノンブロッキングI/Oのマクロ名」
============================================================================
マクロ名 4.3BSD SunOS4 AIX3 HP-UX8 RISC/os4 EWS-UX/VR4
----------------------------------------------------------------------------
<sys/file.h>
FNDELAY BSD BSD SysV/POSIX BSD BSD/SysV SysV
FNBIO - SysV - - - -
FNONBIO - POSIX - - - -
FNONBLOCK - - POSIX - - POSIX
FNBLOCK - - - POSIX - -
<fcntl.h>
O_NDELAY BSD BSD/SysV SysV BSD BSD/SysV SysV
O_NONBLOCK - POSIX POSIX POSIX POSIX POSIX
----------------------------------------------------------------------------
BSD、SysV、POSIXはそれぞれノンブロッキングI/Oの動作を表す。
SysV .... read()が0を返す
BSD ..... read()が-1を返し、errnoにEWOULDBLOCKが設定される
POSIX ... read()が-1を返し、errnoにEAGAINが設定される
複数の動作が記述されている場合は、コンパイル環境によって動作が異なる。
============================================================================
--
太田純(Junn Ohta) (株)リコー/新横浜事業所
oh...@sdg.mdd.ricoh.co.jp
> fj.comp.lang.cの記事<8kodod$ar3$1...@nozomi.amico.co.jp>で
> wata...@gta.amita.co.jpさんは書きました。
> > 私の知っている範囲では他にpoll, select, usleep とか、
> > termiosをカノニカル モードにしておいて
> > 制御文字を適当に設定して(0.1秒の精度で設定できます)readするとか、
>
> カノニカルモード → 非カノニカルモード
>
うっかりしていました。
# 最近はXばかり使っていて、コンソール向けの(端末を意識した)
# コードに対するミスを見落としがちになっているのかもしれません。
struct termios attr;
attr.c_lflag &= ~ICANON;
このように記述して ICANON のビットを落とすので、非カノニカルモードですね。
私はSunOS4のリファレンスマニュアルで勉強したくちですが、(確か man termio)
今、手元で使える Red Hat Linux 6.2では付属のオンラインマニュアルに
この辺の具体的な使い方の記述が見あたらないですね。(man termios)
Linux から勉強をはじめる人には何を勧めたらよいでしょうね。