All programs, including the current Ruby program, have a process id.
Print it out with:
puts Process.pid
# or
puts $$The process also has a parent process ID:
puts Process.ppidWith this information you can use the OS commands to view the process:
ps -p 87435
#=> PID TTY TIME CMD
#=> 87838 ttys000 0:00.41 pryYou can also get and set the current process's name
puts $PROGRAM_NAME
# => pry
$PROGRAM_NAME = 'foo foo foo'ps -p 87435
#=> PID TTY TIME CMD
#=> 87838 ttys000 0:00.41 foo foo fooIn Unix, files, pipes, and sockets are all considered files and they have descriptors
passwd_file = File.open('/etc/password')
puts passwd_file.fileno
#=> 10When you close the file, the file descriptor number is released to the OS
passwd_file.close #=> releases file descriptor 10Ruby has access to the standard streams:
puts STDIN.fileno
#=> 0
puts STDOUT.fileno
#=> 1
puts STDERR.fileno
#=> 2ENV in Ruby implements Enumerable and some of Hash, but it isn't a hash.
puts ENV['SHELL']
#=> /bin/zsh
puts ENV.has_key?('PATH')
#=> true
puts ENV.is_a?(Hash)
#=> falseEvery process has access to an ARGV array (stands for "argument vector"), which contains arguments passed to the program
Say you have a simple ruby program print_args.rb:
#!/usr/bin/env ruby
puts ARGV$ ruby print_args.rb foo bar -a baz
#=> ["foo", "bar", "-a", "baz"]It's just an array, which you can manipulate and modify as an array. For example:
puts ARGV.include?('-a')
#=> true
puts ARGV.include?('-a') && ARGV[ARGV.index('-a') + 1]
#=> bazUnix processes have exit codes from 0-255. Exit code 0 = success. All other codes are error cases. Exit code 1 = generic error case.
You can exit from a ruby command with
exit # defaults to 0, or success
exit 1 # error caseYou can perform actions on exit
at_exit { puts 'doing some exit stuff' }
exit
#=> doing some exit stuffYou can also force Ruby to exit immediately without calling at_exit with exit!
at_exit { puts 'doing some exit stuff' }
exit!
#=>With abort you can exit with code 1 and optionally pass a message to STDERR:
abortabort 'with a message'
#=> with a message [on stderr]You can also raise an exception, which sets exit code to 1, sends a message to STDERR and outputs a backtrace
raise :hell
#=> raise.rb:2:in `raise': exception class/object expected (TypeError)
#=> from raise.rb:2:in `<main>'A process can fork off a new child process. The process is a copy of the parent process, except:
- The new process has it's own process ID
- The new process has a different parent process ID (the process that just forked it)
- The child process has it's own copy of the parent's file descriptors
Forking utilizes copy-on-write, which means the child process uses all of the same data as the parent, until the data needs to be modified. So, in Ruby, if you create an array in a script and then fork, the sub process will get the same array. But, if you modify the array in any way, the array is copied and becomes distinct from the array in the parent process.
fork utilizes the Unix system call fork(2) under the hood. This is not available on Windows.
View documentation page for fork(2) with:
$ man 2 forkThe fork method executes its code in a child process. And it returns the process ID of the child.
child_pid = fork do
# code executed in child process
endParent processes can exit before child processes. Notice how the child process in the following prints out after the parent exists:
fork do
puts 'I am an orphan'
end
puts 'Parent process died...'
#=> Parent process died...
#=> I am an orphanYou can keep the parent process alive until the child process exits with Process.wait.
fork do
sleep 3
puts 'Orphan exiting'
end
Process.wait
puts 'Parent process exiting'
#=> Orphan exiting
#=> Parent process exitingYou can also tell wait to wait for a specified process ID
favorite_child_id = fork do
sleep 1
puts 'Favorite child exit'
end
second_favorite_child_id = fork do
sleep 2
puts 'Second favorite child exit'
end
Process.wait(favorite_child_id)
puts 'Parent exit'
#=> Favorite child exit
#=> Parent exit
#=> Second favorite child exitYou can also get the status of the exiting sub-process with wait2:
3.times do
fork do
exit(rand(10) % 2 == 0 ? 2 : 1)
end
end
3.times do
pid, status = Process.wait2
puts "Child pid #{pid} exited with status #{status}"
end
#=> Child pid 28253 exited with status pid 28253 exit 2
#=> Child pid 28252 exited with status pid 28252 exit 2
#=> Child pid 28251 exited with status pid 28251 exit 1Zombies are child processes that have exited but the OS is saving information about their status.
When you wait for a process, the process is not a zombie. If you don't want to wait, but you want to "fire and forget", you can call detach:
pid = fork do
puts 'Child exited'
end
Process.detach(pid)
puts 'Parent exited'The ppid of an orphaned process is 1, the master system process init.
The combination of these two is at the heart of process spawning in Unix. You use fork to create a copy of the current process and then exec to transform it.
When you fork, file descriptors are shared with the child process. If you want to communicate between the processes you can create a pipe, which has it's own file descriptors for readers and writers.
require('open3')
reader, writer = IO.pipe
pid = Kernel.fork do
reader.close
std_err_and_out, status = Open3.capture2e('ls', '-l')
writer.puts std_err_and_out
end
writer.close
puts reader.read
Process.wait(pid)Since the reader and writers are copied into the subprocess, we close the reader in the forked process. We're not going to use it. We also close the writer in the parent process since we're not going to use it.
There are a myriad of options when it comes to creating sub processes in Ruby. They all do things slightly differently, but they all use some form of system fork and exec under the hood.
ls_out = `ls -l`Captures and returns stdout so it can be assigned to a variable.
This doesn't work with array syntax, so it's a security risk and best used for quick and dirty system calls)
This replaces the current process with the subprocess.
Kernel.exec('ls', '-l')
puts('after exec')The "after exec" statement never gets sent to stdout because calling exec replaces the parent process before the puts statement can do it's thing.
Executes a command in a sub-shell but doesn't wait for it to return.
It returns the process id.
# this is blocking
pid = Process.spawn('tar xf ruby-2.0.0-p195.tar.bz2')
Process.wait(pid)
# this is non-blocking
pid = Process.spawn('tar xf ruby-2.0.0-p195.tar.bz2')
Process.detach(pid)If you don't either wait or detach you run the risk of creating zombie processes, which can eat up system resources.
You can also pass in a hash of environment variables:
pid = Process.spawn({ 'FOO' => 'bar' }, 'some-cmd-that-requires-foo')
Process.detach(pid)Use Process.spawn if you want to execute another application in a subprocess. Use fork and exec if you want to execute arbitrary Ruby code in a sub process.
Get the output of the command:
r, w = IO.pipe
pid = spawn(command, out: w) # r, w is closed in the child process.
w.closeExecutes a command in a sub-shell but waits for it to execute. Like a blocking version of Process.spawn:
Kernel.system({ 'BUNDLER_EDITOR' => 'vim' }, 'bundle', 'open', 'devise')System also prints out the status of the subprocess, true for success and false for any error code exit.
status = Kernel.system('which', 'ruby') # status = true
#=> /Users/Elliot/.rbenv/versions/2.4.2/bin/ruby
status = Kernel.system('which', 'foo') # status = falseIO.popen runs the specified command as a subprocess and the subprocesses standard input and output will be connected to the IO object.
io_obj = IO.popen(['ls', '-l'])
puts io_obj.readNote that the File descriptor is a stream IO object. When you read, you flush the buffer (is that the correct terminology?) and the content is gone.
You can also do it in one step and achieve something similar to backticks:
header_str = IO.popen(['head', '-n', '1', 'my_data.csv']).readNote that this is better than backticks because the array approach you pass to IO.popen is escaped, which is more secure than the string you pass to backticks.
This also leaves the file handle open, so this might be better:
header_str = IO.popen(['head', '-n', '1', 'my_data.csv']) { |fh| fh.read }or
header_str = IO.popen(['head', '-n', '1', 'my_data.csv'], &:read)