Linux 10,000-word graphic learning process signal

1. Signal concept

Signals are a way of asynchronous notification of events between processes and are soft interrupts.

1.1 In Linux, we often use Ctrl+c to kill a foreground process.

  • 1. The signal generated by Ctrl-C can only be sent to the foreground process. Adding an & after a command can run it in the background, so that the Shell can accept new commands and start new processes without waiting for the process to end.
  • 2. Shell can run a foreground process and any number of background processes at the same time. Only the foreground process can receive signals generated by control keys like Ctrl-C.
  • 3. When the foreground process is running, the user may press Ctrl-C at any time to generate a signal. That is to say, the user space code of the process may receive the SIGINT signal and terminate wherever it is executed, so the signal is relative to the process. The control flow is asynchronous.

2. Common signals

Use the kill -l command to view the system-defined signal list

  • Each signal has a number and a macro definition name. These macro definitions can be found in signal.h
  • Numbers 34 and above are real-time signals
  • The conditions under which these signals are generated and what the default processing actions are are detailed in signal(7): man 7 signal

3. Signal nature

        Signals are software interrupts, which are a simulation of the interrupt mechanism at the software level . In principle, a process receiving a signal is the same as a processor receiving an interrupt request. Signals are asynchronous, and a process does not have to wait for the arrival of the signal through any operations. In fact, the process does not know when the signal arrives.

        Signals are the only asynchronous communication mechanism among inter-process communication mechanisms . They can be regarded as asynchronous notifications, notifying the process receiving the signals of what has happened. After the POSIX real-time extension, the signal mechanism becomes more powerful. In addition to the basic notification function, it can also deliver additional information.

4. Signal source

4.1 Generate signals through terminal keys

The default processing action of SIGINT is to terminate the process. The default processing action of SIGQUIT is to terminate the process and Core Dump.

4.1.1 What is Core Dump

When a process is about to terminate abnormally, you can choose to save all the user space memory data of the process to the disk. The file name is usually core. This is called Core Dump. Abnormal termination of the process is usually due to bugs, such as illegal memory access leading to segmentation faults. You can use a debugger to check the core file afterwards to find out the cause of the error. This is called Post-mortem Debug. How large a core file a process is allowed to generate depends on the Resource Limit of the process (this information is stored in the PCB). By default, core files are not allowed to be generated because core files may contain sensitive information such as user passwords and are not safe.

During the development and debugging phase, you can use the ulimit command to change this limit and allow core files to be generated. First, use the ulimit command to change the Resource Limit of the Shell process  $ ulimit -c 1024  to allow the core file to be up to 1024K

        

 

4.1.2 Signal capture of terminal buttons 

#include <stdio.h>
#include <signal.h>
int count = 0;
void handler(int sig)
{
    printf("the catched count is %d  ",++count);

    printf("catch a sig : %d\n", sig);
}
int main()
{
    signal(2, handler); // 信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的

    while (1)
        ;
    return 0;
}

4.2. Call system functions to send signals to the process

Commonly used kills include pkill and killall, etc.

The kill command is implemented by calling the kill function. The kill function can send a specified signal to a specified process. The raise function can send a specified signal to the current process (send a signal to itself).

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
abort函数使当前进程接收到信号而异常终止。

4.3 Signals generated by software conditions

SIGPIPE is a signal generated by software conditions. In the pipeline, the read end is closed and the write end keeps writing.

Broken pipe: write to pipe with no readers.
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,
读管道没打开或者意外终止就往管道写,
写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,
写进程在写Socket的时候,读进程已经终止。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
可以重复设置,新的会覆盖旧的并返回上次闹钟剩余的时间,可以设置为0,意思就是取消闹钟,并返回上次闹钟剩余的时间

4.4 Hardware exception generates signal

Hardware exceptions are somehow detected by the hardware and notified to the kernel, which then sends appropriate signals to the current process. For example, if the current process executes the instruction of dividing by 0 , the CPU's arithmetic unit will generate an exception, and the kernel will interpret this exception as a SIGFPE signal and send it to the process. For another example, if the current process accesses an illegal memory address (wild pointer) , the MMU will generate an exception, and the kernel will interpret this exception as a SIGSEGV signal and send it to the process.

4.4.1 Capture of signals generated by hardware exceptions

And after the hardware exception, the program will not continue to execute.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int count = 0;
int count2=0;
void handler(int sig)
{
    printf("the catched count is %d count2:%d ",++count,count2);

    printf("catch a sig : %d\n", sig);
}
int main()
{
    signal(11, handler); // 信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的

    while (1){
        sleep(1);
        count2++;
        int *p=NULL;
        *p=100;
    }
    return 0;
}

5. Classification of signals

1. Reliable signals and unreliable signals

Unreliable signals:

The Linux signal mechanism is basically inherited from Unix systems. The signal mechanism in early Unix systems was relatively simple and primitive, and later exposed some problems in practice. Therefore, those signals based on the early mechanism were called "unreliable signals", and the signal value was smaller than SIGRTMIN (in Red Hat 7.2, SIGRTMIN =32, SIGRTMAX=63) signals are all unreliable signals .

The main problems with unreliable signals are:

  • Each time a process handles a signal, the response to the signal is set as the default action . In some cases, this will result in incorrect handling of the signal; therefore, if the user does not want such an operation, he or she must call signal() again at the end of the signal processing function to reinstall the signal.
  • Signal may be lost , more on this later. If the signal occurs multiple times when the process is processing a signal , and subsequent signals of this type are not queued, then the signal is only transmitted once , that is, signal loss occurs.
    Therefore, unreliable signals under early Unix mainly refer to processes that may respond incorrectly to signals and signals that may be lost.

Linux supports unreliable signals, but has made improvements to the unreliable signal mechanism: after calling the signal processing function, there is no need to re-call the signal's installation function (the signal installation function is implemented on the reliable mechanism). Therefore, the problem of unreliable signals under Linux mainly refers to the possible loss of signals .

reliable signal

Signals with signal values ​​between SIGRTMIN and SIGRTMAX are reliable signals. Reliable signals overcome the problem of possible signal loss . While Linux supports the new version of the signal installation function sigation() and the signal sending function sigqueue(), it still supports the early signal() signal installation function and the signal sending function kill().

       Note: Do not have this misunderstanding: the signal sent by sigqueue() and installed by sigaction is reliable. In fact, a reliable signal refers to a new signal added later (the signal value is between SIGRTMIN and SIGRTMAX); an unreliable signal is a signal with a signal value less than SIGRTMIN . The reliability or unreliability of a signal is only related to the signal value and has nothing to do with the signal sending and installation functions. Currently, signal() in Linux is implemented through the sigation() function. Therefore, even if the signal is installed through signal(), the signal installation function does not need to be called again at the end of the signal processing function. At the same time, real-time signals installed by signal() support queuing and will not be lost.

       For the current two signal installation functions of Linux: signal() and sigaction(), they cannot turn the signal before SIGRTMIN into a reliable signal (neither supports queuing, it may still be lost, and it is still an unreliable signal). Moreover, queuing is supported for signals after SIGRTMIN . The biggest difference between these two functions is that signals installed through sigaction can pass information to the signal processing function (this is true for all signals), while signals installed through signal cannot pass information to the signal processing function. The same goes for signaling functions.

6. Common methods of signal processing

  • 1. Ignore this signal.
  • 2. Execute the default processing action of the signal.
  • 3. Provide a signal processing function and require the kernel to switch to user mode to execute the processing function when processing the signal. This method is called catching a signal.

7. Signal set operation function

7.1 Some related concepts of signals

  • The actual execution of the signal processing action is called signal delivery (Delivery)
  • The state between signal generation and delivery is called signal pending .
  • A process can choose to block a signal.
  • The blocked signal will remain in the pending state when it is generated, and the delivery action will not be executed until the process unblocks the signal.
  • Note that blocking and ignoring are different. As long as the signal is blocked, it will not be delivered, while ignoring is an optional processing action after delivery.

7.2 Representation of signals in the kernel

  • Each signal has two flag bits indicating blocking and pending, and a function pointer indicating processing action. When a signal is generated, the kernel sets the pending flag of the signal in the process control block and does not clear the flag until the signal is delivered. In the example above, the SIGHUP signal is neither blocked nor generated, and the default processing action is performed when it is delivered.
  • The SIGINT signal has been generated, but is being blocked, so it cannot be delivered temporarily. Although its processing action is to ignore, this signal cannot be ignored before unblocking, because the process still has the opportunity to change the processing action and then unblock.
  • The SIGQUIT signal has never been generated. Once the SIGQUIT signal is generated, it will be blocked. Its processing action is the user-defined function singhandler. What happens if a signal is generated multiple times before the process unblocks it? POSIX.1 allows the system to deliver the signal one or more times. Linux implements this: regular signals that are generated multiple times before delivery are counted only once, while real-time signals that are generated multiple times before delivery can be placed in a queue in sequence. Note that real-time signals are not discussed here. 

7.3 sigset_t

From the above figure, each signal has only one bit of pending flag, which is either 0 or 1. It does not record how many times the signal has been generated . The blocking flag is also expressed in this way. Therefore, pending and blocked flags can be stored with the same data type sigset_t. sigset_t is called a signal set. This type can represent the "valid" or "invalid" status of each signal, and "valid" and "invalid" in the blocking signal set . " means whether the signal is blocked, and "valid" and "invalid" in the pending signal set mean whether the signal is in a pending state.

The blocking signal set is also called the signal mask of the current process. The " mask" here should be understood as blocking rather than ignoring.

7.4 Signal set operation function

The sigset_t type uses a bit to represent the "valid" or "invalid" status for each signal. As for how to store these bits inside this type, it depends on the system implementation. From the user's perspective, it does not need to be concerned. The user should call the following function . Operate the sigset_t variable without any explanation of its internal data. For example, it makes no sense to directly print the sigset_t variable with printf.

#include <signal.h>
int sigemptyset(sigset_t *set);//置空
int sigfillset(sigset_t *set);//置满
int sigaddset (sigset_t *set, int signo);//添加信号到信号集
int sigdelset(sigset_t *set, int signo);//在信号集中删除指定信号
int sigismember(const sigset_t *set, int signo); //在信号集查找是否有该信号

These four functions all return 0 on success and -1 on error. sigismember is a Boolean function used to determine whether a certain signal is included in the valid signals of a signal set. If it is included, it returns 1, if it is not included, it returns 0, and if it fails, it returns -1.


 

7.5 Calling the function sigprocmask can read or change the signal mask word (blocking signal set) of the process.

7.6 sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 

 7.7 Printing of pending signals when signals are blocked 

#include<iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;
void my_printf_sig(sigset_t* a){
    for(int i=0;i<65;i++){
        if(sigismember(a,i)){
            cout<<"1";
            //cout<<"         "<<i<<"**********";

        }
        else{
            cout<<"0";
        }
    }
}

int main(){
    sigset_t block,pending;
    sigemptyset(&block);
    for(int i=0;i<65;i++){//将所有的信号都屏蔽了,但是kill -9 还是可以生效。
        sigaddset(&block,i);
    }
    sigprocmask(SIG_BLOCK,&block,NULL);

    while(1){
        sigpending(&pending);
        my_printf_sig(&pending);
        cout<<endl;
        sleep(1);
        cout<<"my_pid is"<<getpid()<<endl;

    }
}

7.8 How the kernel implements signal capture 

If the signal processing action is a user-defined function, this function is called when the signal is delivered, which is called catching the signal. Since the code of the signal processing function is in user space, the processing process is relatively complicated. For example, the user program registers the SIGQUIT signal processing function singhandler. The main function is currently being executed. When an interrupt or exception occurs, the system switches to the kernel state. After the interrupt is processed, it is checked that the signal SIGQUIT is delivered before returning to the main function in user mode. The kernel decides not to restore the context of the main function and continue execution after returning to user mode, but to execute the sighandler function. The sighandler and main functions use different stack spaces. There is no relationship between calling and being called. They are two independent control processes. . After the sighandler function returns, it automatically executes the special system call sigreturn and enters the kernel state again. If there are no new signals to be delivered, returning to user mode this time will restore the context of the main function and continue execution.

7.9 sigaction

7.9.1 Definition of siaction 

  • sa_handler is a function pointer that specifies the signal processing function. In addition to our custom processing function, it can also be SIG_DFL (using the default processing method) or SIG_IGN (ignoring the signal). Its handler function has only one parameter, the signal value.
  •    sa_mask is a signal set that can specify which signals should be masked during the execution of the signal handler. Before calling the signal capture function, the signal set must be added to the signal mask word of the signal.
  •    sa_flags contains many flag bits, which are various options for signal processing. Its common optional values ​​are shown in the following table

8. volatile:

The role of volatile: maintains the visibility of memory and tells the compiler that variables modified by this keyword are not allowed to be optimized. Any operation on this variable must be performed in real memory. 

9. SIGCHLD signal

The parent process calls sigaction to set the SIGCHLD processing action to SIG_IGN, so that the forked child process will be automatically cleaned up when terminated, and no zombie process will be generated, and the parent process will not be notified. There is usually no difference between the system's default ignore action and the user-defined ignore action using the sigaction function, but this is a special case. This method is available for Linux, but is not guaranteed to be available on other UNIX systems.

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

void sigchld_handler(int signum)
{
    // 处理SIGCHLD信号,可以不需要具体的逻辑
    // 在这个示例中,我们可以仅仅打印一条消息
    printf("Received SIGCHLD signal\n");
}

int main()
{
    // 设置SIGCHLD的处理动作为SIG_IGN
    // struct sigaction sa;
    // sa.sa_handler = SIG_IGN;
    // sigemptyset(&sa.sa_mask);
    // sa.sa_flags = 0;
    // sigaction(SIGCHLD, &sa, NULL);

    pid_t pid = fork();

    if (pid == 0)
    {
        // 子进程
       // printf("Child process\n");
        cout << "child process pid is : " << getpid() << endl;

        // do something...
        sleep(30);
        cout << "child exit" << endl;
        exit(0);
    }
    else if (pid > 0)
    {
        // 父进程
       // printf("Parent process\n");
        cout << "parent process pid is : " << getpid() << endl;

        // 等待子进程结束
        sleep(100); // 等待足够长的时间,以确保子进程已经结束
        printf("Parent process exiting\n");
    }
    else
    {
        // fork失败
        printf("Fork failed\n");
        return 1;
    }

    return 0;
}

// while :; do ps axj|head -1&& ps axj |grep sig_ignore|grep -v grep;sleep 1; done;

Thinking summary:

所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者 




信号的处理是否是立即处理的?在合适的时候 




信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢? 
在Linux中,如果信号在接收时不能立即处理,进程通常需要将信号记录下来以便稍后处理。Linux使用信号队列来存储接收到但尚未处理的信号。
每个Linux进程都有一个信号处理器表,用于存储信号处理程序的地址。当进程收到一个信号时,操作系统会根据进程的信号处理器表确定应该执行哪个信号处理程序。
在信号处理期间,Linux使用信号屏蔽字和挂起信号队列来处理挂起的信号。挂起信号队列是内核中的数据结构,用于存储挂起的信号。当进程正在处理信号时,其他同类信号可能会被挂起,并在当前信号处理程序完成后按照一定的优先级顺序逐个处理。
挂起信号队列位于内核中,因此信号被记录在内核的上下文中。这种设计可以保证在进程切换时仍然可以正确处理挂起信号。
最合适记录信号的位置是在Linux内核的信号挂起队列中。这样,在进程重新调度执行时,操作系统可以检查信号挂起队列并将挂起的信号传递给进程进行处理。
需要注意的是,Linux对于不同类型的信号有不同的处理机制。一些信号(如SIGSTOP和SIGKILL)不能被挂起,它们会立即中断进程的执行。但大多数其他信号可以挂起并在适当的时候处理。
总之,Linux使用信号挂起队列来记录未立即处理的信号。这种机制确保了信号的顺序和正确处理,同时保证了信号的可靠交付。





一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
能够,信号的默认处理动作在linux的内核中 








如何理解OS向进程发送信号?
理解操作系统(OS)向进程发送信号可以从两个方面来考虑:

1.异步事件通知:操作系统可以在特定事件发生时向进程发送信号,以通知进程发生了某些异步事件。这些事件可能包括:


用户按下特定的键盘组合,例如Ctrl+C,触发SIGINT信号。
定时器到期,触发SIGALRM信号。
子进程终止,触发SIGCHLD信号。
硬件故障,例如除零错误,触发SIGFPE信号。
当这些事件发生时,操作系统会向目标进程发送相应的信号,以便进程可以作出相应的反应。这可以通过操作系统的信号传递机制实现,如Linux中的信号队列。


2.进程间通信:除了异步事件通知外,操作系统还可以通过信号在进程之间进行通信。进程可以向其他进程发送信号以触发特定的行为或进行协调。这可以用于以下情况:


进程间的同步和通知:一个进程可以向另一个进程发送信号来通知某个事件的发生,以便目标进程进行相应的处理。例如,父进程向子进程发送信号以指示某个任务已完成。
进程控制和管理:某些信号可以用于控制和管理进程。例如,通过SIGKILL信号可以强制终止一个进程,通过SIGSTOP信号可以暂停一个进程的执行。
信号处理程序的使用:进程可以注册自定义的信号处理程序,当接收到特定信号时,操作系统会调用该处理程序来执行特定的操作。这样进程就可以利用信号处理程序来处理特定事件或执行自定义逻辑。

总体而言,操作系统通过向进程发送信号,实现了事件通知、进程间通信和进程控制等功能。这为进程提供了一种有效的方式来与操作系统进行交互,并实现合适的响应和协作。




能否描述一下完整的发送处理过程?
当进程发送信号时,它调用kill()、raise()或发送信号的相关系统调用。内核接收到信号后,会检查接收进程的权限和状态。如果接收进程具有对该信号的处理程序,并且信号未被阻塞,则内核会将信号递交给接收进程的信号处理程序。如果接收进程没有对信号进行处理或信号被阻塞,则内核采取默认的行为,如终止进程(SIGKILL)或忽略该信号(SIGIGN)。整个过程涉及信号发送、接收进程状态检查以及信号的处理或默认行为的执行。
详细版:
发送信号:

信号源:信号可以来自多个源头,例如操作系统触发的异步事件、其他进程的信号发送、硬件中断等。
信号发送:信号源通过系统调用(如kill())或其他适当的机制向目标进程发送信号。发送信号时,需要指定目标进程的标识符,如进程ID(PID)或进程组ID(PGID)。
接收信号:

目标进程:操作系统在目标进程上选择一个合适的执行点来接收信号。这通常是在进程的用户态代码执行时进行检查。
信号递交检查:目标进程在执行期间会定期进行对信号的检查,以确定是否有新的信号需要处理。这个过程通常是由操作系统提供的信号传递机制完成的。
信号处理:

默认行为:如果进程未注册特定信号的处理程序,或者没有明确指定默认操作的信号,操作系统将使用默认的信号处理行为来处理信号。这可能包括终止进程、忽略信号或者进行其他特定的操作。
自定义处理程序:进程可以使用系统调用(如signal()或sigaction())注册自定义的信号处理程序。当信号到达时,操作系统会调用相应的信号处理程序来执行特定的操作或指令。
信号处理程序执行:

信号处理程序执行在进程的上下文中,可以执行特定的代码逻辑来响应信号。信号处理程序可以进行一系列操作,如记录日志、修改进程状态、发送其他信号等。
注意,由于信号处理程序在进程的执行上下文中执行,它必须是轻量级且可靠的。一些操作(如堆内存分配和长时间的计算)可能不适合在信号处理程序中执行。
信号处理完成:

处理结果:信号处理程序完成后,进程可以根据需要执行后续操作,并继续执行原始代码逻辑。
信号状态更新:处理完信号后,操作系统将清除信号的挂起状态,并允许其他信号继续递交给进程。
这是一个基本的信号发送和处理过程的概述。具体的实现细节和行为取决于操作系统和编程语言的规范。不同的操作系统和编程语言可能有自己的信号处理机制和可调整的参数。因此,在具体的应用环境中,还需要参考相关的文档和规范来了解操作系统的具体行为和限制





挂起的信号是指在信号处理程序执行期间接收到的但还没有被处理的信号。当一个进程正在执行信号处理程序时,如果这个进程收到了其他信号(除了一些特定的硬件相关信号),这些新的信号将会被挂起,即暂时不会被递交给进程。相反,这些挂起的信号会等待当前正在执行的信号处理程序完成后再进行递交。这种机制保证了信号处理程序的一致性和可靠性。

Guess you like

Origin blog.csdn.net/m0_74234485/article/details/132875208