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.

42 TokyoAdvent Calendar 2022

Day 21

タイムアウト付きの fgetsを実現する3種類の方法からプロセス、スレッド、fdの監視を学ぶ

Last updated at Posted at 2022-12-21

この記事では、「タイムアウト付きのfgets」を実現する3種類の方法について解説していきます。

具体的な実装内容は以下となります。

  • select のタイムアウト機能を利用する。
  • fork を使用して子プロセスで時間を計測する。
  • pthread_create を使用してスレッドで時間を計測する。

マルチプロセス、マルチスレッド、ファイルディスクリプタの監視についてざっくりと理解できるような内容となっています。

※ こちらは 42 Tokyo Advent Calendar 2022 の 21日目の記事です。

使用するコード

fgetsの動作

char *fgets(char *str, int size, FILE *stream);
  • fgets は、stream から最大で size - 1 個の文字を読み込み、 str が指すバッファーに格納します。
  • 通常のファイル以外にも、streamにstdinを与えることで標準入力から読み込むこともできます。(それはそう)

今回はタイムアウト機能を実装することで、一定時間経過したら処理を中断させるようにします。

select関数で実装する

まずは、selectのタイムアウト機能を利用して実装します。

select はファイルディスクリプタを監視し、I/O操作(readやwrite)が実行可能かを知らせてくれます。
例えば標準入力では「キー入力」+「エンター」のタイミングで読み込み可能だと判定されます。

selectの引数は以下の通りで、タイムアウト時間を設定することができます。

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  • nfds:以下の3つの集合に含まれるファイルディスクリプタの最大値に 1 を足したもの。
  • readfds:読み込み状態を監視するファイルディスクリプタの集合
  • writefds:書き込み状態を監視するファイルディスクリプタの集合
  • exceptfds:例外状態を監視するファイルディスクリプタの集合
  • timeout:タイムアウト時間

タイムアウト時間を設定した場合は、監視対象に変化がない場合でも、タイムアウト時間が経過した時点で selectがreturnされるようになります。

この仕組みを利用して、実行可能の場合は、fgetsで読み込みをして、タイムアウトした場合は、NULLを返すような実装にします。

ファイルディスクリプタの集合の操作には、以下の2つを使用します。

  • FD_ZERO:集合(fds)を初期化する。
  • FD_SET:集合(fds)にファイルディスクリプタ(fd)をセットする。

タイムアウト時間を設定するtimeval構造体は以下の情報を持ちます。

struct timeval {
    long    tv_sec;         /* 秒 */
    long    tv_usec;        /* マイクロ秒 */
};

コード例

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

#define TIMEOUT 5

char *tfgets(char *str, int size, FILE *stream)
{
  fd_set rfds;
  struct timeval tv;
  int retval;

  // FILE構造体からディスクリプタを取得する
  int fd = fileno(stream);

  // 監視対象のfdをセットする
  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);

  // 監視する時間を設定する
  tv.tv_sec = TIMEOUT;
  tv.tv_usec = 0;

  // timeoutする -> 0
  // fdが読み込み可能になる -> 1 (読み込み可能になったfdの数)
  retval = select(1, &rfds, NULL, NULL, &tv);
  if (retval > 0)
    return fgets(str, size, stream);
  else
    return NULL;
}

とてもシンプルに実装できました。
実は上記のコード例は、selectman page に記載されているサンプルコードを数行変更したものです。これは本題ではないですが、使い方が分からない関数と遭遇したときに、一度サンプルコードを写経してみると理解できることがあるのでおすすめです。

この関数のポイント

  • selectを使用したファイルディスクリプタの監視方法
  • (おまけ)man pageのサンプルコードは分かりやすい。

マルチプロセスで実装する

次に fork を使用して、マルチプロセスで実装します。

fork は呼び出し元のプロセス(親プロセス)を複製して、新しいプロセス(子プロセス)を生成します。

そのため以下の実装が考えられます。

  • 親プロセスは子プロセス生成後に、fgets を実行する。
  • 子プロセスでタイムアウト時間を計測する。

子プロセスが終了すると、親プロセスには終了シグナル(SIGCHLD)が送られます。
そのため、タイムアウト秒経過したことを親プロセスが検知して、fgetsの処理を中断することができそうです。

しかし、ここで一つ問題となるのは、fgetsの処理を中断させる方法がないことです。
それでは、タイムアウト時間経過したことが判定できても意味がありません。

そこで今回は、sigsetjmpsiglongjmp を使用します。

  • sigsetjmp によってスタック環境およびシグナル・マスクを保管する。
  • siglongjmp で保管した環境とシグナル・マスクを復元する。

上記の仕組みを利用して、タイムアウト時間経過した場合は、fgetsの処理を途中で中断させることができます。
ちなみに fgets が時間内に行われた場合は、親プロセスで kill を実行してスリープしている子プロセスを終了させます。

コード例

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/wait.h>
#include <unistd.h>

#define TIMEOUT 5
static sigjmp_buf env;

static void handler(int sig)
{
  wait(NULL);
  // sigsetjmpが1を返す
  siglongjmp(env, 1);
}

char *tfgets(char *s, int size, FILE *stream)
{
  pid_t pid;
  char *str = NULL;

  // SIGCHLDを補足するシグナルハンドラを設定する
  signal(SIGCHLD, handler);

  // 子プロセス -> pid = 0
  // 親プロセス -> pid = 子プロセスのID
  if ((pid = fork()) == 0)
  {
    /// 子プロセス
    sleep(TIMEOUT);
    exit(0);
  }
  else
  {
    /// 親プロセス

    // 子プロセスによってsiglongjmpが実行された場合は0を返さない
    if (sigsetjmp(env, 1) == 0)
    {
      str = fgets(s, size, stream);
      kill(pid, SIGKILL);
      // siglongjmpが呼ばれるまで待つ
      pause();
    }
    return str;
  }
}

注意点は、 wait を実行しないと終了した子プロセスがゾンビプロセスとなって残り続けてしまうことです。忘れずに回収してあげましょう。

この関数のポイント

  • プロセスの管理方法
  • 子プロセスの回収
  • シグナルハンドリング
  • sigsetjmp、siglongjmpの使い方

マルチスレッドで実装する

最後に pthread_create を使用して、マルチスレッドで実装します。

pthread_create は呼び出したプロセス内に新しいスレッドを生成します。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • thread:スレッドを識別する値
  • attr:スレッドの属性
  • start_routine:スレッドが行う処理(第4引数の arg を受け取って実行する)

2つのスレッドを生成して、start_routinefgetssleep それぞれの処理を与えます。

また今回は、fgets の実行中にタイムアウトしても割り込んで処理を中断させる必要はありません。
なぜなら、スレッドをデタッチ状態にすることによって、処理を待つ必要がなくなるからです。もちろんリソースも解放されます。

逆にスレッドが終わるまで処理を待ちたい場合は、pthread_joinを使います。

そして次のようにスレッドを実行します。

  • fgets:処理が終わるまで待たない。(デタッチ状態)
  • sleep:処理が終わるまで待つ。

fgets が時間内に実行された場合は、pthread_cancelを使って、スリープのスレッドを途中で中断させます。

コード例

#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

#define TIMEOUT 5

// スレッドの処理に与えられる引数が1つのみなので構造体にまとめる
typedef struct
{
  char *str;
  int size;
  FILE *stream;
  pthread_t sleep_tid;
} fgets_args_t;

// fgetsの戻り値として使用する
static char *retval;

static void *fgets_thread(void *vargp)
{
  // pthread_self:呼び出したスレッドのIDを取得する
  // pthread_detach:実行中のスレッドをデタッチ状態にする
  pthread_detach(pthread_self());
  fgets_args_t *argp = (fgets_args_t *)vargp;

  // fgetsを実行する
  retval = fgets(argp->str, argp->size, argp->stream);

  // スリープを実行しているスレッドにキャンセル要求を送る
  pthread_cancel(argp->sleep_tid);
  return NULL;
}

static void *sleep_thread(void *vargp)
{
  sleep(TIMEOUT);
  return NULL;
}

char *tfgets(char *str, int size, FILE *stream)
{
  pthread_t fgets_tid;
  pthread_t sleep_tid;

  // fgetsのスレッドの引数を作成する
  fgets_args_t args = {.str = str, .size = size, .stream = stream};
  retval = NULL;

  // sleepを実行するスレッドを作成する
  pthread_create(&sleep_tid, NULL, sleep_thread, NULL);

  // sleepスレッドのIDを引数に追加する
  args.sleep_tid = sleep_tid;

  // fgetsを実行するスレッドを作成する
  pthread_create(&fgets_tid, NULL, fgets_thread, &args);

  // sleepスレッドの終了を待つ
  pthread_join(sleep_tid, NULL);
  return retval;
}

スレッドの場合、プロセスと違ってメモリ空間が共有されるので 、fgets が読み込んだ値をそのままスレッドの生成元に反映されているところがミソですね。

この関数のポイント

  • スレッドの使い方
  • メモリの空間の共有
  • pthread_joinとpthread_detachの使い分け

まとめ

今回は3つの異なる手法でタイムアウト付きのfgetsを実装していきました。
マルチプロセス、マルチスレッド、ファイルディスクリプタの監視についての理解につながれば幸いです。

ちなみに42 Tokyo ではこれらの仕組みについてより詳しく学ぶことができます。

興味ある方はぜひ一緒に学びましょう!お待ちしてます!

おわりに

本記事で使用したソースはGitHubに上げているので参考程度にご覧ください。(簡略化のためエラー処理は省いています)

明日は、@pettei47さんが「フルタイムの社会人でも42は楽しめる」という話を書いてくれるようです。結構ハードそうですが本当なんでしょうか...?これは記事を読んでみないと分かりませんね!お楽しみに!

参考

コンピュータ・システム ~プログラマの視点から~

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?