Exploring the /proc filesystem with Python and shell commands
Get the Environment of a Process
Another useful trick is to list the set of environment variables and their values. Files of the form /proc/[pid]/environ
contain the initial environment that was set up when the process with PID [pid]
was started. Caveat: It is a snapshot, so if the process changes some of the environment variables later using the putenv
(C) or os.putenv
(Python) library functions or equivalent, those changes will not be reflected in the file's contents. The entries are of the form var=value
and are terminated by null bytes (\0
). So, to print out the environment of process 1234
, you could enter
$ strings /proc/1234/environ
where strings
is a Linux command that prints only the strings of printable characters in any file (including binary files); for example:
# strings /proc/$$/environ TERM=xterm MAIL=/var/mail/root HOME=/root SHELL=/bin/bash USER=root LOGNAME=root (some lines of output deleted)
(See man strings
on your local Linux system for more on the strings
command.) The get_proc_environ.py
script in Listing 5 gets the environment of a process and is similar to get_proc_cmdline.py
. The format of the data they both retrieve is the same (both use null-terminated strings), the overall program structure is the same, and the output and error messages are similar. The script refers to proc_info.py
(Listing 2), as well as read_proc_environ.py
(Listing 6).
Listing 5
get_proc_environ.py
01 # A program to get the environments of processes, given their PIDs. 02 03 from __future__ import print_function 04 import sys 05 from proc_info import read_proc_environ 06 07 from error_exit import error_exit 08 09 def main(): 10 if len(sys.argv) < 2: 11 error_exit("{}: Error: Need at least one PID to process.\n".format(sys.argv[0])) 12 pids = sys.argv[1:] 13 print("Getting environment for these PIDs:\n{}".format(' '.join(pids))) 14 for pid in pids: 15 proc_filename = "/proc/{}/environ".format(pid) 16 ok, result = read_proc_environ(proc_filename) 17 if ok: 18 sys.stdout.write("PID: {}\nEnvironment:\n{}".format(pid, result)) 19 else: 20 sys.stderr.write("PID: {} Error: {}\n".format(pid, result)) 21 22 if __name__ == '__main__': 23 main()
Listing 6
read_proc_environ.py
01 function 02 import sys 03 04 pid = sys.argv[1] 05 print("Trying to get environment for process with PID:", pid) 06 07 try: 08 #print(open('/proc/{}/cmdline'.format(pid), 'r').read().replace('\0', ' ')) 09 filename = '/proc/{}/environ'.format(pid) 10 fil = open(filename, 'r') 11 env_with_nulls = fil.read() 12 env_with_newlines = env_with_nulls.replace('\0', '\n') 13 print("Process with PID: {} has command-line:\n{}".format(pid, env_with_newlines)) 14 except IOError as ioe: 15 sys.stderr.write("Caught IOError while opening file {}:\n{}\n".format(filename, str(ioe))) 16 except Exception as e: 17 sys.stderr.write("Caught Exception: {}".format(str(e)))
To get the environment of a process, run the get_proc_environ.py
script:
$ python get_proc_environ.py $$ Getting environment for these PIDs: 3807 PID: 3807 Environment: USER=vram HOME=/home/vram MAIL=/var/mail/vram SHELL=/bin/bash TERM=xterm (some lines of output deleted)
Note that the command uses $$
to represent the PID of the current shell instance.
If you try to get the environment for the init
process (PID 1) or other processes that require superuser access, you'll need to use su
:
$ su Password: # python get_proc_environ.py 1 Getting environment for these PIDs: 1 PID: 1 Environment: HOME=/ TERM=linux PATH=/sbin:/usr/sbin:/bin:/usr/bin PWD=/ (some lines of output deleted)
Getting the Process Status
Files of the form /proc/[pid]/status
describe the status of the running process. The Linux ps
command itself uses this file to get process status information.
The status of a process contains some of the same information provided with the ps
command, but in this case, you are getting it programmatically, and you also get other information, such as the username of the user in whose name the process is running.
The /proc/[pid]/status
file for a process contains many field_name: field_value
pairs. Each pair gives information about some aspect of the process.
The following are a few useful pieces of status data:
- the process name (the
Name:
line) - the process ID (
Pid:
) - the parent process ID (
PPid:
) - the process user ID (
Uid:
) - the process group ID (
Gid:
) - the process user name (this field is not in the status information; I get it from the
/etc/passwd
file using the user ID as the key)
The awk script that fetches the status data is shown in Listing 7. The script gets the required fields and their values from any file of the form /proc/[pid]/status
, with the file name given as a command-line argument.
Listing 7
proc_status.awk
01 # proc_status.awk 02 FILENAME != OLDFILENAME {print ""; OLDFILENAME = FILENAME} 03 /^Name:/ {print "Name:", $2} 04 /^State:/ {print "State:", $2} 05 /^Pid:/ {print "Pid:", $2} 06 /^PPid:/ {print "PPid:", $2} 07 /^Uid:/ {print "Uid:", $2} 08 /^Gid:/ {print "Gid:", $2}
The first line in Listing 7 prints a blank line as a separator whenever the input file name changes. FILENAME
is a built-in awk
variable representing the current input file name. OLDFILENAME
is a variable defined by me. Because OLDFILENAME
is compared with FILENAME
, which is a string variable, OLDFILENAME
is also treated as a string variable.
String variables are initialized by default to an empty string (""
), so from the result of the evaluation of the pattern, that first script line prints a blank line in all cases, whether you have one input file name or many. (print ""
prints an empty line.) Also, each time the input file name changes, that first line updates OLDFILENAME
to be equal to the new value of FILENAME
.
I use the status field names as patterns inside slash characters, and the corresponding actions in braces are run for each line in the input (to be shown) where the pattern matches the line. The caret (^
) at the start of each pattern tells awk
to anchor the pattern to the beginning of the line.
The patterns match the desired lines from the input, and the second field ($2
) of each line is printed with an appropriate text label before it.
To get the status information for the current process, the line
$ awk -f proc_status.awk /proc/$$/status
outputs:
Name: bash State: S Pid: 2123 PPid: 2122 Uid: 1002 Gid: 1004
The State field in the output shows the current state of the process. It can have one of the following values: R (running), S (sleeping), D (disk sleep), T (stopped), t(tracing stop), Z (zombie), or X (dead).
The -f
option of awk
says to read the script (to be run) from the file name that follows the option. The next argument to awk
is the file name from which to get the input. In this case, it is the procfs pseudo-file that holds the status information from the current process ($$
).
Adding the username in the output is slightly complex (Listing 8).
Listing 8
proc_status_with_username.awk
01 # proc_status_with_username.awk 02 FILENAME != OLDFILENAME { print ""; OLDFILENAME = FILENAME } 03 /^Name:/ { print "Name:", $2 } 04 /^State:/ { print "State:", $2 } 05 /^Pid:/ { print "Pid:", $2 } 06 /^PPid:/ { print "PPid:", $2 } 07 /^Uid:/ { 08 print "Uid:", $2; 09 uid=$2; 10 printf "Username: " 11 system("awk -F: ' $3 == '"uid"' { print $1; exit } ' /etc/passwd") 12 } 13 /^Gid:/ { print "Gid:", $2 }
The main change between Listing 7 and Listing 8 is the change to the line with the pattern ^Uid:
; instead of printing just "Uid:", $2
, it has a block of four statements between braces (lines 7-12). Those statements are executed instead of the previous single statement whenever the pattern matches.
Awk's -F:
option (line 11) sets the input field delimiter to a different value from the default – here a colon, because that is the delimiter used in the password file.
Those four statements between the braces (in Listing 8) do the following, with the last statement performing a bit of shell quoting magic:
- print the text Uid: and
$2
, the second field (the user ID); - set variable
uid
equal to$2
; - print the text Username: (with a space after) without a newline; hence, the
printf
, notprint
, becauseprintf
does not add a newline unless asked to); - use the
awk
built-in functionsystem
to run another instance ofawk
as a child process; that instance has a pattern that tries to match the third field of the current line of input (the user ID) from the password file with the value of variableuid
, and if it matches, it prints the first field of that line (the user name) and then exits.
The end result, therefore, is a call to an inner awk
script in the middle of the outer awk
script, which fetches the user name and inserts it into the middle of the outer script's output.
Because awk
supports multiple file name arguments (as any well-written Linux filter should), you can run the script in Listing 8 with more than one file name.
Figure 2 shows how to get the status of processes using awk
.
You can also get the status output using a Python program. See the get_proc_status.py
program with the listings for this article at the Linux Magazine website [4].
Conclusion
In this article, I described how to get useful information from the Linux /proc
filesystem using shell commands, Python scripts, and awk. A couple of ideas for further exploration of procfs are:
- Use the files named
/proc/[pid]/io
to get information about I/O being performed by a process (e.g., the progress of a file tree copy). - Get some of the
/proc
filesystem information remotely from another machine using a distributed computing technology, such as HTTP REST calls or XML-RPC.
Exploring the /proc
filesystem will give you a deeper understanding of Linux, and along the way, you'll get some useful practice with scripting and classic command-line tools.
Infos
- Proc Filesystem: https://en.wikipedia.org/wiki/Procfs
- In Unix, everything is a file: https://en.wikipedia.org/wiki/Everything_is_a_file
- man page for the
/proc
file system: http://man7.org/linux/man-pages/man5/proc.5.html - Listings for this article: ftp://ftp.linux-magazine.com/pub/listings/linux-magazine.com/217/
« Previous 1 2
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.