LOADING

加载过慢请开启缓存 浏览器默认开启

信号

信号的概念

  1. 进程一定要有识别和处理信号的能力,信号的处理能力属于进程内置功能。
  2. 当进程接收到信号有可能不会立即处理。

为什么ctrl + c 会导致进程终止?

linux中,一次登录,只允许一个进程是前台进程,可以多个进程是后台进程,(区别是,前台进程可以获取键盘输入)
ctrl + c 产生一个信号,发送给前台进程,前台进程收到信号后,就会终止。
本质是被进程解释为收到的信号,2号信号(SIGINT)。

键盘数据如何输入给内核,ctrl + c 如何给内核发送信号?

  1. 当用户输入数据时,键盘驱动程序将数据写入到终端设备文件中,如:/dev/tty1
    写入内核相应的缓冲区,如果缓冲区满就给CPU发出中断申请,操作系统根据中断向量表找到相应的中断处理程序,中断处理程序将数据从缓冲区拷贝到内核的读缓冲区中。
  2. 当用户按下ctrl + c时,键盘驱动程序将ctrl + c解释为2号信号,写入到终端设备文件中,如:/dev/tty1
  3. 当终端设备文件有数据可读时,内核会读取数据,并判断数据是否是信号,如果是信号,则发送给前台进程。

信号的产生

首先不是所有的信号都可以被signal捕捉,SIGKILL和SIGSTOP信号不能被捕捉,也不能被忽略,只能被阻塞。

快捷键产生信号

ctrl + c 产生2号信号(SIGINT)
ctrl + \ 产生3号信号(SIGQUIT)
ctrl + z 产生18号信号(SIGTSTP)
ctrl + d 产生4号信号(SIGTERM)

系统调用产生信号

kill()函数产生信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/types.h>
#include <string>

using namespace std;

void myhandler(int signo) {
    cout << "Process received signal: " << signo << endl;
    exit(2);
}

void Usage(string proc) {
    cout << "Usage:\n\t" << proc << " signum pid\n\n";
}

int main(int argc, char *argv[]) {

    signal(2, myhandler); 

    if (argc != 3) {
        Usage(argv[0]);
        exit(1);
    }

    int signum = stoi(argv[1]); // 字符串转为 INT
    pid_t pid = stoi(argv[2]);

   
    int n = kill(pid, signum);
    if (n == -1) {
        perror("kill");
        exit(2);
    }

    return 0;
}

raise()函数产生信号

SYNOPSIS
       #include <signal.h>

       int raise(int sig);  
       //本质是
       kill(getpid(), sig);

abort()函数产生信号

SYNOPSIS
       #include <signal.h>

       void abort(void);

信号的捕捉和处理

信号范围[1,31],每种信号都有自己的一种处理方法
类型是typedef void (*handler_t)(int);函数指针
handler_t handler[31];这个类型的数组,其中每个指针都可以通过信号处理函数修改为自定义处理函数

信号处理方式

  1. 忽略信号
  2. 执行默认操作
  3. 执行用户自定义的信号处理函数

信号处理函数

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

第一个参数为信号编号,第二个参数为信号处理函数的指针。

三张表

每个进程对应的结构体中对应三个表
分别是block表,作用是表面当前信号是否被屏蔽
pending表,作用是表面当前信号是否被挂起(也就是是否到达,如果到达并且没被屏蔽,就会进行信号的处理)。
handler表,表面当前信号的处理方式

接口

#include <signal.h>

int sigemptyset(sigset_t *set);

将一个信号集(sigset_t 类型)清空,也就是说,将该信号集中所有的信号都设置为不包含状态。

#include <signal.h>

int sigaddset(sigset_t *set, int signum);

sigaddset 是一个用于向信号集(sigset_t 类型)中添加特定信号的函数。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

sigprocmask 是一个用于修改当前进程的信号屏蔽字的系统调用。信号屏蔽字定义了哪些信号在进程执行期间被阻塞(即不被处理)。
how: 指定如何修改信号屏蔽字的操作。可以是以下值之一:
SIG_BLOCK: 将 set 中的信号添加到当前的信号屏蔽字中。
SIG_UNBLOCK: 从当前的信号屏蔽字中删除 set 中的信号。
SIG_SETMASK: 将当前的信号屏蔽字设置为 set 中的信号。
set: 指向一个 sigset_t 类型的变量,表示要添加、删除或设置的信号集。
oldset: 指向一个 sigset_t 类型的变量,用于存储修改前的信号屏蔽字(如果不需要,可以传递 NULL)。

#include <signal.h>

int sigpending(sigset_t *set);

sigpending 是一个用于检查当前进程中被阻塞但尚未处理的信号的系统调用。也就是得到pending表中的内容

小案例(先屏蔽2号信号,后释放)

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

using namespace std;

void handler(int singno){
    cout<<"catch a signo: "<<singno<<endl;
}

void PrintPending(sigset_t &pending){
    for(int signo=31;signo>=1;signo--){
        if(sigismember(&pending,signo))
        {
            cout<<"1";
        }else{
            cout<<"0";
        }
    }
    cout<<"\n\n";
}

int main(){
    //对2号新号进行自定义处理
    signal(2,handler);

    //初始化两个信号集
    sigset_t bset,oset;
    sigemptyset(&bset);
    sigemptyset(&oset);

    sigaddset(&bset,2);
    sigprocmask(SIG_SETMASK,&bset,&oset);

    sigset_t pending;
    int cnt=10;
    while(true){
        int n=sigpending(&pending);
        if(n<0){
            continue;
        }

        PrintPending(pending);
        sleep(1);
        cnt--;
        if(cnt==0){
            sigprocmask(SIG_SETMASK,&oset,nullptr);
            cout<<"unblock 2 signo "<<endl;
        }
    }
    //继续发送2号信号结果应该为
    //全0
}

信号什么时候被处理

当进程从内核态返回到用户态的时候,进行信号的检测和处理
内核态:允许访问操作系统对应的代码和数据
用户态:允许访问自己的代码和数据

信号的捕捉

  1. 用户态,在主控制流程中因为执行某条指令因为中断,异常或者系统调用进入内核。
  2. 内核态,内核处理完异常准备返回用户模式之前可以先处理当前进程中pending的信号
  3. do_signal(),如果函数的处理动作是自定义的信号,处理函数则返回用户态执行
  4. 用户态,执行自定义函数,当调用sigreturn返回内核态
  5. 内核态,sys_sigreturn(),返回上一次被中断的地方继续执行

子进程退出会发送信号

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号
的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理
函数中调用wait清理子进程即可。

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;
void handler(int signo)
{
    sleep(5);
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "I am proccess: " << getpid() << " catch a signo: " << signo << "child process quit: " << rid << endl;
    }
}


int main()
{
    //signal(17, SIG_IGN); // SIG_DFL -> action -> IGN
    // 如果我们有10个子进程呢??如果同时退出呢?
    signal(17, handler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (true)
            {
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!!!" << endl;
            exit(0);
        }
         sleep(rand()%5+3);
        //sleep(1);
    }
    // father
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

将17号信号捕捉,可以直观看出退出的时候子进程不是悄悄地退出,会主动的向父进程发送17号信号。