Skip to content

Commit

Permalink
perf(proc): improve stat file parsing
Browse files Browse the repository at this point in the history
Remove the use of library functions to parse the stat file and instead
parse it manually (on the fly) to reduce the number of allocations and
improve performance.

chore(proc): align parsing of stat field with the formats size

This also align parsing sizes with the formats to avoid wrong parsing
of the stat file. The internal fields are represented aligned with the
actual kernel fields to avoid any confusion (signed/unsigned).
  • Loading branch information
geyslan committed Feb 6, 2025
1 parent 2afe594 commit 764ba4a
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 99 deletions.
9 changes: 6 additions & 3 deletions pkg/utils/proc/parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ func parseInt(value string) int {
return val
}

func parseUint64(value string) uint64 {
val, _ := strconv.ParseUint(value, 10, 64)
return val
func ParseInt64(value string) (int64, error) {
return strconv.ParseInt(value, 10, 64)
}

func ParseUint64(value string) (uint64, error) {
return strconv.ParseUint(value, 10, 64)
}

func parseString(value string) string {
Expand Down
265 changes: 175 additions & 90 deletions pkg/utils/proc/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,104 @@ package proc

import (
"bytes"
"fmt"
"os"

"github.com/aquasecurity/tracee/pkg/errfmt"
)

//
// ProcStat
// https://elixir.bootlin.com/linux/v6.11.4/source/fs/proc/array.c#L467
// https://elixir.bootlin.com/linux/v6.13/source/fs/proc/array.c#L589
// https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html
//

// Fields from /proc/<pid>/[task/<tid>/]stat file:
//
// Pid int // process id
// Comm string // the filename of the executable
// State byte // process state
// Ppid int // parent process id
// Pgrp int // process group id
// Session int // session id
// TtyNr int // controlling terminal
// Tpgid int // foreground process group id of the controlling terminal
// Flags uint64 // process flags
// MinFlt uint64 // number of minor faults
// CminFlt uint64 // number of minor faults (all childs)
// MajFlt uint64 // number of major faults
// CmajFlt uint64 // number of major faults (all childs)
// Utime uint64 // user mode jiffies (clock ticks)
// Stime uint64 // kernel mode jiffies (clock ticks)
// Cutime int64 // user mode jiffies (all childs)
// Cstime int64 // kernel mode jiffies (all childs)
// Priority int // process priority
// Nice int // process nice value
// NumThreads int // number of threads in this process
// ItRealValue uint64 // (obsolete, always 0)
// StartTime uint64 // time the process started after system boot (in clock ticks)
// Vsize uint64 // virtual memory size
// Rss uint64 // resident set memory size
// Rsslim uint64 // current limit in bytes on the rss
// Startcode int64 // address above which program text can run
// Endcode int64 // address below which program text can run
// StartStack int64 // address of the start of the main process stack
// Kstkesp int64 // current value of stack pointer
// Kstkeip int64 // current value of instruction pointer
// Signal uint64 // bitmap of pending signals
// Blocked uint64 // bitmap of blocked signals
// SigIgnore uint64 // bitmap of ignored signals
// Sigcatch uint64 // bitmap of catched signals
// Wchan int64 // address of the syscall where process is in sleep mode
// Nswap int64 // number of swapped pages
// Cnswap int64 // cumulative nswap for child processes
// ExitSignal int // signal to be sent to parent when we die
// Processor int // current CPU
// RtPriority int // realtime priority
// Policy int // scheduling policy
// DelayacctBlkioTicks int64 // time spent waiting for block IO
// GuestTime int64 // guest time of the process
// CguestTime int64 // guest time of the process's children
// StartData int64 // address above which program data+bss is placed
// EndData int64 // address below which program data+bss is placed
// StartBrk int64 // address above which program heap can be expanded with brk()
// ArgStart int64 // address above which program command line is placed
// ArgEnd int64 // address below which program command line is placed
// EnvStart int64 // address above which program environment is placed
// EnvEnd int64 // address below which program environment is placed
// ExitCode int // the thread's exit_code in the form reported by the waitpid system call
type StatField byte

// Fields from /proc/<pid>/[task/<tid>/]stat file
const (
// There are signedness discrepancies between the fmt and the kernel C type in some cases, e.g.:
// - StatCutime, StatCstime and StatCguestTime are `int64` in fmt but `u64` in kernel C type.
// - StatRss is `int64` in fmt but `unsigned long` in kernel C type.
// To avoid confusion, the parse type is based on the fmt since it is the representation made
// available in the stat file. A conversion to the actual kernel C type should be done after parsing.
//
// parse type: type to be used to parse the field value.
// fmt: format specifier string specified in stat man page.
// kernel C type: actual type of the field in the kernel.
//
// // parse type // fmt // kernel C type // description
// // ---------- // --- // ------------------ // ----------------------------------------------------------------------
StatPid StatField = iota // int32 %d pid_t (int) process id
StatComm // string %s char[64] the name of the task - up to 64 + 2 for ()
StatState // byte %c char process state
StatPpid // int32 %d pid_t (int) parent process id
StatPgrp // int32 %d pid_t (int) process group id
StatSession // int32 %d pid_t (int) session id
StatTtyNr // int32 %d int controlling terminal
StatTpgid // int32 %d int foreground process group id of the controlling terminal
StatFlags // uint32 %u unsigned int process flags
StatMinFlt // uint64 %lu unsigned long number of minor faults
StatCminFlt // uint64 %lu unsigned long number of minor faults (all childs)
StatMajFlt // uint64 %lu unsigned long number of major faults
StatCmajFlt // uint64 %lu unsigned long number of major faults (all childs)
StatUtime // uint64 %lu u64 user mode jiffies (clock ticks)
StatStime // uint64 %lu u64 kernel mode jiffies (clock ticks)
StatCutime // int64 %ld u64 user mode jiffies (all childs)
StatCstime // int64 %ld u64 kernel mode jiffies (all childs)
StatPriority // int32 %ld int process priority
StatNice // int32 %ld int process nice value
StatNumThreads // int32 %ld int number of threads in this process
StatItRealValue // always 0 (obsolete)
StatStartTime // uint64 %llu unsigned long long time the process started after system boot (in clock ticks)
StatVsize // uint64 %lu unsigned long virtual memory size
StatRss // int64 %ld unsigned long resident set memory size
StatRsslim // uint64 %lu unsigned long current limit in bytes on the rss
StatStartcode // uint64 %lu unsigned long address above which program text can run
StatEndcode // uint64 %lu unsigned long address below which program text can run
StatStartStack // uint64 %lu unsigned long address of the start of the main process stack
StatKstkesp // uint64 %lu unsigned long current value of stack pointer
StatKstkeip // uint64 %lu unsigned long current value of instruction pointer
StatSignal // uint64 %lu unsigned long bitmap of pending signals
StatBlocked // uint64 %lu unsigned long bitmap of blocked signals
StatSigIgnore // uint64 %lu unsigned long bitmap of ignored signals
StatSigcatch // uint64 %lu unsigned long bitmap of catched signals
StatWchan // uint64 %lu unsigned long address of the syscall where process is in sleep mode
StatNswap // always 0 (not maintained)
StatCnswap // always 0 (not maintained)
StatExitSignal // int32 %d int signal to be sent to parent when we die
StatProcessor // uint32 %d unsigned int current CPU
StatRtPriority // uint32 %u unsigned int realtime priority
StatPolicy // uint32 %u unsigned int scheduling policy
StatDelayacctBlkioTicks // uint64 %llu u64 time spent waiting for block IO
StatGuestTime // uint64 %lu u64 guest time of the process
StatCguestTime // int64 %ld u64 guest time of the process's children
StatStartData // uint64 %lu unsigned long address above which program data+bss is placed
StatEndData // uint64 %lu unsigned long address below which program data+bss is placed
StatStartBrk // uint64 %lu unsigned long address above which program heap can be expanded with brk()
StatArgStart // uint64 %lu unsigned long address above which program command line is placed
StatArgEnd // uint64 %lu unsigned long address below which program command line is placed
StatEnvStart // uint64 %lu unsigned long address above which program environment is placed
StatEnvEnd // uint64 %lu unsigned long address below which program environment is placed
StatExitCode // int32 %d int the thread's exit_code in the form reported by the waitpid system call
)

const (
StatNumFields = 52
StatReadFileInitialBufferSize = 256 // greater than average size calculated from 10k stat files
StatLastField = StatExitCode
StatMaxNumFields = StatLastField + 1
StatReadFileInitialBufferSize = 256 // greater than average size (~95) calculated from ~1.4k stat files
)

// ProcStat represents the minimal required fields of the /proc stat file.
type ProcStat struct {
startTime uint64 // time the process started after system boot (in clock ticks)
startTime uint64 // StatStartTime
// rss uint64 // StatRss (parsed as int64)
}

type procStatValueParser func(value string, s *ProcStat)
type procStatValueParser func(value []byte, s *ProcStat)

// procStatValueParserArray maps the index of the field in the stat file to its respective value parser.
// If a parser is nil, the field is ignored on parsing.
var procStatValueParserArray = [StatNumFields]procStatValueParser{
21: parseStartTime, // StartTime
var procStatValueParserArray = [StatMaxNumFields]procStatValueParser{
StatStartTime: parseStartTime, // StartTime
}

// NewThreadProcStat reads the /proc/<pid>/task/<tid>/stat file and parses it into a ProcStat struct.
Expand All @@ -102,49 +120,111 @@ func newProcStat(filePath string) (*ProcStat, error) {
return nil, err
}

// replace spaces in comm with 0x80 (so it can be parsed as a single field).
// after parsing, if comm is required, fix it back to spaces.
commStart := bytes.IndexByte(statBytes, '(')
commEnd := bytes.LastIndexByte(statBytes, ')')
if commStart == -1 || commEnd == -1 {
return nil, fmt.Errorf("comm field not found in proc stat file")
// Fields to parse from the stat file.
// Even though a subset, they must be in the correct order.
fields := []StatField{
StatStartTime,
}
for i := commStart; i <= commEnd; i++ {
if statBytes[i] != ' ' {
continue
}
statBytes[i] = 0x80 // out of ASCII range to avoid conflicts

stat := &ProcStat{}
err = stat.parse(statBytes, fields)
if err != nil {
return nil, err
}

statFields := bytes.Fields(statBytes)
if len(statFields) != StatNumFields {
return nil, fmt.Errorf("unexpected number of fields in proc stat file: %d", len(statFields))
return stat, nil
}

// parse parses the stat file for the required fields filling the ProcStat struct.
// fields can be a subset but must always be ordered as in the StatField enum.
func (s *ProcStat) parse(statBytes []byte, fields []StatField) error {
if len(statBytes) == 0 {
return errfmt.Errorf("empty stat file")
}
if len(fields) == 0 {
return errfmt.Errorf("none stat fields specified")
}

remainingFields := len(procStatValueParserArray)
stat := &ProcStat{}
for i, parseValue := range procStatValueParserArray {
if parseValue == nil {
// skip fields that are not required, see procStatValueParserArray and ProcStat struct
continue
reqFieldIdx := 0
statIdx := 0

var parser procStatValueParser

// handle `Pid` field if requested
pidEnd := bytes.IndexByte(statBytes, ' ')
if pidEnd == -1 {
return errfmt.Errorf("pid field not found in proc stat file")
}
if reqFieldIdx < len(fields) && fields[reqFieldIdx] == StatPid {
parser = procStatValueParserArray[StatPid]
parser(statBytes[:pidEnd], s)
reqFieldIdx++
}
statIdx = pidEnd + 1

// handle `Comm` field if requested
commEnd := bytes.LastIndexByte(statBytes[statIdx:], ')')
if commEnd == -1 {
return errfmt.Errorf("comm field not found in proc stat file")
}
if reqFieldIdx < len(fields) && fields[reqFieldIdx] == StatComm {
commStart := bytes.IndexByte(statBytes[statIdx:], '(')
if commStart == -1 {
return errfmt.Errorf("comm field not found in proc stat file")
}

parseValue(string(statFields[i]), stat)
remainingFields--
if remainingFields == 0 {
break
parser = procStatValueParserArray[StatComm]
parser(statBytes[statIdx+commStart+1:statIdx+commEnd], s)
reqFieldIdx++
}
statIdx += commEnd + 2

// skip to and extract remaining required fields
parsingFieldIdx := 2 // start after `Pid` (0) and `Comm` (1)
for i := statIdx; i < len(statBytes) && reqFieldIdx < len(fields); {
// find the next field boundary (space)
fieldEnd := bytes.IndexByte(statBytes[i:], ' ')
if fieldEnd == -1 {
fieldEnd = len(statBytes)
} else {
fieldEnd += i
}

field := fields[reqFieldIdx]
// check if the requested field matches the current parsing field
if field == StatField(parsingFieldIdx) {
if field == StatLastField {
fieldEnd-- // trim the newline character
}

parser = procStatValueParserArray[field]
parser(statBytes[i:fieldEnd], s)
reqFieldIdx++
}

// move to the next field to parse
parsingFieldIdx++
i = fieldEnd + 1
}

return stat, nil
if reqFieldIdx < len(fields) {
return errfmt.Errorf("some requested fields were not found in the proc stat file")
}

return nil
}

// stat fields parsers

func parseStartTime(value string, s *ProcStat) {
s.startTime = parseUint64(value)
func parseStartTime(value []byte, s *ProcStat) {
s.startTime, _ = ParseUint64(string(value))
}

// func parseRss(value []byte, s *ProcStat) {
// rss, _ := ParseInt64(string(value)) // parse as available in the stat file
// s.rss = uint64(rss)
// }

//
// Public methods
//
Expand All @@ -153,3 +233,8 @@ func parseStartTime(value string, s *ProcStat) {
func (s *ProcStat) GetStartTime() uint64 {
return s.startTime
}

// // GetRss returns the resident set memory size.
// func (s *ProcStat) GetRss() uint64 {
// return s.rss
// }
2 changes: 1 addition & 1 deletion pkg/utils/proc/stat_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func Benchmark_newProcStat(b *testing.B) {
file := tests.CreateTempFile(b, content)
file := tests.CreateTempFile(b, statContent)
defer os.Remove(file.Name())

b.ResetTimer()
Expand Down
17 changes: 12 additions & 5 deletions pkg/utils/proc/stat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const (
maxProcStatLength = 8
)

// TestProcStat_PrintSizes prints the sizes of the structs used in the ProcStat type.
// Run it as DEBUG test to see the output.
func TestProcStat_PrintSizes(t *testing.T) {
procStat := ProcStat{}
tests.PrintStructSizes(t, os.Stdout, procStat)
}

func TestProcStatSize(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -45,14 +52,14 @@ func TestProcStatSize(t *testing.T) {
}
}

var content = "3529367 (Isolated (((Web))) Co) S 3437358 3433422 3433422 0 -1 4194560 " +
var statContent = "3529367 (Isolated (((Web))) Co) S 3437358 3433422 3433422 0 -1 4194560 " +
"50679 0 0 0 566 643 0 0 20 0 29 0 46236871 2609160192 33222 " +
"18446744073709551615 94165013317536 94165014109840 140730010890672 " +
"0 0 0 0 16846850 1082134264 0 0 0 17 29 0 0 0 0 0 94165014122560 " +
"94165014122664 94165887094784 140730010895394 140730010895699 " +
"140730010895699 140730010898399 0\n"
"140730010895699 140730010898399 -1\n"

func TestProcStatParsing(t *testing.T) {
func Test_newProcStat(t *testing.T) {
t.Parallel()

tt := []struct {
Expand All @@ -71,12 +78,12 @@ func TestProcStatParsing(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

file := tests.CreateTempFile(t, content)
file := tests.CreateTempFile(t, statContent)
defer os.Remove(file.Name())

result, err := newProcStat(file.Name())
if err != nil {
t.Fatalf("Error parsing the proc stat: %v", err)
t.Fatalf("Error creating new ProcStat: %v", err)
}

if !cmp.Equal(*result, tc.expected, cmp.AllowUnexported(ProcStat{})) {
Expand Down

0 comments on commit 764ba4a

Please sign in to comment.