This article explores basic concepts behind Linux processes and how one can manage them using the
Go programming language.
Linux Processes
Link to heading
As far as Linux is concerned, a process is just a running program. There is a special process called
init which is the first process to start during OS startup.
It continues running until the system shuts down. Init is the “ultimate” parent process for all other
processes and has a PID (process ID) of 1. It is responsible for starting all other processes.
They can start children of their own as well. However, when the parent process exits, it is the init
who becomes the new parent.
There are several popular init systems for Linux such as SysV, SystemD, and Upstart, but they are
out of scope of this article.
Foreground vs Background Processes
Link to heading
Linux, as do many other operating systems, distinguishes between two types of processes:
foreground and background. A foreground process is also called an interactive process. It has to be started
by the user and requires direct user input. Whereas a background process is run independently of the
user and is not attached to a terminal.
Process State
Link to heading
A process can have one of the five possible states at any given time:
- Running/Runnable (‘R’)
- Interruptable Sleep (‘S’)
- Uninterruptable Sleep (‘D’)
- Stopped (‘T’)
- Zombie (‘Z’)
Running/Runnable indicates that the process is either currently running or is ready to be run.
Interruptable sleep indicates that the process is waiting for external resources (i.e.,
data from a file on the disk). However, the process can react to signals while waiting for the resources.
Uninterruptable sleep also denotes the process that is waiting for resources, but such a process
does not react to any signals.
Stopped indicates that the process was put on hold by the SIGSTOP or SIGTSTP signals. Such
a process can be brought back into a Runnable state by sending a SIGCONT signal.
Zombie indicates that the process has exited but its parent hasn’t removed it from the process
list. The parent needs to “reap” the child by waiting on it and reading its exit status. This
prevents the child process from becoming a zombie.
Signals
Link to heading
Linux supports various signals to aid in managing processes.
A signal is a software interrupt sent to a program to announce some kind of event.
It has a name and a number representation. A program will receive a signal and choose what to do with it.
The three ways a process can handle a signal:
- React with a custom action. A program may specify a custom behavior for handling a signal
(i.e., reread a configuration file or terminate itself). - Use the default action. Every signal has an associated default action. It may, however, be an action
to ignore a signal. - Ignore a signal. Although not all signals can be ignored. For example, SIGKILL cannot be ignored.
This table describes some of the common signals:
Number | Name | Default Action | Description |
---|---|---|---|
1 | SIGHUP | Terminate | Hang up controlling terminal or process. Sometimes used as a signal to reread configuration file for the program. |
2 | SIGINT | Terminate | Interrupt from keyboard, Ctrl + C. |
3 | SIGQUIT | Dump | Quit from keyboard, Ctrl + D. |
9 | SIGKILL | Terminate | Forced termination. |
15 | SIGTERM | Terminate | Graceful termination. |
17 | SIGCHLD | Ignore | Child process exited. |
18 | SIGCONT | Continue | Resume process execution. |
19 | SIGSTOP | Stop | Stop process execution, Ctrl + Z. |
You can read more about signals here.
Managing Processes in Go
Link to heading
Relevant Go Packages
Link to heading
Packages os
, os/exec
, and syscall
provide a lot of useful functionality for interacting with an OS
from a Go application. os
provides a platform-independent interface to operating system functionality.
os/exec
allows running external shell commands. syscall
provides an interface to the
low-level OS primitives and allows executing system calls.
The behavior and functionality of these packages are OS-specific. This article
focuses specifically on Linux behavior.
Start Process
Link to heading
Here is a simple example showing how to start an external process from a Go application:
package main
import (
"log"
"os/exec"
)
func main() {
runtimeArgs := []string{"-v", "-f"}
cmd := exec.Command("/usr/bin/myapp", runtimeArgs...)
err := cmd.Start()
if err != nil {
log.Fatalln(err)
}
select {} // block forever for demo purposes
}
runtimeArgs := []string{"-v", "-f"}
defines a list of optional parameters you may wish to pass
to a program.cmd := exec.Command("/usr/bin/myapp", runtimeArgs...)
creates an instance of*exec.Cmd
struct
which represents an external shell command. The first parameter is a path to
executable you wish to invoke, the second is a variadic list of runtime arguments.err := cmd.Start()
executes the specified command but does not wait for it to complete. This line
of code is non-blocking. The caveat is that you should callcmd.Wait()
method at some point to release
the associated system resources. Otherwise, the executed program will become a zombie process once it
exits.
Note: there is also
cmd.Run()
method that can be used instead ofcmd.Start()
. The
difference is thatRun()
actually blocks the code execution and waits for the command to complete
releasing the associtated resources.
Reap Child Process
Link to heading
Expanding on the previous example, here is how to properly wait for the child process in a non-blocking
manner:
package main
import (
"log"
"os/exec"
)
func main() {
runtimeArgs := []string{"-v", "-f"}
cmd := exec.Command("/usr/bin/myapp", runtimeArgs...)
err := cmd.Start()
if err != nil {
log.Fatalln(err)
}
log.Println("started the child process")
go func(cmd *exec.Cmd) {
err := cmd.Wait()
if err != nil {
log.Fatalln(err)
}
log.Println("cleaned up the child process")
}(cmd)
select {} // block forever for demo purposes
}
- This example adds an additional function executed in a separate goroutine that takes
cmd *exec.Cmd
as
a parameter and callscmd.Wait()
on it. - It is important to note that the command had to be started by
cmd.Start()
forcmd.Wait()
to work. cmd.Wait()
is a blocking operation that returnsnil
if the process exited with status 0,
otherwise it returns an error.
Detach Child Process on Parent Exit
Link to heading
Another issue worth considering is what happens to the child process when the parent (the Go application)
exits as a result of SIGINT. By default, the child process will exit as well.
However, in some cases, it may be necessary to keep the child process alive.
This can be achieved by placing the child in a different process group:
package main
import (
"log"
"os/exec"
"syscall"
)
fun