Deep understanding of Linux kernel-signals (Reading Notes)
Chapter 4 signal
Signals are used for communication between user-mode processes. The kernel also uses signals to notify the process system.
1. Functions of Signals
A signal is a short message that can be sent to a process or a group of processes. The unique information sent to the process is usually a number to identify the signal.
Two main purposes of using signals:
Let the process know that a specific event has occurred.
Force the process to execute the signal processing program in its own code.
Of course, these two purposes are not mutually exclusive, because the process often responds to an event by executing a specific routine.
Regular signal: First 31
Real-time signal: 32-64
Real-time signals are very different from conventional signals because they must be queued so that multiple signals can be received. On the other hand, regular signals of the same type are not queued; if a regular signal is sent multiple times consecutively, only one of them is sent to the receiving process. Although the Linux kernel does not use real-time signals, it fully implements the POSIX standard through several specific system calls.
Many system calls allow programmers to send signals and determine how their processes respond to received signals.
An important feature of signals is that they can be sent to processes in unpredictable states at any time. Signals sent to non-running processes must be saved by the kernel until the process resumes execution.
Blocking a signal requires a delay in the transmission of the signal until it is removed, which makes it more serious to transmit the signal after a period of time.
The kernel distinguishes two different phases of signal transmission:
Signal generation:
The Kernel updates the data structure of the target process to indicate that a new signal has been sent.
Signal transmission:
The kernel forces the target process to respond to signals by changing the execution status of the target process, or starting to execute a specific signal processing program, or both.
Each generated signal is transmitted at most once. Signals are consumable resources: once they are passed out, all information about the signal in the process descriptor is canceled.
A signal that has been generated but has not yet been transmitted is called a pendingsignal ). At any time, a process only has one pending signal of the given type. Other signals of the same process type are not queued and simply discarded. However, real-time signals are different: there may be several pending signals of the same type.
The signal can retain unpredictable suspension times. The following factors must be taken into account:
The signal is usually transmitted only by the currently running process.
Signals of a given type can be selectively blocked by processes.
When a process executes a function of a signal processing program, it usually blocks the corresponding signal, that is, the signal is automatically blocked until the processing program ends. Therefore, another occurrence of the processed signal cannot interrupt the signal processing program. Therefore, the signal processing function does not have to be reentrant.
Kernel implementation:
Remember the signals blocked by each process
When switching from the kernel state to the user State, check whether a signal has arrived for any process. This occurs when almost every scheduled interruption occurs.
Determine whether the signal can be ignored. This occurs when all of the following conditions are met:
The target process is not tracked by another process.
The signal is not blocked by the target process.
The signal is ignored by the target process.
Process such a signal, that is, the signal may request to switch the process to a signal processing function at any time during the process running, and restore the original execution context after the function returns.
[1] operations performed before Signal Transmission
The process responds to a signal in three ways:
(1) Ignore the signal in the display area
(2) perform default signal-related operations. The default operations predefined by the kernel depend on the signal type. The following types:
Terminate: the process is terminated (killed)
Dump: the process is terminated (killed)
Ignore: the signal is ignored.
Stop: the process is stopped. That is, the process is set to task_stopped.
Continue: if the process is stopped, set it to task_running.
(3) Capture signals by calling corresponding signal processing functions
Note that blocking or ignoring a signal is different: as long as the signal is blocked, it will not be transmitted; it will only be transmitted after the signal is blocked. However, a neglected signal is always transmitted, but no further operation is performed.
Sigkill and sigstop signals cannot be explicitly ignored, captured, or blocked. Therefore, they must be executed by default. Therefore, sigkill and sigstop allow users with proper privileges to terminate and stop any process, regardless of the defense measures taken during process execution.
If the signal transmission will cause the kernel to kill a process, the signal will be fatal to the process. The sigkill signal is always fatal. By default, each signal of the terminate operation and the signal not captured by the process are also fatal to the process. Note: If the corresponding signal processing function of a Signal captured by the process terminates the process, the signal is not fatal because the process has terminated, instead of being killed by the kernel.
[2] POSIX signal and multithreading applications
Posix1003.1 has strict requirements on Signal Processing for multi-threaded applications:
The signal processing program must be shared among all threads of a multi-threaded application. However, each thread must have its own pending signal mask and blocking signal mask.
POSIX library functions kill () and sigqueue () must send signals to all multi-threaded applications rather than a special thread. The same is true for all signals generated by the kernel.
Each signal sent to a multithreaded application is sent to only one thread, Which is randomly selected by the kernel from the thread that does not block the signal.
If a fatal signal is sent to a multi-threaded application, the kernel will kill all the threads of the application, not just the thread that receives the signal.
Linux kernel 2.6 implements multi-threaded applications as a group of lightweight processes belonging to the same thread group.
If a pending signal is sent to a specific process, the signal is private. If it is sent to the entire thread group, it is shared.
[3] signal-related data structure
For each process in the system, the kernel must track what signals are currently suspended or blocked, and how each thread group processes all signals. To complete these operations, the kernel uses several processor descriptors to access the data structure: see Figure 11-1 ***
(1) signal Descriptor and signal processing program Descriptor
The process descriptor signal field points to the signal Descriptor (signaldescriptor)-A signal_struct structure used to track shared suspended signals.
In addition to signal descriptors, each process also references a signal processing program Descriptor (signal handler deseriplor), which is a sighand_struct-type structure that describes how each signal must be processed by a thread group.
(2) sigaction Data Structure
Some architectures assign features to signals that are only visible to the kernel. Therefore, the signal features are stored in the k_sigaction structure. The k_sigaciton structure contains not only the features hidden from user-State processes, but also the familiar sigaction structure, this structure stores all the features visible to user-mode processes. In fact, on the 80x86 platform, all the features of the signal are visible to user-State processes. Therefore, the k_sigaction structure is simplified to a single SA structure of sigaction type. Field:
Sa_handler: specifies the type of operation to be executed. Its value can be a pointer to the signal processing program, sig_dfl (that is, the value 0, specify to execute the default operation), or sig_ign (that is, the value 1, specify to ignore the signal)
Sa_flags: A flag set that specifies how signals must be processed.
Sa_mask: variable of Type sigset_t, which specifies the signal to be blocked when the signal processing program is running
(3) suspend the signal queue
Several system calls can generate signals sent to the entire thread group, such as kill () and rt_sigqueueinfo (), while others generate signals sent to specific processes, such as tkill () and tgkill ()
To track the current pending signal, the kernel associates two pending signal Queues with each process:
The shared suspended signal queue is located in the shared_pending field of the signal Descriptor and stores the suspended signal of the entire thread group.
Private suspension signal queue, which is located in the pending field of the Process Descriptor and stores the suspension signal of a specific process
[4] operations on the signal data structure: reference the function list of the p429-430
2. generate signals
Many kernel functions generate signals: they complete the first step of signal processing, that is, updating the descriptors of one or more processes as needed. Instead of directly performing the signal transfer operation in step 2, they may wake up some processes based on the signal type and the status of the target process and urge these processes to receive signals.
When sending a signal to a process, the signal may come from the kernel or another process. The kernel generates a signal by calling a function shown in table 11-9.
When a signal is sent to the entire thread group, the signal may come from the kernel or another process. The kernel calls a function shown in Table 11-10 to generate a signal.
[1] specific_send_sig_info () function: sends a signal to a specified process. Step: see p433
[2] send_signal () function: Insert a new element into the suspended signal queue. Step: see p434
[3] group_send_sig_info () function: sends a signal to the entire thread group, step: Refer to p435-437
3. Transmit signals
To ensure that the pending signal of a process is processed by the kernel.
The kernel checks the value of the process tif_sigpending flag before allowing the process to resume execution in the user State. When the kernel finishes processing an interrupt or exception, it checks whether there is a pending signal.
To handle non-blocking pending signals, the kernel calls the do_signal () function.
The do_signal () function is called only when the CPU returns to the user State.
The core of the do_signal () function is a loop that repeatedly calls the dequeue_signal () function until no non-blocking pending signals exist in both the private and shared pending signal queues, the loop ends.
The dequeue_singal () function first considers all signals in the private suspended signal queue and starts from the suspended signal with the lowest number. Then consider sharing the signals in the queue. It updates the data structure to indicate that the signal is no longer suspended and returns its number.
How does the do_signal () function process each suspended signal whose number is returned by dequeue_signal. First, it checks whether the current receiving process is being monitored by other processes. In the Affirmative case, do_signal () calls do_policy_parent_cldstop () and Schedule () let the monitoring process know the signal processing of the process.
Then, do_signal () assigns the address of the k_sigaction data structure of the signal to the local variable ka. Three operations can be performed based on the Ka content: ignore signals, perform default operations, or execute signal processing programs. If the transmitted signal is explicitly ignored, the do_signal () function only continues to execute the loop and considers another pending signal.
[1] default signal operations
If Ka-> SA. sa_handler is equal to sig_dfl, do_signal () must perform the default signal operation. The only exception is that this signal is discarded when the receiving process is init.
The difference between sigstop and other signals is subtle: sigstop always stops the thread group, while other signals only stop the thread group that is not in the orphan process group. POSIX standards stipulate that as long as a process in a process group has a parent process, although the process is in a different process group but in the same session, this process group is not an orphan. Therefore, if the parent process dies, but the user who starts the process logs in online, the process group is not an orphan.
The default operation is dump. You can create a dump file in the working directory of the process. This file lists all the content of the process address space and CPU register.
[2] capture Signals
If the signal has a special processing program, the do_signal () function must force the processing program to execute. This is done by calling handle_signal ().
Note how do_signal () returns a single signal. Other pending signals are considered until do_signal () is called next time. This method ensures that the real-time signal will be processed in the appropriate order
It is quite complicated to execute a signal processing program. Therefore, when switching between the user State and the kernel state, you must carefully process the content in the stack. We will correctly explain the tasks undertaken here
A signal processing program is a function defined by a user-state process and included in a user-state code segment. The handle_signal () function runs in the kernel state, while the signal processing program runs in the user State, which means that before the current process resumes "normal" execution, it must first execute the user-state signal processing program. In addition, when the kernel intends to resume normal execution of the process, the kernel state stack no longer contains the hardware context of the interrupted program. Therefore, whenever the kernel state is switched to the user State, all kernel-state stacks are cleared. Another complexity is that the signal processing program can call the system call. In this case, after the service routine called by the system is executed, control must be returned to the signal processing program rather than to the normal code stream of the interrupted program.
In Linux, the solution is to copy the hardware context stored in the kernel state stack to the user State stack of the current process. The user State stack is also modified in this way. When the signal processing program ends, the sigreturn () system call is automatically called to copy the hardware context back to the kernel state stack, and restore the original content in the user State stack.
Figure 11-2 illustrates the execution flow of a function that captures a signal:
A non-blocking signal is sent to a process. When an interruption or exception occurs, the process switches to the kernel state.
Before returning to the user State, the kernel executes the do_signal () function,
This function processes the signal in sequence (by calling handle_signal () and creating a user State stack (by calling setup_frame () or setup_rt_frame ())
When the process switches to the user State again, the starting address of the signal processing program is forced into the program counter, so the signal processing program is started.
When the processing program is terminated, the returned code of the setup_frame () or setup_rt_frame () function in the user State stack is executed. This Code calls the sigreturn () or rt_sigrenturn () system call. The corresponding service routine copies the user-state stack hardware context of the normal program to the kernel stack, and restore the user State stack to its original state (by calling restore_sigcongtext ()). when this system call ends, normal processes can resume their own execution.
Figure: 11-2 ***
(1) create a frame
To properly establish the user State stack of a process, the handle_signal () function can call setup_frame () or setup_rt_frame ()
The setup_frame () function pushes a data structure called a frame to the user State stack. This frame contains the information required to process the signal, and ensures that it is correctly returned to the handle_signal () function.
The setup_frame () function resets the segment register content stored in the kernel state stack to their default values before the end. Now, all the information required by the signal processing program is at the top of the user State stack.
(2) Check the signal mark
After the user State stack is established, the handle_signal () function checks the signal-related flag values. If the sa_nodefer flag is not set for the signal, the signal corresponding to the sa_make field in the sigaction table must be blocked during the execution of the signal processing program. Then, handle_signal () is returned to do_signal (), do_signal () return immediately
(3) start to execute the signal processing program
When do_signal () is returned, the current process resumes its execution in the user State. As described above, due to the preparation of setup_frame (), the EIP register points to the first instruction of the signal processing program, while ESP points to the first memory unit of the frame that has pushed the top of the user State stack. Therefore, the signal processing program is executed.
(4) Terminate the signal processing procedure
When the signal processing program ends, the top stack address is returned, which points to the Code on the vsyscall page referenced by the pretcode field of the frame. Therefore, the signal number (the SIG field of the frame) is discarded from the stack, and then the sigreturn () system call is called.
The sys_rt_sigreturn () service routine copies the hardware context of the process from the extended frame to the kernel state stack, and deletes the extended frame from the user State stack to restore the original content of the user State stack.
(5) re-execution of system calls
The kernel does not always satisfy the requests sent by the system call immediately. In this case, set the process that sends the system call to task_interruptible or task_uninterruptible.
If the process is in the task_interruptible state and a process sends a signal to it, the process is set to task_running State if the kernel does not complete the system call. When switching back to the user State, the signal is passed to the process. When this happens, the system calls the service routine and does not complete its work, but returns the eintr, erestartnohand, erestart_restartblock, erestartsys, or erestartnointr error codes. In fact, in this case, the only error code obtained by a user-state process is eintr. This error code indicates that the system call has not been completed. The kernel uses the remaining error codes to specify whether to automatically re-execute the system call after the signal processing program ends.
The error codes related to unfinished system calls and the impact of these error codes on the three possible operations of signals.
Terminate: The system call will not be automatically re-executed
Reexecut: the kernel forces the user-state process to reload the system call number into the eax register and re-execute the int $0x80 or sysenter command. The process does not realize this type of re-execution, so the error code is not passed to the process.
Depends: The system call is re-executed only when the sa_restart mark of the transmitted signal is set. Otherwise, the system call-ter error code ends.
When the signal is transmitted, the kernel must confirm that the process has actually sent this system call before attempting to re-execute a system call. This is where the orig_eaz field of the regs hardware context plays an important role.
A. Re-execute the system call that is interrupted by the uncaptured Signal
If the signal is explicitly ignored, or if its default operation has been forcibly executed, do_signal () analyzes the error code of the system call, as described in Table 11-11, it determines whether to re-automatically execute unfinished system calls. If the system call must be re-executed, do_signal () modifies the regs hardware context so that when the process returns to the user State, the EIP points to the int $0x80 or sysenter command, and eax contains the system call number.
B. Re-execute the system call for the captured Signal
If the signal is captured, the handle_signal () Analysis Error Code may also analyze the sa_restart flag of the sigaction table to determine whether to re-Execute unfinished system calls.
If the system call must be re-executed, handle_signal () will continue execution exactly the same as do_signal (); otherwise, it will return an error code-entr to the user-state process
4. system calls related to Signal Processing
Processes running in user mode can send and receive signals. This means that a group of system calls must be defined to complete these operations. Unfortunately, due to historical reasons, there are already several system calls with the same functions. Therefore, some of these system calls have never been called. For example, the system calls sys_sigaction () and sys_rt_sigaciton () are almost the same. Therefore, the function sigaction () in library C calls sys_rt_sigaction () instead of sys_sigaction ().
[1] Kill () system call
Generally, a kill (PID, sig) system call is used to send signals to common processes or multi-threaded applications. The corresponding service routine is the sys_kill () function.
Kill () system calls can send any signal, even if the number is between 32-64 real-time signals. The kill () System Call cannot ensure that a new element is added to the pending signal queue of the target process. Therefore, multiple instances with pending signals may be lost. Real-time signals should be sent through rt_siggueueinfo () system call
[2] tkill and gkill () system call
The tkill () and tgkill () system calls send signals to the specified process in the thread group.
[3] signal change operations
The sigaction (SIG, act, oact) System Call allows you to specify an operation for the signal. Of course, if there is no custom signal operation, the kernel executes the default operation related to the transmitted signal.
[4] Check suspended blocking signals
Sigpending () System Call allows the process to check the set of blocked signals suspended, that is, to check the signals generated when the signal is blocked
[5] modifying the set of blocking signals
The sigprocmask () System Call allows the process to modify the set of blocking signals. This system call is only applicable to conventional signals.
[6] suspending a process
The sigsuspend () System Call sets the process to the task_interruptible state. Of course, this is set after blocking the standard signal specified by the bitmask array to which the mask parameter points. The process is awakened only when a non-ignore or non-blocking signal is sent to the process.
[7] Real-Time Signal System Call
System calls are only applicable to standard signals. Therefore, additional system calls must be introduced to allow user-State processes to process real-time signals.
System calls for real-time signals: rt_sigaction () rt_sigpending () rt_sigprocmask () rt_sigsuspend ()