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

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

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

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

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

※ こちらは [42 Tokyo Advent Calendar 2022](https://qiita.com/advent-calendar/2022/42tokyo) の 21日目の記事です。

## 使用するコード

https://github.com/ryo-manba/fgets-timeout

## fgetsの動作

```c
char *fgets(char *str, int size, FILE *stream);
```

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

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

## select関数で実装する

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

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

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

```c
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構造体は以下の情報を持ちます。

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

### コード例

```c
#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;
}
```
とてもシンプルに実装できました。
実は上記のコード例は、`select`の [man page](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/select.2.html) に記載されているサンプルコードを数行変更したものです。これは本題ではないですが、使い方が分からない関数と遭遇したときに、一度サンプルコードを写経してみると理解できることがあるのでおすすめです。

**この関数のポイント**

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


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

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

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

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

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

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

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

そこで今回は、`sigsetjmp` と `siglongjmp` を使用します。

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

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

### コード例
```c
#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` は呼び出したプロセス内に新しいスレッドを生成します。

```c
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_routine` に `fgets` と `sleep` それぞれの処理を与えます。

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

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

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

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

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

### コード例
```c
#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](https://42tokyo.jp/) ではこれらの仕組みについてより詳しく学ぶことができます。

- selectによるファイルディスクリプタの監視：[HTTPサーバーの実装](https://github.com/corvvs/webserv)
- マルチプロセス：[shellの実装](https://github.com/ryo-manba/minishell)
- マルチスレッド：[食事をする哲学者の問題](https://github.com/ryo-manba/philosophers)

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

## おわりに

本記事で使用したソースは[GitHubに上げている](https://github.com/ryo-manba/fgets-timeout)ので参考程度にご覧ください。（簡略化のためエラー処理は省いています）

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


### 参考

[コンピュータ・システム ~プログラマの視点から~](https://www.amazon.co.jp/%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%BB%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0-%E4%BA%94%E5%B3%B6-%E6%AD%A3%E8%A3%95/dp/4621302019)