From 75442886c09cf7350ba5578207a8f298407deed1 Mon Sep 17 00:00:00 2001 From: Akizon77 Date: Tue, 2 Dec 2025 16:49:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=86=85=E5=AD=98?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E5=91=BD=E4=BB=A4=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=8E=20/proc/meminfo=20=E8=AF=BB=E5=8F=96=E5=86=85?= =?UTF-8?q?=E5=AD=98=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/checkMem.go | 54 +++++++++ monitoring/unit/mem.go | 232 +++++++++++++++++++++++++++++++++--- monitoring/unit/mem_note.md | 163 +++++++++++++++++++++++++ 3 files changed, 431 insertions(+), 18 deletions(-) create mode 100644 cmd/checkMem.go create mode 100644 monitoring/unit/mem_note.md diff --git a/cmd/checkMem.go b/cmd/checkMem.go new file mode 100644 index 0000000..27a3304 --- /dev/null +++ b/cmd/checkMem.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "log" + + monitoring "github.com/komari-monitor/komari-agent/monitoring/unit" + "github.com/spf13/cobra" +) + +var CheckMemCmd = &cobra.Command{ + Use: "check-mem", + Short: "Check memory usage", + Long: `Check memory usage`, + Run: func(cmd *cobra.Command, args []string) { + log.Println("--- Memory Check ---") + + // Print raw /proc/meminfo if on Linux + if info, err := monitoring.ReadProcMeminfo(); err == nil { + log.Println("--- /proc/meminfo ---") + log.Printf("MemTotal: %d MiB", info.MemTotal/1024/1024) + log.Printf("MemFree: %d MiB", info.MemFree/1024/1024) + log.Printf("MemAvailable: %d MiB", info.MemAvailable/1024/1024) + log.Printf("Buffers: %d MiB", info.Buffers/1024/1024) + log.Printf("Cached: %d MiB", info.Cached/1024/1024) + log.Printf("SwapTotal: %d MiB", info.SwapTotal/1024/1024) + log.Printf("SwapFree: %d MiB", info.SwapFree/1024/1024) + log.Printf("SwapCached: %d MiB", info.SwapCached/1024/1024) + log.Printf("Shmem: %d MiB", info.Shmem/1024/1024) + log.Printf("SReclaimable: %d MiB", info.SReclaimable/1024/1024) + log.Printf("Zswap: %d MiB", info.Zswap/1024/1024) + log.Printf("Zswapped: %d MiB", info.Zswapped/1024/1024) + log.Println("---------------------") + } + + printRamInfo := func(info monitoring.RamInfo) { + log.Printf("[%s] Total: %d bytes (%d MiB), Used: %d bytes (%d MiB)", + info.Mode, + info.Total, info.Total/(1024*1024), + info.Used, info.Used/(1024*1024), + ) + } + + printRamInfo(monitoring.GetMemHtopLike()) + printRamInfo(monitoring.GetMemGopsutil()) + printRamInfo(monitoring.CallFree()) + + log.Println("--- Current Configured ---") + printRamInfo(monitoring.Ram()) + }, +} + +func init() { + RootCmd.AddCommand(CheckMemCmd) +} diff --git a/monitoring/unit/mem.go b/monitoring/unit/mem.go index 49e77cb..b224889 100644 --- a/monitoring/unit/mem.go +++ b/monitoring/unit/mem.go @@ -1,47 +1,243 @@ package monitoring import ( + "bufio" + "bytes" + "os" + "os/exec" "runtime" + "strconv" + "strings" + pkg_flags "github.com/komari-monitor/komari-agent/cmd/flags" "github.com/shirou/gopsutil/v4/mem" ) type RamInfo struct { Total uint64 `json:"total"` Used uint64 `json:"used"` + Mode string } -func Ram() RamInfo { - raminfo := RamInfo{} - v, err := mem.VirtualMemory() +type ProcMemInfo struct { + MemTotal uint64 + MemFree uint64 + MemAvailable uint64 + Buffers uint64 + Cached uint64 + SwapTotal uint64 + SwapFree uint64 + SwapCached uint64 + Shmem uint64 + SReclaimable uint64 + Zswap uint64 + Zswapped uint64 +} + +// readProcMeminfo reads /proc/meminfo and returns a filled ProcMemInfo struct +func ReadProcMeminfo() (*ProcMemInfo, error) { + file, err := os.Open("/proc/meminfo") if err != nil { - raminfo.Total = 0 - raminfo.Used = 0 - return raminfo + return nil, err } - if flags.MemoryIncludeCache { + defer file.Close() + + info := &ProcMemInfo{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + + key := strings.TrimSuffix(parts[0], ":") + valStr := parts[1] + val, err := strconv.ParseUint(valStr, 10, 64) + if err != nil { + continue + } + val *= 1024 // Convert kB to bytes + + switch key { + case "MemTotal": + info.MemTotal = val + case "MemFree": + info.MemFree = val + case "MemAvailable": + info.MemAvailable = val + case "Buffers": + info.Buffers = val + case "Cached": + info.Cached = val + case "SwapTotal": + info.SwapTotal = val + case "SwapFree": + info.SwapFree = val + case "SwapCached": + info.SwapCached = val + case "Shmem": + info.Shmem = val + case "SReclaimable": + info.SReclaimable = val + case "Zswap": + info.Zswap = val + case "Zswapped": + info.Zswapped = val + } + } + return info, scanner.Err() +} + +func GetMemHtopLike() RamInfo { + raminfo := RamInfo{Mode: "htoplike"} + if runtime.GOOS == "linux" { + info, err := ReadProcMeminfo() + if err == nil && info.MemTotal > 0 { + raminfo.Total = info.MemTotal + // htop logic: + // usedDiff = free + cached + sreclaimable + buffers + usedDiff := info.MemFree + info.Cached + info.SReclaimable + info.Buffers + + if info.MemTotal >= usedDiff { + raminfo.Used = info.MemTotal - usedDiff + } else { + raminfo.Used = info.MemTotal - info.MemFree + } + + // Adjust for Zswap + if info.Zswap > 0 || info.Zswapped > 0 { + if raminfo.Used > info.Zswap { + raminfo.Used -= info.Zswap + } else { + raminfo.Used = 0 + } + } + return raminfo + } + } + return raminfo +} + +func GetMemGopsutil() RamInfo { + raminfo := RamInfo{Mode: "gopsutil"} + v, err := mem.VirtualMemory() + if err == nil { raminfo.Total = v.Total - raminfo.Used = v.Total - v.Free + raminfo.Used = v.Total - v.Available + } + return raminfo +} + +// 这我还能干嘛,大伙天天说和free显示不一样,我也没办法 +func CallFree() RamInfo { + raminfo := RamInfo{Mode: "callFree"} + + // Only works on Linux/Unix systems + if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { return raminfo } - raminfo.Total = v.Total - if runtime.GOOS == "windows" || !flags.MemoryReportRawUsed { - raminfo.Used = v.Total - v.Available - } else { - raminfo.Used = v.Total - v.Free - v.Buffers - v.Cached + + // Execute 'free -b' command to get memory in bytes + cmd := exec.Command("free", "-b") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return raminfo + } + + // Parse the output + scanner := bufio.NewScanner(&out) + lineNum := 0 + for scanner.Scan() { + line := scanner.Text() + lineNum++ + + // Skip the header line + if lineNum == 1 { + continue + } + + // Parse the "Mem:" line + if strings.HasPrefix(line, "Mem:") { + fields := strings.Fields(line) + // Format: Mem: total used free shared buff/cache available + if len(fields) >= 3 { + total, err := strconv.ParseUint(fields[1], 10, 64) + if err == nil { + raminfo.Total = total + } + + used, err := strconv.ParseUint(fields[2], 10, 64) + if err == nil { + raminfo.Used = used + } + } + break + } } return raminfo } + +func Ram() RamInfo { + // Use global config + if pkg_flags.GlobalConfig.MemoryIncludeCache { + v, err := mem.VirtualMemory() + if err != nil { + return RamInfo{} + } + return RamInfo{ + Total: v.Total, + Used: v.Total - v.Free, + Mode: "includeCache", + } + } + + if pkg_flags.GlobalConfig.MemoryReportRawUsed { + return GetMemHtopLike() + } + + if runtime.GOOS == "linux" { + h := CallFree() + if h.Total > 0 { + return h + } + h = GetMemHtopLike() + if h.Total > 0 { + return h + } + } + + // Default fallback + return GetMemGopsutil() +} + func Swap() RamInfo { swapinfo := RamInfo{} + + if runtime.GOOS == "linux" { + info, err := ReadProcMeminfo() + if err == nil { + swapinfo.Total = info.SwapTotal + // used = total - free - cached + // Check for underflow + usedDeductions := info.SwapFree + info.SwapCached + if info.SwapTotal >= usedDeductions { + swapinfo.Used = info.SwapTotal - usedDeductions + } else { + swapinfo.Used = info.SwapTotal - info.SwapFree + } + return swapinfo + } + } + s, err := mem.SwapMemory() if err != nil { - swapinfo.Total = 0 - swapinfo.Used = 0 - } else { - swapinfo.Total = s.Total - swapinfo.Used = s.Used + return swapinfo } + swapinfo.Total = s.Total + swapinfo.Used = s.Used return swapinfo } diff --git a/monitoring/unit/mem_note.md b/monitoring/unit/mem_note.md new file mode 100644 index 0000000..782660b --- /dev/null +++ b/monitoring/unit/mem_note.md @@ -0,0 +1,163 @@ +## Htop + +被 Zswap/Zram 占用的物理内存,不应该算作应用程序的内存消耗 +SReclaimable 归为 Cached + +```c +static void LinuxMachine_scanMemoryInfo(LinuxMachine* this) { + Machine* host = &this->super; + memory_t availableMem = 0; + memory_t freeMem = 0; + memory_t totalMem = 0; + memory_t buffersMem = 0; + memory_t cachedMem = 0; + memory_t sharedMem = 0; + memory_t swapTotalMem = 0; + memory_t swapCacheMem = 0; + memory_t swapFreeMem = 0; + memory_t sreclaimableMem = 0; + memory_t zswapCompMem = 0; + memory_t zswapOrigMem = 0; + + FILE* file = fopen(PROCMEMINFOFILE, "r"); + if (!file) + CRT_fatalError("Cannot open " PROCMEMINFOFILE); + + char buffer[128]; + while (fgets(buffer, sizeof(buffer), file)) { + + #define tryRead(label, variable) \ + if (String_startsWith(buffer, label)) { \ + memory_t parsed_; \ + if (sscanf(buffer + strlen(label), "%llu kB", &parsed_) == 1) { \ + (variable) = parsed_; \ + } \ + break; \ + } else (void) 0 /* Require a ";" after the macro use. */ + + switch (buffer[0]) { + case 'M': + tryRead("MemAvailable:", availableMem); + tryRead("MemFree:", freeMem); + tryRead("MemTotal:", totalMem); + break; + case 'B': + tryRead("Buffers:", buffersMem); + break; + case 'C': + tryRead("Cached:", cachedMem); + break; + case 'S': + switch (buffer[1]) { + case 'h': + tryRead("Shmem:", sharedMem); + break; + case 'w': + tryRead("SwapTotal:", swapTotalMem); + tryRead("SwapCached:", swapCacheMem); + tryRead("SwapFree:", swapFreeMem); + break; + case 'R': + tryRead("SReclaimable:", sreclaimableMem); + break; + } + break; + case 'Z': + tryRead("Zswap:", zswapCompMem); + tryRead("Zswapped:", zswapOrigMem); + break; + } + + #undef tryRead + } + + fclose(file); + + /* + * Compute memory partition like procps(free) + * https://gitlab.com/procps-ng/procps/-/blob/master/proc/sysinfo.c + * + * Adjustments: + * - Shmem in part of Cached (see https://lore.kernel.org/patchwork/patch/648763/), + * do not show twice by subtracting from Cached and do not subtract twice from used. + */ + host->totalMem = totalMem; + host->cachedMem = cachedMem + sreclaimableMem - sharedMem; + host->sharedMem = sharedMem; + const memory_t usedDiff = freeMem + cachedMem + sreclaimableMem + buffersMem; + host->usedMem = (totalMem >= usedDiff) ? totalMem - usedDiff : totalMem - freeMem; + host->buffersMem = buffersMem; + host->availableMem = availableMem != 0 ? MINIMUM(availableMem, totalMem) : freeMem; + host->totalSwap = swapTotalMem; + host->usedSwap = swapTotalMem - swapFreeMem - swapCacheMem; + host->cachedSwap = swapCacheMem; + this->zswap.usedZswapComp = zswapCompMem; + this->zswap.usedZswapOrig = zswapOrigMem; +} + + + +static void MemoryMeter_updateValues(Meter* this) { + char* buffer = this->txtBuffer; + size_t size = sizeof(this->txtBuffer); + int written; + + Settings *settings = this->host->settings; + + /* shared, compressed and available memory are not supported on all platforms */ + this->values[MEMORY_METER_SHARED] = NAN; + this->values[MEMORY_METER_COMPRESSED] = NAN; + this->values[MEMORY_METER_AVAILABLE] = NAN; + Platform_setMemoryValues(this); + if ((this->mode == GRAPH_METERMODE || this->mode == BAR_METERMODE) && !settings->showCachedMemory) { + this->values[MEMORY_METER_BUFFERS] = 0; + this->values[MEMORY_METER_CACHE] = 0; + } + /* Do not print available memory in bar mode */ + static_assert(MEMORY_METER_AVAILABLE + 1 == MEMORY_METER_ITEMCOUNT, + "MEMORY_METER_AVAILABLE is not the last item in MemoryMeterValues"); + this->curItems = MEMORY_METER_AVAILABLE; + + /* we actually want to show "used + shared + compressed" */ + double used = this->values[MEMORY_METER_USED]; + if (isPositive(this->values[MEMORY_METER_SHARED])) + used += this->values[MEMORY_METER_SHARED]; + if (isPositive(this->values[MEMORY_METER_COMPRESSED])) + used += this->values[MEMORY_METER_COMPRESSED]; + + written = Meter_humanUnit(buffer, used, size); + METER_BUFFER_CHECK(buffer, size, written); + + METER_BUFFER_APPEND_CHR(buffer, size, '/'); + + Meter_humanUnit(buffer, this->total, size); +} + +void Platform_setMemoryValues(Meter* this) { + const Machine* host = this->host; + const LinuxMachine* lhost = (const LinuxMachine*) host; + + this->total = host->totalMem; + this->values[MEMORY_METER_USED] = host->usedMem; + this->values[MEMORY_METER_SHARED] = host->sharedMem; + this->values[MEMORY_METER_COMPRESSED] = 0; /* compressed */ + this->values[MEMORY_METER_BUFFERS] = host->buffersMem; + this->values[MEMORY_METER_CACHE] = host->cachedMem; + this->values[MEMORY_METER_AVAILABLE] = host->availableMem; + + if (lhost->zfs.enabled != 0 && !Running_containerized) { + // ZFS does not shrink below the value of zfs_arc_min. + unsigned long long int shrinkableSize = 0; + if (lhost->zfs.size > lhost->zfs.min) + shrinkableSize = lhost->zfs.size - lhost->zfs.min; + this->values[MEMORY_METER_USED] -= shrinkableSize; + this->values[MEMORY_METER_CACHE] += shrinkableSize; + this->values[MEMORY_METER_AVAILABLE] += shrinkableSize; + } + + if (lhost->zswap.usedZswapOrig > 0 || lhost->zswap.usedZswapComp > 0) { + this->values[MEMORY_METER_USED] -= lhost->zswap.usedZswapComp; + this->values[MEMORY_METER_COMPRESSED] += lhost->zswap.usedZswapComp; + } +} +```