自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

聊聊No.js---基于V8和io_uring的JS運行時

開發(fā) 前端
本文介紹運行時No.js的一些設計和實現(xiàn),取名No.js一來是受Node.js的影響,二來是為了說明不僅僅是JS,也就是利用V8拓展了JS的功能,同時,前端開發(fā)者要學習的知識也不僅僅是JS了。

[[421775]]

前言:閱讀Node.js的源碼已經(jīng)有一段時間了,最近也看了一下新的JS運行時Just的一些實現(xiàn),就產(chǎn)生了自己寫一個JS運行時的想法,雖然幾個月前就基于V8寫了一個簡單的JS運行時,但功能比較簡單,這次廢棄了之前的代碼,重新寫了一遍,寫這個JS運行時的目的最主要是為了學習,事實也證明,寫一個JS運行時的確可以學到很多東西。本文介紹運行時No.js的一些設計和實現(xiàn),取名No.js一來是受Node.js的影響,二來是為了說明不僅僅是JS,也就是利用V8拓展了JS的功能,同時,前端開發(fā)者要學習的知識也不僅僅是JS了。

1 為什么選io_uring

io_uring是Linux下新一代的高性能異步IO框架,也是No.js的核心。在No.js中,io_uring用于實現(xiàn)事件循環(huán)。為什么不選用epoll呢?因為epoll不支持文件IO,如果選用epoll,還需要自己實現(xiàn)一個線程池,還需要實現(xiàn)線程和主線程的通信,以及線程池任務和事件循環(huán)的融合,No.js希望把事件變得純粹,簡單。而io_uring是支持異步文件IO的,并且io_uring是真正的異步IO框架,支持的功能也非常豐富,比如在epoll里我們監(jiān)聽一個socket后,需要把socket fd注冊到epoll中,等待有連接時執(zhí)行回調(diào),然后調(diào)用accept獲取新的fd,而io_uring直接就幫我們獲取新的fd,io_uring通知我們的時候,我們就已經(jīng)拿到新的fd了,epoll時代,epoll通知我們可以做什么事情了,然后我們自己去做,io_uring時代,io_uring通知我們什么事情完成了。

2 No.js框架的設計

No.js目前的實現(xiàn)比較清晰簡單,所有的功能都通過c和c++實現(xiàn),然后通過V8暴露給JS實現(xiàn)。No.cc是初始化的入口,core目錄是所有功能實現(xiàn)的地方,core下面按照模塊功能劃分。下面我們看看整體的框架實現(xiàn)。

  1. int main(int argc, char* argv[]) { 
  2.   // ... 
  3.   Isolate* isolate = Isolate::New(create_params); 
  4.   { 
  5.     Isolate::Scope isolate_scope(isolate); 
  6.     HandleScope handle_scope(isolate); 
  7.     // 創(chuàng)建全局對象 
  8.     Local<ObjectTemplate> global = ObjectTemplate::New(isolate); 
  9.     // 創(chuàng)建執(zhí)行上下文 
  10.     Local<Context> context = Context::New(isolate, nullptr, global); 
  11.     Environment * env = new Environment(context); 
  12.     Context::Scope context_scope(context); 
  13.     // 創(chuàng)建No,核心對象 
  14.     Local<Object> No = Object::New(isolate); 
  15.     // 注冊c、c++模塊 
  16.     register_builtins(isolate, No); 
  17.     // 獲取全局對象 
  18.     Local<Object> globalInstance = context->Global(); 
  19.     // 設置全局屬性 
  20.     globalInstance->Set(context, String::NewFromUtf8Literal(isolate, "No",  
  21.     NewStringType::kNormal), No); 
  22.     // 設置全局屬性global指向全局對象 
  23.     globalInstance->Set(context, String::NewFromUtf8Literal(isolate,  
  24.       "global",  
  25.       NewStringType::kNormal), globalInstance).Check(); 
  26.     { 
  27.       // 打開文件 
  28.       int fd = open(argv[1], O_RDONLY); 
  29.       struct stat info; 
  30.       // 取得文件信息 
  31.       fstat(fd, &info); 
  32.       // 分配內(nèi)存保存文件內(nèi)容 
  33.       char *ptr = (char *)malloc(info.st_size + 1); 
  34.       read(fd, (void *)ptr, info.st_size); 
  35.       // 要執(zhí)行的js代碼 
  36.       Local<String> source = String::NewFromUtf8(isolate, ptr, 
  37.                           NewStringType::kNormal, 
  38.                           info.st_size).ToLocalChecked(); 
  39.  
  40.       // 編譯 
  41.       Local<Script> script = Script::Compile(context, source).ToLocalChecked(); 
  42.       // 解析完應該沒用了,釋放內(nèi)存 
  43.       free(ptr); 
  44.       // 執(zhí)行 
  45.       Local<Value> result = script->Run(context).ToLocalChecked(); 
  46.       // 進入事件循環(huán) 
  47.       Run(env->GetIOUringData()); 
  48.     } 
  49.   } 
  50.   return 0; 
  51.  

大部分邏輯都是V8初始化的標準流程,添加的內(nèi)容主要包括注冊c、c++模塊、掛載No到全局作用域、開啟事件循環(huán)。

2.1 注冊模塊

No在初始化的時候會把所有C++模塊注冊到No中,因為No是全局屬性,所以在JS里可以直接訪問C++模塊,不需要require。我們看看register_builtins。

  1. void No::Core::register_builtins(Isolate * isolate, Local<Object> target) { 
  2.     FS::Init(isolate, target);  
  3.     TCP::Init(isolate, target);  
  4.     Process::Init(isolate, target);  
  5.     Console::Init(isolate, target); 
  6.     IO::Init(isolate, target); 
  7.     Net::Init(isolate, target); 
  8.     UDP::Init(isolate, target); 
  9.     UNIX_DOMAIN::Init(isolate, target); 
  10.     Signal::Init(isolate, target); 
  11.     Timer::Init(isolate, target); 
  12.  

register_builtins會調(diào)用各個模塊的Init函數(shù),各個模塊自己實現(xiàn)需要掛載的功能,從代碼中可以看到目前實現(xiàn)的功能。我們隨便找一個模塊看看初始化的邏輯。

  1. void No::FS::Init(Isolate* isolate, Local<Object> target) { 
  2.   Local<ObjectTemplate> fs = ObjectTemplate::New(isolate); 
  3.   setMethod(isolate, fs, "open"No::FS::Open); 
  4.   setMethod(isolate, fs, "openat"No::FS::OpenAt); 
  5.   setMethod(isolate, fs, "close"No::IO::Close); 
  6.   setMethod(isolate, fs, "read"No::IO::Read); 
  7.   setMethod(isolate, fs, "write"No::IO::Write); 
  8.   setMethod(isolate, fs, "readv"No::IO::ReadV); 
  9.   setMethod(isolate, fs, "writev"No::IO::WriteV); 
  10.   setObjectValue(isolate, target, "fs", fs->NewInstance(isolate->GetCurrentContext()).ToLocalChecked()); 
  11.  

掛載的邏輯就是新建一個對象,然后設置對象的屬性,最后把這個對象作為No對象的一個屬性掛載到No中,最后形成如下一個結(jié)構。

  1. var No = { 
  2.     fs: {}, 
  3.     tcp: {} 
  4.  

這就完成了所有核心模塊的注冊。

2.2 執(zhí)行JS

注冊完核心模塊后就是執(zhí)行業(yè)務JS。我們隨便看個例子。

  1. const { 
  2.     fs, 
  3.     console 
  4. } = No;const fd = fs.open('./test/file/1.txt');const arr = new ArrayBuffer(100); 
  5. fs.readv(fd,arr , 0, (res) => {console.log(res)}); 
  6. console.log(new Uint8Array(arr)); 

以上是讀取一個文件的例子,從中也可以看到No的使用方式。No沒有實現(xiàn)類似Node.js的Buffer,是直接使用V8的ArrayBuffer的,ArrayBuffer使用的是V8堆外內(nèi)存,readv是C++層實現(xiàn)的函數(shù),我們一會單獨介紹。

2.3 開啟事件循環(huán)

執(zhí)行完JS后,最后進入事件循環(huán)。

  1. void No::io_uring::RunIOUring(struct io_uring_info *io_uring_data) { 
  2.     struct io_uring* ring = &io_uring_data->ring; 
  3.     struct io_uring_cqe* cqe; 
  4.     struct request* req; 
  5.     while(io_uring_data->stop != 1 && io_uring_data->pending != 0) { 
  6.         // 提交請求給內(nèi)核 
  7.         int count = io_uring_submit_and_wait(ring, 1); 
  8.         // 處理每一個完成的請求 
  9.         while (1) {  
  10.             io_uring_peek_cqe(ring, &cqe); 
  11.             if (cqe == NULL
  12.                 break; 
  13.             --io_uring_data->pending; 
  14.             // 拿到請求上下文 
  15.             req = (struct request*) (uintptr_t) cqe->user_data; 
  16.             req->res = cqe->res; 
  17.             io_uring_cq_advance(ring, 1); 
  18.             // 執(zhí)行回調(diào) 
  19.             if (req->cb != nullptr) { 
  20.                 req->cb((void *)req); 
  21.             } 
  22.         } 
  23.     } 
  24.  

從事件循環(huán)的代碼中大致可以看到原理,首先判斷事件循環(huán)是不是停止或者可以停止了,如果還沒有停止,則等待任務完成,然后取出任務執(zhí)行任務的對象。

3 任務的封裝和處理

io_uring的任務是以結(jié)構體io_uring_sqe表示的,但是io_uring_sqe只是記錄了和io_uring框架本身相關的一些數(shù)據(jù)結(jié)構,因為是異步的模式,所以在任務完成的時候,我們需要知道,這個任務關聯(lián)的上下文和回調(diào)。io_uring_sqe提供了user_data字段用于保存請求對應的上下文。流程如下。設置和提交請求

  1. // 獲取一個io_uring的請求結(jié)構體 
  2.  struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_data->ring); 
  3.  // 自定義結(jié)構體 
  4.  struct io_request * file_req = (struct io_request *)req; 
  5.  // 設置請求的字段 
  6.  io_uring_prep_read(sqe, file_req->fd, file_req->buf, file_req->len, file_req->offset); 
  7.  // 保存請求上下文,響應的時候用 
  8.  io_uring_sqe_set_data(sqe, (void *)req); 
  9.  // 提交請求 
  10.  io_uring_submit(&io_uring_data->ring); 

我們看到提交請求的時候,設置了請求上下文是我們自定義的結(jié)構體,具體結(jié)構體類型根據(jù)操作類型而不同。我們看看請求完成時是如何處理的。

  1. struct io_uring_cqe* cqe;io_uring_peek_cqe(ring, &cqe);// 拿到請求上下文 
  2. req = (struct request*) (uintptr_t) cqe->user_data;// 記錄請求結(jié)果 
  3. req->res = cqe->res; 
  4. req->cb((void *)req); 

以上就是一個No請求和響應的處理過程。No為不同的操作類型封裝了不同的結(jié)構體。首先封裝了一個請求的基類。

  1. #define REQUEST \ 
  2.         int op; \ 
  3.         // io_uring執(zhí)行的回調(diào) 
  4.         request_cb cb; \ 
  5.         // io_uring請求的結(jié)果 
  6.         int res;\ 
  7.         // 業(yè)務上下文 
  8.         void * data; \ 
  9.         int flag; 

類似io_uring通過user_data字段關聯(lián)請求響應上下文。REQUEST 里通過data關聯(lián)請求和響應上下文,通過user_data字段,我們在任務完成時可以執(zhí)行應該執(zhí)行哪個回調(diào)以及對應的上下文。但是執(zhí)行某個回調(diào)時,該回調(diào)函數(shù)需要的上下文可能不僅僅是io_uring返回的結(jié)果,這時候就可以使用data字段記錄額外的上下文。一會會具體介紹?;赗EQUEST,針對不同的操作封裝了不同的結(jié)構體,比如文件請求。

  1. struct io_request { 
  2.     REQUEST 
  3.     int fd;  
  4.     int offset;  
  5.     void *buf; 
  6.     int len;  
  7. }; 

下面我們分析一個具體請求的過程,這里以read為例。

  1. void read_write_request(V8_ARGS, int op) {  
  2.     V8_ISOLATE 
  3.     int fd = args[0].As<Uint32>()->Value(); 
  4.     int offset = 0; 
  5.     if (args.Length() > 2 && args[2]->IsNumber()) { 
  6.         offset = args[2].As<Integer>()->Value(); 
  7.     } 
  8.     Local<ArrayBuffer> arrayBuffer = args[1].As<ArrayBuffer>(); 
  9.     std::shared_ptr<BackingStore> backing = arrayBuffer->GetBackingStore(); 
  10.     V8_CONTEXT 
  11.     Environment *env = Environment::GetEnvByContext(context); 
  12.     struct io_uring_info *io_uring_data = env->GetIOUringData(); 
  13.     struct request *req; 
  14.     // 文件操作對應的request結(jié)構體 
  15.     struct io_request *io_req = (struct io_request *)malloc(sizeof(struct io_request)); 
  16.     memset(io_req, 0, sizeof(*io_req)); 
  17.     io_req->buf = backing->Data(); 
  18.     io_req->len = backing->ByteLength(); 
  19.     io_req->fd = fd; 
  20.     io_req->offset = offset; 
  21.     req = (struct request *)io_req; 
  22.     // JS層回調(diào) 
  23.     req->cb = makeCallback<onread>; 
  24.     req->op = op; 
  25.     // 保存回調(diào)上下文 
  26.     if (args.Length() > 3 && args[3]->IsFunction()) { 
  27.         Local<Object> obj = Object::New(isolate); 
  28.         Local<String> key = newStringToLcal(isolate, onread); 
  29.         obj->Set(context, key, args[3].As<Function>()); 
  30.         req->data = (void *)new RequestContext(env, obj); 
  31.     } else { 
  32.         req->data = (void *)new RequestContext(env, Local<Function>()); 
  33.     } 
  34.     // 提交請求 
  35.     SubmitRequest((struct request *)req, io_uring_data);   

初始化請求的上下文后,調(diào)用SubmitRequest提交任務和io_uring。我們看看SubmitRequest。

  1. void No::io_uring::SubmitRequest(struct request * req, struct io_uring_info *io_uring_data) { 
  2.     // 獲取一個io_uring的請求結(jié)構體 
  3.     struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_data->ring); 
  4.     // 填充請求 
  5.     switch (req->op) 
  6.     { 
  7.  
  8.         case IORING_OP_READ: 
  9.             { 
  10.                 struct io_request * file_req = (struct io_request *)req; 
  11.                 io_uring_prep_read(sqe, file_req->fd, file_req->buf, file_req->len, file_req->offset); 
  12.                 break; 
  13.             } 
  14.         default
  15.             return
  16.     } 
  17.     ++io_uring_data->pending; 
  18.     // 保存請求上下文,響應的時候用 
  19.     io_uring_sqe_set_data(sqe, (void *)req); 
  20.     io_uring_submit(&io_uring_data->ring); 
  21.  

SubmitRequest根據(jù)不同的操作設置io_uring的請求結(jié)構體,并保存對應的請求上下文。當任務完成時執(zhí)行回調(diào)makeCallback。makeCallback是模板函數(shù)。

  1. template <const char * event> 
  2.   void makeCallback(void * req) { 
  3.        struct request * _req = (struct request *)req; 
  4.        RequestContext* ctx =(RequestContext *)_req->data; 
  5.        if (!ctx->object.IsEmpty()) { 
  6.            Local<Object> object = ctx->object.Get(ctx->env->GetIsolate()); 
  7.            Local<Value> cb; 
  8.            Local<Context> context = ctx->env->GetContext(); 
  9.            Local<String> onevent = newStringToLcal(ctx->env->GetIsolate(), event);        
  10.            object->Get(context, onevent).ToLocal(&cb); 
  11.            if (cb->IsFunction()) {   
  12.                Local<Value> argv[] = { 
  13.                    Integer::New(context->GetIsolate(), _req->res) 
  14.                }; 
  15.                // 執(zhí)行JS層回調(diào) 
  16.                cb.As<v8::Function>()->Call(context, object, 1, argv); 
  17.            } 
  18.        } 
  19.    }; 

makeCallback做的事情就是執(zhí)行JS回調(diào)。

4 非io_uring的處理

io_uring目前已經(jīng)支持了非常多的操作,但我們也不可避免地會碰到io_uring不支持的操作,比如信號的處理。No里目前定時器和信號不是使用io_uring處理的。定時器目前使用內(nèi)核的posix timer實現(xiàn)的,io_uring有個timeout類型的請求,可能會使用io_uring的,信號處理io_uring就無能無力了。因為No是單線程的架構,所以非io_uring的任務完成后也需要通過io_uring事件循環(huán)執(zhí)行,下面看一下非io_uring支持的操作如何處理的。在業(yè)務里,我們可能需要監(jiān)聽一個信號。

  1. const { 
  2.     signal, 
  3.     console, 
  4.     process, 
  5.     timer,} = No
  6.  
  7. signal.on(signal.constant.SIG.SIGUSR1, () => { 
  8.     process.exit(); 
  9.  
  10. }); 
  11.  
  12. // for keep process alive 
  13.  
  14. timer.setInterval(() => {},10000, 10000); 

可以通過signal模塊的on實現(xiàn)監(jiān)聽信號,接下來看看具體實現(xiàn)。

  1. void No::Signal::RegisterSignal(V8_ARGS) { 
  2.     V8_ISOLATE 
  3.     V8_CONTEXT 
  4.     Environment *env = Environment::GetEnvByContext(context); 
  5.     Local<Object> obj = Object::New(isolate); 
  6.     Local<String> key = newStringToLcal(isolate, onsignal); 
  7.     obj->Set(context, key, args[1].As<Function>()); 
  8.     int sig = args[0].As<Integer>()->Value();  
  9.     // 新建一個上下文 
  10.     shared_ptr<SignalRequestContext> ctx = make_shared<SignalRequestContext>(env, obj, sig); 
  11.     auto ret = signalMap.find(sig); 
  12.     // 是否在map里,不是則新建一個vector,否則直接追加 
  13.     if (ret == signalMap.end()) { 
  14.         signal(sig, signalHandler); 
  15.         vector<shared_ptr<SignalRequestContext>> vec; 
  16.         vec.push_back(ctx); 
  17.         signalMap.insert(map<int, vector<shared_ptr<SignalRequestContext>>>::value_type (sig, vec));   
  18.         return
  19.     } 
  20.     ret->second.push_back(ctx); 
  21.  

No使用一個map管理信號和監(jiān)聽函數(shù),因為支持多個監(jiān)聽函數(shù),所以map的key是信號的值,value是一個回調(diào)函數(shù)數(shù)組。如果是第一次注冊該信號,則調(diào)用signal注冊該信號的處理函數(shù),所有信號的處理函數(shù)都是signalHandler。接著看信號產(chǎn)生時的處理邏輯。

  1. static void signalHandler(int signum){    
  2.     auto vec = signalMap.find(signum); 
  3.     if (vec != signalMap.end()) { 
  4.         vector<shared_ptr<SignalRequestContext>>::iterator it; 
  5.         for(it=vec->second.begin();it!=vec->second.end(); it++) 
  6.         { 
  7.             struct signal_request * req = (struct signal_request *)malloc(sizeof(*req));  
  8.             memset(req, 0, sizeof(*req)); 
  9.             req->cb = signal_cb; 
  10.             req->data = (void *)(*it).get(); 
  11.             req->op = IORING_OP_NOP; 
  12.             SubmitRequest((struct request *)req, (*it).get()->env->GetIOUringData()); 
  13.         } 
  14.     } 
  15.  

信號產(chǎn)生時,從map中找到對應的處理函數(shù)列表,然后生成一個io_uring請求,這樣在事件循環(huán)時就會被執(zhí)行,也實現(xiàn)了非io_uring任務和io_uring任務的整合,這里主要是利用了io_uring提供了nop類型的請求,這個類型的請求不做任何操作,主要是用于測試io_uring請求和響應鏈路,利用這點恰好可以實現(xiàn)我們的需求。從代碼中可以看到io_uring事件循環(huán)時會執(zhí)行信號處理的回調(diào)signal_cb,signal_cb會回調(diào)JS層。

5 上下文的設計

因為No各種請求都是異步的,所以避免不了需要保持請求和響應的上下文。類似Node.js,No里也存在一個env作為整個進程級的上下文。

  1. enum { 
  2.     CONTEXT_INDEX 
  3. } ENV_INDEX; 
  4.  
  5. class Environment { 
  6.     public
  7.         Environment(Local<Context> context); 
  8.         static Environment * GetEnvByContext(Local<Context> context); 
  9.         struct io_uring_info * GetIOUringData() { 
  10.             return io_uring_data; 
  11.         } 
  12.         Isolate * GetIsolate() const { 
  13.             return _isolate; 
  14.         } 
  15.          Local<Context> GetContext() const { 
  16.             return PersistentToLocal::Strong(_context); 
  17.         } 
  18.     private: 
  19.         struct io_uring_info *io_uring_data; 
  20.         Global<Context> _context; 
  21.         Isolate * _isolate; 
  22.  
  23. }; 

env目前的功能還不多,只要負責管理context、isolate、io_uring等數(shù)據(jù)結(jié)構。另外還有一些和具體操作相關的上下文。

  1. struct RequestContext { 
  2.    RequestContext(Environment * passEnv, Local<Object> _object) 
  3.    : env(passEnv),  object(passEnv->GetIsolate(), _object) {} 
  4.    ~RequestContext() { 
  5.        if (!object.IsEmpty()) { 
  6.            object.Reset(); 
  7.        } 
  8.    } 
  9.    Environment * env; 
  10.    Global<Object> object; 
  11.  
  12. }; 
  13.  
  14.  
  15.  
  16. struct SignalRequestContext: public RequestContext 
  17.  
  18.    SignalRequestContext(Environment * passEnv, Local<Object> _object, int _sig) 
  19.    : RequestContext(passEnv, _object), sig(_sig) {} 
  20.    int sig; 
  21.  
  22. }; 

前面介紹過io_uring層的上下文request,request主要是用于io_uring任務完成時,知道執(zhí)行哪個回調(diào)函數(shù),并且記錄了少量的上下文,但是reuqest的字段不一定夠用,所以RequestContext主要記錄額外的上下文,其實把RequestContext的字段合進request也是可以的。

6 事件循環(huán)的設計

No的事件循環(huán)是io_uring實現(xiàn)的,事件循環(huán)的本質(zhì)就是在一個循環(huán)里不斷等待任務和執(zhí)行任務,那么什么時候結(jié)束呢?

  1. while(io_uring_data->stop != 1 && io_uring_data->pending != 0) { 
  2.         // 等待和處理任務 
  3.  

目前可以通過設置stop直接停止事件循環(huán),正常情況下,沒有任務了就會結(jié)束事件循環(huán),通過pending字段記錄,比如發(fā)起一個讀取文件的請求,pending就是1,讀完后就會減一,這時候,事件循環(huán)就會結(jié)束,相對Node.js的handle和request,No里是沒有的,No里通過控制pending的值去控制事件循環(huán)的狀態(tài)。

7 如何使用

No是基于Linux的io_uring的,目前在Linux5.5及以上的系統(tǒng)可以運行,可以安裝ubuntu21.04及以上的虛擬機使用,具體可以參考倉庫說明(https://github.com/theanarkh/No.js)。目前支持了TCP、UCP、Unix域、文件、信號、定時器、log,進程還沒有寫完,總體只是支持一些簡單的操作,后續(xù)慢慢更新。

8 后記

寫No是一個讓人非常深刻的過程,已經(jīng)很多年沒有正經(jīng)寫過c、c++代碼,或許代碼里有不對的用法,但是整個過程里的思考、編碼和調(diào)試讓我學到了很多東西,也給我了一段深刻的時光。目前實現(xiàn)的功能還不多,也不足以用起來,還有很多事情需要做,紙上得來終覺淺,絕知此事要躬行。后續(xù)慢慢學習、慢慢思考、慢慢更新!

 

責任編輯:姜華 來源: 編程雜技
相關推薦

2023-10-20 06:26:51

Libuvio_uring

2021-07-03 08:04:10

io_uringNode.js異步IO

2024-03-21 09:15:58

JS運行的JavaScrip

2023-02-07 19:46:35

NIOCQ內(nèi)核

2021-08-27 00:21:19

JSJust源碼

2023-10-10 10:23:50

JavaScriptV8

2021-07-07 23:38:05

內(nèi)核IOLinux

2021-07-11 23:25:29

Libuvepoll文件

2023-04-12 18:36:20

IO框架內(nèi)核

2022-10-08 00:00:00

V8channel對象

2023-09-12 17:38:41

2024-01-29 08:07:42

FlinkYARN架構

2022-01-19 08:50:53

設備樹Linux文件系統(tǒng)

2016-04-18 09:33:52

nodejswebapp

2021-07-10 07:39:38

Node.js C++V8

2022-10-08 00:06:00

JS運行V8

2023-12-28 11:24:29

IO系統(tǒng)請求

2021-10-14 09:53:38

鴻蒙HarmonyOS應用

2021-09-07 11:19:42

操作系統(tǒng)華為鴻蒙

2021-05-24 11:25:13

Node.js 16V8前端
點贊
收藏

51CTO技術棧公眾號