20210731のLinuxに関する記事は2件です。

epollを使ってみる(selectと対比しながら)

はじめに:本記事の趣旨 以下、C言語、Linux環境を前提とする。 複数のファイルディスクリプタへのイベントを管理したいとき、例えばselect()を使用する方法があるが、噂では1これはかなり遅く(O(n))、性能を求めるならepollインターフェースを使うのが良い、とのことらしい。 しかしながら、 epollインターフェースは各関数が何を行なっているのかがよく分からない。故にどこで何を呼べば求めている処理を実現できるのかが明瞭でない。 以下では、 select()の使用法と対応させながら、epollインターフェースの使用法を簡明にまとめる。 よって、前者の挙動をある程度理解していることが前提となる。 また、例として、 listen用のソケットディスクリプタと、各クライアントと接続されたソケットディスクリプタ群とを管理するコードを使用 する。その為、ソケット通信に関する最低限の知識も前提とする。 なお(これは予防線だが)本記事は、厳密な理解を追求するものではなく、一先ずepollを大きな違和感なく使用できるようにすることを目的とするものであり、またあくまでも自分用のメモに過ぎない。 select(), epollとは 私の拙い日本語でこれを改めて説明することに意味はない。 誰もが何度も参照するであろうLinuxマニュアルへのリンクを貼ることで、説明を終えたことにしよう。 1. select関数 2. epollインターフェース インターフェース全体 epoll_create() epoll_ctl() epoll_wait() では以下より本題、即ちselectと対比する形でのepoll使用法の説明に入っていこう。 具体的には、 以下のフェーズに分けて、読み込みイベント(ソケットへの受信)を待つ場合の使用法(呼び出すべき関数とその使い方)をみていく。 よって、受信のあったソケットディスクリプタの管理を目的とすることを前提とした記述となっていることに留意して欲しい。 I. 初期化及びlisten用ソケットの追加 II. ディスクリプタへのアクセスを検知 III. ディスクリプタを管理対象から削除 I. 初期化及びlisten用ソケットの追加 1. select関数 select()を使う場合には先ず、fd_set構造体に対象のlisten用ソケットディスクリプタをセットする作業が必要となる。 なお、同様のファイルディスクリプタ集合に対して何度もループしてselect()を実行する場合には、以下のコードのように2つのfd_set構造体を用意しておくと簡便であろう。 select.c /* 管理対象となるファイルディスクリプタの集合 */ fd_set target_fds; /* selectの結果の格納先 */ fd_set readable_fds; /* 読み込みを待つ、listen用のソケットディスクリプタ */ int sd_listen; /* 管理対象保存用fd_setを初期化 */ FD_ZERO(&target_fds); /* 管理対象保存用fd_setにlisten用ソケットを追加 */ FD_SET(sd_listen, &target_fds); while ( 1 ) { /* 結果格納用fd_setに、管理用fd_setの中身をコピーする */ /* 結果格納用fd_setはselectが書き換えてしまうので、毎回コピーする必要あり */ memcpy(&readable_fds, &target_fds, sizeof(fd_set)); /* 以下で、selectを使用(省略) */ } 2. epollインターフェース epollインターフェースを使用する場合には、少々複雑な作業が必要となる。 まず、インターフェース使用の為のepollインスタンスを作成する。当該インスタンスとはepoll_create()関数が返すファイルディスクリプタを通じてやり取りできる。 次に、listen用ソケットディスクリプタを追加する処理だが、これも少しばかり厄介で、以下の2つの処理が必要である。 ディスクリプタに紐づけるイベント情報を設定する。(epoll_event構造体2を使用) epoll_ctl()関数(EPOLL_CTL_ADDオプションを渡す)を使って、当該ディスクリプタとイベント情報とをepollインスタンスに追加する。 epoll.c /* 管理対象とするファイルディスクリプタの上限数 */ #define N_FDS (5) /* epoll_waitの結果の格納先 */ struct epoll_event events[N_FDS]; /* epollインスタンスを参照するファイルディスクリプタ */ int epfd; /* ファイルディスクリプタと紐付けるイベント情報 */ struct epoll_event ev; /* 読み込みを待つ、listen用のソケットディスクリプタ */ int sd_listen; /* epollインスタンスを作成 */ epfd = epoll_create(N_FDS); if ( epfd < 0) { /* エラー処理 */ } /* listen用ソケットに紐付けるイベント情報を設定する */ memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */ ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */ ev.data.fd = sd_listen; /* epollインスタンスに、sd_listenと上記のイベント情報とを追加する */ epoll_ctl(epfd, EPOLL_CTL_ADD, sd_listen, &ev); while ( 1 ) { /* 以下で、epoll_waitを使用(省略) */ } II. ディスクリプタへのアクセスを検知 listen用ソケットに受信があった場合には、接続要求をしてきたクライアントとの通信を確立し、当該クライアントと接続されたソケットディスクリプタを管理対象に追加する。 なお、select、epollのいずれも、タイムアウトは設定せずブロッキングさせる場合の実装を示す。 (よって、select()の第4引数はNULL、epoll_wait()の第4引数は-1とする。) 1. select関数 select()を使うと、引数でポインタを渡したfd_set構造体(readable_fds)に読み込み可能ディスクリプタがセットされる。 この状態でFD_ISSET()を使用することで、特定のディスクリプタが読み込み可能になっているかを確認することができる。 新たなソケット(クライアントとのソケット)を管理対象に追加する方法は、I. 初期化及びlisten用ソケットの追加でlisten用ソケットを追加したときと同じである。 select.c /* 初期化部分は省略 */ while ( 1 ) { /* 結果格納用fd_setに、管理用fd_setの中身をコピーする(省略) */ /* 以下で、selectを使用 */ /* ディスクリプタの最大値を計算(省略) */ int max_fd = /*省略*/; /* selectを実行 */ if ( select(max_fd + 1, &readable_fds, NULL, NULL) <= 0 ) { /* 例外処理 */ } /* listen用ソケットに通信が来ている場合 */ if ( FD_ISSET(sd_listen, &readable_fds) ) { /* クライアントとの通信を確立(省略) */ int sd_client = accept(/*省略*/); /* 管理対象保存用fd_setにクライアントとのソケットを追加 */ FD_SET(sd_client, &target_fds); } /* クライアントソケットに通信が来ている場合 */ if ( FD_ISSET(/*省略*/) ) { /* クライアントとの処理(省略) */ } } 2. epollインターフェース epoll_wait()を使うと、引数で渡したepoll_events構造体配列(events)に、読み込み可能状態のディスクリプタの情報が格納される。 この配列の要素にアクセスすることで、読み込み可能なディスクリプタを取得できる。 新たなソケット(クライアントとのソケット)を管理対象に追加する方法は、I. 初期化及びlisten用ソケットの追加でlisten用ソケットを追加したときと同じである。 epoll.c /* 初期化部分は省略 */ while ( 1 ) { /* 以下で、epoll_waitを使用 */ /* epoll_waitは、イベントの発生したディスクリプタの数を返す */ int n_events = epoll_wait(epfd, events, N_FDS, -1); if ( n_events <= 0 ) { /* 例外処理 */ } /* 通信のあったディスクリプタを順にチェック */ int i; for ( i = 0; i < n_events; i++) { /* ディスクリプタが不正の場合 */ if ( events[i].data.fd < 0 ) { continue; } /* listen用ソケットに通信が来ている場合 */ if ( events[i].data.fd == sd_listen ) { /* クライアントとの通信を確立(省略) */ int sd_client = accept(/*省略*/); /* クライアントとのソケットに紐付けるイベント情報を設定する */ memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */ ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */ ev.data.fd = sd_client; /* epollインスタンスに、sd_clientと上記のイベント情報とを追加する */ epoll_ctl(epfd, EPOLL_CTL_ADD, sd_client, &ev); } /* その他のソケット(クライアントとのソケット)に通信が来ている場合 */ else { /* 通信してきたクライアントとのソケット */ int sd_client = events[i].data.fd; /* 当該クライアントとの処理(省略) */ } } } III. ディスクリプタを管理対象から削除 最後に、特定のディスクリプタを(追加ではなく)削除するコードを提示する。 1. select関数 FD_CLR()を使うだけである。引数はFD_SET()と同様。 select.c /* 削除対象たるディスクリプタ */ int poor_socket = /*省略*/; /* 管理対象保存用fd_setから削除 */ FD_CLR(poor_socket, &target_fds); 2. epollインターフェース 追加のときと同様に、epoll_ctl()関数を使う。 但し今回は削除なので、EPOLL_CTL_DELオプションを指定する。3 epoll.c /* 削除対象たるディスクリプタ */ int poor_socket = /*省略*/; /* epollインスタンスからpoor_socketの情報を削除 */ epoll_ctl(epfd, EPOLL_CTL_ADD, poor_socket, NULL); おわりに select()、epollいずれについても、その本質、深いところには全く触れない記事となった4が、epollを修得する助けに、或いは少なくとも一種の備忘録に、なることを願う。 おまけに、ソースコード全体を貼っておく。各処理を説明する為のツギハギ状態のコードなので、あくまで参考程度に。 1. select関数 select.c /* 管理対象となるディスクリプタの集合 */ fd_set target_fds; /* selectの結果の格納先 */ fd_set readable_fds; /* 読み込みを待つ、listen用のソケットディスクリプタ */ int sd_listen; /* 管理対象保存用fd_setを初期化 */ FD_ZERO(&target_fds); /* 管理対象保存用fd_setにlisten用ソケットを追加 */ FD_SET(sd_listen, &target_fds); while ( 1 ) { /* 結果格納用fd_setに、管理用fd_setの中身をコピーする */ /* 結果格納用fd_setはselectが書き換えてしまうので、毎回コピーする必要あり */ memcpy(&readable_fds, &target_fds, sizeof(fd_set)); /* 以下で、selectを使用 */ /* ディスクリプタの最大値を計算(省略) */ int max_fd = /*省略*/; /* selectを実行 */ if ( select(max_fd + 1, &readable_fds, NULL, NULL) <= 0 ) { /* 例外処理 */ } /* listen用ソケットに通信が来ている場合 */ if ( FD_ISSET(sd_listen, &readable_fds) ) { /* クライアントとの通信を確立(省略) */ int sd_client = accept(/*省略*/); /* 管理対象保存用fd_setにクライアントとのソケットを追加 */ FD_SET(sd_client, &target_fds); } /* クライアントソケットに通信が来ている場合 */ if ( FD_ISSET(/*省略*/) ) { /* クライアントとの処理(省略) */ } /* 削除対象たるディスクリプタ */ int poor_socket = /*省略*/; /* 管理対象保存用fd_setから削除 */ FD_CLR(poor_socket, &target_fds); } 2. epollインターフェース epoll.c /* 管理対象とするファイルディスクリプタの上限数 */ #define N_FDS (5) /* epoll_waitの結果の格納先 */ struct epoll_event events[N_FDS]; /* epollインスタンスを参照するファイルディスクリプタ */ int epfd; /* ファイルディスクリプタと紐付けるイベント情報 */ struct epoll_event ev; /* 読み込みを待つ、listen用のソケットディスクリプタ */ int sd_listen; /* epollインスタンスを作成 */ epfd = epoll_create(N_FDS); if ( epfd < 0) { /* エラー処理 */ } /* listen用ソケットに紐付けるイベント情報を設定する */ memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */ ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */ ev.data.fd = sd_listen; /* epollインスタンスに、sd_listenと上記のイベント情報とを追加する */ epoll_ctl(epfd, EPOLL_CTL_ADD, sd_listen, &ev); while ( 1 ) { /* 以下で、epoll_waitを使用 */ /* epoll_waitは、イベントの発生したディスクリプタの数を返す */ int n_events = epoll_wait(epfd, events, N_FDS, -1); if ( n_events <= 0 ) { /* 例外処理 */ } /* 通信のあったディスクリプタを順にチェック */ int i; for ( i = 0; i < n_events; i++) { /* ディスクリプタが不正の場合 */ if ( events[i].data.fd < 0 ) { continue; } /* listen用ソケットに通信が来ている場合 */ if ( events[i].data.fd == sd_listen ) { /* クライアントとの通信を確立(省略) */ int sd_client = accept(/*省略*/); /* クライアントとのソケットに紐付けるイベント情報を設定する */ memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */ ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */ ev.data.fd = sd_client; /* epollインスタンスに、sd_clientと上記のイベント情報とを追加する */ epoll_ctl(epfd, EPOLL_CTL_ADD, sd_client, &ev); } /* その他のソケット(クライアントとのソケット)に通信が来ている場合 */ else { /* 通信してきたクライアントとのソケット */ int sd_client = events[i].data.fd; /* 当該クライアントとの処理(省略) */ } } /* 削除対象たるディスクリプタ */ int poor_socket = /*省略*/; /* epollインスタンスからpoor_socketの情報を削除 */ epoll_ctl(epfd, EPOLL_CTL_ADD, poor_socket, NULL); } http://kamiyasu2.blog.fc2.com/blog-entry-45.html を参照。 ↩ 構造体epoll_eventの簡単な説明は、例えばepoll_ctlのマニュアルページ( https://linuxjm.osdn.jp/html/LDP_man-pages/man2/epoll_ctl.2.html )にある。 ↩ 今回のコードでは削除時にepoll_ctl()の第4引数にNULLを渡しているが、Linux 2.6.9 以前ではNULLを渡してはいけないらしい。( https://linuxjm.osdn.jp/html/LDP_man-pages/man2/epoll_ctl.2.html 「バグ」参照) ↩ というのも執筆者自身が理解できていないので。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

erase? trim? discard? の用語を整理してみる。

ちょっと早い夏休みだ自由研究をしよう(1日目) TL;DR TRIMという会話で不整合が起きることがたまにある。 FS TRIMと、MMC TRIMとは別概念。そこを注意して会話をしましょう。 FS TRIM : ファイルシステムの未使用ブロックの探索と開放。 MMC TRIM : 不要ブロックの開放 ■ はじめに eMMC周りの仕事をしていると、「erase」「trim」「discard」」って言葉をよく聞く。 なんか会話をしていると、どうにも話がかみ合わないことがある。 何となく雰囲気で使ってきたので、ちょっと時間を使って、それぞれの言葉の意味を整理したいと思う。 ■ 結論 file systemに対するTRIM指示とは、「割り当てられたブロック内で、未使用ブロックに対する解放を要求」。 使用中ブロックについては何もしない。 ファイルシステムが考えている「未使用」部分である。 例えば、1度確保した領域を解放した部分など。 mmc driverに対するTRIM指示とは、「指定ブロックに対する破棄の要求」。 指定されたブロックは利用中であろうとなかろうと失われる。 TRIMを実行できる条件(Host and Card)だった場合にはMMC TRIMを実行。 TRIMを実行できない条件(Host and/or Card)だった場合にはMMC ERASEを実行。 Application Layer ユーザーは、file systemに明示的にTRIM指示できる(fstrim / ioctl FITRIM)。 ユーザーは、block deviceに明示的にDISCARD指示できる(blkdiscard) Kernel Layer file systemは、TRIM指示を受けると未使用ブロックを再利用可能にするために、block deviceに破棄(DISCARD)を指示する。 block deviceはDISCARD指示を受けると、mmc driverにDISCARD指示を出す。 mmc driverはDISCARD指示を受けると、trimをハードウェアとしてサポートしているならばMMC TRIMを実行する。 サポートしていない場合には、もしくは、MMC ERASEを実行する。 ■ コマンド blkdiscard blkdiscard - discard sectors on a device https://man7.org/linux/man-pages/man8/blkdiscard.8.html https://github.com/karelzak/util-linux/blob/master/sys-utils/blkdiscard.c (memo) zero fill / discard / secure discardができる call sequence main() case 1) => ioctl(fd, BLKZEROOUT, &range) case 2) => ioctl(fd, BLKSECDISCARD, &range) case 3) => ioctl(fd, BLKDISCARD, &range) fstrim fstrim - discard unused blocks on a mounted filesystem https://man7.org/linux/man-pages/man8/fstrim.8.html https://github.com/karelzak/util-linux/blob/master/sys-utils/fstrim.c (memo) FITRIMすることでdiscardする call sequence main() case 1: => fstrim_all() => fstrim_all_from_file() => fstrim_filesystem() => ioctl(fd, FITRIM) case 2: => fstrim_filesystem() => ioctl(fd, FITRIM) ■ Linux Kernel ext4 FITRIM https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/ext4/ioctl.c?h=v5.13.6#n1047 call sequence ioctl(fd, FITRIM) => ext4_trim_fs() => ext4_trim_all_free() => ext4_trim_extent() => ext4_issue_discard() case 1: => __blkdev_issue_discard() case 2: => sb_issue_discard() block device https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/block/ioctl.c?h=v5.13.6#n455 call sequence blkdev_common_ioctl() case BLKDISCARD: blk_ioctl_discard(bdev, mode, arg, 0); => blkdev_issue_discard() => __blkdev_issue_discard() => bio_set_op_attrs(bio, REQ_OP_DISCARD, 0); case BLKSECDISCARD: blk_ioctl_discard(bdev, mode, arg, BLKDEV_DISCARD_SECURE); => blkdev_issue_discard() => __blkdev_issue_discard() => bio_set_op_attrs(bio, REQ_OP_SECURE_ERASE, 0); case BLKZEROOUT: blk_ioctl_zeroout(bdev, mode, arg); => blkdev_issue_zeroout() => __blkdev_issue_write_zeroes or __blkdev_issue_zero_pages mmc driver https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/mmc/core/block.c?h=v5.13.6#n2209 mmc_blk_mq_issue_rq() case REQ_OP_DISCARD) => mmc_blk_issue_discard_rq(mq, req); (ERASEの場合) Step1: mmc_switch(INAND_CMD38_ARG_ERASE) Step2: mmc_erase(MMC_TRIM_ERASE) => mmc_do_erase() => Step1) Execute ( MMC_ERASE_GROUP_START) => Step2) Execute ( MMC_ERASE_GROUP_END) => Step3) Execute ( MMC_ERASE, MMC_ERASE_ARG) (TRIMの場合) Step1: mmc_switch(MNAND_CMD38_ARG_TRIM) Step2: mmc_erase(MMC_TRIM_ARG) => mmc_do_erase() => Step1) Execute ( MMC_ERASE_GROUP_START) => Step2) Execute ( MMC_ERASE_GROUP_END) => Step3) Execute ( MMC_ERASE, MMC_TRIM_ARG) case REQ_OP_SECURE_ERASE) => mmc_blk_issue_secdiscard_rq(mq, req); (SECURE_ERASEの場合) Step1: mmc_switch(INAND_CMD38_ARG_SECERASE) Step2: mmc_erase(MMC_SECURE_SECERASE_ARG) => mmc_do_erase() => Step1) Execute ( MMC_ERASE_GROUP_START) => Step2) Execute ( MMC_ERASE_GROUP_END) => Step3) Execute ( MMC_ERASE, MMC_SECERASE_ARG) (SECURE_TRIMの場合) Step1: mmc_switch(INAND_CMD38_ARG_SECTRIM1) Step2: mmc_erase(MMC_SECURE_TRIM1_ARG) => mmc_do_erase() => Step1) Execute ( MMC_ERASE_GROUP_START) => Step2) Execute ( MMC_ERASE_GROUP_END) => Step3) Execute ( MMC_ERASE, MMC_SECURE_TRIM1_ARG) Step3: mmc_switch(INAND_CMD38_ARG_SECTRIM2) Step2: mmc_erase(MMC_SECURE_TRIM2_ARG) => mmc_do_erase() => Step1) Execute ( MMC_ERASE_GROUP_START) => Step2) Execute ( MMC_ERASE_GROUP_END) => Step3) Execute ( MMC_ERASE, MMC_SECURE_TRIM2_ARG)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む