Unix Processes
Working with Processes
Primer
System calls
The kernel of your Unix system sits atop the hardware of your computer.
It’s the middle man for any interactions that need to happen with the hardware.
This include things like writing/reading from the filesystem, sending data over the network, allocating memory, or playing audio over the speakers.
Given its power, programs are not allowed direct access to the kernel. Any communication is done via system calls.
System calls are at the heart of C programming.
System calls allow you user-space programs to interact indirectly with the hardware of your computer, via the kernel.
Processes: The atoms of Unix
Processes are the building block of a Unix system. Why? Because any code that executed happens inside a process.
One process can spawn and manage many others.
Processes have IDs
Every process running on your system has a unique process identifier. Hereby referred to as pid
$ ps aux | grep irb
namtx 69633 0.0 0.0 34122844 864 s005 S+ 12:55PM 0:00.01 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox irb
namtx 69625 0.0 0.1 34249756 22564 s002 S+ 12:54PM 0:00.79 irb
Sytem calls
Ruby's Process.pid
maps to getpid(2)
There is alos a global variable that holds the value of the current pid
. You can access it with $$
.
Processes have parents
every process running on your system has a parent process. Each process knows its parent process identifier ppid
.
In the majority of cases the parent process for a given process is the process that invoked it.
$ ps aux | grep 69398
namtx 70430 0.0 0.0 34122844 864 s003 S+ 1:01PM 0:00.01 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox 69398
namtx 69398 0.0 0.1 34191384 8408 s005 Ss 12:54PM 0:02.76 -zsh
In the real world
There aren't a ton of uses for the ppid
in the real world. It can be important when detecting deamon processes.
System calls
Ruby's Process.ppid
maps to getppid(2)
.
Processes Have File Descriptors
Everything is a File
A part of the Unix philosophy: in the land of Unix everything is a file
.
This means that devices a treated as files, sockets and pipes are treated as files. and files are treated as files.
Descriptors represent resources
Anytime that you open a resource in a running process it is assigned a file descriptor number. File descriptors are not shared between unrelated processes, they live and die with the process they are bound to, just as any open resources for a process are close when it exits.
In Ruby, open resources are presented by the IO
class. Any IO
object can have an associated file descriptor number. Use IO[#fileno](/tags/fileno)
to get access to it.
- File descriptor numbers are assigned the lowest unused value.
- Once a resource is closed its file descriptor number becomes available again.
File descriptors keep track of open resources only. Closed resources are not given a file descriptor number.
Trying to read file descriptor number from closed resource will raise ans exception:
passwd = File.open('/etc/passwd')
puts passwd.fileno
3
passwd.close
puts passwd.fileno
-e:4:in `fileno': closed stream (IOError)
You may have notices that when we open a file and ask for its file descriptor number, the lowest value we get is 3. What happend to 0, 1 and 2?
Standard Streams
Every Unix process comes with three open resources. These are your standard input (STDIN
), standard output (STDOUT
) and standard error (STDERR
) resources.
These standard resources exits for a very important reason that we take for granted today. STDIN
provides generic way to read input from keyboard devices or pipes, STDOUT
and STDERR
provide generic way to write output to monitors, files, printers, etc. This was one of innovations of Unix.
In the real world
File descriptors are at the core of network programming using sockets, pipes, etc. and are also at the core of any file system descriptors.
Processes Have Resource Limit
finding the limit
The soft limit isn't really a limit. Meaning that if you exceed the soft limit (in this case by opening more than 256 resouces at once) an exception will be raised, but you can always change that limit if you want.
hard_limit == Process::RLIM_INFINITY
So any process is able to change its own soft limit, but only superuser can change the hard limit.
However, you process is also able to bump the hard limit assuming it has the required permissions.
If you interested in changing the limit at a system-wide level then start by having a look at sysctl(8)
.
Bump the Soft limit
You can see that we set the new limit for the number of open files, and upon asking for that limit again both the hard limit and the soft limit were set to the new value 4-96
.
Exceeding the limit
Process.setrlimit(:NOFILE, 3)
File.open('/dev/null')
# Errno::EMFILE: Too many open files - /dev/null
Other resources
# The maximum number of simultaneous processes
# allowed for the current user.
Process.getrlimit(:NPROC)
# The largest size file that may be created.
Process.getrlimit(:FSIZE)
# The maximum size of the stack segment of the
# process.
Process.getrlimit(:STACK)
In the real world
Needing to modify limits for system resources isn't a common need for most programs.
httperf(1)
is the http performance tool, and it has to change the soft limit when does something like:
httperf --hog --server www --num-conn 5000
Processes Have an Environment
-
environment variables
-
Every process inherits ENVs from its parent. They are set by parent process and inherited by its children processes. Environment variables are per-process and are global to each process.
It's a hash, right?
Although ENV
uses the hash-style accessor API, it's not actually a Hash
. For instance, it implements Enumerable
and some of Hash
API, but not all of it.
System calls
-
setenv(3)
-
getenv(3)
-
environ(7)
Processes Have Arguments
Every process has access to a special array called ARGV
. Other programming languages may implement it slightly differently, but every one has something called argv
.
argv
is a short form for argument vector
. In other words: a vector, or array, of arguments.
It's an Array!
ARGV
is simply an Array
. You can add elements to it, remove elements from it, change the element it contains, whatever you like.
Some libraries will read from ARGV
to parse command line optinos, for example. You can programmatically change ARGV
before they have a chance to see it in order to modify the options at runtime.
In the real world
- file names
- parsing command line input. There are many Ruby libraries for dealing with command line input. One called
optparse
is available as part of the standard library.
Processes Have Names
Unix processes have very few inherent ways of communicating about their state.
Programmers have worked around this and invented things like logfiles
.
Logfiles
allow processes to communicate anything they want about their state by writing to the filesystem, but this operates at the level of the filesystem rather than being inherent to the process itself.
Similarly, processes can use the network to open sockets and communicate with other processes. But again, that operates at a different level than the process itself, since it relies on the network.
There are two mechanisms that operate at the level of process itself that can be used to communicate information. One is the process name, the other is exit codes.
Naming Processes
Every process on the system has a name.
$PROGRAM_NAME
variable in Ruby You can assign a value to ther global variable to change the name of current process.
Processes Have Exit Codes
When a process comes to the end of it has one last chance to make its mark on the world: its exit code
.
Every process that exits does so with a numeric exit code (0-255) denoting whether it exited successfully or with an error.
Traditionally, a process that exits with an exit code 0 is said to be succesful
.
Any other exit code denotes an error, with different codes poiting to different errors.
It's usually a good idea to stick with the 0 as success
exit code tradition so that your program will play nicely with other Unix tools.
How to exit a Process
exit
,Kernel[#exit](/tags/exit)
You can pass a custom exit code to this method
# When Kernel[#exit](/tags/exit) is invoked, before exiting Ruby invokes any blocks
# defined by Kernel[#at_exit](/tags/at_exit).
at_exit { puts 'Last!' }
exit
will output:
exit!
is almost exactly the same as Kernel[#exit](/tags/exit)
, but with two key differences. The first is that it sets unsuccessful status code by default (1), and the second is that it will not invoke any blocks defined using Kernel[#at_exit](/tags/at_exit)
abort
Kernel[#abort](/tags/abort)
provides a generic way to exit a process unsuccessful. Kernel[#abort](/tags/abort)
will set the exit code to 1
for the current process.
# Will exit with exit code 1.
abort
# You can pass a message to Kernel[#abort](/tags/abort). This message will be printed
# to STDERR before the process exits.
abort "Something went horribly wrong."
# Kernel[#at_exit](/tags/at_exit) blocks are invoked when using Kernel[#abort](/tags/abort).
at_exit { puts 'Last!' }
abort "Something went horribly wrong."
Something went horribly wrong.
Last!
raise
A different way to end a process is with an unhandled exception.
Ending process this way still invoke any at_exit
handlers and will print the exception message and backtrace to STDERR
Processes Can Fork
Forking is one of the most powerful concepts in Unix programming.
The fork(2)
system call allows a running process to create new process programmatically. This new process is en exact copy of the original process.
When forking, the process that initiates the fork(2)
is called the parent
, and the newly created process is called the child
.
The child process inherits a copy of all of the memory in use by the parent proces, as well as open file descriptors belongings to the parent process.
Since the child process is an entirely new process, it gets its own unique pid
The parent of the child process is, obviously, its parent process. So its pid
is set to the pid of the processs that initiated the fork(2)
.
The child proces inherits any open file descriptors from the parent process at the time of fork(2)
. It's given the same map of the file descriptor numbers that the parent process has. In this way, the two processes can share open files, sockets, etc.
The child process inherits copy of everything that the parent process has in the main memory. In this way a process could loaded up a large code base, say a Rails app, that occupies 500MB of main mermory. Then this process can fork 2 new child processes. Each of these child processes effectivly have their own copy of that code base loaded in memory.
The call to fork
returns nearly-instantly so we have 3 processes with each using 500MB of memory. Perfect for when you want to have multiple instances of your application loaded in memory at the same time. Because, only once process needs to load the appl and forking is fast, this method is faster than loading the app 3 times in separate instances.
The child processes would be free to modify their copy of the memory without effecting what the parent process has in memory.
if fork
puts "entered the if block"
else
puts "entered the else block"
end
# entered the if block
# entered the else block
One call to ther fork
method actually returns twice. Remember that fork
create a new process. So it returns once in the calling process parent and once in the newly created process child.
puts "parent process pid is #{Process.pid}"
if fork
puts "entered the if block from #{Process.pid}"
else
puts "entered the else block from #{Process.pid}"
end
output:
Now it becomes clear that the code in the if block is being excuted by the parent process, while the code in the else block is being excuted by the child process. Both the child process and the parent process will carry on excuting the code after the if construct.
In the child process fork
return nil
. In the parent process fork
returns the pid if the newly created child process.
Multicore programming?
By making new process it means that your code is able, but not guaranteed, to be distributed accross multiple CPU cores.
However, there's no guarantee that stuff will be happening in parallel. On a busy system it's possible that all 4 of your processes are handled by the same CPU.
fork(2) creates a new process that's a copy of the old process. So if a process is using 500MB of main memory, then it forks, now you have 1GB in main memory.
Do this another ten times and you can quickly exhaust main memory. This is often called a fork bomb. Before you turn up the concurrency make sure that you know the consequences.
Using a Block
In the example above we've demonstrated fork
with an if/else construct. It's also possible, and more common in Ruby code, to use fork
with a block.
When you pass a block to the fork
method that block will be execute in the new child process, while the parent process simply skips over it. The child process exits when it's done excuting the block. It doesn not continue along the same code path as the parent.
for do
# Code here is only executed in the child process
end
# Code here is only executed in the parent process
Orphaned Processes
Abandoned Children
What happens to a child process when its parent dies?
Nothing, that is to say, the OS doesn't treat child processes any differently than any other processes. So, when the parent process dies the child process continues on; the parent process does not take the child down with it.
Managing Orphans
Daemon processes
are long running processes that are intentionally orphaned and meant to stay running forever.- Communicationg with processes that are not attached to a terminal session. You can do this using something
CoW
As metioned, fork(2)
creates a new child process that's an exact copy of the parent process. This includes a copy of everything the parent process has in memory.
Physically copying all of data can be considerable overhead, so modern Unix systems employ something called copy-on-write
sematics (CoW) to combat this.
CoW delays the actual copying on memory until it needs to be written.
So the parent process and child process will actually share the same physical data in memory until one of them need to modify it, at which point the memory will be copied so the proper separation between the two processes can be preserved.
arr = [1,2,3] fork do
# At this point the child process has been initialized.
# Using CoW this process doesn't need to copy the arr variable,
# since it hasn't modified any shared values it can continue reading
# from the same memory location as the parent process.
p arr
end
arr = [1,2,3] fork do
# At this point the child process has been initialized.
# Because of CoW the arr variable hasn't been copied yet.
arr << 4
# The above line of code modifies the array, so a copy of
# the array will need to be made for this process before
# it can modify it. The array in the parent process remains
# unchanged.
end
MRI's garbage collector uses a
mark-and-sweep
algorithm. In a nutshell this means that when the GC is invoked it must traverse the graph of live objects, and for each one the GC mustmark
it as alive.In MRI <= 1.9, this
mark
step was implemented as a modification to that object in memory. So when the GC was invoked right after afork
, all live objects were modified, forcing the OS to make copies of all live Ruby objects and foregoing any benefit from CoW semantics.MRI >= 2.0 still uses a
mark-and-sweep
GC, but preserves CoW semantics by storing all of themarks
in a small data structure in a disparate region of memory. So when the GC runs after afork
, this small region of memory must be copied, but the graph of live Ruby objects can be shared between parent and child until your code modifies an object.
Processes can wait
- fire and forget is useful when you want a child process to handle something asynchrously, but the parent process still has its own work to do.
message = 'Good Morning'
recipient = 'tree@mybackyard.com'
fork do
# In this contrived example the parent process forks a child to take
# care of sending data to the stats collector. Meanwhile the parent
# process has continued on with its work of sending the actual payload.
# The parent process doesn't want to be slowed down with this task, and
# it doesn't matter if this would fail for some reason.
StatsCollector.record message, recipient
end
# send message to recipient
Process.wait
fork do
5.times do
sleep 1
puts "I am an orphan!"
end
end
Process.wait
abort "Parent process died..."
I am an orphan!
I am an orphan!
I am an orphan!
I am an orphan!
I am an orphan!
Parent process died...
Control will not be returned to the terminal until all of the output has been printed.
Process.wait
is a blocking call instructing the parent process to wait for one of its child processes to exit before continuing.
# We create 3 child processes.
3.times do
fork do
# Each one sleeps for a random amount of number less than 5 seconds.
sleep rand(5)
end
end
3.times do
# We wait for each child process to exit and print the pid that
# gets returned.
puts Process.wait
end
Race Conditions
# We create two child processes.
2.times do
fork do
# Both processes exit immediately.
abort "Finished!"
end
end
# The parent process waits for the first process, then sleeps for 5 seconds.
# In the meantime the second child process has exited and is no
# longer running.
puts Process.wait
sleep 5
# The parent process asks to wait once again, and amazingly enough, the second # process' exit information has been queued up and is returned here.
puts Process.wait
The kernel queues up information about exited processes so that parent always receives the information in order that children exited.
In the real World
- babysitting processes
At the core of this pattern is the concept that you have one process that forks serveral child processes, for concurrency, and then spends ti
waitpid(2)
Zoombie Process
fire and forget manner
Kernel queues up information about child processes that have exited. So even if you Process.wait
long after process has exited its information is still available.
The kernel will retain the status of exited child process until the parent process requests that status using Process.wait
.
If the parent never requests the status t hen the kernel will never reap that status information. So creating fire and forget child process without collecting their status information is poor use of kernel resources.