An overview of the

Nodejs is a well-known asynchronous language. So how does Libuv, the heart of NodeJS, really work? How does it relate to NodeJS?

explore

In fact, in the Uv__io_poll phase, if the I/O execution queue is empty, libuv will block until the next loop time, and during this time, it can be woken up by epoll, which is libuv’s core secret to keeping the CPU idle.

The specifics of epoll will be covered in the next chapter.

This chapter is mainly divided into several parts:

  • Nodejs and Libuv coordination

  • Coordination of Libuv and epoll

  • Combat: NodeJS callback trigger process

    How does Nodejs relate to libuv’s epoll

The observer

Note that the observer here is not the same as the Uv_io observer in libuv, which is the cc layer observer used to connect Libuv to javascript

This is a very complicated problem, our code is javascript, and the glue code of cc layer is executed by binding, which becomes one native call after another. There must be a data structure to store the incoming and return values of both layers across the layers.

This structure is called AsyncWrap. We call it an observer, and of course, no observer uses it directly, but a subclass of it, like FsReqWrap

FSReqWrap -> ReqWrap -> AsyncWrap
Copy the code

Next, we store callback, context, input, and other information on the observer

communication

Once we have an observer, we can use this intermediary to communicate.

For example, in FS, we call native FS to make the actual function call.

Binding. Open (pathModule _makeLong (path), stringToFlags (options. The flag | | 'r'), 0 o666, / / we observer the req);Copy the code

Note that when we call fs.read, we also pass through an asynchronous fs.open layer and invoke subsequent read operations in the fs.open callback.

// deps/uv/src/unix/fs.c int uv_fs_read(uv_loop_t* loop, uv_fs_t* req, uv_file file, const uv_buf_t bufs[], unsigned int nbufs, int64_t off, uv_fs_cb cb) { if (bufs == NULL || nbufs == 0) return -EINVAL; INIT(READ); req->file = file; req->nbufs = nbufs; req->bufs = req->bufsml; if (nbufs > ARRAY_SIZE(req->bufsml)) req->bufs = uv__malloc(nbufs * sizeof(*bufs)); if (req->bufs == NULL) { if (cb ! = NULL) uv__req_unregister(loop, req); return -ENOMEM; } memcpy(req->bufs, bufs, nbufs * sizeof(*bufs)); req->off = off; do { \ if (cb ! = NULL) { \ uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done); \ return 0; \ } \ else { \ uv__fs_work(&req->work_req); \ return req->result; \ } \ } \ while (0) }Copy the code

We can see that the most important asynchronous operation, uv__work_submit, passes the observer into libuv’s ThreadPool.

At this point, Nodejs and Libuv are completely related.

A profound

Libuv exposes the function uv_queue_work to join the worker queue. This function allows us to join synchronous tasks as asynchronous callbacks.

Let’s start with a simple use of uv_queue_work

int count_fibs() { const fib_count = 10000; uv_work_t reqs[fib_count]; for(int i = 0; i < fib_count; i++) { uv_work_t cur_work = reqs[i]; Cur_work. data = I uv_queue_work(uv_get_current_loop(), &cur_work,fib, after_FIB_cb)} Do not use Once uv_run(loop,UV_RUN_DEFAULT)} void fib(uv_work_t req) {// count fib} void after_FIB_cb (uv_work_t req) {int ret = *(int *)req.data; makeCallback(ret) }Copy the code

Let’s take a look at the definition of uv_queue_work

int uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb) { assert(work_cb ! = null); uv__req_init(loop, req, UV_WORK); req->loop = loop; req->work_cb = work_cb; req->after_work_cb = after_work_cb; uv__work_submit(loop, &req->work_req, UV__WORK_CPU, uv__queue_work, uv__queue_done); return 0; }Copy the code

It simply wraps what we want to do with REQ and passes it to our thread pool via uv__work_submit

The key clue is uv_work_t. What is this structure?

struct uv_work_s {
  UV_REQ_FIELDS
  uv_loop_t* loop;
  uv_work_cb work_cb;
  uv_after_work_cb after_work_cb;
  UV_WORK_PRIVATE_FIELDS
};
Copy the code

C language inheritance is very interesting, all through the structure contains another macro definition to achieve

UV_WORK_PRIVATE_FIELDS is actually a macro definition of struct uv__work

struct uv__work {
  void (*work)(struct uv__work *w);
  void (*done)(struct uv__work *w, int status);
  struct uv_loop_s* loop;
  // wq == work_queue
  void* wq[2];
};
Copy the code

This structure encapsulates all the information we need, and it will always be passed, eventually triggered by the callback triggering function described in the next section.

How do I invoke a callback

We’ve already made it clear that our callback is attached to the Complete property of AsyncWrap, but it’s still not clear when the callback is executed.

For a simple explanation, let’s take a look at the demo provided by Libuv.

int main() {
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    uv_loop_init(loop);

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);
    free(loop);
    return 0;
}
Copy the code

Uv_loop_init is the key to implementing the callback, which internally calls uv_async_init to register the event loop and callback function.

In NodeJS, initialization of libuv

Of course, libuv’s main thread initialization is still not as simple as the demo above.

Node is initialized by calling uv_default_loop at startup

// node/src/node.cc

NodeMainInstance main_instance(&params,
                                   uv_default_loop(),
                                   per_process::v8_platform.Platform(),
                                   result.args,
                                   result.exec_args,
                                   indexes);
Copy the code

But the only thing this function does inside is call uv_loop_init. This is just putting the nodeJS entry out.

Multithreaded operation

In fact, uv_async_send will be called to notify the main thread when each worker finishes its task.

The main thread is initially used to call uv_loop_init as in the demo above, so when it receives a signal from uv_async_send, it calls the default uv_work_done callback.

Uv_loop_init actually initializes a Uv_async_T async handle, giving it the ability to be woken up by another thread. It calls uv__async_start to construct a pipe and assigns uv__async_io’s wake up callback function

static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) { char buf[1024]; ssize_t r; QUEUE queue; QUEUE* q; uv_async_t* h; assert(w == &loop->async_io_watcher); // This for loop is used to check whether there is a uv_async_send call // this method is used to check whether the write is 1 for (;). {... } // QUEUE_MOVE(&loop->async_handles, &queue); while (! QUEUE_EMPTY(&queue)) { q = QUEUE_HEAD(&queue); h = QUEUE_DATA(q, uv_async_t, queue); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->async_handles, q); If (cmpxchgi(&h-> Pending, 1, 0) == 0) continue; if (h->async_cb == NULL) continue; // uv_work_done h->async_cb(h); }}Copy the code

As the above comment indicates, the callback mounted on async_cb is uv__work_done. What does this function do

// libuv/src/threadpool.c void uv__work_done(uv_async_t* handle) { struct uv__work* w; uv_loop_t* loop; QUEUE* q; QUEUE wq; int err; loop = container_of(handle, uv_loop_t, wq_async); Uv_mutex_lock (&loop->wq_mutex); QUEUE_MOVE(&loop->wq, &wq); uv_mutex_unlock(&loop->wq_mutex); while (! QUEUE_EMPTY(&wq)) { q = QUEUE_HEAD(&wq); QUEUE_REMOVE(q); w = container_of(q, struct uv__work, wq); err = (w->work == uv__cancelled) ? UV_ECANCELED : 0; W ->done(w, err); }}Copy the code

Note that the uv__work_done function keeps fetching req from the loop -> wq queue to perform the callback

This w->done is the stage where we execute the callback, but this is not a JS callback, this is a callback registered with uv__work_submit. Let’s look at w->done at fs.read

static void uv__fs_done(struct uv__work* w, int status) {
  uv_fs_t* req;

	// 得到该观察者
  req = container_of(w, uv_fs_t, work_req);
  // 取消在循环里的注册
  uv__req_unregister(req->loop, req);

  if (status == UV_ECANCELED) {
    assert(req->result == 0);
    req->result = UV_ECANCELED;
  }
  
  // 准备执行js回调
  req->cb(req);
}
Copy the code

Of course, req->cb is the observer callback registered by the glue layer. This is a wrapper around the JS callback.

How does epoll relate to Libuv

As we mentioned above, uv_async_send can wake up our main thread, but we still don’t know which phase or how.

Let’s take a look at the outer while function uv_run of the event loop.

It’s worth noting that uv_run has many phases, but we only need to worry about the most important uv__io_poll for now

if (no_epoll_wait ! = 0 || (sigmask ! = 0 && no_epoll_pwait == 0)) {// IO multiplexing NFDS = epoll_pwait(loop->backend_fd, events, ARRAY_SIZE(events), timeout, &sigset); if (nfds == -1 && errno == ENOSYS) { uv__store_relaxed(&no_epoll_pwait_cached, 1); no_epoll_pwait = 1; }} else {// IO multiplexing NFDS = epoll_wait(loop->backend_fd, events, ARRAY_SIZE(events), timeout); if (nfds == -1 && errno == ENOSYS) { uv__store_relaxed(&no_epoll_wait_cached, 1); no_epoll_wait = 1; }}Copy the code

Those of you familiar with epoll know that epoll_wait is our core.

It doesn’t matter if you’re not familiar with epoll, just that ePoll can listen to a lot of states, and once that state is triggered, epoll gets a response.

Does our Uv_asynC_send trigger one of these states?

r = write(fd, buf, len);
Copy the code

As you might expect, the uv_async_send function writes a byte to the buffer, entering the epoll process described above, and finally our epoll_wait receives the response and enters the w->done phase mentioned above, entering the callback.

At this point, epoll is completely associated with Libuv.

The wake up is in the epoll wait phase of the Uv_IO_POLL of uv_run’s Kevent (Unix) or epoll.wait(Linux).

reference

Libuv thread pool and main thread communication principle

Node – Asynchronous IO and event loops

Explore the Libuv series

Explore Libuv (3) – Loop! Cycle! cycle

Explore Libuv (4) – Mission mount