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

一文詳解Node.js操作磁盤(pán)文件底層原理與實(shí)踐指南

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

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

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

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

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

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

(補(bǔ)充一句:文件 I/O 本身是等磁盤(pán),不會(huì)占滿(mǎn) CPU;你覺(jué)得「卡」多半是線(xiàn)程池被占滿(mǎn),新任務(wù)在排隊(duì)。)

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

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

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

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

你寫(xiě)的可能是:

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

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

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

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

這里會(huì):

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

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

對(duì)文件 I/O,它一般不會(huì)用 epoll/kqueue 這種「純事件」機(jī)制,而是:

把實(shí)際讀文件的工作丟進(jìn)「線(xiàn)程池」,在池里某條線(xiàn)程里做阻塞式的 read。

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

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

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

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

所以「Node 單線(xiàn)程」指的是 JS 只在一個(gè)線(xiàn)程跑,磁盤(pán) I/O 并不在主線(xiàn)程上阻塞,而是在線(xiàn)程池里。

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

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

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

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

所以:

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

這就是為什么:

  • 你寫(xiě) fs.readFileSync 時(shí),主線(xiàn)程會(huì)阻塞(因?yàn)橥?API 就是在主線(xiàn)程上直接調(diào)系統(tǒng)調(diào)用讀文件);
  • fs.readFile 不會(huì)阻塞主線(xiàn)程,因?yàn)樽x是在線(xiàn)程池里做的。

四、線(xiàn)程池:別被「單線(xiàn)程」三個(gè)字騙了

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

所以:

  • 同時(shí)發(fā) 10 個(gè) fs.readFile,只有 4 個(gè)在「真·讀磁盤(pán)」,剩下 6 個(gè)在排隊(duì)。
  • 線(xiàn)程池既管文件 I/O,也管部分 crypto、部分 DNS 等,所以文件多的時(shí)候你會(huì)感覺(jué)「怎么慢下來(lái)了」——因?yàn)槌刈颖徽紳M(mǎn)了。

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

# 例如把線(xiàn)程池改成 8
set UV_THREADPOOL_SIZE=8   # Windows
export UV_THREADPOOL_SIZE=8  # Linux/macOS
// 你可以自己試:同時(shí)讀多個(gè)文件,看完成順序
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} 個(gè)完成: ${f}`));
});
// 前 4 個(gè)往往先完成(線(xiàn)程池只有 4),第 5 個(gè)要等池里有空位

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

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

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

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

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

const fs = require('fs');

// 小文件沒(méi)問(wèn)題(記得先判斷 err,否則文件不存在時(shí) 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)給你的「取餐號(hào)」

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

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

Node 里:

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

用 fd + read 可以自己實(shí)現(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、每次讀多少,不依賴(lài) readFile 一次性裝進(jìn)內(nèi)存。

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

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

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

所以:大文件用 ReadStream + 管道或逐 chunk 處理,就不會(huì) 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 異步:什么時(shí)候該用誰(shuí)?

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

原則:

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

九、新特性與最新知識(shí)點(diǎn)(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 線(xiàn)程池,只是把 callback 換成了 Promise 的 resolve/reject。

2.FileHandle:長(zhǎng)期持有 fd 的「句柄」

fs.promises.open() 返回的是 FileHandle,可以多次讀/寫(xiě)再關(guān)閉,適合「同一個(gè)文件反復(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 做更高性能的異步磁盤(pán) I/O;后來(lái)默認(rèn)又改回線(xiàn)程池。若要用 io_uring,需要在創(chuàng)建 event loop 時(shí)顯式開(kāi)啟(如 UV_LOOP_USE_IO_URING_SQPOLL)。

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

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

下面這段把「底層」和「實(shí)用」串起來(lái):用 ReadStream 讀大日志,按行切分、逐行處理(不會(huì)把整個(gè)文件載入內(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); // 你可以在這里做解析、寫(xiě)庫(kù)、發(fā) MQ 等
  }
}

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

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

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

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

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

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

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

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

記住這幾件事:

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

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

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

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

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

相關(guān)文章

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

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

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

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

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

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

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

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

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

    Node.js 進(jìn)程平滑離場(chǎng)剖析小結(jié)

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

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

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

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

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

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

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

    Koa2框架快速入門(mén)與基本使用方式

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

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

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

最新評(píng)論

正镶白旗| 嘉义县| 阜新市| 攀枝花市| 富川| 普宁市| 长海县| 垫江县| 阿拉善右旗| 法库县| 青神县| 车险| 民县| 贵定县| 高碑店市| 信宜市| 安泽县| 威海市| 库车县| 海安县| 改则县| 偏关县| 韩城市| 兰西县| 嵊州市| 都匀市| 阿克陶县| 仙游县| 西畴县| 安西县| 桐城市| 类乌齐县| 昭通市| 弥渡县| 石渠县| 巴南区| 儋州市| 高青县| 旌德县| 宁德市| 承德市|