System Programming in Go – 3

System Programming Part 3

Hello, Gophers. How are things? Welcome to the third part of the System Programming in Go series. If you haven’t checked out the previous two, here they are:

  1. System programming in Go – 1
  2. System programming in Go – 2

As a quick refresher, previously we had looked into file-system operations, and then I/O operations. In this section, we’ll cover the manipulation of processes and signals. We want to create applications that can handle the Unix signals that can be caught and handled. Go offers the os/signal package for dealing with signals, which uses Go channels.

So let’s get on with it !

Signals and Processes

We can walk through a simple understanding of processes and signals. A process refers to a program in execution; it’s a running instance of a program. It is made up of the program instruction, data read from files, other programs or input from a system user.

There are two types of processes in Linux:

  • Foreground processes (also referred to as interactive processes) – these are initialised and controlled through a terminal session.
  • Background processes (also referred to as non-interactive/automatic processes) – are processes not connected to a terminal; they don’t expect any user input.

Daemons are special types of background processes that start at system startup and keep running forever as a service; they don’t die. They are started as system tasks (run as services).

Signals are software interrupts sent to a program to indicate that an important event has occurred. The events can vary from user requests to illegal memory access errors. Some signals, such as the interrupt signal, indicate that a user has asked the program to do something that is not in the usual flow of control.

There is an easy way to list down all the signals supported by your system.

Just issue the kill -l command and it would display all the supported signals:

Signals Linux Cmd Kill
Signals Linux Cmd Kill

Every signal has a default action associated with it, like:

  • Terminate the process.
  • Ignore the signal.
  • Dump core. This creates a file called core containing the memory image of the process when it received the signal.
  • Stop the process.
  • Continue a stopped process

1. List all Processes in Go

We can start this with an easy piece of programming. Let’s create a Go program to list all the processes on the system:

package main

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	PS, err := exec.LookPath("ps")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(PS)
	command := []string{"ps", "-a", "-x"}
	env := os.Environ()
	err = syscall.Exec(PS, command, env)
}
Process List Go System Programming
Process List Go System Programming

If we change the argument from “ps” to “ls“, we see that the output is the same as if we run the ls command from terminal:

Ls Command With Go
Ls Command With Go

2. Manipulating Signals in Go

A program cannot handle all signals; some of them are non-catchable and non-ignorable. The SIGKILL and SIGSTOP signals cannot be caught, blocked, or ignored. The reason for this is that they provide the kernel and the root user a way of stopping any process.

Let’s create a Go application that can handle three different signals: SIGTERM , SIGINT , and SIGHUP.

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

First, we create a function that will be used to “handle” our signal. We’re just printing it, because we currently don’t have any use for these.

func handleSignal(signal os.Signal) {
	fmt.Println("* Got:", signal)
}

The main function is defined below. We use goroutines and channels for concurrency:

func main() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
	go func() {
		for {
			sig := <-sigs
			switch sig {
			case os.Interrupt:
				handleSignal(sig)
			case syscall.SIGTERM:
				handleSignal(sig)
			case syscall.SIGHUP:
				fmt.Println("Got:", sig)
				os.Exit(-1)
			}
		}
	}()
	for {
		fmt.Printf(".")
		time.Sleep(10 * time.Second)
	}
}
  • We create a signal channel using the os package, and then use the method Notify, which causes package signal to relay incoming signals to channel. If no signals are provided, all incoming signals will be relayed.
  • We use a switch, to define cases for each of the signals.
  • Then we use an infinite loop to basically run that app until interrupted.
Three Signals Handling In Golang
Three Signals Handling In Golang

This is the basic pattern for all signal handling that we’ll be doing. With that, we are at the end of our System Programming series. Pat yourself on the back if you’ve been with me so far. However, there’s a bunch of conceptual questions that I would like to clarify that are commonly asked by programmers.

So you can find that in the final part : System Programming in Go – Important Questions

References

  1. Hands-On System Programming with Go: Build modern and concurrent applications for Unix and Linux systems using Golang, by Alex Guerrieri
  2. Go Systems Programming: Master Linux and Unix system level programming with Go, by Mihalis Tsoukalos
  3. https://pkg.go.dev/os