Linuxネットワークプログラミング-epollの基盤となる実装の分析

基本的なデータ構造

epollで使用されているデータ構造であるeventpoll、epitem、eppoll_entryを見てみましょう。

eventpollデータ構造。このデータ構造は epollインスタンスを表すepoll_createを呼び出した後にカーネル側で作成されるハンドルです後で、 epoll_ctlやepoll_waitなどを呼び出すと、このeventpollデータ操作しますデータのこの部分は、epoll_createによって作成された匿名ファイルファイルのprivate_dataフィールド保存されます

/*
 * This structure is stored inside the "private_data" member of the file
 * structure and represents the main data structure for the eventpoll
 * interface.
 */
struct eventpoll {
    /* Protect the access to this structure */
    spinlock_t lock;

    /*
     * This mutex is used to ensure that files are not removed
     * while epoll is using them. This is held during the event
     * collection loop, the file cleanup path, the epoll file exit
     * code and the ctl operations.
     */
    struct mutex mtx;

    /* Wait queue used by sys_epoll_wait() */
    //这个队列里存放的是执行epoll_wait从而等待的进程队列
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    //这个队列里存放的是该eventloop作为poll对象的一个实例,加入到等待的队列
    //这是因为eventpoll本身也是一个file, 所以也会有poll操作
    wait_queue_head_t poll_wait;

    /* List of ready file descriptors */
    //这里存放的是事件就绪的fd列表,链表的每个元素是下面的epitem
    struct list_head rdllist;

    /* RB tree root used to store monitored fd structs */
    //这是用来快速查找fd的红黑树
    struct rb_root_cached rbr;

    /*
     * This is a single linked list that chains all the "struct epitem" that
     * happened while transferring ready events to userspace w/out
     * holding ->lock.
     */
    struct epitem *ovflist;

    /* wakeup_source used when ep_scan_ready_list is running */
    struct wakeup_source *ws;

    /* The user that created the eventpoll descriptor */
    struct user_struct *user;

    //这是eventloop对应的匿名文件,充分体现了Linux下一切皆文件的思想
    struct file *file;

    /* used to optimize loop detection check */
    int visited;
    struct list_head visited_list_link;

#ifdef CONFIG_NET_RX_BUSY_POLL
    /* used to track busy poll napi_id */
    unsigned int napi_id;
#endif
};

コードでエピテムについて言及しましたが、このエピテム構造は何のためのものですか?

epoll_ctlを呼び出してfdを追加するたびに、カーネルはエピテムインスタンスを作成し、このインスタンスを赤黒木の子ノードとしてeventpoll構造の赤黒木に追加します。対応するフィールドはrbrです。その後、各fdでイベントが発生したかどうか確認するために、赤黒木のエピテムを介して操作します

/*
 * Each file descriptor added to the eventpoll interface will
 * have an entry of this type linked to the "rbr" RB tree.
 * Avoid increasing the size of this struct, there can be many thousands
 * of these on a server and we do not want this to take another cache line.
 */
struct epitem {
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    /* List header used to link this structure to the eventpoll ready list */
    //将这个epitem连接到eventpoll 里面的rdllist的list指针
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
    //epoll监听的fd
    struct epoll_filefd ffd;

    /* Number of active wait queue attached to poll operations */
    //一个文件可以被多个epoll实例所监听,这里就记录了当前文件被监听的次数
    int nwait;

    /* List containing poll wait queues */
    struct list_head pwqlist;

    /* The "container" of this item */
    //当前epollitem所属的eventpoll
    struct eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct list_head fllink;

    /* wakeup_source used when EPOLLWAKEUP is set */
    struct wakeup_source __rcu *ws;

    /* The structure that describe the interested events and the source fd */
    struct epoll_event event;
};

fdがepollインスタンスに関連付けられるたびに、eppoll_entryが生成されます。eppoll_entryの構造は次のとおりです。

/* Wait structure used by the poll hooks */
struct eppoll_entry {
    /* List header used to link this structure to the "struct epitem" */
    struct list_head llink;

    /* The "base" pointer is set to the container "struct epitem" */
    struct epitem *base;

    /*
     * Wait queue item that will be linked to the target file wait
     * queue head.
     */
    wait_queue_entry_t wait;

    /* The wait queue head that linked the "wait" wait queue item */
    wait_queue_head_t *whead;
};

epoll_create

epollを使用する場合、最初にepoll_createを呼び出してepollインスタンスを作成します。

まず、epoll_createは、渡されたフラグパラメータを確認するだけです。

/* Check the EPOLL_* constant for consistency.  */
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);

if (flags & ~EPOLL_CLOEXEC)
    return -EINVAL;
/*

カーネルは、eventpollに必要なメモリスペースの割り当てに適用されます。

/* Create the internal data structure ("struct eventpoll").
*/
error = ep_alloc(&ep);
if (error < 0)
  return error;

次のステップで、epoll_createは匿名ファイルとファイル記述ワードをepollインスタンスに割り当てます。ここで、fdはファイル記述ワードであり、fileは匿名ファイルです。これは、すべてがUNIXではファイルであるという考えを完全に具体化したものです。eventpollのインスタンスは匿名ファイルへの参照を保存し、fd_install関数を呼び出すことによって匿名ファイルをファイルの説明にバインドすることに注意してください。

特に注意が必要な点がもう1つあります。anon_inode_getfileを呼び出すと、epoll_createはeventpollを匿名ファイルファイルのprivate_dataとして保存するため、後でepollインスタンスeventpollオブジェクトのファイル記述ワードから見つけたときにすばやく見つけることができます。

最後に、このファイル記述ワードはepollのファイルハンドルとして使用され、epoll_createの呼び出し元に返されます。

/*
 * Creates all the items needed to setup an eventpoll file. That is,
 * a file structure and a free file descriptor.
 */
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
if (fd < 0) {
    error = fd;
    goto out_free_ep;
}
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
             O_RDWR | (flags & O_CLOEXEC));
if (IS_ERR(file)) {
    error = PTR_ERR(file);
    goto out_free_fd;
}
ep->file = file;
fd_install(fd, file);
return fd;

epoll_ctl

次に、ソケットがepollインスタンスにどのように追加されるかを確認します。

epollインスタンスを検索します。

まず、epoll_ctl関数は、epollインスタンスハンドルを介して対応する匿名ファイルを取得します。これは理解しやすいです。UNIXではすべてがファイルであり、epollのインスタンスも匿名ファイルです。

//获得epoll实例对应的匿名文件
f = fdget(epfd);
if (!f.file)
    goto error_return;

次に、追加されたソケットに対応するファイルを取得します。ここで、tfは、処理されるターゲットファイルであるターゲットファイルを表します。

/* Get the "struct file *" for the target file */
//获得真正的文件,如监听套接字、读写套接字
tf = fdget(fd);
if (!tf.file)
    goto error_fput;

次に、一連のデータ検証が実行され、ユーザーから渡されたパラメーターが正当であることを確認します。たとえば、epfdは実際にはepollインスタンスハンドルであり、通常のファイル記述子ではありません。

/* The target file descriptor must support poll */
//如果不支持poll,那么该文件描述字是无效的
error = -EPERM;
if (!tf.file->f_op->poll)
    goto error_tgt_fput;
...

実際のepollインスタンスハンドルを取得する場合は、private_dataを介して以前に作成されたeventpollインスタンスを取得できます。

/*
 * At this point it is safe to assume that the "private_data" contains
 * our own data structure.
 */
ep = f.file->private_data;

赤黒木検索:

epoll_ctlは、ターゲットファイルと対応する説明を通じて、ソケットが赤黒木に存在するかどうかを検出します。これが、epollが効率的である理由です。赤黒木(RBツリー)は一般的なデータ構造であり、eventpollは赤黒木を通じて現在監視されているすべてのファイル記述を追跡し、このツリーのルートはeventpollデータ構造に格納されます。

/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;

監視されているファイル記述ワードごとに、それに対応する対応するエピテムがあり、エピテムは赤黒木内のノードとして赤黒木に格納されます。

/*
 * Try to lookup the file inside our RB tree, Since we grabbed "mtx"
 * above, we can be sure to be able to use the item looked up by
 * ep_find() till we release the mutex.
 */
epi = ep_find(ep, tf.file, fd);

赤黒木は二分木です。二分木のノードとして、エピテムは、順序付けられた二分木をサイズ順に構築できるように、比較機能を提供する必要があります。そのソート機能は、epoll_filefd構造に依存することによって行われます。epoll_filefdは、監視する必要のあるファイル記述ワードとして簡単に理解できます。これは、バイナリツリーのノードに対応します。

これは、ファイルアドレスサイズでソートすると、比較的理解しやすいことがわかります。2つが同じである場合、ファイルの説明に従ってソートされます。

struct epoll_filefd {
  struct file *file; // pointer to the target file struct corresponding to the fd
  int fd; // target file descriptor number
} __packed;

/* Compare RB tree keys */
static inline int ep_cmp_ffd(struct epoll_filefd *p1,
                            struct epoll_filefd *p2)
{
  return (p1->file > p2->file ? +1:
       (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}

赤黒木検索を実行した後、それがADD操作であることが判明し、対応する二分木ノードがツリー内に見つからない場合、ep_insertが呼び出されて二分木ノードが増加します。

case EPOLL_CTL_ADD:
    if (!epi) {
        epds.events |= POLLERR | POLLHUP;
        error = ep_insert(ep, &epds, tf.file, fd, full_check);
    } else
        error = -EEXIST;
    if (full_check)
        clear_tfile_check_list();
    break;

ep_insert

まず、現在監視されているファイルの値が/ proc / sys / fs / epoll / max_user_watchesの事前設定された最大値を超えているかどうかを判断します。超えている場合は、エラーが直接返されます。

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

次のステップは、リソースを割り当ててアクションを初期化することです。

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

    /* Item initialization follow here ... */
    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が追加されたファイル記述ワードごとにコールバック関数を設定することです。このコールバック関数は、関数ep_ptable_queue_procを介して設定されます。このコールバック関数は何をしますか?実際、対応するファイル記述でイベントが発生すると、この関数が呼び出されます。たとえば、ソケットバッファーにデータがある場合、この関数はコールバックされます。この関数はep_poll_callbackです。ここでは、元のカーネル設計にもイベントコールバックの原則が満載されていることがわかります。

/*
 * This is the callback that is used to add our wait queue to the
 * target file wakeup lists.
 */
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;
        if (epi->event.events & EPOLLEXCLUSIVE)
            add_wait_queue_exclusive(whead, &pwq->wait);
        else
            add_wait_queue(whead, &pwq->wait);
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        /* We have to signal that an error occurred */
        epi->nwait = -1;
    }
}

ep_poll_callback

ep_poll_callback関数は非常に重要であり、カーネルイベントをepollオブジェクトに実際に接続します。それはどのように達成されますか?

まず、このファイルのwait_queue_entry_tオブジェクトから対応するエピテムオブジェクトを見つけます。これは、wait_quue_entry_tがeppoll_entryオブジェクトに格納されており、eppoll_entryオブジェクトのアドレスはwait_quue_entry_tオブジェクトのアドレスに基づいて簡単に計算できるためです。エピテムオブジェクトを取得できます。作業のこの部分は、ep_item_from_wait関数で完了しますエピテムオブジェクトが取得されると、eventpollインスタンスを追跡できます。

/*
 * This is the callback that is passed to the wait queue wakeup
 * mechanism. It is called by the stored file descriptors when they
 * have events to report.
 */
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
    int pwake = 0;
    unsigned long flags;
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;

次に、ロック操作を実行します。

spin_lock_irqsave(&ep->lock, flags);

以下は、発生するイベントをフィルタリングするためのものですが、なぜフィルタリングする必要があるのですか?パフォーマンスを考慮して、ep_insertはすべてのイベントを対応する監視ファイルに登録しますが、ユーザー側がサブスクライブした実際のイベントはカーネルイベントに対応しない場合があります。たとえば、ユーザーはソケット読み取りイベントのカーネルをサブスクライブし、ソケット書き込みイベントが特定の時間に発生した場合、そのイベントをユーザースペースに渡す必要はありません。

/*
 * Check the events coming with the callback. At this stage, not
 * every device reports the events in the "key" parameter of the
 * callback. We need to be able to handle both cases here, hence the
 * test for "key" != NULL before the event match test.
 */
if (key && !((unsigned long) key & epi->event.events))
    goto out_unlock;

次に、イベントをユーザースペースに渡す必要があるかどうかを判断します。

if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
  if (epi->next == EP_UNACTIVE_PTR) {
      epi->next = ep->ovflist;
      ep->ovflist = epi;
      if (epi->ws) {
          /*
           * Activate ep->ws since epi->ws may get
           * deactivated at any time.
           */
          __pm_stay_awake(ep->ws);
      }
  }
  goto out_unlock;
}

必要に応じて、イベントに対応するevent_itemがeventpollに対応する完了キューにない場合は、イベントをユーザースペースに配信できるように、キューに入れます。

/* If this file is already in the ready list we exit soon */
if (!ep_is_linked(&epi->rdllink)) {
    list_add_tail(&epi->rdllink, &ep->rdllist);
    ep_pm_stay_awake_rcu(epi);
}

epoll_waitを呼び出すと、呼び出しプロセスが一時停止されることがわかっています。カーネルの観点からは、呼び出しプロセスはスリープ状態になります。epollインスタンスの対応する記述子でイベントが発生した場合は、休止状態のプロセスを起動して、イベントを時間内に処理する必要があります。次のコードはこの目的のためのものです。wake_up_locked関数は、現在のeventpollで待機中のプロセスをウェイクアップします。

/*
 * Wake up ( if active ) both the eventpoll wait list and the ->poll()
 * wait list.
 */
if (waitqueue_active(&ep->wq)) {
    if ((epi->event.events & EPOLLEXCLUSIVE) &&
                !((unsigned long)key & POLLFREE)) {
        switch ((unsigned long)key & EPOLLINOUT_BITS) {
        case POLLIN:
            if (epi->event.events & POLLIN)
                ewake = 1;
            break;
        case POLLOUT:
            if (epi->event.events & POLLOUT)
                ewake = 1;
            break;
        case 0:
            ewake = 1;
            break;
        }
    }
    wake_up_locked(&ep->wq);
}

epollインスタンスを見つける

epoll_wait関数は、最初に一連のチェックを実行します。たとえば、着信maxeventsは0より大きい必要があります。

/* The maximum number of event must be greater than zero */
if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
    return -EINVAL;
/* Verify that the area passed by the user is writeable */
if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event)))
    return -EFAULT;

前述のepoll_ctlと同様に、epollインスタンスから対応する匿名ファイルと説明を見つけ、確認してください。

/* Get the "struct file *" for the eventpoll file */
f = fdget(epfd);
if (!f.file)
    return -EBADF;
/*
 * We have to check that the file structure underneath the fd
 * the user passed to us _is_ an eventpoll file.
 */
error = -EINVAL;
if (!is_file_epoll(f.file))
    goto error_fput;

epollインスタンスに対応する匿名ファイルのprivate_dataを読み取って、eventpollインスタンスを取得します。

/*
 * At this point it is safe to assume that the "private_data" contains
 * our own data structure.
 */
ep = f.file->private_data;

次に、ep_pollを呼び出して、対応するイベントの収集を完了し、それらをユーザースペースに配信します。

/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout);

ep_poll

epoll関数が以前に導入されたとき、対応するタイムアウト値を0より大きく、0に等しく、0未満にすることはできますか?ここで、ep_pollはタイムアウトの値が異なるシナリオを扱います。0より大きい場合はタイムアウト期間が生成され、0より大きい場合は、イベントが発生したかどうかがすぐにチェックされます。

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
u64 slack = 0;
wait_queue_entry_t wait;
ktime_t expires, *to = NULL;

if (timeout > 0) {
    struct timespec64 end_time = ep_set_mstimeout(timeout);
    slack = select_estimate_accuracy(&end_time);
    to = &expires;
    *to = timespec64_to_ktime(end_time);
} else if (timeout == 0) {
    /*
     * Avoid the unnecessary trip to the wait queue loop, if the
     * caller specified a non blocking operation.
     */
    timed_out = 1;
    spin_lock_irqsave(&ep->lock, flags);
    goto check_events;
}

次に、eventpollのロックを取得してみてください。

spin_lock_irqsave(&ep->lock, flags);

ロックを取得した後、現在発生しているイベントがあるかどうかを確認します。ない場合は、現在のプロセスをeventpoll待機キューwqに追加します。これは、イベントが発生したときに、ep_poll_callback関数が待機中のプロセスをウェイクアップできるようにすることを目的としています。

if (!ep_events_available(ep)) {
    /*
     * Busy poll timed out.  Drop NAPI ID for now, we can add
     * it back in when we have moved a socket with a valid NAPI
     * ID onto the ready list.
     */
    ep_reset_busy_poll_napi_id(ep);

    /*
     * We don't have any available event to return to the caller.
     * We need to sleep here, and we will be wake up by
     * ep_poll_callback() when events will become available.
     */
    init_waitqueue_entry(&wait, current);
    __add_wait_queue_exclusive(&ep->wq, &wait);

次に、無限ループが発生します。このループでは、schedule_hrtimeout_rangeを呼び出して現在のプロセスをスリープ状態にし、他のプロセスのスケジューラーによってCPU時間をスケジュールします。もちろん、現在のプロセスを起動することもできます。起動条件には、次の4つ:

  1. 現在のプロセスはタイムアウトしました。
  2. 現在のプロセスはシグナル信号を受信します。
  3. 特定の説明でイベントが発生しました。
  4. 現在のプロセスはCPUによって再スケジュールされ、forループに入って再判断されます。最初の3つの条件が満たされない場合、プロセスは再びスリープ状態になります。
//这个循环里,当前进程可能会被唤醒,唤醒的途径包括
//1.当前进程超时
//2.当前进行收到一个signal信号
//3.某个描述字上有事件发生
//对应的1.2.3都会通过break跳出循环
//第4个可能是当前进程被CPU重新调度,进入for循环的判断,如果没有满足1.2.3的条件,就又重新进入休眠
for (;;) {
    /*
     * We don't want to sleep if the ep_poll_callback() sends us
     * a wakeup in between. That's why we set the task state
     * to TASK_INTERRUPTIBLE before doing the checks.
     */
    set_current_state(TASK_INTERRUPTIBLE);
    /*
     * Always short-circuit for fatal signals to allow
     * threads to make a timely exit without the chance of
     * finding more events available and fetching
     * repeatedly.
     */
    if (fatal_signal_pending(current)) {
        res = -EINTR;
        break;
    }
    if (ep_events_available(ep) || timed_out)
        break;
    if (signal_pending(current)) {
        res = -EINTR;
        break;
    }

    spin_unlock_irqrestore(&ep->lock, flags);

    //通过调用schedule_hrtimeout_range,当前进程进入休眠,CPU时间被调度器调度给其他进程使用
    if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
        timed_out = 1;

    spin_lock_irqsave(&ep->lock, flags);
}

プロセスがスリープ状態から戻った場合は、eventpollの待機キューから現在のプロセスを削除し、現在のプロセスをTASK_RUNNING状態に設定します。

//从休眠中结束,将当前进程从wait队列中删除,设置状态为TASK_RUNNING,接下来进入check_events,来判断是否是有事件发生
    __remove_wait_queue(&ep->wq, &wait);
    __set_current_state(TASK_RUNNING);

最後に、ep_send_eventsを呼び出して、イベントをユーザースペースにコピーします。

//ep_send_events将事件拷贝到用户空间
/*
 * Try to transfer events to user space. In case we get 0 events and
 * there's still timeout left over, we go trying again in search of
 * more luck.
 */
if (!res && eavail &&
    !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
    goto fetch_events;

return res;

ep_send_event

ep_send_eventsこの関数は、コールバック関数としてep_send_events_procを使用し、ep_scan_ready_list関数を呼び出します。ep_scan_ready_list関数は、ep_send_events_procを呼び出して、準備ができている各イベントループを処理します。

ep_send_events_procループがreadyイベントを処理するとき、各ファイル記述子のpollメソッドを再度呼び出して、イベントが実際に発生したことを確認します。なぜこれをするのですか?これは、登録されたイベントが現時点でも有効であることを確認するためです。

ep_send_events_procは、ユーザースペースによって取得されたイベント通知を現実的かつ効果的にするために可能な限り考慮されていますが、それでも一定の確率があります。ep_send_events_procがファイルのポーリング関数を再度呼び出すと、取得されたイベント通知が取得されます。ユーザースペース別有効ではなくなりました。これは、ユーザースペースが処理されたか、その他の状況である可能性があります。この場合、ソケットが非ブロッキングでない場合、プロセス全体がブロックされます。そのため、epollで非ブロッキングソケットを使用することがベストプラクティスです。

単純なイベントマスクチェックの後、ep_send_events_procはイベント構造をユーザースペースに必要なデータ構造にコピーします。これは、__ put_userメソッドを介して行われます。

レベルトリガーVSエッジトリガー

以前は、レベルトリガーとエッジトリガーの違いを強調してきました。

実装の観点からは、実際には非常に単純です。ep_send_events_proc関数の最後に、レベルトリガーの状況で、現在のepoll_itemオブジェクトがeventpollの準備完了リストに再度追加されるため、これらのepoll_itemオブジェクトは次のようになります。次のepoll_waitが呼び出されたときに再処理されます。

前述したように、最終的にユーザースペースの有効なイベントリストにコピーする前に、対応するファイルのpollメソッドが呼び出され、イベントがまだ有効かどうかが判断されます。したがって、ユーザースペースプログラムがイベントを処理した場合、それは再度通知されません。処理されない場合、イベントはまだ有効であり、再度通知されることを意味します。

//这里是Level-triggered的处理,可以看到,在Level-triggered的情况下,这个事件被重新加回到ready list里面
//这样,下一轮epoll_wait的时候,这个事件会被重新check
else if (!(epi->event.events & EPOLLET)) {
    /*
     * If this file has been added with Level
     * Trigger mode, we need to insert back inside
     * the ready list, so that the next call to
     * epoll_wait() will check again the events
     * availability. At this point, no one can insert
     * into ep->rdllist besides us. The epoll_ctl()
     * callers are locked out by
     * ep_scan_ready_list() holding "mtx" and the
     * poll callback will queue them in ep->ovflist.
     */
    list_add_tail(&epi->rdllink, &ep->rdllist);
    ep_pm_stay_awake(epi);
}

epollVSポーリング/選択

実装の観点から、epollがpoll / selectよりもはるかに効率的である理由を説明しましょう。

まず、poll / selectは、監視対象のfdをユーザースペースからカーネルスペースにコピーし、次にカーネルスペースで処理してからユーザースペースにコピーします。これには、メモリのカーネル空間アプリケーション、メモリ解放などのプロセスが含まれます。これは、多数のfdの場合に非常に時間がかかります。ファイルディスクリプタは、黒のマングローブの木を操作することにより、赤黒木を維持し、あなたは非常に高速なメモリ操作とアプリケーションのリリース、および外観の多くを回避することができます

次のコードは、ポーリング/選択がカーネル空間のメモリにどのように適用されるかを示しています。selectが最初にスタック上のリソースを申請しようとしていることがわかります。監視する必要のあるfdがさらにある場合は、ヒープスペースのリソースを申請します。

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
               fd_set __user *exp, struct timespec64 *end_time)
{
    fd_set_bits fds;
    void *bits;
    int ret, max_fds;
    size_t size, alloc_size;
    struct fdtable *fdt;
    /* Allocate small arguments on the stack to save memory and be faster */
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];

    ret = -EINVAL;
    if (n < 0)
        goto out_nofds;

    /* max_fds can increase, so grab it once to avoid race */
    rcu_read_lock();
    fdt = files_fdtable(current->files);
    max_fds = fdt->max_fds;
    rcu_read_unlock();
    if (n > max_fds)
        n = max_fds;

    /*
     * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
     * since we used fdset we need to allocate memory in units of
     * long-words. 
     */
    size = FDS_BYTES(n);
    bits = stack_fds;
    if (size > sizeof(stack_fds) / 6) {
        /* Not enough space in on-stack array; must use kmalloc */
        ret = -ENOMEM;
        if (size > (SIZE_MAX / 6))
            goto out_nofds;


        alloc_size = 6 * size;
        bits = kvmalloc(alloc_size, GFP_KERNEL);
        if (!bits)
            goto out_nofds;
    }
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;
    ...

次に、select / pollがスリープから復帰したときに、複数のfdを監視している場合、fdの1つにイベントがある限り、カーネルは内部リストをトラバースして、到着したイベントを確認します。epollとは異なります。eventpollを直接関連付けます。 fdを介してオブジェクトを作成し、すぐにfdをeventpollの準備完了リストに直接追加します。

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
    ...
    retval = 0;
    for (;;) {
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
        bool can_busy_loop = false;

        inp = fds->in; outp = fds->out; exp = fds->ex;
        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
            unsigned long in, out, ex, all_bits, bit = 1, mask, j;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;

            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;
            if (all_bits == 0) {
                i += BITS_PER_LONG;
                continue;
            }
        
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
                   to, slack))
        timed_out = 1;
...

要するに 

Epollは、検出されるすべてのファイルの説明を追跡するために赤黒木を維持します。黒赤木を使用すると、カーネルとユーザースペースでの大量のデータコピーとメモリ割り当てが削減され、パフォーマンスが大幅に向上します。同時に、epollは、準備完了イベントを記録するためのリンクリストを維持します。カーネルは、各ファイルでイベントが発生すると、準備完了イベントリストに自分自身を登録します。カーネル自体のファイルイベントポール間のコールバックおよびウェイクアップメカニズムを通じて、エラーの数が減ります。カーネル記述ワードをトラバースすると、イベントの通知と検出の効率が大幅に向上し、レベルトリガーとエッジトリガーの実現にも便利になります。

poll / selectの実装を比較することにより、epollは実際にpoll / selectの欠点を克服しており、Linuxでの高性能ネットワークプログラミングの頂点であることがわかりました。Linuxの下で高性能ネットワークサーバーによってもたらされるさまざまな技術的利益を享受できるように、このような強力なイベント配信メカニズムを設計してくれたLinuxコミュニティの偉大な神々に感謝する必要があります。

 

過去を振り返って新しいことを学びましょう!

 

おすすめ

転載: blog.csdn.net/qq_24436765/article/details/104829648