C ++バックエンド開発プログラマーとして、Epoll実装の原則を完全に理解する必要があります

この記事を理解していなくても、最初にブックマークすることができます。

もちろん、これらのコアアイデアは後続の記事でゆっくりと詳細に説明されますので、注意してください。

記事を本当に理解していない場合は、このビデオの説明を振り返ることができます:epoll主成分分析

Epollは、Linuxプラットフォームでの独自の多重化IO実装方法です。従来の選択と比較して、epollのパフォーマンスは大幅に向上しています。この記事では、主にepollの実装原理について説明しますが、epollの使用については、関連する書籍や記事を参照してください。

epollの作成

epollを使用するには、最初にepoll_create()関数を呼び出してepollハンドルを作成する必要があります。epoll_create()関数は次のように定義されています。

int epoll_create(int size);

パラメータサイズは、歴史的な理由により残されており、現在は機能しません。ユーザーがepoll_create()関数を呼び出すと、カーネルスペースに入り、sys_epoll_create()カーネル関数を呼び出してepollハンドルを作成します。sys_epoll_create()関数コードは次のとおりです。

asmlinkage long sys_epoll_create(int size)
{
    int error, fd = -1;
    struct eventpoll *ep;

    error = -EINVAL;
    if (size <= 0 || (error = ep_alloc(&ep)) < 0) {
        fd = error;
        goto error_return;
    }

    fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep);
    if (fd < 0)
        ep_free(ep);

error_return:
    return fd;
}

sys_epoll_create()は、主に2つのことを行います。

  1. ep_alloc()関数を呼び出して、eventpollオブジェクトを作成および初期化します。
  2. anon_inode_getfd()関数を呼び出して、eventpollオブジェクトをファイルハンドルにマップし、ファイルハンドルを返します。

最初にeventpollオブジェクトを見てみましょう。eventpollオブジェクトは、epollによって監視されるファイルのリストを管理するために使用されます。その定義は次のとおりです。

struct eventpoll {
    ...
    wait_queue_head_t wq;
    ...
    struct list_head rdllist;
    struct rb_root rbr;
    ...
};

まず、eventpollオブジェクトの各メンバーの役割について説明します。

  1. wq:待機キュー。epoll_wait(fd)を呼び出すと、プロセスはeventpollオブジェクトのwq待機キューに追加されます。
  2. rdllist:準備ができているファイルのリストを保存します。
  3. rbr:赤黒木を使用して、すべての監視対象ファイルを管理します。

次の図は、eventpollオブジェクトと監視対象ファイルの関係を示しています。

 

記事の理解について質問がある場合は、私にqunを追加してください:議論するために飛び込んください

C / C ++ Linuxサーバー開発の高度なアーキテクチャアーキテクチャの学習資料についてもっと共有します 。コンテンツの知識ポイントには、Linux、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、ストリーミングメディア、CDN、P2P、K8S、Docker、TCP / IP、 Coroutine、DPDKなど。

ラーニングビデオリンクをクリックしますC / C ++ Linuxサーバー開発/ Linuxバックグラウンド開発アーキテクト-ラーニングビデオ

監視対象ファイルはエピテムオブジェクトによって管理されているため、上の図のノードはすべてエピテムオブジェクトの形式で存在します。監視対象ファイルの管理に赤黒木を使用するのはなぜですか?これは、ファイルハンドルを介して対応するエピテムオブジェクトをすばやく見つけることができるようにするためです。赤黒木は平衡二分木です。わからない場合は、関連資料を参照してください。

epollにファイルハンドルを追加する

以前にepollを作成する方法を紹介し、次に監視対象のファイルをepollに追加する方法を紹介します。

epoll_ctl()関数を呼び出すことにより、監視対象のファイルをepollに追加できます。プロトタイプは次のとおりです。

long epoll_ctl(int epfd, int op, int fd,struct epoll_event *event);

次に、各パラメーターの役割について説明します。

  1. epfd:epoll_create()関数を呼び出すことによって返されるファイルハンドル。
  2. op:実行する操作。3つのオプションがあります。
  3. EPOLL_CTL_ADD:追加操作を実行することを示します。
  4. EPOLL_CTL_DEL:削除操作を実行することを示します。
  5. EPOLL_CTL_MOD:変更操作を実行することを示します。
  6. fd:監視するファイルハンドル。
  7. イベント:何を監視するかをカーネルに指示します。その定義は次のとおりです。
struct epoll_event {
    __uint32_t events;  /* Epoll events */
    epoll_data_t data;  /* User data variable */
};

イベントは、次のマクロのコレクションにすることができます。

  • EPOLLIN:対応するファイルハンドルを読み取ることができることを示します(ピアソケットの通常のクローズを含む)。
  • EPOLLOUT:対応するファイルハンドルを書き込むことができることを示します。
  • EPOLLPRI:対応するファイルハンドルに読み取る緊急データがあることを示します。
  • EPOLLERR:対応するファイルハンドルにエラーがあることを示します。
  • EPOLLHUP:対応するファイルハンドルがハングアップしていることを示します。
  • EPOLLET:EPOLLをエッジトリガーモードに設定します。これは、レベルトリガーに関連しています。
  • EPOLLONESHOT:イベントを1回だけリッスンします。このイベントをリッスンした後、ソケットをリッスンし続ける必要がある場合は、ソケットをEPOLLキューに再度追加する必要があります。

データは、ユーザー定義データを保存するために使用されます。

epoll_ctl()関数はsys_epoll_ctl()カーネル関数を呼び出します。sys_epoll_ctl()カーネル関数の実装は次のとおりです。

asmlinkage long sys_epoll_ctl(int epfd, int op,
    int fd, struct epoll_event __user *event)
{
    ...
    file = fget(epfd);
    tfile = fget(fd);
    ...
    ep = file->private_data;

    mutex_lock(&ep->mtx);

    epi = ep_find(ep, tfile, fd);

    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;

            error = ep_insert(ep, &epds, tfile, fd);
        } else
            error = -EEXIST;
        break;
    ...
    }
    mutex_unlock(&ep->mtx);

    ...
    return error;
}

sys_epoll_ctl()関数は、渡されたさまざまなopsの値に基づいてさまざまな操作を実行します。たとえば、EPOLL_CTL_ADDを渡すと、追加することを示し、ep_insert()関数を呼び出して追加します。

追加操作ep_insert()関数の実装の分析を続けましょう。

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
             struct file *tfile, int fd)
{
    ...
    error = -ENOMEM;
    // 申请一个 epitem 对象
    if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
        goto error_return;

    // 初始化 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;

    epq.epi = epi;
    // 等价于: epq.pt->qproc = ep_ptable_queue_proc
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

    // 调用被监听文件的 poll 接口.
    // 这个接口又各自文件系统实现, 如socket的话, 那么这个接口就是 tcp_poll().
    revents = tfile->f_op->poll(tfile, &epq.pt);
    ...
    ep_rbtree_insert(ep, epi); // 把 epitem 对象添加到epoll的红黑树中进行管理

    spin_lock_irqsave(&ep->lock, flags);

    // 如果被监听的文件已经可以进行对应的读写操作
    // 那么就把文件添加到epoll的就绪队列 rdllink 中, 并且唤醒调用 epoll_wait() 的进程.
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);

        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

    spin_unlock_irqrestore(&ep->lock, flags);
    ...
    return 0;
    ...
}

監視対象ファイルはエピテムオブジェクトによって管理されます。つまり、監視対象ファイルはエピテムオブジェクトにカプセル化され、管理のためにeventpollオブジェクトの赤黒木に追加されます(ep_rbtree_insert(ep、epi)など)。 )。

tfile-> f_op-> poll(tfile、&epq.pt)このコード行の機能は、監視対象ファイルのpoll()インターフェイスを呼び出すことです。監視対象ファイルがソケットハンドルの場合、tcp_poll()が呼び出されます。 。tcp_poll()の機能を見てみましょう。

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
    struct sock *sk = sock->sk;
    ...
    poll_wait(file, sk->sk_sleep, wait);
    ...
    return mask;
}

各ソケットオブジェクトには待機キューがあり(待機キューについては、待機キューの原理と実装の記事を参照してください)、ソケットの状態が変化するのを待機しているプロセスを格納するために使用されます。

上記のコードから、tcp_poll()がpoll_wait()関数を呼び出し、poll_wait()が最終的にep_ptable_queue_proc()関数を呼び出すことがわかります。ep_ptable_queue_proc()関数は次のように実装されます。

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()関数の主な作業は、現在のエピテムオブジェクトをソケットオブジェクトの待機キューに追加し、ウェイクアップ関数をep_poll_callback()に設定することです。つまり、ソケットステータスが変更されると、 ep_poll_callback()関数を呼び出します。ep_poll_callback()関数は次のように実装されます。

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    ...
    // 把就绪的文件添加到就绪队列中
    list_add_tail(&epi->rdllink, &ep->rdllist);

is_linked:
    // 唤醒调用 epoll_wait() 而被阻塞的进程
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
    ...
    return 1;
}

記事の理解について質問がある場合は、私にqunを追加してください:飛び込んで、入って、話し合ってください

C / C ++ Linuxサーバー開発の高度なアーキテクチャアーキテクチャの学習資料についてもっと共有します 。コンテンツの知識ポイントには、Linux、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、ストリーミングメディア、CDN、P2P、K8S、Docker、TCP / IP、 Coroutine、DPDKなど。

ラーニングビデオリンクをクリックしますC / C ++ Linuxサーバー開発/ Linuxバックグラウンド開発アーキテクト-ラーニングビデオ

ep_poll_callback()関数の主な仕事は、readyファイルをeventepollオブジェクトのreadyキューに追加し、epoll_wait()を呼び出してブロックされたプロセスをウェイクアップすることです。

監視対象ファイルのステータスが変化するのを待っています

監視対象のファイルハンドルをepollに追加した後、epoll_wait()を呼び出すことにより、監視対象のファイルのステータスが変化するのを待つことができます。epoll_wait()呼び出しは、現在のプロセスをブロックします。監視対象ファイルのステータスが変更されると、epoll_wait()呼び出しが返されます。

epoll_wait()システムコールのプロトタイプは次のとおりです。

long epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

各パラメーターの意味:

  1. epfd:epoll_create()関数を呼び出して作成されたepollハンドル。
  2. イベント:準備ができたファイルのリストを保存するために使用されます。
  3. maxevents:イベント配列のサイズ。
  4. タイムアウト:待機タイムアウト時間を設定します。

epoll_wait()関数はsys_epoll_wait()カーネル関数を呼び出し、sys_epoll_wait()関数は最終的にep_poll()関数を呼び出します。ep_poll()関数の実装を見てみましょう。

static int ep_poll(struct eventpoll *ep,
    struct epoll_event __user *events, int maxevents, long timeout)
{
    ...
    // 如果就绪文件列表为空
    if (list_empty(&ep->rdllist)) {
        // 把当前进程添加到epoll的等待队列中
        init_waitqueue_entry(&wait, current);
        wait.flags |= WQ_FLAG_EXCLUSIVE;
        __add_wait_queue(&ep->wq, &wait);

        for (;;) {
            set_current_state(TASK_INTERRUPTIBLE); // 把当前进程设置为睡眠状态
            if (!list_empty(&ep->rdllist) || !jtimeout) // 如果有就绪文件或者超时, 退出循环
                break;
            if (signal_pending(current)) { // 接收到信号也要退出
                res = -EINTR;
                break;
            }

            spin_unlock_irqrestore(&ep->lock, flags);
            jtimeout = schedule_timeout(jtimeout); // 让出CPU, 切换到其他进程进行执行
            spin_lock_irqsave(&ep->lock, flags);
        }
        // 有3种情况会执行到这里:
        // 1. 被监听的文件集合中有就绪的文件
        // 2. 设置了超时时间并且超时了
        // 3. 接收到信号
        __remove_wait_queue(&ep->wq, &wait);

        set_current_state(TASK_RUNNING);
    }
    /* 是否有就绪的文件? */
    eavail = !list_empty(&ep->rdllist);

    spin_unlock_irqrestore(&ep->lock, flags);

    if (!res && eavail
        && !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
        goto retry;

    return res;
}

ep_poll()関数は、主に次のことを行います。

  1. 監視対象ファイルコレクションに準備完了ファイルがあるかどうかを確認し、ある場合は戻ります。
  2. そうでない場合は、現在のプロセスをepollの待機キューに追加し、スリープ状態にします。
  3. プロセスは、次の状況が発生するまでスリープします。
    1. 監視対象ファイルコレクションに準備完了ファイルがあります
    2. タイムアウト期間が設定され、タイムアウトします
    3. 受信信号
  4. 準備完了ファイルがある場合は、ep_send_events()関数を呼び出して、準備完了ファイルをeventsパラメーターにコピーします。
  5. 準備ができているファイルの数を返します。

最後に、epollの原理を図で要約します。

 

次のテキストは、このプロセスを説明しています。

  1. epoll_create()関数を呼び出して、eventpollオブジェクトを作成および初期化します。
  2. epoll_ctl()関数を呼び出して、監視対象のファイルハンドル(ソケットハンドルなど)をepitemオブジェクトにカプセル化し、管理のためにeventpollオブジェクトの赤黒木に追加します。
  3. epoll_wait()関数を呼び出して、監視対象ファイルのステータスが変化するのを待ちます。
  4. 監視対象ファイルのステータスが変化すると(たとえば、ソケットがデータを受信すると)、ファイルハンドルに対応するエピテムオブジェクトがeventpollオブジェクトのレディキューrdllistに追加されます。そして、レディキューのファイルリストをepoll_wait()関数のeventsパラメータにコピーします。
  5. epoll_wait()関数を呼び出して、ブロックされている(スリープしている)プロセスをウェイクアップします。

おすすめ

転載: blog.csdn.net/Linuxhus/article/details/114135530