最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文詳解Node.js操作磁盤文件底層原理與實踐指南

 更新時間:2026年04月16日 08:42:39   作者:瓏墨  
這篇文章主要為大家詳細(xì)介紹了Node.js操作磁盤文件底層原理和具體應(yīng)用方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

你以為 fs.readFile 是讓 Node 幫你「拿一下文件」?不,其實是:你下單 → 前臺記單 → 后廚線程池做菜 → 做好了再叫你。這篇文章帶你看看這份「外賣」是怎么從磁盤端到你手里的。

一、先別急著寫代碼:為什么你要關(guān)心「底層」?

很多人學(xué) Node.js 的 fs 模塊,會背兩句口訣就收工:「用異步別用同步」「大文件用 Stream」

背完發(fā)現(xiàn):

  • 為什么我 readFile 讀個 10GB 的日志直接 OOM?
  • 為什么說 Node 是單線程,但一堆文件操作時還是會「卡」?
  • fs.promisesfs.readFile 回調(diào)版,底層是不是同一套?

(補充一句:文件 I/O 本身是等磁盤,不會占滿 CPU;你覺得「卡」多半是線程池被占滿,新任務(wù)在排隊。)

要回答這些,就得知道:你的 JS 代碼 → Node 的 C++ 綁定 → libuv → 操作系統(tǒng) → 磁盤,這條鏈上每一環(huán)在干什么。

知道之后,你選 API、調(diào)參數(shù)、排查性能問題,都會心里有數(shù)——而不是靠「玄學(xué)調(diào)參」。

所以這篇東西的目標(biāo)很簡單:用盡量人話 + 一點幽默,把 Node.js 操作磁盤文件的底層原理講清楚,順便帶上能跑的示例。

二、從你敲下fs.readFile開始:調(diào)用鏈長什么樣?

你寫的可能是:

const fs = require('fs');
fs.readFile('/tmp/hello.txt', (err, data) => {
  if (err) throw err;
  console.log(data.toString());
});

在底層,大概發(fā)生了這些事(簡化版):

JavaScript 層:fs.readFile 是 Node 內(nèi)置模塊 fs 上的方法,實現(xiàn)里會做路徑解析、編碼處理、以及「把回調(diào)塞進某個流程里」。

C++ 綁定層(Node 的 node_file.cc 等):JS 調(diào)用的其實是 C++ 里封裝好的函數(shù)。

這里會:

  • 把路徑、回調(diào)、選項等轉(zhuǎn)成 C++ 能用的東西;
  • 調(diào) libuv 的 API,發(fā)起「異步文件讀請求」。

libuv 層:libuv 是 Node 用來抽象「異步 I/O」的 C 庫,跨平臺(Windows / Linux / macOS 都靠它)。

文件 I/O,它一般不會用 epoll/kqueue 這種「純事件」機制,而是:

把實際讀文件的工作丟進「線程池」,在池里某條線程里做阻塞式的 read。

所以:你以為的單線程,只是 JS 執(zhí)行單線程;文件讀寫是在別的線程里阻塞地干的。

操作系統(tǒng) → 磁盤:線程池里的線程調(diào)的就是 OS 的 read(或類似)系統(tǒng)調(diào)用,由內(nèi)核去和磁盤驅(qū)動、塊設(shè)備打交道,把數(shù)據(jù)從磁盤讀到內(nèi)核緩沖區(qū),再拷到用戶態(tài)(Node 的 Buffer)。

回到 JS:讀完后,libuv 在某個時機(下一次事件循環(huán)的 I/O 階段)把結(jié)果和你的回調(diào)塞回主線程,于是你的 (err, data) => { ... } 被調(diào)到了,data 就是那個 Buffer。

一句話:fs.readFile = 你在 JS 里下單 → Node 通過 libuv 把「讀文件」這個任務(wù)派給線程池 → 線程池里的線程阻塞地讀磁盤 → 讀完再通過事件循環(huán)把結(jié)果回傳給 JS。

所以「Node 單線程」指的是 JS 只在一個線程跑,磁盤 I/O 并不在主線程上阻塞,而是在線程池里。

三、事件循環(huán)與 libuv:誰在真正「干活」?

Node 的事件循環(huán)(event loop)是由 libuv 實現(xiàn)的。

和文件相關(guān)的部分可以粗分為:

  • Poll 階段:等 I/O(網(wǎng)絡(luò)、部分原生異步 API 等)。
  • 線程池完成回調(diào):文件 I/O 在池里做完后,會在合適的階段把「完成」事件插回事件循環(huán),從而執(zhí)行你傳的 callback 或 resolve Promise。

所以:

  • 主線程(跑 JS 的那條):只負(fù)責(zé)執(zhí)行你的 JS、跑定時器、處理已完成 I/O 的回調(diào),不直接去讀磁盤。
  • 真正摸磁盤的:是 libuv 的線程池里那幾條 worker 線程(默認(rèn) 4 個,可配 UV_THREADPOOL_SIZE)。

這就是為什么:

  • 你寫 fs.readFileSync 時,主線程會阻塞(因為同步 API 就是在主線程上直接調(diào)系統(tǒng)調(diào)用讀文件);
  • fs.readFile 不會阻塞主線程,因為讀是在線程池里做的。

四、線程池:別被「單線程」三個字騙了

默認(rèn)情況下,libuv 的線程池大小是 4(和你的 CPU 核數(shù)無關(guān),就是個固定值)。

所以:

  • 同時發(fā) 10 個 fs.readFile,只有 4 個在「真·讀磁盤」,剩下 6 個在排隊。
  • 線程池既管文件 I/O,也管部分 crypto、部分 DNS 等,所以文件多的時候你會感覺「怎么慢下來了」——因為池子被占滿了。

可以通過環(huán)境變量把池子調(diào)大(建議不超過 CPU 數(shù)太多,否則上下文切換會變多):

# 例如把線程池改成 8
set UV_THREADPOOL_SIZE=8   # Windows
export UV_THREADPOOL_SIZE=8  # Linux/macOS
// 你可以自己試:同時讀多個文件,看完成順序
const fs = require('fs');
const files = ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt', 'file5.txt'];

files.forEach((f, i) => {
  fs.readFile(f, () => console.log(`第 ${i + 1} 個完成: ${f}`));
});
// 前 4 個往往先完成(線程池只有 4),第 5 個要等池里有空位

五、Buffer:內(nèi)存里那塊「黑板」

Buffer 是 Node 里表示「一塊二進制數(shù)據(jù)」的類型,本質(zhì)是 V8 外的一塊連續(xù)內(nèi)存(不經(jīng)過 V8 堆的 GC,由 Node 自己管理)。

文件讀進來、網(wǎng)絡(luò)收來的裸字節(jié),在 JS 里最常見的就是用 Buffer 拿著。fs.readFiledata 就是 Buffer。

  • data.toString() 是把這塊內(nèi)存按指定編碼(默認(rèn) UTF-8)解碼成字符串。
  • 大文件一次性 readFile,就是一次性在內(nèi)存里開一塊和文件一樣大的 Buffer——所以 10GB 文件會直接 OOM,和「底層」沒關(guān)系,就是設(shè)計如此。

所以:大文件不要用 readFile,用 Stream 或 read(fd, buffer, offset, length, position) 分段讀。

const fs = require('fs');

// 小文件沒問題(記得先判斷 err,否則文件不存在時 buf 為 undefined)
fs.readFile('small.txt', (err, buf) => {
  if (err) return console.error(err);
  console.log(Buffer.isBuffer(buf)); // true
  console.log(buf.length);            // 字節(jié)數(shù)
});

// 大文件:別這么干,用 createReadStream
// fs.readFile('huge.log', ...);  // 可能 OOM

六、文件描述符:操作系統(tǒng)給你的「取餐號」

文件描述符(file descriptor, fd) 是操作系統(tǒng)里「打開的文件」的整數(shù)句柄。

open 一個文件,內(nèi)核給你一個 fd(比如 3、4、5),后續(xù) read/write 都用這個數(shù)字來指代「哪個打開的文件」。

Node 里:

  • fs.open(path, flags, callback) 會得到 (err, fd)。
  • fs.read(fd, buffer, offset, length, position, callback) 表示:從 fd 對應(yīng)的文件里,從 position 開始,讀 length 字節(jié),放進 bufferoffset 位置,讀完再回調(diào)。
  • 用完后要 fs.close(fd),否則會占用內(nèi)核資源(可打開 fd 數(shù)量有限制)。

用 fd + read 可以自己實現(xiàn)「分段讀大文件」:

const fs = require('fs');

function readInChunks(filePath, chunkSize = 64 * 1024) {
  const buffer = Buffer.alloc(chunkSize);
  let position = 0;

  fs.open(filePath, 'r', (err, fd) => {
    if (err) return console.error(err);
    function readNext() {
      fs.read(fd, buffer, 0, chunkSize, position, (err, bytesRead) => {
        if (err) return fs.close(fd, () => console.error(err));
        if (bytesRead === 0) return fs.close(fd, () => console.log('讀完了'));
        console.log(`讀到 ${bytesRead} 字節(jié),position=${position}`);
        position += bytesRead;
        readNext();
      });
    }
    readNext();
  });
}

readInChunks('./some-big-file.log');

這里就是「底層」用法:自己控 Buffer、position、每次讀多少,不依賴 readFile 一次性裝進內(nèi)存。

七、Stream:別一口吞,一口一口吃

Stream(流) 是「一塊一塊處理數(shù)據(jù)」的抽象:不要求一次性把整個文件讀進內(nèi)存,而是讀一塊、處理一塊、再讀下一塊。

  • fs.createReadStream(path) 會打開文件,并返回一個 Readable 流。
  • 底層一般也是用 fd + 多次 read,每次讀滿一塊 Buffer(默認(rèn) 64KB,可配),通過 data 事件或 read() 推給你。
  • 流內(nèi)部有 highWaterMark:內(nèi)部緩沖超過這個值就暫停從底層拉數(shù)據(jù),避免內(nèi)存爆掉。

所以:大文件用 ReadStream + 管道或逐 chunk 處理,就不會 OOM。

const fs = require('fs');

// 大文件拷貝:流式,內(nèi)存占用穩(wěn)定
function copyBigFile(src, dest) {
  const readStream = fs.createReadStream(src, { highWaterMark: 64 * 1024 });
  const writeStream = fs.createWriteStream(dest, { highWaterMark: 64 * 1024 });
  readStream.pipe(writeStream);
  writeStream.on('finish', () => console.log('拷貝完成'));
}

// 邊讀邊處理:例如數(shù)行數(shù)
let lines = 0;
fs.createReadStream('huge.log')
  .on('data', (chunk) => {
    for (let i = 0; i < chunk.length; i++) if (chunk[i] === 10) lines++;
  })
  .on('end', () => console.log('總行數(shù):', lines));

八、同步 vs 異步:什么時候該用誰?

方式誰在干活阻塞主線程?適用場景
fs.readFile線程池小文件、配置等
fs.readFileSync主線程啟動時讀配置、腳本
createReadStream線程池 + 事件大文件、日志
fs.read(fd, ...)線程池需要精細(xì)控制位置/塊

原則:

  • 能異步就異步,避免阻塞事件循環(huán)。
  • 只有在「進程剛啟動、必須立刻拿到結(jié)果才能往下跑」的場景,才考慮用 Sync(例如讀一個 config.json 再啟動服務(wù))。

九、新特性與最新知識點(Promise、FileHandle、io_uring)

1.fs.promises與 async/await

Node 內(nèi)置了基于 Promise 的 fs API,不用自己包一層:

const fs = require('fs').promises;

async function main() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(data);
    console.log(config);
  } catch (e) {
    console.error(e);
  }
}
main();

底層和回調(diào)版是同一套:都是走 libuv 線程池,只是把 callback 換成了 Promise 的 resolve/reject。

2.FileHandle:長期持有 fd 的「句柄」

fs.promises.open() 返回的是 FileHandle,可以多次讀/寫再關(guān)閉,適合「同一個文件反復(fù)讀」:

const fsp = require('fs').promises;

async function readHeadAndTail(path, headBytes = 100, tailBytes = 100) {
  const handle = await fsp.open(path, 'r');
  const stat = await handle.stat();
  const head = Buffer.alloc(headBytes);
  const tail = Buffer.alloc(tailBytes);
  await handle.read(head, 0, headBytes, 0);
  if (stat.size > tailBytes) {
    await handle.read(tail, 0, tailBytes, stat.size - tailBytes);
  }
  await handle.close();
  return { head: head.toString(), tail: tail.toString() };
}

3. Linux 上的 io_uring(了解即可)

從 libuv 1.45 起,Linux 上部分文件 I/O 曾嘗試用 io_uring 做更高性能的異步磁盤 I/O;后來默認(rèn)又改回線程池。若要用 io_uring,需要在創(chuàng)建 event loop 時顯式開啟(如 UV_LOOP_USE_IO_URING_SQPOLL)。

對寫業(yè)務(wù)代碼的我們來說:知道「文件 I/O 主要走線程池」就夠了,除非你在做極致性能調(diào)優(yōu)。

十、綜合示例:一個「帶流式讀 + 行解析」的日志處理器

下面這段把「底層」和「實用」串起來:用 ReadStream 讀大日志,按行切分、逐行處理(不會把整個文件載入內(nèi)存)。

const fs = require('fs');
const readline = require('readline');

async function processLargeLog(filePath, onLine) {
  const stream = fs.createReadStream(filePath, {
    highWaterMark: 256 * 1024, // 256KB 一塊
  });
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
  for await (const line of rl) {
    await onLine(line); // 你可以在這里做解析、寫庫、發(fā) MQ 等
  }
}

// 使用示例:只打印包含 "ERROR" 的行
processLargeLog('./app.log', async (line) => {
  if (line.includes('ERROR')) console.log(line);
}).then(() => console.log('處理完畢'));

這里用到的就是:fs 的 ReadStream(底層 fd + 分塊 read)+ readline 按行消費,既不會 OOM,又符合「流式」的思維方式。

十一、小結(jié):一張「外賣流程圖」收尾

你調(diào) fs.readFile / createReadStream 等 → Node fs 模塊(JS)

C++ 綁定 調(diào) libuv

libuv 把文件 I/O 丟給 線程池(默認(rèn) 4 個 worker)

線程池里 阻塞式 read內(nèi)核 → 磁盤

讀到的數(shù)據(jù)放進 Buffer,完成后通過 事件循環(huán) 把回調(diào)/Promise 推回 主線程
若是 Stream,則是多次「讀一塊 → 推一塊」,由 highWaterMark 等控制背壓

記住這幾件事:

  1. 單線程指的是 JS,文件 I/O 在 libuv 線程池里。
  2. 大文件用 Stream 或 fd + read,別用 readFile 一把梭。
  3. Buffer 是那塊「裝字節(jié)」的內(nèi)存;fd 是操作系統(tǒng)給你的「取餐號」。
  4. 新代碼優(yōu)先用 fs.promisesFileHandle,邏輯更清晰;底層和回調(diào)版一致。

如果你愿意再往深挖,可以看:

這樣,下次有人問「Node 讀文件到底是同步還是異步」「為什么我讀大文件會崩」,你就能從事件循環(huán)講到線程池、從 Buffer 講到 Stream,順便用「外賣下單 → 后廚線程池 → 取餐號 fd」的比喻把對方講懂。

祝寫 Node 少踩坑,磁盤 I/O 穩(wěn)如狗。

到此這篇關(guān)于一文詳解Node.js操作磁盤文件底層原理與實踐指南的文章就介紹到這了,更多相關(guān)Node.js操作磁盤文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 修改node.js默認(rèn)的npm安裝目錄實例

    修改node.js默認(rèn)的npm安裝目錄實例

    今天小編就為大家分享一篇修改node.js默認(rèn)的npm安裝目錄實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-05-05
  • 命令行批量截圖Node腳本示例代碼

    命令行批量截圖Node腳本示例代碼

    這篇文章主要給大家介紹了關(guān)于命令行批量截圖Node腳本的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-01-01
  • Windows8下搭建Node.js開發(fā)環(huán)境教程

    Windows8下搭建Node.js開發(fā)環(huán)境教程

    這篇文章主要介紹了Windows8下搭建Node.js開發(fā)環(huán)境教程,Win8下安裝node.js也比較簡單,只是一些權(quán)限比較麻煩,需要的朋友可以參考下
    2014-09-09
  • node(koa2) web應(yīng)用模塊介紹詳解

    node(koa2) web應(yīng)用模塊介紹詳解

    這篇文章主要介紹了node(koa2) web應(yīng)用模塊介紹詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • Node.js 進程平滑離場剖析小結(jié)

    Node.js 進程平滑離場剖析小結(jié)

    這篇文章主要介紹了Node.js 進程平滑離場剖析小結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • NodeJS模塊Buffer原理及使用方法解析

    NodeJS模塊Buffer原理及使用方法解析

    這篇文章主要介紹了NodeJS模塊Buffer原理及使用方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-11-11
  • nodejs連接ftp上傳下載實現(xiàn)方法詳解【附:踩坑記錄】

    nodejs連接ftp上傳下載實現(xiàn)方法詳解【附:踩坑記錄】

    這篇文章主要介紹了nodejs連接ftp上傳下載實現(xiàn)方法,結(jié)合實例形式詳細(xì)分析了node.js使用ftp模塊實現(xiàn)針對ftp上傳、下載相關(guān)操作的方法,并附帶記錄了傳輸速度慢的解決方法,需要的朋友可以參考下
    2023-04-04
  • Node.js數(shù)據(jù)庫操作之連接MySQL數(shù)據(jù)庫(一)

    Node.js數(shù)據(jù)庫操作之連接MySQL數(shù)據(jù)庫(一)

    前一陣在做項目的時候,需要通過nodejs連接到MySQL數(shù)據(jù)庫,于是簡單地學(xué)習(xí)了一下MySQL這個庫,分享一些學(xué)習(xí)心得給大家,希望對大家有幫助。下面這篇文章主要介紹了Node.js數(shù)據(jù)庫操作之連接MySQL數(shù)據(jù)庫的相關(guān)資料,需要的朋友可以參考下。
    2017-03-03
  • Koa2框架快速入門與基本使用方式

    Koa2框架快速入門與基本使用方式

    這篇文章主要介紹了Koa2框架快速入門與基本使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • node.js實現(xiàn)的裝飾者模式示例

    node.js實現(xiàn)的裝飾者模式示例

    這篇文章主要介紹了node.js實現(xiàn)的裝飾者模式,簡單說明了裝飾者模式的原理、功能并結(jié)合實例形式給出了node.js裝飾者模式的實現(xiàn)方法,需要的朋友可以參考下
    2017-09-09

最新評論

茂名市| 边坝县| 金山区| 霞浦县| 三原县| 广汉市| 项城市| 确山县| 雷山县| 武定县| 上思县| 安溪县| 南京市| 江永县| 临猗县| 台州市| 宁夏| 荔波县| 平凉市| 宝兴县| 阿拉尔市| 资溪县| 马鞍山市| 万安县| 哈密市| 萨嘎县| 文昌市| 濮阳县| 张家口市| 盐池县| 始兴县| 伊金霍洛旗| 三亚市| 定西市| 肥西县| 呼图壁县| 那曲县| 垣曲县| 南京市| 姜堰市| 南汇区|