node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链。

  首先上一张整体的图,网上翻到的,自己懒得画:

  这里的层次结构十分的清晰,从上到下如果翻译成语言层面,依次是JS、C++、windows(UNIX)的系统API。

  最高层也就是我们自己写的JS代码,node会首先通过V8引擎进行编译解析成C++,随后将其分发给libuv,libuv根据操作系统的类型来分别调用底层的系统API。

  下面通过fs.stat这个API来一步步探索整个过程。

JS => require('fs')

  这个方法的调用从开发者的角度讲,只需要两行代码:

  1. const fs = require('fs');
  2. fs.stat(path, [options], callback);

  其中第一步,是获取内置模块fs,第二步,就是调用对应的方法。

  其实两个可以合一起讲了,弄懂了模块来源,对应的api也就简单了。

  在前面几章,只是很模糊和浅显的讲了一个注册内置模块的过程,其实在node的目录,有一个本地的JS库,简单的处理了参数:

  1. // node/lib/fs.js
  2. fs.stat = function(path, callback) {
  3. callback = makeStatsCallback(callback);
  4. path = getPathFromURL(path);
  5. validatePath(path);
  6. const req = new FSReqWrap();
  7. req.oncomplete = callback;
  8. // const binding = process.binding('fs');
  9. binding.stat(pathModule.toNamespacedPath(path), req);
  10. };

  这是方法的源码,需要注意的只有最后一行,通过binding.stat来调用下层的C++代码,而这个binding是来源于process对象。

  在之前内置模块初探的时候,我提到过一个代码包装,就是对于require的JS文件的外层有一个简单的wrap:

  1. NativeModule.wrapper = [
  2. '(function (exports, require, module, process) {',
  3. '\n});'
  4. ];
  5. NativeModule.wrap = function(script) {
  6. return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  7. };
  8. source = NativeModule.wrap(source);

  这里的script对应的就是JS文件字符串,实际上最后生成的其实是一个自调用匿名函数。

node => process.binding

  隐去了V8引擎编译JS代码的过程(主要这一步很恶心,暂时不想讲),直接进入C++模块。

  这个方法在内置模块引入时也提到过,就是GetBinding方法:

  1. static void GetBinding(const FunctionCallbackInfo<Value>& args) {
  2. // ...
  3. // 找到对应的模块节点
  4. node_module* mod = get_builtin_module(*module_v);
  5. Local<Object> exports;
  6. if (mod != nullptr) {
  7. // 初始化并返回一个对象
  8. exports = InitModule(env, mod, module);
  9. }
  10. // ...
  11. }

  需要关注的代码只有get_builtin_module和InitModule两个。

  在前面的某一章我讲过,node初始化会通过NODE_BUILTIN_MODULES宏将所有内置模块的相关信息整理成一个链表,通过一个静态指针进行引用。

  所以,这里就通过那个指针,找到对应名字的内置模块,代码如下:

  1. node_module* get_builtin_module(const char* name) {
  2. // modlist_builtin就是那个静态指针
  3. return FindModule(modlist_builtin, name, NM_F_BUILTIN);
  4. }
  5. inline struct node_module* FindModule(struct node_module* list,const char* name,int flag) {
  6. struct node_module* mp;
  7. // 遍历链表找到符合的模块信息
  8. for (mp = list; mp != nullptr; mp = mp->nm_link) {
  9. if (strcmp(mp->nm_modname, name) == )
  10. break;
  11. }
  12. // 没找到的话mp就是nullptr
  13. CHECK(mp == nullptr || (mp->nm_flags & flag) != );
  14. return mp;
  15. }

  这里传入的字符串是fs,而每一个模块信息节点的nm_modname代表模块名,所以直接进行字符串匹配就行了。

  返回后只是第一步,第二步就开始真正的加载了:

  1. static Local<Object> InitModule(Environment* env, node_module* mod, Local<String> module) {
  2. // 生成一个新对象作为fs
  3. Local<Object> exports = Object::New(env->isolate());
  4. // ...
  5. mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);
  6. return exports;
  7. }

  这里调用的是模块内部的一个方法,从名字来看也很直白,即带有上下文的模块注册函数。

  在前面生成模块链表的方法,有这么一段注释:

  1. // This is used to load built-in modules. Instead of using
  2. // __attribute__((constructor)), we call the _register_<modname>
  3. // function for each built-in modules explicitly in
  4. // node::RegisterBuiltinModules(). This is only forward declaration.
  5. // The definitions are in each module's implementation when calling
  6. // the NODE_BUILTIN_MODULE_CONTEXT_AWARE.
  7. #define V(modname) void _register_##modname();
  8. NODE_BUILTIN_MODULES(V)
  9. #undef V

  从最后面一行可以看出,注册方法时来源于另外一个宏,如下:

  1. #define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) \
  2. NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)

  这个宏会在每一个单独的模块C++文件的末尾调用,形式大同小异,以fs模块为例:

  1. NODE_BUILTIN_MODULE_CONTEXT_AWARE(fs, node::fs::Initialize)

  这里的第一个参数fs是模块名,而第二个是初始化方法,一般来说负责初始化一个对象,然后给对象添加一些方法。

  当然,以fs为例,看一下初始化的内容:

  1. void Initialize(Local<Object> target, Local<Value> unused, Local<Context> context, void* priv) {
  2. Environment* env = Environment::GetCurrent(context);
  3. // ...大量SetMethod
  4. env->SetMethod(target, "mkdir", MKDir);
  5. env->SetMethod(target, "readdir", ReadDir);
  6. env->SetMethod(target, "stat", Stat);
  7. env->SetMethod(target, "lstat", LStat);
  8. env->SetMethod(target, "fstat", FStat);
  9. env->SetMethod(target, "stat", Stat);
  10. // ...还有大量代码
  11. }

  可见,初始化就是给传入的对象设置一些属性,属性名就是那些熟悉的api了。

  这里只看stat,本地方法对应Stat,简化后如下:

  1. static void Stat(const FunctionCallbackInfo<Value>& args) {
  2. Environment* env = Environment::GetCurrent(args);
  3. // 参数检测 options是可选的
  4. const int argc = args.Length();
  5. CHECK_GE(argc, );
  6. // 第一个参数必定是路径
  7. BufferValue path(env->isolate(), args[]);
  8. CHECK_NE(*path, nullptr);
  9. // 这玩意不管
  10. FSReqBase* req_wrap_async = GetReqWrap(env, args[]);
  11. if (req_wrap_async != nullptr) { // stat(path, req)
  12. // 注意倒数第二个参数!!!
  13. AsyncCall(env, req_wrap_async, args, "stat", UTF8, AfterStat,
  14. uv_fs_stat, *path);
  15. } else { // stat(path, undefined, ctx)
  16. // ...
  17. // 注意倒数第二个参数!!!
  18. int err = SyncCall(env, args[], &req_wrap_sync, "stat", uv_fs_stat, *path);
  19. // ...
  20. }
  21. }
  22. // AsyncCall => AsyncDestCall
  23. template <typename Func, typename... Args>
  24. inline FSReqBase* AsyncDestCall(/*很多参数*/, Func fn, Args... fn_args) {
  25. // ...
  26. int err = fn(env->event_loop(), req_wrap->req(), fn_args..., after);
  27. // ...
  28. }
  29. template <typename Func, typename... Args>
  30. inline int SyncCall(/*很多参数*/, Func fn, Args... args) {
  31. // ...
  32. int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr);
  33. // ...
  34. }

  省略了很多很多(大家都不想看)的代码,浓缩出了核心的调用,就是uv_fs_stat。

  这里的if、else主要是区别同步和异步调用,那个after就是代表有没有callback,简单了解下就OK了。

libuv => uv_fs_stat

  至此,正式进入第三阶段,libuv层级。

  这个框架的代码十分清爽,给你们看一下:

  1. int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) {
  2. int err;
  3. // 初始化一些信息
  4. INIT(UV_FS_STAT);
  5. // 处理路径参数
  6. err = fs__capture_path(req, path, NULL, cb != NULL);
  7. if (err) {
  8. return uv_translate_sys_error(err);
  9. }
  10. // 实际操作
  11. POST;
  12. }

  完全不用省略任何代码,每一步都很清晰,INIT宏的参数是一个枚举,该枚举类包含所有文件操作的枚举值。

  这里首先是初始化stat相关的一些信息,如下:

  1. #define INIT(subtype) \
  2. do { \
  3. if (req == NULL) \
  4. return UV_EINVAL; \
  5. uv_fs_req_init(loop, req, subtype, cb); \
  6. } \
  7. while ()
  8.  
  9. INLINE static void uv_fs_req_init(uv_loop_t* loop, uv_fs_t* req, uv_fs_type fs_type, const uv_fs_cb cb) {
  10. uv__once_init();
  11. UV_REQ_INIT(req, UV_FS);
  12. req->loop = loop;
  13. req->flags = ;
  14. // 只有这一步是类型相关的
  15. req->fs_type = fs_type;
  16. req->result = ;
  17. req->ptr = NULL;
  18. req->path = NULL;
  19. req->cb = cb;
  20. memset(&req->fs, , sizeof(req->fs));
  21. }

  因为代码比较简单直白,所以就懒得省略了。

  这里的宏是一个公共宏,所有文件操作相关的调用都要经过这个宏来进行初始化。在参数上,loop(事件轮询)、req(文件操作的相关对象)、cb(回调函数)都基本上不会变,所以实际上唯一区别操作类型的只有subtype。

  第二步是对路径的处理,我觉得应该不会有人想知道内容是什么。

  所以直接进入最后一步,POST。这个框架也真是可以的,所有的文件操作都通过三件套批量处理了。

  这个宏如下:

  1. #define POST \
  2. do { \
  3. if (cb != NULL) { \
  4. uv__req_register(loop, req); \
  5. uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done); \
  6. return ; \
  7. } else { \
  8. uv__fs_work(&req->work_req); \
  9. return req->result; \
  10. } \
  11. } \
  12. while ()

  cb来源于node调用中的最后一个参数,同步情况下传的是一个Undefined,并不需要一个回调函数。

  对于开发者来说同步异步可能只是书写流程的小变化,但是对于libuv来说却不太一样,因为框架本身同时掌控着事件轮询,在异步情况下,这里的处理需要单独开一个线程进行处理,随后通过观察者模式通知异步调用结束,需要执行回调函数。

  另外一个不同点是,同步调用直接返回一个结果,异步调用会包装结果作为回调函数的参数然后进行调用,通过上面的if、else结构也能看出来。

windowsAPI

  这里的处理分同步和异步。

  先看同步:

  1. static void uv__fs_work(struct uv__work* w) {
  2. uv_fs_t* req;
  3. // ...
  4.  
  5. #define XX(uc, lc) case UV_FS_##uc: fs__##lc(req); break;
  6. // 枚举值为UV_FS_STAT
  7. switch (req->fs_type) {
  8. // ...
  9. XX(CLOSE, close)
  10. XX(READ, read)
  11. XX(WRITE, write)
  12. XX(FSTAT, fstat)
  13. // ...
  14. default:
  15. assert(!"bad uv_fs_type");
  16. }
  17. }

  这个地方,上面的那个枚举值终于起了作用,省略了一些无关代码,最终的结果通过宏,指向了一个叫fs__fstat函数。

  1. static void fs__fstat(uv_fs_t* req) {
  2.  
  3. int fd = req->file.fd;
  4. HANDLE handle;
  5.  
  6. VERIFY_FD(fd, req);
  7. // 保证可以获取到对应的文件句柄
  8. handle = uv__get_osfhandle(fd);
  9. // 错误处理
  10. if (handle == INVALID_HANDLE_VALUE) {
  11. SET_REQ_WIN32_ERROR(req, ERROR_INVALID_HANDLE);
  12. return;
  13. }
  14. // 这里进行变量赋值
  15. if (fs__stat_handle(handle, &req->statbuf, ) != ) {
  16. SET_REQ_WIN32_ERROR(req, GetLastError());
  17. return;
  18. }
  19.  
  20. req->ptr = &req->statbuf;
  21. // 返回0
  22. req->result = ;
  23. }

  这里有两个方法需要注意:

1、uv__get_osfhandle   获取文件句柄

2、fs__stat_handle      获取文件信息

  源码如下:

  1. INLINE static HANDLE uv__get_osfhandle(int fd)
  2. {
  3. HANDLE handle;
  4. UV_BEGIN_DISABLE_CRT_ASSERT();
  5. // windowsAPI 根据文件描述符获取文件句柄
  6. handle = (HANDLE) _get_osfhandle(fd);
  7. UV_END_DISABLE_CRT_ASSERT();
  8. return handle;
  9. }
  10.  
  11. INLINE static int fs__stat_handle(HANDLE handle, uv_stat_t* statbuf, int do_lstat) {
  12. // ...
  13. // windowsAPI
  14. nt_status = pNtQueryInformationFile(handle,
  15. &io_status,
  16. &file_info,
  17. sizeof file_info,
  18. FileAllInformation);
  19.  
  20. /* Buffer overflow (a warning status code) is expected here. */
  21. if (NT_ERROR(nt_status)) {
  22. SetLastError(pRtlNtStatusToDosError(nt_status));
  23. return -;
  24. }
  25. // windowsAPI
  26. nt_status = pNtQueryVolumeInformationFile(handle,
  27. &io_status,
  28. &volume_info,
  29. sizeof volume_info,
  30. FileFsVolumeInformation);
  31. // ...文件信息对象的处理
  32. }

  可以看出,最后的底层调用了windows的API来获取对应的文件句柄,然后继续获取对应句柄的文件信息,将信息处理后弄到req->ptr上,而node中对于同步处理的结果代码如下:

  1. Local<Value> arr = node::FillGlobalStatsArray(env, static_cast<const uv_stat_t*>(req_wrap_sync.req.ptr));
  2. args.GetReturnValue().Set(arr);

  这里的req_wrap_sync.req.ptr就是上面通过windowAPI获取到的文件信息内容。

  异步情况如下:

  1. void uv__work_submit(uv_loop_t* loop,
  2. struct uv__work* w,
  3. void (*work)(struct uv__work* w),
  4. void (*done)(struct uv__work* w, int status)) {
  5. uv_once(&once, init_once);
  6. w->loop = loop;
  7. w->work = work;
  8. w->done = done;
  9. post(&w->wq);
  10. }

  先看那个奇怪的post:

  1. static void post(QUEUE* q) {
  2. // 上锁
  3. uv_mutex_lock(&mutex);
  4. // 关于QUEUE的分析可见https://www.jianshu.com/p/6373de1e117d
  5. // 知道是个队列就行了
  6. QUEUE_INSERT_TAIL(&wq, q);
  7. if (idle_threads > )
  8. uv_cond_signal(&cond);
  9. // 解锁
  10. uv_mutex_unlock(&mutex);
  11. }

  由于异步涉及到事件轮询,所以代码实质上要稍微复杂一点,但是总体来说并不需要关心那么多。

  这里有一个空闲线程的判断,不管,直接看那个处理方法:

  1. void uv_cond_signal(uv_cond_t* cond) {
  2. if (HAVE_CONDVAR_API())
  3. uv_cond_condvar_signal(cond);
  4. else
  5. // 初始化一个状态变量防止线程的竞争情况
  6. // 反正也是个windowsAPI
  7. uv_cond_fallback_signal(cond);
  8. }
  9.  
  10. static void uv_cond_condvar_signal(uv_cond_t* cond) {
  11. // windowsAPI
  12. pWakeConditionVariable(&cond->cond_var);
  13. }

  你会发现,这只是防止线程竞态而需要生成一个状态变量。

  其实这个地方已经涉及到libuv中事件轮询的控制了,每次loop会从handle中取一个req,然后执行work,然后通知node完成,可以执行回调函数done了。

  暂时不需要知道那么多,在uv__work_submit方法中,对应的赋值是这4个参数:

  1. uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done);

  其中第三个参数就是刚才同步获取文件信息的方法,而第四个就是在获取完毕会回调函数的调用:

  1. static void uv__fs_done(struct uv__work* w, int status) {
  2. uv_fs_t* req;
  3.  
  4. req = container_of(w, uv_fs_t, work_req);
  5. uv__req_unregister(req->loop, req);
  6.  
  7. if (status == UV_ECANCELED) {
  8. assert(req->result == );
  9. req->result = UV_ECANCELED;
  10. }
  11. // 执行回调
  12. req->cb(req);
  13. }

  异步调用因为在回调函数带了结果,所以返回值不能跟同步一样,最后的处理有些许不一样:

  1. template <typename Func, typename... Args>
  2. inline FSReqBase* AsyncDestCall(/*很多参数*/) {
  3. // ...
  4. if (err < ) {
  5. // ...
  6. } else {
  7. req_wrap->SetReturnValue(args);
  8. }
  9. // 返回另外的值
  10. return req_wrap;
  11. }
  12.  
  13. void FSReqWrap::SetReturnValue(const FunctionCallbackInfo<Value>& args) {
  14. // 设成undefined
  15. args.GetReturnValue().SetUndefined();
  16. }

  简单讲,fs.statSync返回一个Stat对象,而fs.stat返回undefined。这个可以很简单的测试得到结果,我这里就不贴图了,已经够长了。

深入出不来nodejs源码-从fs.stat方法来看node架构的更多相关文章

  1. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

  2. 深入出不来nodejs源码-流程总览

    花了差不多两周时间过了下primer C++5th,完成了<C++从入门到精通>.(手动滑稽) 这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造 ...

  3. 深入出不来nodejs源码-编译启动(1)

    整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...

  4. 深入出不来nodejs源码-timer模块(JS篇)

    鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看. 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单 ...

  5. 深入出不来nodejs源码-events模块

    这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼. 学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定.这个东西贯穿了整个JS ...

  6. 深入出不来nodejs源码-timer模块(C++篇)

    终于可以填上坑了. 简单回顾一下之前JS篇内容,每一次setTimeout的调用,会在一个对象中添加一个键值对,键为延迟时间,值为一个链表,将所有该时间对应的事件串起来,图如下: 而每一个延迟键值对的 ...

  7. 深入出不来nodejs源码-内置模块引入再探

    我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...

  8. 深入出不来nodejs源码-内置模块引入初探

    重新审视了一下上一篇的内容,配合源码发现有些地方说的不太对,或者不太严谨. 主要是关于内置模块引入的问题,当时我是这样描述的: 需要关注的只要那个RegisterBuiltinModules方法,从名 ...

  9. Docker源码分析(一):Docker架构

    1 背景 1.1 Docker简介 Docker是Docker公司开源的一个基于轻量级虚拟化技术的容器引擎项目,整个项目基于Go语言开发,并遵从Apache 2.0协议.目前,Docker可以在容器内 ...

随机推荐

  1. 如何利用JUnit开展一个简单的单元测试(测试控制台输出是否正确)

    待测类(CreateString)如下: public class CreateString { public void createString() { //Output the following ...

  2. JS学习笔记5_DOM

    1.DOM节点的常用属性(所有节点都支持) nodeType:元素1,属性2,文本3 nodeName:元素标签名的大写形式 nodeValue:元素节点为null,文本节点为文本内容,属性节点为属性 ...

  3. C#实现枚举的相关操作

    枚举中的Descript()描述值,以及枚举值是一种一一对应的关系.我们可以获取其描述值和枚举值,存放到字典中, 在实际的使用中我们就可以轻松的根据枚举值来获取其描述值,也可以通过枚举的描述值来获取其 ...

  4. vue项目经验:图形验证码接口get请求处理

    一般图形验证码处理: 直接把img标签的src指向这个接口,然后在img上绑定点击事件,点击的时候更改src的地址(在原来的接口地址后面加上随机数即可,避免缓存) <img :src=" ...

  5. select2插件使用小记

    插件官网:https://select2.github.io/examples.html 页面引入: // 页面顶部 <link rel="stylesheet" type= ...

  6. centos 安装nginx笔记

    添加nginx 存储库 yum install epel-release 安装 yum install nginx 启动 systemctl start nginx

  7. Vue2.5开发去哪儿网App 城市列表开发

     一,城市选择页面路由配置                                                                                        ...

  8. Java DB访问(一) JDBC

    项目说明 项目采用 maven 组织 ,jdbc 唯一的依赖就是 mysql-connector-java pom 依赖如下: <dependency> <groupId>my ...

  9. (转)shlex — 解析 Shell 风格语法

    原文:https://pythoncaff.com/docs/pymotw/shlex-parse-shell-style-syntaxes/171 这是一篇协同翻译的文章,你可以点击『我来翻译』按钮 ...

  10. 使用Svn的版本号[转载]

    1. 生成一个名为autover的项目 注意项目的Properties文件夹下有一个名为AssemblyInfo.cs的文件,autover程序的版本号就写在它里面. 2. 创建模板文件 在Windo ...