This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 进程监控指标采集(仅支持 Linux)
|
||||
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
|
||||
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
|
||||
* - 句柄数:/proc/<pid>/fd 目录条目数
|
||||
* - 端口 -> pid:解析 /proc/net/tcp 与 /proc/net/tcp6,结合 /proc/<pid>/fd 的 socket inode
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
function isLinux() {
|
||||
return os.type() === "Linux";
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function readPidTicks(pid) {
|
||||
const txt = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
// 字段 14=utime 15=stime,comm 可能含空格,按最后一个 ) 截断
|
||||
const rest = txt.slice(txt.lastIndexOf(")") + 1).trim().split(/\s+/);
|
||||
return Number(rest[11]) + Number(rest[12]);
|
||||
}
|
||||
|
||||
function readCpuTotal() {
|
||||
const line = fs.readFileSync("/proc/stat", "utf8").split("\n")[0];
|
||||
// user nice system idle iowait irq softirq steal(guest 已计入 user)
|
||||
const parts = line.trim().split(/\s+/).slice(1, 9).map(Number);
|
||||
let total = 0;
|
||||
for (const v of parts) {
|
||||
total += Number.isFinite(v) ? v : 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function readMemMB(pid) {
|
||||
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
||||
const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
|
||||
if (!m) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(Number(m[1]) / 1024 * 100) / 100;
|
||||
}
|
||||
|
||||
function readHandleCount(pid) {
|
||||
return fs.readdirSync(`/proc/${pid}/fd`, { withFileTypes: true }).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 采集单个进程的监控数据
|
||||
* @param {number} pid
|
||||
* @returns {Promise<{pid:number,cpu:number,mem:number,handle:number}|null>} 进程不存在/不可读返回 null
|
||||
*/
|
||||
async function collectPid(pid) {
|
||||
try {
|
||||
const beforePid = readPidTicks(pid);
|
||||
const beforeTotal = readCpuTotal();
|
||||
await sleep(1000);
|
||||
const afterPid = readPidTicks(pid);
|
||||
const afterTotal = readCpuTotal();
|
||||
|
||||
const totalDelta = afterTotal - beforeTotal;
|
||||
const pidDelta = afterPid - beforePid;
|
||||
const cpu = totalDelta > 0 ? Math.round(pidDelta / totalDelta * 10000) / 100 : 0;
|
||||
|
||||
const mem = readMemMB(pid);
|
||||
const handle = readHandleCount(pid);
|
||||
return { pid, cpu, mem, handle };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据端口查找监听进程 pid
|
||||
* @param {number} port
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function findPortPid(port) {
|
||||
const targetInodes = new Set();
|
||||
|
||||
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(file, "utf8");
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
const lines = text.split("\n").slice(1);
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 10) {
|
||||
continue;
|
||||
}
|
||||
// st == 0A 表示 LISTEN
|
||||
if (parts[3] !== "0A") {
|
||||
continue;
|
||||
}
|
||||
const local = parts[1];
|
||||
const idx = local.lastIndexOf(":");
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
const realPort = parseInt(local.slice(idx + 1), 16);
|
||||
if (Number.isFinite(realPort) && realPort === Number(port)) {
|
||||
targetInodes.add(parts[9]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetInodes.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync("/proc");
|
||||
for (const entry of entries) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
const fdDir = `/proc/${entry}/fd`;
|
||||
let names;
|
||||
try {
|
||||
names = fs.readdirSync(fdDir);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
for (const fd of names) {
|
||||
try {
|
||||
const target = fs.readlinkSync(path.join(fdDir, fd));
|
||||
const m = target.match(/^socket:\[(\d+)\]$/);
|
||||
if (m && targetInodes.has(m[1])) {
|
||||
return Number(entry);
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略无法读取的 fd
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isLinux,
|
||||
collectPid,
|
||||
findPortPid
|
||||
};
|
||||
Reference in New Issue
Block a user