From 1b532ccd037a8fb7dea3a6ebb14f14a70424657f Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Fri, 29 Mar 2024 17:19:43 +0100 Subject: [PATCH] Add parse_proc_pid_stat() We need this to unbreak is_alive() in the zombie main thread case. Relates-to: https://github.com/rfjakob/earlyoom/issues/309 --- go.mod | 5 ++- go.sum | 2 + proc_pid.c | 72 ++++++++++++++++++++++++++++++ proc_pid.h | 14 ++++++ testsuite_c_wrappers.go | 12 +++++ testsuite_unit_test.go | 97 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 proc_pid.c create mode 100644 proc_pid.h diff --git a/go.mod b/go.mod index b0cc0cd..7aba615 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/rfjakob/earlyoom go 1.16 -require golang.org/x/sys v0.6.0 +require ( + github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect + golang.org/x/sys v0.6.0 +) diff --git a/go.sum b/go.sum index 789d7a1..47ef679 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g= +github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/proc_pid.c b/proc_pid.c new file mode 100644 index 0000000..36ce2cb --- /dev/null +++ b/proc_pid.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +#include "globals.h" +#include "msg.h" +#include "proc_pid.h" + +// Parse a buffer that contains the text from /proc/$pid/stat. Example: +// $ cat /proc/self/stat +// 551716 (cat) R 551087 551716 551087 34816 551716 4194304 94 0 0 0 0 0 0 0 20 0 1 0 5017160 227065856 448 18446744073709551615 94898152189952 94898152206609 140721104501216 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 94898152221328 94898152222824 94898185641984 140721104505828 140721104505848 140721104505848 140721104510955 0 +bool parse_proc_pid_stat_buf(pid_stat_t* out, char* buf) +{ + char* closing_bracket = strrchr(buf, ')'); + if (!closing_bracket) { + return false; + } + // If the string ends (i.e. has a null byte) after the closing bracket: bail out. + if (!closing_bracket[1]) { + return false; + } + // Because of the check above, there must be at least one more byte at + // closing_bracket[2] (possibly a null byte, but sscanf will handle that). + char* state_field = &closing_bracket[2]; + int ret = sscanf(state_field, + "%c " // state + "%d %*d %*d %*d %*d " // ppid, pgrp, sid, tty_nr, tty_pgrp + "%*u %*u %*u %*u %*u " // flags, min_flt, cmin_flt, maj_flt, cmaj_flt + "%*u %*u %*u %*u " // utime, stime, cutime, cstime + "%*d %*d " // priority, nice + "%ld ", // num_threads + &out->state, + &out->ppid, + &out->num_threads); + if (ret != 3) { + return false; + }; + return true; +}; + +// Read and parse /proc/$pid/stat. Returns true on success, false on error. +bool parse_proc_pid_stat(pid_stat_t* out, int pid) +{ + // Largest /proc/*/stat file here is 363 bytes acc. to: + // wc -c /proc/*/stat | sort + // 512 seems safe given that we only need the first 20 fields. + char buf[512] = { 0 }; + + // Read /proc/$pid/stat + snprintf(buf, sizeof(buf), "%s/%d/stat", procdir_path, pid); + FILE* f = fopen(buf, "r"); + if (f == NULL) { + // Process is gone - good. + return false; + } + memset(buf, 0, sizeof(buf)); + + // File content looks like this: + // 10751 (cat) R 2663 10751 2663[...] + // File may be bigger than 256 bytes, but we only need the first 20 or so. + int len = (int)fread(buf, 1, sizeof(buf) - 1, f); + bool read_error = ferror(f) || len == 0; + fclose(f); + if (read_error) { + warn("%s: fread failed: %s\n", __func__, strerror(errno)); + return false; + } + // Terminate string at end of data + buf[len] = 0; + return parse_proc_pid_stat_buf(out, buf); +} \ No newline at end of file diff --git a/proc_pid.h b/proc_pid.h new file mode 100644 index 0000000..a14c4ad --- /dev/null +++ b/proc_pid.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef PROC_PID_H +#define PROC_PID_H + +typedef struct { + char state; + int ppid; + long int num_threads; +} pid_stat_t; + +bool parse_proc_pid_stat_buf(pid_stat_t* out, char* buf); +bool parse_proc_pid_stat(pid_stat_t* out, int pid); + +#endif diff --git a/testsuite_c_wrappers.go b/testsuite_c_wrappers.go index 75faa5e..535084a 100644 --- a/testsuite_c_wrappers.go +++ b/testsuite_c_wrappers.go @@ -10,6 +10,7 @@ import ( // #include "kill.h" // #include "msg.h" // #include "globals.h" +// #include "proc_pid.h" import "C" func parse_term_kill_tuple(optarg string, upper_limit int) (error, float64, float64) { @@ -81,3 +82,14 @@ func procdir_path(str string) { cstr := C.CString(str) C.procdir_path = cstr } + +func parse_proc_pid_stat_buf(buf string) (res bool, out C.pid_stat_t) { + cbuf := C.CString(buf) + res = bool(C.parse_proc_pid_stat_buf(&out, cbuf)) + return res, out +} + +func parse_proc_pid_stat(pid int) (res bool, out C.pid_stat_t) { + res = bool(C.parse_proc_pid_stat(&out, C.int(pid))) + return res, out +} diff --git a/testsuite_unit_test.go b/testsuite_unit_test.go index 7e19a4d..bd68026 100644 --- a/testsuite_unit_test.go +++ b/testsuite_unit_test.go @@ -1,12 +1,15 @@ package earlyoom_testsuite import ( + "fmt" "io/ioutil" "os" "strings" "syscall" "testing" "unicode/utf8" + + linuxproc "github.com/c9s/goprocinfo/linux" ) // On Fedora 31 (Linux 5.4), /proc/sys/kernel/pid_max = 4194304. @@ -207,6 +210,87 @@ func Test_get_cmdline(t *testing.T) { } } +func Test_parse_proc_pid_stat_buf(t *testing.T) { + should_error_out := []string{ + "", + "x", + "\000\000\000", + ")", + } + for _, v := range should_error_out { + res, _ := parse_proc_pid_stat_buf(v) + if res { + t.Errorf("Should have errored out at %q", v) + } + } +} + +func Test_parse_proc_pid_stat_Self(t *testing.T) { + pid := os.Getpid() + path := fmt.Sprintf("/proc/%d/stat", pid) + stat, err := linuxproc.ReadProcessStat(path) + if err != nil { + t.Fatal(err) + } + + res, out := parse_proc_pid_stat(pid) + if !res { + t.Fatal(res) + } + if byte(out.state) != stat.State[0] { + t.Error() + } + if int64(out.ppid) != stat.Ppid { + t.Error() + } + if int64(out.num_threads) != stat.NumThreads { + t.Error() + } +} + +func Test_parse_proc_pid_stat_Mock(t *testing.T) { + mockProcdir, err := ioutil.TempDir("", t.Name()) + if err != nil { + t.Fatal(err) + } + procdir_path(mockProcdir) + defer procdir_path("/proc") + + if err := os.Mkdir(mockProcdir+"/100", 0700); err != nil { + t.Fatal(err) + } + + // Real /proc/pid/stat string for gnome-shell + template := "549077 (%s) S 547891 549077 549077 0 -1 4194560 245592 104 342 5 108521 28953 0 1 20 0 23 0 4816953 5260238848 65528 18446744073709551615 94179647238144 94179647245825 140730757359824 0 0 0 0 16781312 17656 0 0 0 17 1 0 0 0 0 0 94179647252976 94179647254904 94179672109056 140730757367876 140730757367897 140730757367897 140730757369827 0" + content := []string{ + fmt.Sprintf(template, "gnome-shell"), + fmt.Sprintf(template, ""), + fmt.Sprintf(template, ": - )"), + fmt.Sprintf(template, "()()()())))(((()))()()"), + fmt.Sprintf(template, " \n\n "), + } + + // Stupid hack to get a C.pid_stat_t + _, want := parse_proc_pid_stat(1) + want.state = 'S' + want.ppid = 547891 + want.num_threads = 23 + + for _, c := range content { + statFile := mockProcdir + "/100/stat" + if err := ioutil.WriteFile(statFile, []byte(c), 0600); err != nil { + t.Fatal(err) + } + res, have := parse_proc_pid_stat(100) + if !res { + t.Error() + } + if have != want { + t.Errorf("have=%v, want=%v for /proc/100/stat=%q", have, want, c) + } + } +} + func Benchmark_parse_meminfo(b *testing.B) { for n := 0; n < b.N; n++ { parse_meminfo() @@ -275,3 +359,16 @@ func Benchmark_get_cmdline(b *testing.B) { } } } + +func Benchmark_parse_proc_pid_stat(b *testing.B) { + pid := os.Getpid() + for n := 0; n < b.N; n++ { + res, out := parse_proc_pid_stat(pid) + if out.num_threads == 0 { + b.Fatalf("no threads???") + } + if !res { + b.Fatal("failed") + } + } +}