Module: Process::Metrics::General::Linux

Defined in:
lib/process/metrics/general/linux.rb

Overview

General process information by reading /proc. Used on Linux to avoid spawning ps. We read directly from the kernel (proc(5)) so there is no subprocess and no parsing of external command output; same data source as the kernel uses for process accounting. Parses /proc/[pid]/stat and /proc/[pid]/cmdline for each process.

Constant Summary collapse

CLK_TCK =

Clock ticks per second for /proc stat times (utime, stime, starttime).

Etc.sysconf(Etc::SC_CLK_TCK) rescue 100
PAGE_SIZE =

Page size in bytes for RSS (resident set size is in pages in /proc/pid/stat).

Etc.sysconf(Etc::SC_PAGESIZE) rescue 4096

Class Method Summary collapse

Class Method Details

.capture(pid: nil, ppid: nil, memory: Memory.supported?) ⇒ Object

Capture process information from /proc. If given pid, captures only those process(es). If given ppid, captures that parent and all descendants. Both can be given to capture a process and its children.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/process/metrics/general/linux.rb', line 31

def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
	# When filtering by ppid we need the full process list to build the parent-child tree,
	# so we enumerate all numeric /proc entries; when only pid is set we read just those.
	pids_to_read = if pid && ppid.nil?
		Array(pid)
	else
		Dir.children("/proc").filter{|e| e.match?(/\A\d+\z/)}.map(&:to_i)
	end
	
	uptime_jiffies = nil
	
	processes = {}
	pids_to_read.each do |pid|
		stat_path = "/proc/#{pid}/stat"
		next unless File.readable?(stat_path)
		
		stat_content = File.read(stat_path)
		# comm field can contain spaces and parentheses; find the closing ')' (proc(5)).
		closing_paren_index = stat_content.rindex(")")
		next unless closing_paren_index
		
		executable_name = stat_content[1...closing_paren_index]
		fields = stat_content[(closing_paren_index + 2)..].split(/\s+/)
		# After comm: state(3), ppid(4), pgrp(5), ... utime(14), stime(15), ... starttime(22), vsz(23), rss(24). 0-based: ppid=1, pgrp=2, utime=11, stime=12, starttime=19, vsz=20, rss=21.
		parent_process_id = fields[1].to_i
		process_group_id = fields[2].to_i
		utime = fields[11].to_i
		stime = fields[12].to_i
		starttime = fields[19].to_i
		virtual_size = fields[20].to_i
		resident_pages = fields[21].to_i
		
		# Read /proc/uptime once per capture and reuse for every process (starttime is in jiffies since boot).
		uptime_jiffies ||= begin
			uptime_seconds = File.read("/proc/uptime").split(/\s+/).first.to_f
			(uptime_seconds * CLK_TCK).to_i
		end
		
		processor_time = (utime + stime).to_f / CLK_TCK
		elapsed_time = [(uptime_jiffies - starttime).to_f / CLK_TCK, 0.0].max
		
		command = read_command(pid, executable_name)
		
		processes[pid] = General.new(
			pid,
			parent_process_id,
			process_group_id,
			0.0, # processor_utilization: would need two samples; not available from single stat read
			virtual_size,
			resident_pages * PAGE_SIZE,
			processor_time,
			elapsed_time,
			command,
			nil
		)
	rescue Errno::ENOENT, Errno::ESRCH, Errno::EACCES
		# Process disappeared or we can't read it.
		next
	end
	
	# Restrict to the requested pid/ppid subtree using the same tree logic as the ps backend.
	if ppid
		pids = Set.new
		hierarchy = General.build_tree(processes)
		General.expand_children(Array(pid), hierarchy, pids) if pid
		General.expand_children(Array(ppid), hierarchy, pids)
		processes.select!{|process_id, _| pids.include?(process_id)}
	end
	
	General.capture_memory(processes) if memory
	
	processes
end

.read_command(pid, command_fallback) ⇒ Object

Read command line from /proc/[pid]/cmdline; fall back to executable name from stat if empty. Use binread because cmdline is NUL-separated and may contain non-UTF-8 bytes; we split on NUL and join for display.



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/process/metrics/general/linux.rb', line 107

def self.read_command(pid, command_fallback)
	path = "/proc/#{pid}/cmdline"
	return command_fallback unless File.readable?(path)
	
	cmdline_content = File.binread(path)
	return command_fallback if cmdline_content.empty?
	
	# cmdline is NUL-separated; replace with spaces for display.
	cmdline_content.split("\0").join(" ").strip
rescue Errno::ENOENT, Errno::ESRCH, Errno::EACCES
	command_fallback
end

.supported?Boolean

Whether /proc is available so we can list processes without ps.

Returns:

  • (Boolean)


22
23
24
# File 'lib/process/metrics/general/linux.rb', line 22

def self.supported?
	File.directory?("/proc") && File.readable?("/proc/self/stat")
end