Boost.ASIO源码:deadline_timer源码级解析(二)——epoll_reactor定时器逻辑

前文回顾

前面讲deadline_timer::async_wait()讲到了epoll_reactor::scheduler_timer(),那时候讲得很模糊,这里稍微展开再讲解一下。首先先回顾下scheduler_timer的源码:

template <typename Time_Traits>
void epoll_reactor::schedule_timer(timer_queue<Time_Traits>& queue,
    const typename Time_Traits::time_type& time,
    typename timer_queue<Time_Traits>::per_timer_data& timer, wait_op* op)
{
  mutex::scoped_lock lock(mutex_);

  if (shutdown_)
  {
    scheduler_.post_immediate_completion(op, false);
    return;
  }

  bool earliest = queue.enqueue_timer(time, timer, op);  // 把定时器添加到deadline_timer_service的timer_queue_成员中
  scheduler_.work_started();   // scheduler_的类型是scheduler,即io_service的实现类
  if (earliest)
    update_timeout();
}

// work_started是scheduler的成员函数
  void work_started()
  {
    ++outstanding_work_;
  }

本文主要关注第14行到第17行这段代码的逻辑。这段逻辑乍看之下十分的晦涩,这逻辑与epoll_reactor的实现逻辑紧密相关,接下来便讲解为何是这样的逻辑。

epoll_reactor大致介绍

epoll_reactor就是一种异步触发器,它的功能概括起来就是能在需要的时候触发scheduler来执行处理函数(这里的处理函数就是外面传进来的回调函数)。这个需要的时候分2种情况,一种是某个描述符变为可读或可写或出错,这个是通过epoll来实现的;另一种是定时器到时间了,这个是通过timerfd来实现的(实际上这个触发条件也是timerfd变为可读,而这个还是得通过前面的epoll来监听)。epoll的相关逻辑在前面的博客已经讲过了,故这里主要讲timerfd的逻辑。

epoll_reactor的定时器逻辑

先从timerfd的相关源码讲起。理所当然的,epoll_reactor里持有一个timerfd的描述符作为它的私有成员,这个描述符名叫timer_fd_。在epoll_reactor的构造函数中会调用epoll_reactor::do_timerfd_create(),这个函数将会完成timer_fd_的初始化,下面是这个函数的源码:

int epoll_reactor::do_timerfd_create()
{
  int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);

  if (fd == -1 && errno == EINVAL)
  {
    fd = timerfd_create(CLOCK_MONOTONIC, 0);
    if (fd != -1)
      ::fcntl(fd, F_SETFD, FD_CLOEXEC);
  }

  return fd;
}

这段代码逻辑没什么好讲的,就是timerfd的标准用法,不懂的可以自行查看相关资料。创建出timerfd之后还要把这个timer_fd_描述符添加到epoll的监听描述符集中,这也是在epoll_reactor的构造函数中完成的。

timerfd仅是实现最基本的定时触发逻辑,而epoll_reactor需要的复杂的多个定时器维护需要它的另外一个私有成员timer_queues_,它的类型是timer_queue_set,这是个定时器队列的链表,它维护多个定时器队列。像上一篇博客中deadline_timer_service中的私有成员timer_queue_就是一个定时器队列,这个定时器队列会在deadline_timer_service的构造函数添加到epoll_reactor的timer_queues中。下面是deadline_timer_service的构造函数:

  deadline_timer_service(boost::asio::io_context& io_context)
    : service_base<deadline_timer_service<Time_Traits> >(io_context),
      scheduler_(boost::asio::use_service<timer_scheduler>(io_context)) // scheduler_的类型是epoll_reactor,timer_schduler就是epoll_reactor的别名
  {
    scheduler_.init_task();
    scheduler_.add_timer_queue(timer_queue_);
  }

注意这里初始化scheduler_时用的是use_service,这个函数将会在service_registry中查找有没有该对象,没有就创建一个添加到service_registry中并返回,否则就直接将那个对象返回,具体逻辑参考我的第一篇博客。由此保证scheduler(即io_service)对象绑定的epoll_reactor和这里初始化的epoll_reactor时一个对象。
add_timer_queue便是把deadline_timer_service的timer_queue_添加到epoll_reactor的timer_queues中。add_timer_queue的代码逻辑十分简单,就是简单的链表操作,这里就不贴上来了。
接下来看这个timerfd如何触发。

epoll_reactor::run捕获定时器触发

其实这个函数逻辑很简单,因为对于回调函数的调用及处理都交给scheduler处理,在epoll_reactor::run中仅仅判断哪些回调函数是时候处理了并把它转发到scheduler的某些队列中。

void epoll_reactor::run(long usec, op_queue<operation>& ops)
{
  // Calculate timeout. Check the timer queues only if timerfd is not in use.
  int timeout;
  if (usec == 0)
    timeout = 0;
  else
  {
    timeout = (usec < 0) ? -1 : ((usec - 1) / 1000 + 1);
    if (timer_fd_ == -1)  // 如果没有timerfd
    {
      mutex::scoped_lock lock(mutex_);
      timeout = get_timeout(timeout);  // 通过所有的定时器队列获得下次最早的定时器触发时间
    }
  }

  // Block on the epoll descriptor.
  epoll_event events[128];
  int num_events = epoll_wait(epoll_fd_, events, 128, timeout);

  bool check_timers = (timer_fd_ == -1);  // 如果没有timerfd,则强制要检查定时器时间0

  // Dispatch the waiting events.
  for (int i = 0; i < num_events; ++i)
  {
    void* ptr = events[i].data.ptr;
    if (ptr == &interrupter_)  // interrupter用于唤醒阻塞的epoll
    {
      if (timer_fd_ == -1)
        check_timers = true;
    }
    else if (ptr == &timer_fd_)
    {
      check_timers = true;
    }
    else
    {
      // 。。。对其它描述符的处理
    }
  }

  if (check_timers)
  {
    mutex::scoped_lock common_lock(mutex_);
    timer_queues_.get_ready_timers(ops);   // 获取所有已经就绪的定时器,并把已就绪的定时器相关的回调函数添加到ops中

    if (timer_fd_ != -1)
    {
      itimerspec new_timeout;
      itimerspec old_timeout;
      int flags = get_timeout(new_timeout);   // 根据所有的定时器计算下次的触发时间
      timerfd_settime(timer_fd_, flags, &new_timeout, &old_timeout);  // 给timerfd设置新的触发时间
    }
  }
}

这个函数考虑了没有timerfd的情况(可能是运行系统本身就没有timerfd类)。这个函数一般情况下是阻塞在第19行的epoll_wait上的,正如前面所说timerfd的描述符timer_fd_也添加到了epoll的描述符集中,所以当timerfd变为可读时也会自动触发这个run函数的执行。
前半段的关键点就在于check_timer这个flag的设置,至于interrupter这是一个强制触发器,倘若epoll_reactor没有timerfd,而又没有其它的描述符状态变化导致一直阻塞在epoll_reactor上,此时便需要通过interrupter来触发epoll_wait函数返回。interrupter具体逻辑后面再讲。
如果check_timer为true,则说明需要检查定时器是否有到点的。关键函数就是这个timer_queues_.get_ready_timers(ops),它会遍历所有的定时器队列,将已经就绪的定时器绑定的回调函数都添加到ops中。这个ops是调用run时传进来的一个队列。在scheduler::run中会(间接)调用这么一行函数:

task_->run(more_handlers ? 0 : -1, this_thread.private_op_queue);   // task_就是epoll_reactor对象

这里的this_thread.private_op_queue就是传入的ops。这个this_thread可以理解为当前执行线程,众所周知io_service有多个工作线程,每个工作线程都维护自己的私有工作队列,这个private_op_queue就是这个私有工作队列。后面的就交给scheduler::run来自动处理了。

interrupter的实现逻辑

其实interrupter这个名字我感觉起的有点歧义,这个interrupter的意思应该中断阻塞的epoll_wait状态,下面是epoll_reactor的私有成员interrupter_的官方注释:

  // The interrupter is used to break a blocking epoll_wait call.
  select_interrupter interrupter_;  // 这个select_interrupter的真实名字叫eventfd_select_interrupter

interrupter的真实类型是eventfd_select_interrpter,这个类中维护两个私有成员:read_descriptor_和write_descriptor_,分表代表读写描述符。(仔细想想如何“打断”epoll_wait,肯定要通过描述符的可读可写状态改变啊)
在eventfd_select_interrupter的构造函数中会调用open_descriptors()成员方法初始化这两个描述符。下面是open_descriptors()的源码:

void eventfd_select_interrupter::open_descriptors()
{
  write_descriptor_ = read_descriptor_ =
    ::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
  if (read_descriptor_ == -1 && errno == EINVAL)
  {
    write_descriptor_ = read_descriptor_ = ::eventfd(0, 0);
    if (read_descriptor_ != -1)
    {
      ::fcntl(read_descriptor_, F_SETFL, O_NONBLOCK);
      ::fcntl(read_descriptor_, F_SETFD, FD_CLOEXEC);
    }
  }

  if (read_descriptor_ == -1)
  {
    int pipe_fds[2];
    if (pipe(pipe_fds) == 0)
    {
      read_descriptor_ = pipe_fds[0];
      ::fcntl(read_descriptor_, F_SETFL, O_NONBLOCK);
      ::fcntl(read_descriptor_, F_SETFD, FD_CLOEXEC);
      write_descriptor_ = pipe_fds[1];
      ::fcntl(write_descriptor_, F_SETFL, O_NONBLOCK);
      ::fcntl(write_descriptor_, F_SETFD, FD_CLOEXEC);
    }
    else
    {
      boost::system::error_code ec(errno,
          boost::asio::error::get_system_category());
      boost::asio::detail::throw_error(ec, "eventfd_select_interrupter");
    }
  }
}

open_descriptors会先尝试用eventfd来创建一个通信用文件描述符。如果创建失败再用管道方式创建通信描述符。当然这里的通信不是为了跨进程,仅仅是自己给自己传递消息。
此时就完成了消息传递基础了,接下来需要做的就是把这个读描述符read_descriptor_添加到epoll_reactor的epoll描述符集中了,这当然是在epoll_reactor的构造函数中完成的。
接下来至于如何中断呢,非常简单,就是写一个字节到write_descriptor_中:

void eventfd_select_interrupter::interrupt()
{
  uint64_t counter(1UL);
  int result = ::write(write_descriptor_, &counter, sizeof(uint64_t));
  (void)result;  // 欺骗编译器写法,为了不让编译器报警告,本身没有任何用处。
}

你可能感兴趣的:(源码阅读笔记,epoll_reactor,ASIO)