Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node学习笔记2 #2

Open
abbshr opened this issue Mar 29, 2014 · 0 comments
Open

Node学习笔记2 #2

abbshr opened this issue Mar 29, 2014 · 0 comments
Assignees

Comments

@abbshr
Copy link
Owner

abbshr commented Mar 29, 2014

Chapter 1:事件如何被监听?

看完libuv对watchers和事件循环的描述之后,突然发现我一直以来忽略了一个问题:事件是通过什么方式被监听的?

无论是线程还是进程都和我们生物不同,他们不会自发感知外界环境的改变。所以对于这个问题,第一印象往往是:轮询。也就是用一个 while(true) 循环不断询问外部是否有什么新鲜事。

可问题是,我们从来没见过系统内核进程因为监听一個socket而导致CPU狂转、系统挂掉吧。还有,浏览器中监听JavaScript事件是常事,它也没让系统变卡顿啊。

单从这一点来看,轮询事件的产生并非上策!除了轮询,还有什么方法能做到事件的监听呢?或许我们可以从操作系统的底层——计算机硬件工作流程中找到答案。

操作系统在与外设进行交互是典型的事件监听:CPU与设备控制器之间有一条中断请求线,设备控制器会在外设I/O结束时通过电信号向CPU发送中断请求,CPU在原子指令过后检查中断线的状态位判断I/O是否结束,如果结束的话就跳转到内存特定进程位置(中断向量)调度中断处理程序。

我们先来简单分析一下底层的事件监听模型。所谓事件是由源发出,就是一个电信号(或脉冲信号)。进程虽然做不到监听,但硬件CPU却可以,它能接收到电信号的变化。最后CPU对事件做出反应,也就是调度处理进程。

没错,事件监听还可以靠中断来实现。

Chapter 2:基本I/O方式

阻塞I/O、非阻塞I/O、同步I/O、异步I/O是操作系统的几大I/O模式。

我们往往会认为阻塞I/O与同步I/O等同,非阻塞I/O与异步I/O等同。其实这种观点是不准确的,这里科普一下他们的细微区别。

阻塞I/O,即进程/线程在做I/O操作时,被CPU调度到阻塞队列,等待I/O操作的结束,然后进程再被调度回来,处理I/O结果。在等待期间,进程除了休眠无法做任何事情,不过他不占用CPU时间片,这时CPU可以先调度其他进程,当I/O完成时以事件形式通知CPU。

非阻塞I/O与上述相反。进程不会一直等待到I/O操作结束,当I/O请求发出时,进程会立马从系统调用返回,这时进程可以继续工作,也就是CPU不必将其调度到阻塞队列了。但此时进程很可能还没有得到I/O结果,所以要通过轮询来检验I/O是否操作结束。虽说进程没有被阻塞,不过CPU的时间片被白白占用。

同步I/O,就是进程先等待I/O结果,再继续处理其他任务。所以说,同步I/O由阻塞I/O实现。

而异步I/O与非阻塞I/O的差别就是:前者的I/O调用在不阻塞进程的前提下完整的执行,后者的I/O调用为了不阻塞进程会立刻返回,即便是没有得到I/O最终结果。

Chapter 3:Node中的事件循环机制

上面提到的阻塞非阻塞是针对_进程_而言的,和_CPU_的阻塞正好相反,这点必须要认清。

libuv在Linux平台上使用了Linux的_epoll_机制。epoll是Linux平台的I/O事件通知工具,主要用来处理大量的文件句柄。

libuv的事件循环特性就是由epoll提供的,先介绍一下epoll。

epoll的函数在头文件sys/epoll.h中。用epoll编写程序会用到两个数据结构:

    /* 保存触发事件的某个文件描述符相关的数据 */
    typedef union epoll_data {
        void *ptr;
        int fd;
        __uint32_t u32;
        __uint64_t u64;
    } epoll_data_t;
    

    /* 用于注册所感兴趣的事件和回传所发生待处理的事件 */
    struct epoll_event {
        __uint32_t events; /* Epoll events */
        epoll_data_t data; /* User data variable */
    };

其中结构体epoll_event的events成员是表示感兴趣的事件和被触发的事件,可能的取值为:

EPOLLIN:表示对应的文件描述符可以读; EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读; EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET:表示对应的文件描述符有事件发生;

epoll提供的API有如下几个函数:

int epoll_create(int size)

创建一个epoll实例,并返回一个引用该实例的文件描述符。

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)

等待I/O事件,并阻塞调用线程。
最后一个timeout参数表示epoll_wait的超时条件,为0时表示马上返回,为-1时表示函数会一直等下去直到有事件返回,为任意正整数时表示等这么长的时间,如果一直没有事件,则会返回。

对于这几个函数的使用,man手册里给出一个很有代表性的例子:

           #define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Set up listening socket, 'listen_sock' (socket(),
              bind(), listen()) */

           epollfd = epoll_create(10);
           if (epollfd == -1) {
               perror("epoll_create");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }
           /* 这里相当于事件循环的开始,epoll先阻塞进程,等待指定事件到来 */
           for (;;) {
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_pwait");
                   exit(EXIT_FAILURE);
               }
               /* 一旦事件触发,继续事件循环,这里获取事件触发时的数据 */
               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       conn_sock = accept(listen_sock,
                                       (struct sockaddr *) &local, &addrlen);
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

然后我们回过头看看Node(或者说libuv)内部是如何实现事件循环、事件监听、异步回调的。


libuv负责从操作系统那里收集事件或监视其他资源的事件,而用户可以注册在某个事件发生时要调用的回调函数。

监视器(Watchers)是 libuv 用户用于监视特定事件的工具。他们都是以 uv_TYPE_t 命名的抽象结构体,这个类型表明了监视器的用途。

下面是所有的监视器(也称作事件处理器)列表:

    typedef struct uv_loop_s uv_loop_t;
    typedef struct uv_err_s uv_err_t;
    typedef struct uv_handle_s uv_handle_t;
    typedef struct uv_stream_s uv_stream_t;
    typedef struct uv_tcp_s uv_tcp_t;
    typedef struct uv_udp_s uv_udp_t;
    typedef struct uv_pipe_s uv_pipe_t;
    typedef struct uv_tty_s uv_tty_t;
    typedef struct uv_poll_s uv_poll_t;
    typedef struct uv_timer_s uv_timer_t;
    typedef struct uv_prepare_s uv_prepare_t;
    typedef struct uv_check_s uv_check_t;
    typedef struct uv_idle_s uv_idle_t;
    typedef struct uv_async_s uv_async_t;
    typedef struct uv_process_s uv_process_t;
    typedef struct uv_fs_event_s uv_fs_event_t;
    typedef struct uv_fs_poll_s uv_fs_poll_t;
    typedef struct uv_signal_s uv_signal_t;

监视器是通过调用uv_TYPE_init(uv_TYPE_t*)函数来创建。
Note:如上所示,有些监视器初始化函数要用事件循环作为第一个参数。

让监视器监听事件则调用:uv_TYPE_start(uv_TYPE_t*, callback)
而停止监听则调用:uv_TYPE_stop(uv_TYPE_t*)

回调函数是当监视器感兴趣的事件发生时,由 libuv 调用的函数。应用程序指定的逻辑一般会在回调函数中的实现。

只要有活动的监视器,事件循环就会一直运行。没有活动的事件监视器, uv_run() 退出。
ex:

    #include <stdio.h>
    #include <uv.h>

    int64_t counter = 0;

    void wait_for_a_while(uv_idle_t* handle, int status) {
        counter++;

        if (counter >= 10e6)
            uv_idle_stop(handle);
    }

    int main() {
        uv_idle_t idler;

        uv_idle_init(uv_default_loop(), &idler);
        uv_idle_start(&idler, wait_for_a_while);

        printf("Idling...\n");
        uv_run(uv_default_loop(), UV_RUN_DEFAULT);

        return 0;
    }

系统运行中会在监视器启动时给事件循环引用计数加 1,而在监视器停止时给事件循环引用减 1。也可以手动修改处理器引用计数:

    void uv_ref(uv_handle_t*);
    void uv_unref(uv_handle_t*);

使用这些函数可让事件循环在监视器处于活动状态下退出,或让事件循环使用自定义对象来维持其活动状态。

在笔记1中介绍了Node主线程与libuv I/O线程、事件循环的协作关系,这里我们总结一下Node的工作原理。

在Node启动时,主线程内先初始化一些必要的Watchers,比如I/O Watchcers,然后解析js文件,调用相应的libuv函数,最后执行libuv的事件循环函数,先检查watchers队列是否有到来的事件,有就在当前线程中处理,没有阻塞主线程,等待事件唤醒(epoll实现)。

对于js文件中调用libuv函数的语句,将会执行相应函数,利用epoll机制开启一系列I/O线程,设置watchers的回调函数,调用底层API,并进入阻塞态等待调用结束。系统调用结束,返回结果,I/O线程将返回结果赋给watchers回调函数的参数。同时向epoll机制提交状态。

主线程中epoll将再次激活事件循环,从阻塞处向下执行:调用watchers的回调函数。直到再次阻塞在epollwait那里(如果程序并没有设置listen,事件循环在下次检测不到新的事件时就退出循环(引用计数减1),结束程序,例如“文件读取”。如果http上调用了listen函数,将会不断的检测事件的到来~,即引用计数保持为1)。

Chapter 4:Watchers的优先级

上面讲的情况都是I/O,可是事件循环处理的事件不仅仅是I/O事件,还包括process.nextTick产生的Idle事件,计时器的定时事件,setImmediate的check事件。

而我们发现了一个优先级顺序:idle观察者 > I/O观察者 > check观察者。也就是说事件循环每次都是按这个顺序来依次检查watchers的。

进一步的实验我们将会发现:idle观察者和I/O观察者将会在一次事件循环中调用队列中的所有回调函数,而check观察者每次之调用队列头中的回调函数。

@abbshr abbshr added Node and removed libuv labels Mar 29, 2014
@abbshr abbshr self-assigned this Mar 29, 2014
@abbshr abbshr assigned abbshr and unassigned abbshr Aug 22, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant