Skip to content

Commit

Permalink
Add parse_proc_pid_stat()
Browse files Browse the repository at this point in the history
We need this to unbreak is_alive() in the zombie main thread case.

Relates-to: #309
  • Loading branch information
rfjakob committed Mar 29, 2024
1 parent d4f8c74 commit 1b532cc
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 1 deletion.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
72 changes: 72 additions & 0 deletions proc_pid.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

#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);
}
14 changes: 14 additions & 0 deletions proc_pid.h
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions testsuite_c_wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
97 changes: 97 additions & 0 deletions testsuite_unit_test.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
}
}
}

0 comments on commit 1b532cc

Please sign in to comment.