epoll能显著的提高程序的CPU利用率。

本文主要是用于优化uevent监听,降低CPU使用率,uevent监听文章如下:

C++层uevent获取-CSDN博客

本文中代码是源码中healthd中的ueventfd的epoll监听代码 

1. epoll_create1

  epoll_create1 是 Linux 中用于创建一个新的 epoll 实例的系统调用。在使用 epoll 多路复用 I/O 模型时,首先需要创建一个 epoll 实例,以便将文件描述符添加到 epoll 集合中,以便监视其状态变化。

epoll_create1 的函数原型如下:

#include <sys/epoll.h>

int epoll_create1(int flags);

  • flags 参数是一个整数,用来指定 epoll 实例的行为。目前只定义了一个标志位 EPOLL_CLOEXEC,用于在执行 exec 函数时关闭 epoll 实例。

在调用 epoll_create1 函数后,会返回一个新的 epoll 实例的文件描述符。如果调用成功,则返回一个非负整数作为 epoll 实例的文件描述符;如果失败,则返回 -1,并设置相应的错误码(通过 errno 变量获取)

在使用 epoll 实例后,通常会用 epoll_ctl 函数向 epoll 实例中添加、修改或删除需要监听的文件描述符,然后通过 epoll_wait 函数等待文件描述符的状态变化,从而实现高效的 I/O 事件监听和处理。

epoll_create1 是 epoll I/O 多路复用机制中用于创建 epoll 实例的关键函数之一。

int HealthLoop::InitInternal() {
    epollfd_.reset(epoll_create1(EPOLL_CLOEXEC));
    if (epollfd_ == -1) {
        KLOG_ERROR(LOG_TAG, "epoll_create1 failed; errno=%d\n", errno);
        return -1;
    }

    // Call subclass's init for any additional init steps.
    // Note that healthd_config_ is initialized before wakealarm_fd_; see
    // AdjustUeventWakealarmPeriods().
    Init(&healthd_config_);

    WakeAlarmInit();
    UeventInit();

    return 0;
}

2. epoll_wait

  epoll_wait 是 Linux 中用于等待文件描述符上的 I/O 事件发生的系统调用。它会阻塞当前线程,直到指定的 epoll 实例中的文件描述符有事件发生或达到超时时间。

epoll_wait 的函数原型如下:

#include <sys/epoll.h>

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

  • epfd 是 epoll 实例的文件描述符,即通过 epoll_create1 创建的返回值。
  • events 是一个指向 struct epoll_event 结构体数组的指针,用于接收发生事件的文件描述符和事件类型。
  • maxevents 是 events 数组的大小,即最多可以接收多少个事件。
  • timeout 是等待的超时时间(以毫秒为单位),当设置为负数时将永久阻塞,直到有事件发生。

调用 epoll_wait 后,会阻塞当前线程,等待文件描述符上的事件发生。如果有事件发生,epoll_wait 将填充 events 数组,并返回实际发生事件的文件描述符数量。如果超时时间到达或发生错误,epoll_wait 将返回 0 或 -1,并设置相应的错误码(通过 errno 变量获取)。

在使用 epoll_wait 之前,需要使用 epoll_ctl 函数将待监视的文件描述符添加到 epoll 实例中。

epoll_wait 是 epoll I/O 多路复用机制中用于等待文件描述符上的事件发生的函数。它能够高效地监视多个文件描述符并处理相应的 I/O 事件。

void HealthLoop::MainLoop(void) {
    int nevents = 0;
    while (1) {
        reject_event_register_ = true;
        size_t eventct = event_handlers_.size();
        struct epoll_event events[eventct];
        int timeout = awake_poll_interval_;

        nevents = epoll_wait(epollfd_, events, eventct, -1);
        if (nevents == -1) {
            if (errno == EINTR) continue;
            KLOG_ERROR(LOG_TAG, "healthd_mainloop: epoll_wait failed\n");
            break;
        }

        for (int n = 0; n < nevents; ++n) {
            if (events[n].data.ptr) {
                auto* event_handler = reinterpret_cast<EventHandler*>(events[n].data.ptr);
                event_handler->func(event_handler->object, events[n].events);
            }
        }
    }

    return;
}

3. epoll_ctl

epoll_ctl 是 Linux 中用于控制 epoll 实例的系统调用,它可以向 epoll 实例中添加、修改或删除需要监视的文件描述符。

epoll_ctl 的函数原型如下:

#include <sys/epoll.h>

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

  • epfd 是 epoll 实例的文件描述符,即通过 epoll_create1 创建的返回值。
  • op 是操作类型,可以是 EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符)或 EPOLL_CTL_DEL(删除文件描述符)。
  • fd 是需要添加、修改或删除的文件描述符。需要监听是否发生变化的文件描述符
  • event 是一个指向 struct epoll_event 结构体的指针,用于指定事件类型和其他属性。

调用 epoll_ctl 后,根据 op 的不同,可以实现向 epoll 实例中添加、修改或删除需要监视的文件描述符。如果调用成功,则返回 0;如果失败,则返回 -1,并设置相应的错误码(通过 errno 变量获取)。

通过使用 epoll_ctl,可以动态地向 epoll 实例中添加或删除需要监视的文件描述符,从而实现高效的 I/O 事件监听和处理。

epoll_ctl 是 epoll I/O 多路复用机制中用于控制 epoll 实例的函数,是实现高效事件驱动编程的关键之一。

    // Register an epoll event. When there is an event, |func| will be
    // called with |this| as the first argument and |epevents| as the second.
    // This may be called in a different thread from where StartLoop is called
    // (for obvious reasons; StartLoop never ends).
    // Once the loop is started, event handlers are no longer allowed to be
    // registered.
    using BoundFunction = std::function<void(HealthLoop*, uint32_t /* epevents */)>;



    struct EventHandler {
        HealthLoop* object = nullptr;
        int fd;
        BoundFunction func;
    };


int HealthLoop::RegisterEvent(int fd, BoundFunction func, EventWakeup wakeup) {
    CHECK(!reject_event_register_);

    auto* event_handler =
            event_handlers_
                    .emplace_back(std::make_unique<EventHandler>(EventHandler{this, fd, func}))
                    .get();

    struct epoll_event ev;

    ev.events = EPOLLIN;

    if (wakeup == EVENT_WAKEUP_FD) ev.events |= EPOLLWAKEUP;

    ev.data.ptr = reinterpret_cast<void*>(event_handler);

    if (epoll_ctl(epollfd_, EPOLL_CTL_ADD, fd, &ev) == -1) {
        KLOG_ERROR(LOG_TAG, "epoll_ctl failed; errno=%d\n", errno);
        return -1;
    }

    return 0;
}

void HealthLoop::UeventInit(void) {
    uevent_fd_.reset(uevent_open_socket(64 * 1024, true));

    if (uevent_fd_ < 0) {
        KLOG_ERROR(LOG_TAG, "uevent_init: uevent_open_socket failed\n");
        return;
    }

    fcntl(uevent_fd_, F_SETFL, O_NONBLOCK);
//注册uevent_fd_的epoll监听
    if (RegisterEvent(uevent_fd_, &HealthLoop::UeventEvent, EVENT_WAKEUP_FD))
        KLOG_ERROR(LOG_TAG, "register for uevent events failed\n");
}

经过以上步骤后,就可以实现当uevent_fd_中有uevent事件到来,nevents 就会大于0,则会触发HealthLoop::UeventEvent方法,进而开始处理uevent事件,本方法大大提升CPU使用率,将CPU占用率从98%降到了1%。

// TODO(b/140330870): Use BPF instead.
#define UEVENT_MSG_LEN 2048
void HealthLoop::UeventEvent(uint32_t /*epevents*/) {
    // No need to lock because uevent_fd_ is guaranteed to be initialized.

    char msg[UEVENT_MSG_LEN + 2];
    char* cp;
    int n;

    n = uevent_kernel_multicast_recv(uevent_fd_, msg, UEVENT_MSG_LEN);
    if (n <= 0) return;
    if (n >= UEVENT_MSG_LEN) /* overflow -- discard */
        return;

    msg[n] = '\0';
    msg[n + 1] = '\0';
    cp = msg;

    while (*cp) {
        if (!strcmp(cp, "SUBSYSTEM=" POWER_SUPPLY_SUBSYSTEM)) {
            ScheduleBatteryUpdate();
            break;
        }

        /* advance to after the next \0 */
        while (*cp++)
            ;
    }
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐