222 lines
6.9 KiB
Python
222 lines
6.9 KiB
Python
"""Linux 系统监控:功耗(RAPL/电池)、CPU、内存、温度、磁盘、网络、进程。"""
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import psutil
|
|
|
|
|
|
# ---------- 功耗 ----------
|
|
|
|
def _read_energy_uj(path: Path) -> int | None:
|
|
try:
|
|
return int(path.read_text().strip())
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def get_power() -> dict:
|
|
"""返回功耗信息,单位瓦(W)。优先电池,其次 Intel RAPL。"""
|
|
result = {"source": None, "total_watts": None, "domains": {}, "battery": None}
|
|
|
|
# 1. 电池功率(笔记本放电/充电)
|
|
for p in Path("/sys/class/power_supply").glob("BAT*/power_now"):
|
|
try:
|
|
# power_now 单位微瓦
|
|
watts = int(p.read_text().strip()) / 1_000_000
|
|
result["battery"] = round(watts, 2)
|
|
result["source"] = "battery"
|
|
result["total_watts"] = round(watts, 2)
|
|
except (OSError, ValueError):
|
|
pass
|
|
|
|
# 2. Intel RAPL energy 差分
|
|
rapl_dir = Path("/sys/class/powercap")
|
|
domains_now = {}
|
|
for i, d in enumerate(sorted(rapl_dir.glob("intel-rapl:*"))):
|
|
name_file = d / "name"
|
|
energy_file = d / "energy_uj"
|
|
if not energy_file.exists():
|
|
continue
|
|
domain = name_file.read_text().strip() if name_file.exists() else f"rapl{i}"
|
|
energy = _read_energy_uj(energy_file)
|
|
if energy is not None:
|
|
domains_now[domain] = (energy, time.monotonic())
|
|
|
|
if domains_now:
|
|
# 用上一次读数差分计算瓦数
|
|
prev = getattr(get_power, "_prev_rapl", None)
|
|
get_power._prev_rapl = domains_now
|
|
if prev:
|
|
for domain, (energy, ts) in domains_now.items():
|
|
if domain in prev:
|
|
p_energy, p_ts = prev[domain]
|
|
dt = ts - p_ts
|
|
if dt > 0:
|
|
delta = energy - p_energy
|
|
if delta < 0: # 计数器回绕
|
|
max_uj = 1 << 32
|
|
delta += max_uj
|
|
watts = delta / 1_000_000 / dt
|
|
result["domains"][domain] = round(watts, 2)
|
|
if result["domains"]:
|
|
result["source"] = result["source"] or "rapl"
|
|
if result["total_watts"] is None:
|
|
# 累加所有域(通常 dom0 已包含子域,取最大 dom0)
|
|
result["total_watts"] = max(result["domains"].values())
|
|
|
|
return result
|
|
|
|
|
|
# ---------- 其他指标 ----------
|
|
|
|
def get_cpu() -> dict:
|
|
load1, load5, load15 = _safe_getloadavg()
|
|
return {
|
|
"percent": psutil.cpu_percent(interval=None),
|
|
"per_core": psutil.cpu_percent(interval=None, percpu=True),
|
|
"load": [load1, load5, load15],
|
|
"cores": psutil.cpu_count(logical=True),
|
|
}
|
|
|
|
|
|
def _safe_getloadavg():
|
|
try:
|
|
return psutil.getloadavg()
|
|
except (AttributeError, OSError): # Windows
|
|
return (0.0, 0.0, 0.0)
|
|
|
|
|
|
def get_memory() -> dict:
|
|
vm = psutil.virtual_memory()
|
|
sw = psutil.swap_memory()
|
|
return {
|
|
"used_gb": round(vm.used / 1024**3, 2),
|
|
"total_gb": round(vm.total / 1024**3, 2),
|
|
"percent": vm.percent,
|
|
"swap_percent": sw.percent,
|
|
}
|
|
|
|
|
|
def get_temps() -> dict:
|
|
try:
|
|
sensors = psutil.sensors_temperatures()
|
|
except (AttributeError, OSError):
|
|
return {}
|
|
out = {}
|
|
for name, entries in sensors.items():
|
|
out[name] = [
|
|
{"label": e.label or f"temp{i}", "current": e.current}
|
|
for i, e in enumerate(entries)
|
|
if e.current is not None
|
|
]
|
|
return out
|
|
|
|
|
|
def get_disk() -> dict:
|
|
out = []
|
|
for part in psutil.disk_partitions():
|
|
if "loop" in part.device or not part.mountpoint:
|
|
continue
|
|
try:
|
|
u = psutil.disk_usage(part.mountpoint)
|
|
out.append({
|
|
"device": part.device,
|
|
"mount": part.mountpoint,
|
|
"used_gb": round(u.used / 1024**3, 1),
|
|
"total_gb": round(u.total / 1024**3, 1),
|
|
"percent": u.percent,
|
|
})
|
|
except (PermissionError, OSError):
|
|
continue
|
|
return out
|
|
|
|
|
|
_net_prev = {"ts": None, "io": None}
|
|
|
|
|
|
def get_net(interval: float = 0.0) -> dict:
|
|
"""网络速率;interval>0 时阻塞采样该秒数,否则用上次调用差分。"""
|
|
global _net_prev
|
|
if interval > 0:
|
|
io0 = psutil.net_io_counters()
|
|
time.sleep(interval)
|
|
io1 = psutil.net_io_counters()
|
|
dt = interval
|
|
else:
|
|
now = time.monotonic()
|
|
io1 = psutil.net_io_counters()
|
|
if _net_prev["ts"] is None:
|
|
_net_prev = {"ts": now, "io": io1}
|
|
return {"up_mbps": None, "down_mbps": None}
|
|
dt = now - _net_prev["ts"]
|
|
io0 = _net_prev["io"]
|
|
_net_prev = {"ts": now, "io": io1}
|
|
if dt <= 0:
|
|
return {"up_mbps": None, "down_mbps": None}
|
|
return {
|
|
"up_mbps": round((io1.bytes_sent - io0.bytes_sent) * 8 / dt / 1e6, 2),
|
|
"down_mbps": round((io1.bytes_recv - io0.bytes_recv) * 8 / dt / 1e6, 2),
|
|
}
|
|
|
|
|
|
_primed = False
|
|
_prime_ts = 0.0
|
|
|
|
|
|
def get_top_processes(n: int = 10) -> list:
|
|
"""按 CPU 占用排序的 TOP N 进程。首次(或距上次超过60秒)需先基准采样。"""
|
|
global _primed, _prime_ts
|
|
if not _primed or time.monotonic() - _prime_ts > 60:
|
|
for p in psutil.process_iter(["pid"]):
|
|
try:
|
|
p.cpu_percent(interval=None)
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
pass
|
|
time.sleep(0.5)
|
|
_primed = True
|
|
_prime_ts = time.monotonic()
|
|
procs = []
|
|
for p in psutil.process_iter(["pid", "name"]):
|
|
try:
|
|
if p.info["pid"] in (0, 4): # Windows 的 Idle/System 不计入排行
|
|
continue
|
|
cpu = p.cpu_percent(interval=None)
|
|
mem = p.memory_info().rss / 1024**2
|
|
procs.append({
|
|
"pid": p.info["pid"],
|
|
"name": (p.info["name"] or "?")[:24],
|
|
"cpu": round(cpu, 1),
|
|
"mem_mb": round(mem, 1),
|
|
})
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
continue
|
|
_prime_ts = time.monotonic()
|
|
procs.sort(key=lambda x: x["cpu"], reverse=True)
|
|
return procs[:n]
|
|
|
|
|
|
def get_uptime() -> float:
|
|
try:
|
|
with open("/proc/uptime") as f:
|
|
return float(f.read().split()[0])
|
|
except OSError:
|
|
return time.time() - psutil.boot_time()
|
|
|
|
|
|
def get_status(top_n: int | None = None) -> dict:
|
|
"""系统总览。top_n 为 None 时不枚举进程(Windows 上较慢)。"""
|
|
data = {
|
|
"power": get_power(),
|
|
"cpu": get_cpu(),
|
|
"memory": get_memory(),
|
|
"temps": get_temps(),
|
|
"disk": get_disk(),
|
|
"net": get_net(),
|
|
"top": [],
|
|
"uptime_s": get_uptime(),
|
|
}
|
|
if top_n is not None:
|
|
data["top"] = get_top_processes(top_n)
|
|
return data
|