地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Nginx】非同期IOとノンブロッキングIO

違い

厳密に種類を分けると下記はそれぞれ違った特性を持つ

  • 同期ブロッキング - read(2)/write(2)シングルスレッドで実行
  • 同期ノンブロッキング - open(2)時にO_NONBLOCKフラグをつけてfdを生成。
  • 非同期ブロッキング - select(2)などで多重fdを管理。EAGAINが帰ってこなければカーネルバッファにデータは存在する
  • 非同期ノンブロッキング - aio(2)を実行。完了にはシグナルの配送などユーザが通知の方法を選べる

NginxやらNode.jsやらでよく聞くイベント駆動なんかの話で出てくる非同期IOとノンブロッキングIOの違い

f:id:ryuichi1208:20200219215310p:plain

非同期IO

データ処理の入出力が可能になった時点で通知がカーネルから送られてくる

非同期なので読込の場合は通知のあった段階で既にデータの転送は完了(I/O Completion)しバッファ内にデータがある。

シグナルにより通知が来たらアプリケーション側でハンドリングしてIO後の処理を行う

シグナルが来るまではアプリケーションは他の処理を行うことができる。

int main(int argc, char** argv)
{
        int fd;
        void* buf;
        struct aiocb aioc;

        if ((fd = open(argv[0], O_RDONLY)) <= 0 ){
                return -1;
        }

        if ((buf = malloc(1024 * 1024)) == NULL) {
                close(fd);
                return -1;
        }
        aioc.aio_fildes = fd;
        aioc.aio_buf = buf;
        aioc.aio_nbytes = 1024 * 1024;
        aio_read(&aioc);

        free(buf);
        close(fd);
        return 0;
}

aio_read()は、スレッドを生成し、その中で通常のI/O要求を発行することによって、非同期でI/Oを行っている

(スレッドを生成する以上はスレッドセーフなシグナルハンドラなんかの意識も必要になってくるので厄介)

ノンブロッキングIO

IO対象の準備完が了していないことをアプリケーション側に伝えるためIOを行った時点でエラーを返す(erronoはEAGAIN)

EAGAINが返ってきた際の処理はアプリケーション側で行い再度ファイルディスクリプ他にIOが準備完了であるかを問い合わせる。

この問い合わせまでの時間を別の処理に使うことで全体の処理を効率化できる。

/* 非同期の書き込みと計算を同時に行う */
 aio_write(&my_aio);
 do_compute(arr, 123456);

/* 操作が完了する前にタイムアウトにより呼び出しが終了*/
  if ( errno == EAGAIN ) {
    res = aio_suspend(my_aio_list, 1, 0);
    if ( res ) {
      printf("aio_suspend returned non-0\n"); return errno;}
    } else
      if ( res ) {
        printf("aio_suspend returned neither 0 nor EAGAIN\n");
        return errno;
      }
  }
 

ちなみに

aioはnginxでも使われている。

httpリクエストが来てキャッシュヒットした際に「ngx_http_file_cache_read()」が呼ばれる。

ngx_file_aio_readではファイルをaio_readで非同期に読み込む。(ここではブロックされない)

ssize_t
ngx_file_aio_read(ngx_file_t *file, u_char *buf, size_t size, off_t offset,
    ngx_pool_t *pool)
{
    ngx_err_t         err;
    struct iocb      *piocb[1];
    ngx_event_t      *ev;
    ngx_event_aio_t  *aio;

    aio = file->aio;
    ev = &aio->event;

    ngx_memzero(&aio->aiocb, sizeof(struct iocb));

    aio->aiocb.aio_data = (uint64_t) (uintptr_t) ev;
    aio->aiocb.aio_lio_opcode = IOCB_CMD_PREAD;
    aio->aiocb.aio_fildes = file->fd;
    aio->aiocb.aio_buf = (uint64_t) (uintptr_t) buf;
    aio->aiocb.aio_nbytes = size;
    aio->aiocb.aio_offset = offset;
    aio->aiocb.aio_flags = IOCB_FLAG_RESFD;
    aio->aiocb.aio_resfd = ngx_eventfd;

    ev->handler = ngx_file_aio_event_handler;

    // 非同期ioはここで呼ばれてる
    n = aio_read(&aio->aiocb);↲
    
    // 関数最後でaio_readの結果をreturnしてる
    return ngx_file_aio_result(aio->file, aio, ev);
}

aio_readの結果自体は「ngx_file_aio_result()」で確認している。

仮にaio自体が完了してなければaio_result()でerrono、EINPROGRESSを返し呼び出し元はリトライする仕組みのようでした。