Epollの実装、パート2

実装シリーズの最初の記事の翻訳を公開する際にepoll、サイクル翻訳を継続する可能性について調査を実施しました。調査参加者の90%以上が、残りの記事の翻訳を支持しました。したがって、本日、このサイクルの2番目の資料の翻訳を公開します。







Ep_insert()関数



関数ep_insert()は、実装で最も重要な関数の1つですepollそれがどのように機能するかを理解epollすることは、それが監視しているファイルから新しいイベントに関する情報をどのように正確に取得するかを理解するために非常に重要です。



宣言ep_insert()はファイルの1267行目にありますfs/eventpoll.cこの関数のいくつかのコードスニペットを見てみましょう。



user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
  return -ENOSPC;


このコードスニペットでは、関数はep_insert()最初に、現在のユーザーが監視しているファイルの総数がで指定された値を超えていないかどうかを確認します/proc/sys/fs/epoll/max_user_watchesの場合user_watches >= max_user_watches、関数はにerrno設定された状態ですぐに終了しENOSPCます。



次にep_insert()、Linuxカーネルスラブメモリ管理メカニズムを使用してメモリを割り当てます。



if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
  return -ENOMEM;


関数がに十分なメモリを割り当てることができた場合、struct epitem次の初期化プロセスが実行されます。



/*  ... */
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;


その後ep_insert()、ファイル記述子にコールバックを登録しようとします。しかし、それについて話す前に、いくつかの重要なデータ構造に精通する必要があります。



フレームワークpoll_tableは、poll()VFS実装で使用される重要なエンティティです。(これは混乱を招く可能性があることを理解していますが、ここで説明した関数は、システム呼び出しではなく、poll()ファイル操作の実装であることを説明しpoll()ますpoll())。彼女はで発表されinclude/linux/poll.hます:



typedef struct poll_table_struct {
  poll_queue_proc _qproc;
  unsigned long _key;
} poll_table;


エンティティpoll_queue_procは、次のようなコールバック関数のタイプを表します。



typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


_keyテーブルの メンバーは、poll_table実際には最初に表示されたものではありません。つまり、ある「鍵」を連想させる名前にもかかわらず、_key実際、私たちが関心を持っている出来事のマスクが保存されています。実装ではepoll _key~0(complement to 0)に設定されます。これはepoll、あらゆる種類のイベントに関する情報を受信しようとしていることを意味します。これは理にかなっています。ユーザースペースアプリケーションはepoll_ctl()、を使用していつでもイベントマスクを変更でき、VFSからすべてのイベントを受け入れてから、実装epollそれらをフィルタリングするため、作業が簡単になります。元の構造の



復元を容易にするために、それはと呼ばれる単純な構造を使用しますpoll_queue_procepitemepollep_pqueueこれはpoll_table、対応する構造epitem(ファイルfs/eventpoll.c、行243)へのポインターを持つラッパーとして機能します



/* -,    */
struct ep_pqueue {
  poll_table pt;
  struct epitem *epi;
};


次に、をep_insert()初期化しますstruct ep_pqueue次のコードは、最初に、追加しようとしているファイルに対応する構造へepiep_pqueueポインターを構造メンバー書き込みepitem次に構造ep_ptable_queue_proc()メンバー書き込み、それに書き込みます_qprocep_pqueue_key~0



/*      */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);


それはep_insert()なり、その後呼び出すep_item_poll(epi, &epq.pt);実装への呼び出しになりますこれは、poll()ファイルに関連付けられています。Linux TCPスタック



実装を使用する例を見て、poll()その実装がで何をするのかを正確に理解しましょうpoll_table



関数tcp_poll()poll()TCPソケットの実装です。そのコードは、ファイルのnet/ipv4/tcp.c436行目にあります。このコードのスニペットは次のとおりです。



unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
  unsigned int mask;
  struct sock *sk = sock->sk;
  const struct tcp_sock *tp = tcp_sk(sk);

  sock_rps_record_flow(sk);

  sock_poll_wait(file, sk_sleep(sk), wait);

  //  
}


関数tcp_poll()sock_poll_wait()、を2番目の引数sk_sleep(sk)としておよび3番目の引数として渡しますwait(これは、以前に関数に渡されたtcp_poll()テーブルですpoll_table)。



それはなんsk_sleep()ですか?結局のところ、これは特定の構造のイベント待機キューにアクセスするための単なるゲッターですsock(ファイルinclude/net/sock.h、1685行目)。



static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
  return &rcu_dereference_raw(sk->sk_wq)->wait;
}


何されるsock_poll_wait()イベント待ちキューに何をするつもり?この関数はいくつかの簡単なチェックを実行poll_wait()してから、同じパラメーターで呼び出すことがわかります。次に、関数poll_wait()は指定したコールバックを呼び出し、それにイベント待機キューを渡します(ファイルinclude/linux/poll.h、42行目)。



static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
  if (p && p->_qproc && wait_address)
    p->_qproc(filp, wait_address, p);
}


epollエンティティ の場合、1091行目のファイル宣言されて_qprocいる関数ep_ptable_queue_proc()になりますfs/eventpoll.c



/*
*  - ,       
*     ,    .
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
       poll_table *pt)
{
  struct epitem *epi = ep_item_from_epqueue(pt);
  struct eppoll_entry *pwq;

  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->whead = whead;
    pwq->base = epi;
    add_wait_queue(whead, &pwq->wait);
    list_add_tail(&pwq->llink, &epi->pwqlist);
    epi->nwait++;
  } else {
    /*       */
    epi->nwait = -1;
  }
}


まず、作業中の待機キューからファイルに対応するep_ptable_queue_proc()構造を復元しようとしepitemます。epollラッパー構造を使用しているため、ポインターからのep_pqueue復元は単純なポインター操作です。 その後、に必要なだけのメモリを割り当てます。この構造は、監視されているファイルの待機キューとそのファイルの対応する構造の間の「接着剤」として機能します。監視対象のファイルの待機キューヘッドがどこにあるかを知ることは非常に重要です。そうしないと、後で待機キューの登録を解除できなくなります。構造epitempoll_table



ep_ptable_queue_proc()struct eppoll_entryepitemepollepolleppoll_entrypwq->waitプロセス再開機能が提供された待機(キューも含まれますep_poll_callback()このエンティティは次のタスクを解決するために使用されるため、おそらくpwq->waitこれは実装全体の中で最も重要な部分epollです。



  1. 監視対象の特定のファイルで発生するイベントを監視します。
  2. そのような必要が生じた場合に他のプロセスの作業を再開する。


次に、ターゲットファイルの待機キューにep_ptable_queue_proc()アタッチpwq->waitします(whead)。この関数はstruct eppoll_entrystruct epitemepi->pwqlistからリンクリストに追加し、リストepi->nwaitの長さを表す値をインクリメントしますepi->pwqlist



そして、ここで私は1つの質問があります。epollリンクリストを使用してeppoll_entryepitem単一のファイル構造内に構造を格納するのはなぜですか?epitem1つの要素だけがeppoll_entry必要ではありませんか?



しかし、私はこの質問に正確に答えることはできません。私の知る限り、誰かがepollいくつかのクレイジーなループでインスタンスを使用する予定がない限り、リストepi->pwqlistには1つの要素のみが含まれstruct eppoll_entryepi->nwaitほとんどのファイルでは、である可能性があります1



良いことは、周りのあいまいさが、epi->pwqlist以下で説明する内容にまったく影響を与えないことです。つまり、Linuxepollが監視対象のファイルに発生したイベントのインスタンス通知する方法について説明します。



前のセクションで話したことを覚えていますか?それは、ターゲットファイルの待機リストに何がepoll追加さwait_queue_tれるかについてでした(to wait_queue_head_t)。がwait_queue_t最も一般的にプロセスを再開するためのメカニズムとして使用さ、それは基本的に単なる構造である店舗Linuxはキューからプロセスを再開することを決定したときに呼び出される関数へのポインタwait_queue_tに添付wait_queue_head_t。この機能ではepoll再開信号をどうするかを決めることができますが、epollプロセスを再開する必要はありません!後でわかるように、通常、ep_poll_callback()resumeを呼び出しても何も起こりません。



で使用されるプロセス再開メカニズムpoll()は完全に実装に依存していることにも注意する価値があると思います。 TCPソケットファイルの場合、待機キューヘッドはsk_wq構造に格納されているメンバーsockです。これep_ptable_queue_proc()は、待機キューを操作するためにコールバック使用する必要があることも説明しています。異なるファイルのキューの実装では、キューの先頭が完全に異なる場所に表示される可能性があるため、必要な値を見つける方法がありません。wait_queue_head_tコールバックを使用せずに。構造内



の作業の再開はいつ正確に実行されますか?結局のところ、LinuxソケットシステムはVFSと同じ「OO」設計原則に従います。この構造は、ファイルの2312行目で次のフックを宣言していますsk_wqsocksocknet/core/sock.c



void sock_init_data(struct socket *sock, struct sock *sk)
{
  //  ...
  sk->sk_data_ready  =   sock_def_readable;
  sk->sk_write_space =  sock_def_write_space;
  //  ...
}


Bsock_def_readable()およびsock_def_write_space()コールがあるwake_up_interruptible_sync_poll()ため(struct sock)->sk_wq、関数コールバック、再生可能な処理作業の目的。



ときになるsk->sk_data_ready()呼び出されますかsk->sk_write_space()実装によって異なります。例としてTCPソケットを取り上げましょう。この関数sk->sk_data_ready()は、TCP接続が3方向ハンドシェイク手順を完了したとき、または特定のTCPソケットのバッファーを受信したときに、割り込みハンドラーの後半で呼び出されます。この関数sk->sk_write_space()は、バッファの状態がからfull変わるときに呼び出さavailableます。次のトピック、特にフロントトリガーに関するトピックを分析するときにこれを覚えておくと、これらのトピックはより興味深いものになります。



結果



これで、実装に関する一連の記事の2番目の記事は終わりepollです。次回epollは、ソケットプロセス再開キューに登録されたコールバックで正確に何をするかについて話しましょう。



epollを使用したことがありますか?










All Articles