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
-
.capture(pid: nil, ppid: nil, memory: Memory.supported?) ⇒ Object
Capture process information from /proc.
-
.read_command(pid, command_fallback) ⇒ Object
Read command line from /proc/[pid]/cmdline; fall back to executable name from stat if empty.
-
.supported? ⇒ Boolean
Whether /proc is available so we can list processes without ps.
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.(Array(pid), hierarchy, pids) if pid General.(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.
22 23 24 |
# File 'lib/process/metrics/general/linux.rb', line 22 def self.supported? File.directory?("/proc") && File.readable?("/proc/self/stat") end |