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:
- 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. - Wait for signal sending to complete: The signal handling mechanism ensures that all ongoing signal sending operations are completed.
- 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.