Redisでのepollソースコードの分析と実装

1。概要

この記事では、主にネットワーク呼び出しの理解を深めるために、Linuxでのepollの実装原理を分析します。業界にはepollを使用するフレームワークがたくさんあり、その多くは気軽にリストできます。たとえば、Linuxでのjdkのnioの実装や、netty、redis、およびロングリンクネットワークリクエストを含むその他の場所では、epollを直接使用できます。 。記事の最後で、epollを使用してIO多重化を実行し、redisソースコードから高い同時実行性を実現する方法について簡単に説明します。

2.具体的な実現

公式ドキュメントの説明を参照してください。

epoll APIの中心的な概念は、epollインスタンスです。これは、ユーザースペースの観点から、2つのリストのコンテナーと見なすことができるインカーネルデータ構造です。

したがって、実際には、epollはカーネルのデータ構造です。ユーザースペースの観点から、実際には2つのリンクリストがあります。したがって、基本的には2つのリンクリストを維持するだけで十分です。この箇所を理解すると、Epollが次の3つの方法を提供していることも理解できます。

  • createは、このカーネルのデータ構造を初期化することです。fdを返します。ご存知のとおり、Unixはファイルです。したがって、ここで作成されるのはファイルfdです。操作ごとにfdを渡すだけで、カーネルはepollに対応するデータ構造を取得できます。
  • epoll_ctlは、リンクリストの1つに対する操作です。このリンクリストには、ユーザーが関心のあるioイベントが格納されます。もちろん、イベントに登録するときは、他の操作もあります。後で詳しく説明します
  • epoll_waitは、readyイベント(対象のイベント)に戻ります。次に、アプリケーション層に処理させます。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

struct epoll_event {
    __uint32_t events;
    epoll_data_t data;
};

epoll_createメソッド

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    int error, fd;
    struct eventpoll *ep = NULL;
    struct file *file;
    // 创建内部数据结构eventpoll 
    error = ep_alloc(&ep);
    //查询未使用的fd
    fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & O_CLOEXEC));

    ep->file = file;
    fd_install(fd, file);  //建立fd和file的关联关系
    return fd;
out_free_fd:
    put_unused_fd(fd);
out_free_ep:
    ep_free(ep);
    return error;
}

この方法について簡単に説明します。まず、このメソッドはファイル記述子を返します。このファイル記述子を使用して、対応する構造を見つけることができます。つまり、メモリ領域です。epollのすべてのデータはここに保存されます。このメモリ領域は、eventpoll構造によって表されます。したがって、このメソッドのロジックは次のとおりです。

1.eventpoll構造を作成します。構造の対応するデータを初期化します

2.未使用のfdをクエリしてから、epoll用のファイルを作成します。file-> private_dataをepにポイントします。ファイルを作成するプロセスについて言うことはあまりありません

3. ep-> fileをfileにポイントします。実際、それは単なる拘束力です。

4.fdとファイルを関連付けます。したがって、fdを介して対応するファイルを見つけることができます。そして、epに対応する構造(メモリ領域)を見つけます。ここでは、ファイルのprivate_dataがデバイスドライバで実際に非常に重要であると説明します。これは、カスタムデータ構造を指すことができます。これが、1つのデバイスドライバーが複数のデバイスに適応できることが保証されている理由です。デバイスが異なれば属性も異なる可能性があるためです。epollがprivate_dataを使用して独自のデータ構造を指すことに問題はありません。

eventpoll構造の内容は次のとおりです。後で詳しく遭遇しました。

struct eventpoll {
    spinlock_t lock;
    struct mutex mtx;
    wait_queue_head_t wq; //sys_epoll_wait()使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist; //所有准备就绪的文件描述符列表
    struct rb_root rbr; //用于储存已监控fd的红黑树根节点
    
    struct epitem *ovflist; //用于监听文件的结构。如果rdllist被锁定,临时事件会被连接到这里
    struct wakeup_source *ws; // 当ep_scan_ready_list运行时使用wakeup_source
    struct user_struct *user; //创建eventpoll描述符的用户
    struct file *file;
    int visited;           //用于优化循环检测检查
    struct list_head visited_list_link;
};

epoll_ctlメソッド

このメソッドは、主に監視イベントを追加、削除、および変更するためのものです。

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event)
{
    int error;
    int full_check = 0;
    struct fd f, tf;
    struct eventpoll *ep;    
    struct epitem *epi;     
    struct epoll_event epds; 
    struct eventpoll *tep = NULL;
    error = -EFAULT;
    //如果不是删除操作,将用户空间的epoll_event 拷贝到内核
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
    f = fdget(epfd); //epfd对应的文件
    tf = fdget(fd); //fd对应的文件.
    ...
    ep = f.file->private_data; // 取出epoll_create过程创建的ep
    ...
    epi = ep_find(ep, tf.file, fd); //ep红黑树中查看该fd
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_insert(ep, &epds, tf.file, fd, full_check); 
        }
        if (full_check)
            clear_tfile_check_list();
        break;
    case EPOLL_CTL_DEL:
        if (epi)
            error = ep_remove(ep, epi); 
        break;
    case EPOLL_CTL_MOD:
        if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds); 
        }
        break;
    }
    mutex_unlock(&ep->mtx);
    fdput(tf);
    fdput(f);
    ...
    return error;
}

C / C ++ Linuxバックエンドネットワークインフラストラクチャ開発の詳細を共有して、学習の原則に関する知識を強化します。学習教材の取得をクリックし、テクノロジースタックを改善し、Linux、Nginx、ZeroMQ、MySQL、 Redis、スレッドプールなどのコンテンツ知識を向上させます。 、MongoDB、ZK、Linuxカーネル、CDN、P2P、epoll、Docker、TCP / IP、coroutine、DPDKなど。

 

上記では一部の判定コードを省略しています。主なコアは、さまざまなイベントタイプに応じてさまざまな機能を実行することです。

epollはep_findを呼び出して、赤黒木から対応するエピを取得します。すでに存在する場合は、追加する必要はありません。存在しない場合、削除および変更操作は実行できません。プロセス全体がロックされます。赤黒木であるため、検索と挿入のパフォーマンスはどちらもログレベルです。したがって、同時実行性の高いシナリオでは、迅速な登録と監視も実現できます。これら3つの操作のロジックをそれぞれ見てみましょう。

ep_insert操作

名前が示すように、監視イベントを追加することです。

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
             struct file *tfile, int fd, int full_check)
{
    int error, revents, pwake = 0;
    unsigned long flags;
    long user_watches;
    struct epitem *epi;
    struct ep_pqueue epq; //[小节2.4.5]


    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;

    //构造并填充epi结构体
    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); // 将tfile和fd都赋值给ffd
    epi->event = *event;
    epi->nwait = 0;
    epi->next = EP_UNACTIVE_PTR;
    if (epi->event.events & EPOLLWAKEUP) {
        error = ep_create_wakeup_source(epi);
    } else {
        RCU_INIT_POINTER(epi->ws, NULL);
    }
    epq.epi = epi;
    //设置轮询回调函数
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    //执行poll方法
    revents = ep_item_poll(epi, &epq.pt);
    spin_lock(&tfile->f_lock);
    list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);
    spin_unlock(&tfile->f_lock);
    ep_rbtree_insert(ep, epi); //将将当前epi添加到RB树
    spin_lock_irqsave(&ep->lock, flags);
    //事件就绪 并且 epi的就绪队列有数据
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake(epi);

        //唤醒正在等待文件就绪,即调用epoll_wait的进程
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    spin_unlock_irqrestore(&ep->lock, flags);
    atomic_long_inc(&ep->user->epoll_watches);


    if (pwake)
        ep_poll_safewake(&ep->poll_wait); //唤醒等待eventpoll文件就绪的进程
    return 0;
...
}

1.最初に、epiが初期化され、ターゲットファイルの監視はepiを介して維持される必要があります。ファイルの監視はエピに対応します。そして、epの赤黒木に保存されました。

struct epitem {
    union {
        struct rb_node rbn; //RB树节点将此结构链接到eventpoll RB树
        struct rcu_head rcu; //用于释放结构体epitem
    };


    struct list_head rdllink; //时间的就绪队列,主要就是链接到eventpoll的rdllist
    struct epitem *next; //配合eventpoll中的ovflist一起使用来保持单向链的条目
    struct epoll_filefd ffd; //该结构 监听的文件描述符信息,每一个socket fd都会对应一个epitem 。就是通过这个结构关联
    int nwait; //附加到poll轮询中的活跃等待队列数

    struct list_head pwqlist; //用于保存被监听文件的等待队列
    struct eventpoll *ep;  //epi所属的ep
    struct list_head fllink; //主要是为了实现一个文件被多个epoll监听。将该结构链接到文件的f_ep_link。
    struct wakeup_source __rcu *ws; //设置EPOLLWAKEUP时使用的wakeup_source
    struct epoll_event event; //监控的事件和文件描述符
};

2.初期化後、ファイルのfdと対応するファイルポインタがepiのffdにバインドされます。主な機能は、fdをバインドしてepiを変更することです。

struct epoll_filefd {
    struct file *file;
    int fd;
} __packed;

3. epqのpt(実際にはpoll_table)に対応する関数ep_ptable_queue_procを登録します。

struct ep_pqueue {
    poll_table pt;
    struct epitem *epi;
};

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

ここで、epqは、epiとpoll_tableをバインドする構造体です。poll_tableは、主にep_ptable_queue_proc関数を登録します。_keyは、イベントを記録するために使用されます。したがって、epqはepiと対応するep_ptable_queue_procを保存します。コールバック関数のその後の実行は、私たちができるときpoll_table対応EPQのアドレスを取得し、最終的な構造を定義する目的で、対応するエピを得ました。

4.ep_item_pollメソッドを呼び出します。この方法について簡単に説明します。彼はファイルシステムのpollメソッドを呼び出します。

static inline unsigned int ep_item_poll(struct epitem *epi, poll_table *pt)
{
    pt->_key = epi->event.events;、
    return epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;
}

ドライバーごとに独自のポーリングメソッドがあります。TCPソケットの場合、ポーリングメソッドはtcp_pollです。TCPでは、このメソッドは定期的に呼び出され、呼び出し頻度はプロトコルスタックの割り込み頻度の設定によって異なります。イベントが到着すると、対応するtcp_pollメソッドが呼び出され、tcp_pollメソッドがsock_poll_wait()を呼び出します。これにより、ここに登録されているep_ptable_queue_procメソッドが呼び出されます。Epollは実際に、このメカニズムを介してファイルの待機キューに独自のコールバック関数を追加します。これはep_ptable_queue_procの目的でもあります。

5. ep_item_pollを呼び出した後、イベントが返されます。つまり、fdによってトリガーされるイベントです。興味のあるイベントがあれば、epのrdllistに挿入されます。プロセスがファイルの準備完了状態を待機している場合、それはepoll_waitを呼び出してスリープ状態にするプロセスです。その後、待機中のプロセスが目覚めます。

 if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake(epi);

        //唤醒正在等待文件就绪,即调用epoll_wait的进程
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

ep_ptable_queue_procメソッド

プロセス全体は、実際にはファイルのpollメソッドを介してep_ptable_queue_proc関数をバインドすることです。ファイル記述子に対応するファイルにイベントが到着したら、この関数をコールバックします

PS:ファイルは対応するファイルの構造です。もちろん、複数のfdがファイル構造を指すこともあります。複数のファイルが同時に同じインノードノードを指すことができます。Linuxでは、ファイルの内容はinnodeによって定義および記述されます。ファイルは、ファイルを操作するときにのみ作成されます。誰もがこの概念について明確にする必要があります。

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;
        //将ep_poll_callback放入等待队列whead
        add_wait_queue(whead, &pwq->wait);
        //将llink 放入epi->pwqlist的尾部
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        epi->nwait = -1; //标记错误发生
    }
}

static inline void
init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func)
{
    q->flags    = 0;
    q->private    = NULL;
    q->func        = func;
}

ep_ptable_queue_procには3つのパラメーターがあります。fileは監視対象のファイルのポインタであり、wheadはfdに対応するデバイスの待機キューです。ptは、その時点でファイルのポーリングを呼び出して渡したものです。

ep_ptable_queue_procで、eppoll_entryが導入されました。それがpwqです。pwqは主に、epiイベントが発生したときに、epi関数とコールバック関数の間の相関関係を完了します。

上記のコードからわかります。まず、ptに従って対応するエピを取得します。次に、pwqを介して3つを関連付けます。

最後に、add_wait_queueメソッドを介して、eppoll_entryがfdのデバイス待機キューでハングします。つまり、epollのコールバック関数を登録します。

したがって、このメソッドの主な目的は、fdのデバイス待機キューにeppoll_entryをハングアップさせることです。デバイスにハードウェアデータが到着すると、ハードウェア割り込み処理関数がキューで待機中のプロセスをウェイクアップし、ウェイクアップ関数ep_poll_callbackが呼び出されます。

C / C ++ Linuxバックエンドネットワークインフラストラクチャ開発の詳細を共有して、学習の原則に関する知識を強化します。学習教材の取得をクリックし、テクノロジースタックを改善し、Linux、Nginx、ZeroMQ、MySQL、 Redis、スレッドプールなどのコンテンツ知識を向上させます。 、MongoDB、ZK、Linuxカーネル、CDN、P2P、epoll、Docker、TCP / IP、coroutine、DPDKなど。

 

ep_poll_callbackメソッド

この関数の主な機能は、監視対象ファイルのイベントの準備ができたときに、ファイルに対応するエピを準備完了キューに追加することです。アプリケーション層がepoll_wait()を呼び出すと、カーネルはレディキューのイベントをユーザースペースにコピーします。アプリケーションに報告してください。

static int ep_poll_callback(wait_queue_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);
    if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
        if (epi->next == EP_UNACTIVE_PTR) {
            epi->next = ep->ovflist;
            ep->ovflist = epi;
            if (epi->ws) {
                __pm_stay_awake(ep->ws);
            }
        }
        goto out_unlock;
    }
    //如果此文件已在就绪列表中,很快就会退出
    if (!ep_is_linked(&epi->rdllink)) {
        //将epi就绪事件 插入到ep就绪队列
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake_rcu(epi);
    }
    // 如果活跃,唤醒eventpoll等待队列和 ->poll()等待队列
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);  //当队列不为空,则唤醒进程
    if (waitqueue_active(&ep->poll_wait))
        pwake++;
out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);


    if ((unsigned long)key & POLLFREE) {
        list_del_init(&wait->task_list); //删除相应的wait
        smp_store_release(&ep_pwq_from_wait(wait)->whead, NULL);
    }
    return 1;
}

//判断等待队列是否为空
static inline int waitqueue_active(wait_queue_head_t *q)
{
    return !list_empty(&q->task_list);
}

3.epoll実装の概要

現象を通して本質を見ると、実際、epollの魂はep_item_pollとep_poll_callbackです。

epollは、仮想ファイルシステムのep_item_pollに依存しています。ep_poll_callbackを対応するファイルの待機キューに登録します。対応するファイルにデータが来るとき。登録された関数が呼び出されます。epollコールバックは、対応するファイルのエピを準備完了キューに追加します。

ユーザーがepoll_wait()を呼び出すと、epollはキューデータをロックしてユーザースペースに転送し、この時点でのイベントはovflistでハングします。

4.RedisはEpollを使用します

具体的な実装はae_epoll.cにあります

typedef struct aeApiState {

    // epoll_event 实例描述符
    int epfd;
    // 事件槽
    struct epoll_event *events;

} aeApiState;

aeApiCreateメソッド

Redisは、サーバーを初期化するときにaeCreateEventLoopメソッドを呼び出します。aeCreateEventLoopはaeApiCreateを呼び出して、epollインスタンスを作成します。

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));


    if (!state) return -1;
    state->events = zmalloc(sizeof(struct kevent)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    state->kqfd = kqueue();
    if (state->kqfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    eventLoop->apidata = state;
    return 0;    
}

aeApiAddEventメソッド

このメソッドはイベントをepollに関連付けるため、epollのctlメソッドが呼び出されます

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;

    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. 
     *
     * 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。
     * 如果已经关联了某个/某些事件,那么这是一个 MOD 操作。
     */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    // 注册事件到 epoll
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;

    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

このメソッドは、redisサービスが新しいクライアントを作成するときに呼び出されます。このクライアントの読み取りイベントが登録されます。

redisがクライアントにデータを書き込む必要がある場合、redisはprepareClientToWriteメソッドを呼び出します。このメソッドは、主にfdに対応する書き込みイベントを登録するためのものです。

登録が失敗した場合、redisはデータをバッファーに書き込みません。

対応するパッケージワードが書き込み可能である場合、redisイベントループはバッファ内の新しいデータをソケットに書き込みます。

aeMainメソッド

Redisイベントハンドラーのメインループ。

void aeMain(aeEventLoop *eventLoop) {

    eventLoop->stop = 0;

    while (!eventLoop->stop) {

        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

このメソッドは、最終的にepoll_wait()を呼び出して、対応するイベントを取得して実行します。

 

おすすめ

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