banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

Signal Handling in Golang

1. What is a Signal#

Signal is a method used for inter-process communication in an OS. For Linux systems, signals are soft interrupts used to notify a process that a certain event has occurred, typically used to interrupt the normal execution flow of a process to handle specific events or exceptional situations.

The definition of signals may vary across different platforms, with each signal corresponding to different values, actions, and descriptions. In Linux systems, we can use man signal to view the corresponding signal descriptions.

Here is a reference for POSIX signals, which readers can check themselves. A signal may correspond to multiple values because these signal values are platform-dependent.

In the default behavior of signals, Term indicates that the default action is to terminate the process, Ign indicates that the default action is to ignore the signal, Core indicates that the default action is to terminate the process and output a core dump, and Stop indicates that the default action is to stop the process.

Finally, the SIGKILL and SIGSTOP signals cannot be caught by applications, nor can they be blocked or ignored by the operating system.

2. Signal Handling in Golang#

In Golang, there is a corresponding signal handling package: os/signal, primarily using the following two methods:

  • The signal.Notify() method is used to listen for signals.
  • The signal.Stop() method is used to cancel listening.

2.1 signal.Notify Method#

Function Signature: func Notify(c chan <- os.Signal, sig ... os.Signal)

The second parameter of this method is a variadic list, allowing multiple signals to be specified for listening. These signals will be forwarded to the channel passed as the first parameter. If no signals are specified, all signals will be forwarded.

This channel c should be non-blocking; the signal package will not block to send information to c (if it blocks, the signal package will simply abandon it). Generally, if using a single signal notification, a capacity of 1 is sufficient.

Below is a simple example of how to capture signals:

package main

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

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		ch := make(chan os.Signal, 1)
		signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP)
		for {
			s := <-ch
			switch s {
			case syscall.SIGINT:
				fmt.Println("\nSIGINT!")
				return
			case syscall.SIGQUIT:
				fmt.Println("\nSIGQUIT!")
				return
			case syscall.SIGTERM:
				fmt.Println("\nSIGTERM: Elegant exit")
				return
			case syscall.SIGHUP:
				fmt.Println("\nSIGHUP: Terminal connection disconnected")
				return
			default:
				fmt.Println("\nUnknown Signal!")
				return
			}
		}
	}()
	wg.Wait()
}

After running this program, we can use keyboard shortcuts or the kill command to send corresponding signals to this process.

An example of usage is that we can listen for termination signals to complete a graceful exit:

package main

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

// Detect signals
func listenSignal(file *os.File) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	for {
		select {
		case sig := <-c:
			fmt.Printf("Received signal %s, will exit\n", sig)
			file.Close()
			os.Exit(0)
		}
	}
}

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("Unable to open file:", err)
		return
	}
	defer file.Close()

	go listenSignal(file)

	fmt.Println("Program is running, press control+c to exit")
	select {} // Block main thread
}

2.2 signal.Stop Method#

Function Signature: func Stop(c chan<- os.Signal)

The signal.Stop method cancels the channel's signal listening behavior:

  • After calling signal.Stop(c), it will stop forwarding any signals to channel c.
  • The parameter c is a send-only signal channel.
  • The stopped channel can call the Notify method again to listen for signals.

The following example demonstrates the process of a channel calling the Stop method to stop listening and then re-listening:

package main

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

func main() {
	c := make(chan os.Signal, 1)

	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)

	// Start a goroutine to handle signals
	go func() {
		for sig := range c {
			fmt.Printf("Received signal: %s\n", sig)
		}
	}()

	// Simulate program running for a while
	fmt.Println("Listening for signals...")
	time.Sleep(5 * time.Second)

	// Stop signal notifications
	signal.Stop(c)
	fmt.Println("Signal notifications stopped")

	// Simulate program running for a while, during which no signals will be received
	time.Sleep(5 * time.Second)

	// Re-register signal notifications
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	fmt.Println("Restarted listening for signals...")

	// Simulate program running for a while again
	time.Sleep(10 * time.Second)
}

Additionally, to avoid data races and ensure the correctness of signal handling, the signal.Stop method internally maintains a map that maps signal types to channel lists. When a signal arrives, it sends the signal to all corresponding channels. When deleting, it needs to remove the corresponding channel from this map, but direct deletion may lead to data races, especially when there are concurrent operations involving signal handling and channel deletion.

To solve this problem, it does the following:

  1. Temporarily store signals to be stopped: When the Stop method is called, the signal handling mechanism stores the channels that are about to be stopped in a temporary list ([]stopping) instead of immediately deleting them from the map.
  2. Wait for signal sending to complete: The signal handling mechanism ensures that all ongoing signal sending operations are completed.
  3. Finally remove the signal channel: Once all signal operations are completed, the signal handling mechanism formally removes the communication signal channel from the map, ensuring that no signals will be sent to the stopped channel.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.