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
BOOT_TIME =

The Unix timestamp when the system booted.

if File.readable?("/proc/stat")
	File.foreach("/proc/stat") do |line|
		break line.split.last.to_f if line.start_with?("btime ")
	end
end

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.



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
104
105
106
107
108
109
# File 'lib/process/metrics/general/linux.rb', line 38

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
		start_time = 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 - start_time).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,
			BOOT_TIME + start_time.to_f / CLK_TCK,
			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.



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/process/metrics/general/linux.rb', line 113

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)


29
30
31
# File 'lib/process/metrics/general/linux.rb', line 29

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