【linux】进程信号

一、信号相关概念

1、信号的结论

1)进程必须能够识别并处理信号,即使信号没有产生,也要具备处理信号的能力,这种能力属于进程内置功能的一部分。

2)即使进程没有收到信号,也能知道信号的处理方法。

3)当进程收到一个具体的信号时,进程可能不会立即处理这个信号,等到合适的时间才会处理这个信号,信号的处理方式有三种,分别是默认动作,忽略,自定义动作(信号的捕捉)。

4)从信号产生,到信号开始被处理,一定会有时间窗口,进程具有临时保存信号已经发生了的能力。

注意:并不是所有的信号都能被捕获。

eg:SIGKILL(9)和SIGSTOP(19)

2、ctrl + c 为什么能够杀掉我们的前台进程呢?

Linux中,一次登录中,一个终端一般会配上一个bash,每一个登录,只允许一个进程是前台进程(前台进程用来获取键盘输入,键盘输入首先是被前台进程收到的),可以允许多个进程是后台进程。

当一个进程执行时,该进程就变成了前台进程,而bash变成了后台进程。

1~31号信号为普通信号,34~64号信号为实时信号。

ctrl + c 本质是被进程解释成为收到了信号(2号信号)。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

//int: 收到了哪一个信号
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}

int main()
{
    signal(SIGINT, myhandler);//只需设置一次,一直有效!
    //信号的产生和我们自己的代码运行时是异步的

    while(true)
    {
        cout << "I am a crazy process : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

ctrl + c 的默认动作是终止进程,现在我们设置了自定义动作就会按照我们的要求来执行,但是此时进程就不会终止了,可以通过 kill -9 45836杀掉该进程。

如何将进程变成后台进程呢?

ctrl + c 只能杀掉前台进程,无法杀掉后台进程。

3、键盘数据是如何输入给内核的,ctrl + c 又是如何变成信号的?

键盘摁下ctrl + c,产生硬件中断,OS捕获中断,将其转换为SIGINT信号,并发送给当前前台进程。

二、信号的产生

1、键盘组合键

ctrl + c : 2号信号

ctrl + \ : 3号信号

2、kill 命令

kill -signo pid

3、系统调用

1)kill

可以给任意进程发信号。

代码(自主实现kill命令):

mykill.cc:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

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

// ./mykill signum pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signum = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);
    if(n == -1)
    {

        perror("kill");
        exit(2);
    }

    return 0;
}

proc.c:

复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("I am a process, pid: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

运行结果:

终端1:

终端2:

2)raise

给当前进程发信号。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            raise(2);
        }
    }
    return 0;
}

运行结果:

3)abort

向进程自身发送SIGABRT(6号)信号,让当前进程异常终止。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            abort();
        }
    }
    return 0;
}

运行结果:

怎么知道abort发送的是6号信号呢?

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}

int main()
{
    signal(SIGABRT, myhandler);//执行只是注册信号处理函数,当收到信号时才调用函数

    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            //kill(getpid(), 2);
            //raise(2);
            //abort();
        }
    }
    return 0;
}

运行结果:

终端1:

终端2:

4、异常

当发生除零或野指针时,会让进程崩溃,操作系统会给进程发送信号。

操作系统是硬件的管理者,CPU也是硬件,CPU中有状态寄存器,当发生除零时,状态寄存器中的溢出标志位会由0变成1,虽然我们修改的是CPU内部的状态寄存器,但是这只影响当前进程,操作系统会通过调度机制,让其他进程继续执行。当访问野指针时,因为CPU访问的是虚拟地址,MMU(内存管理单元)通过页表将虚拟地址转换为物理地址,但访问野指针会导致地址转换失败。

代码:

复制代码
#include <iostream>

using namespace std;

int main()
{
    // int a = 10;
    // int b = 0;
    // a /= b;
    // cout << "a = " << a << endl;
    int *p = nullptr;
    *p = 100;

    return 0;
}

运行结果:

5、软件条件

异常不只会由硬件产生,还可以由软件产生。

1)闹钟:SIGALRM(14号信号)

返回值:如果之前没设置过闹钟,或之前的闹钟已经超时,则返回0;

如果之前还有未超时的闹钟,则返回距离上次闹钟剩余的秒数。

代码:

复制代码
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    alarm(3);//3秒后发送信号
    while(true)
    {
        cout << "process is running" << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

2)当一个进程向一个没有读端的管道写入数据时,操作系统会向该进程发送SIGPIPE信号。

代码:

复制代码
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    char buffer[1024];
    int n = 1024;
    n = read(4, buffer, n);
    printf("n = %d\n", n);
    perror("read");
    return 0;
}

运行结果:

6、core dump 标志

默认云服务器上面的core功能是被关闭的。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 500;
        while(cnt)
        {
            cout << "i am a child process, pid: " << getpid() << "cnt: " << cnt << endl;
            sleep(1);
            cnt--;
        }
        exit(0);
    }

    //father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        cout << "child quit info, rid: " << rid << " exit code: " <<
        ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) <<
        " core dump: " << ((status>>7)&1) << endl;
    }
    return 0;
}

运行结果:

打开系统的core dump功能(临时生效),一旦进程出异常,OS会将进程在内存中的运行信息(定位到出错行),给dump(转储)到进程的当前目录,形成core.pid文件(磁盘):核心转储(core dump)

先运行,再core-file:事后调试

7、什么是信号的发送?

对于普通信号而言,信号是给进程的PCB发的。

复制代码
task_struct
{
    int signal;// 0000 0000 0000 0000 0000 0000 0000 0010 普通信号,位图管理信号
    ...           
}
//1.比特位的内容是0还是1,表明是否收到信号
//2.比特位的位置(第几个,不算0),表示信号的编号
//3.所谓的"发信号",本质就是OS去修改task_struct的信号位图对应的比特位
//4.OS是进程的管理者,只有它有资格去修改task_struct内部的属性

三、信号的保存

1、信号其他相关概念

1)实际执行信号的处理动作称为信号递达(Delivery)。

2)信号从产生到递达之间的状态,称为信号未决(Pending)。

3)进程可以选择阻塞(Block)某个信号。

4)被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

5)阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2、在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。

常规信号在递达之前产生多次信号只计一次,而实时信号在递达之前产生多次信号可以依次放在一个队列里。

3、sigset_t

sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞(1表示阻塞),而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。

4、信号集操作函数

复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都清零

int sigfillset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都置1

int sigaddset (sigset_t *set, int signo);
//将指定信号signo添加到信号集set中,即把该信号对应的位置1

int sigdelset(sigset_t *set, int signo);
//从信号集set中移除指定信号signo,即把该信号对应的位清零

int sigismember(const sigset_t *set, int signo);
//检查信号signo是否在信号集set中,即判断该信号对应的位是否为1

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//可以读取或更改进程的信号屏蔽字

how:

set:

为NULL时,不修改屏蔽字,仅读取旧值到oldset。

oldset:

输出型参数:保存修改前的信号屏蔽字。

为NULL时,不保存旧值。

复制代码
#include <signal.h>
int sigpending(sigset_t *set);
//用于获取已经发给进程,但被阻塞住还没处理的信号

代码示例:

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

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

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

int main()
{
    // 0. 对2号信号进行自定义捕捉
    signal(2, handler);
    // 1. 对2号信号进行屏蔽 --- 数据预备
    sigset_t bset, oset;//在栈上开辟的空间,属于用户区
    sigemptyset(&bset);
    sigemptyset(&oset);
    sigaddset(&bset, 2);
    // 1.1 调用系统调用,将数据设置进内核
    sigprocmask(SIG_SETMASK, &bset, &oset);//屏蔽2号信号
    // 2. 重复打印当前进程的pending
    sigset_t pending;
    int cnt = 0;
    while(true)
    {
        // 2.1 获取
        int n = sigpending(&pending);
        if(n < 0) continue;
        // 2.2 打印
        PrintPending(pending);
        sleep(1);
        cnt++;
        // 2.3 解除阻塞
        if(cnt == 4)
        {
            cout << "unblock 2 signo" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    // 3. 发送2号信号 
    return 0;
}

运行结果:

1~31个信号中,只有9和19号信号不能被屏蔽。

四、信号的捕捉

1、信号什么时候被处理的?

当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理。

内核态:允许我们访问操作系统的代码和数据。

用户态:只能访问用户自己的代码和数据。

2、谈谈地址空间

有几个进程,就有几份用户级页表 --- 进程具有独立性。

内核级页表只有一份。

每一个进程看到的3~4GB的东西都是一样的。整个系统中,不管进程如何切换,3~4GB空间的内容是不变的。

进程视角:我们调用系统中的方法,就是在我们自己的地址空间中进行执行的。

操作系统视角:任何一个时刻,都有进程在执行。我们想执行操作系统的代码,可以随时执行。

操作系统的本质:基于时钟中断的一个死循环。

计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断。

3、信号的捕捉流程

4、sigaction(注册信号处理函数)

act是输入型参数,oldact是输出型参数。

sa_handler:处理方法,sa_mask:用于屏蔽更多信号。

代码示例:

复制代码
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>

1. pending位图,在执行信号捕捉方法之前从1->0,先清零,再调用
2. 信号被处理时,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

 void PrintPending()
 {
     sigset_t set;
     sigpending(&set);

     for(int signo = 1; signo <= 31; signo++)
     {
         if(sigismember(&set, signo))
         cout << "1";
         else
         cout << "0";
     }
     cout << "\n";
 }

 void handler(int signo)
 {
     PrintPending();
     cout << "catch a signal, signal number: " << signo << endl;
     // while(true)
     // {
     //     PrintPending();
     //     sleep(1);
     // }
 }
 int main()
 {
     struct sigaction act, oact;
     memset(&act, 0, sizeof(act));
     memset(&oact, 0, sizeof(oact));

     // sigemptyset(&act.sa_mask);
     // sigaddset(&act.sa_mask, 1);
     // sigaddset(&act.sa_mask, 3);
     // sigaddset(&act.sa_mask, 4);
     act.sa_handler = handler;
     sigaction(2, &act, &oact);

     while(true)
     {
         cout << "I am a process: " << getpid() << endl;
         sleep(1);
     }
     return 0;
 }

5、可重入函数

insert函数被main和handler执行流重复进入,最后导致node2节点丢失,造成内存泄漏。

当一个函数被重复进入时,出错或可能出错,叫做不可重入函数,否则,叫做可重入函数。

大部分函数都是不可重入的。

6、volatile

关键字 volatile:防止编译器过度优化,保持内存的可见性。

代码示例:

复制代码
 volatile int flag = 0;

 void handler(int signo)
 {
     cout << "catch a signal: " << signo << endl;
     flag = 1;
 }

 int main()
 {
     signal(2, handler);

     while(!flag);
     //在优化条件下,flag变量可能直接被优化到CPU内的寄存器中
     cout << "process quit normal" << endl;

     return 0;
 }

mysignal:mysignal.cc
	g++ -o $@ $^ -O3 -g -std=c++11// -O3:最高级别的优化选项
.PHONY:clean
clean:
	rm -f mysignal

7、SIGCHLD信号

子进程退出的时候,会主动向父进程发送SIGCHLD(17)信号。

代码示例:

复制代码
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.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) // 非阻塞等待,若子进程不退出,返回0
    {
        cout << "I am process: " << getpid() << "catch a signo: " << signo <<
        "child process quit: " << rid << endl;
    }
}

int main()
{
    signal(17, SIG_IGN);//SIG_DFL -> action -> IGN
    // srand(time(nullptr));
    // signal(17, handler);

    for(int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            while(true)
            {
                cout << "I am 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号信号,SIG_IGN和SIG_DFL的区别:

1)signal(SIGCHLD, SIG_IGN)

父进程会忽略子进程结束的通知,内核会自动回收子进程的资源(避免产生僵尸进程)。

父进程不需要调用wait()或waitpid()来回收子进程。

2)signal(SIGCHLD, SIG_DFL)

系统默认行为是忽略信号,但内核不会自动回收子进程资源,子进程会变成僵尸进程。

父进程需要手动调用wait()或waitpid()来回收子进程的退出状态。

相关推荐
funnycoffee1232 小时前
Linux查看版本号命令cat /etc/os-release
linux·服务器
xiaoliuliu123453 小时前
Kylin V10 安装 compat-gcc-44-4.4.7-8.el7.x86_64.rpm 详细步骤
linux·运维·服务器
蓝黑20203 小时前
Ubuntu Linux安装搜狗拼音输入法
linux·ubuntu·输入法·pinyin
峰顶听歌的鲸鱼3 小时前
Kubernetes-Pod
linux·运维·云原生·容器·kubernetes·云计算
敲代码的哈吉蜂3 小时前
haproxy——socat热更新工具
linux·运维·服务器
IvanCodes3 小时前
十、Linux Shell脚本:流程控制语句
linux·云计算
小庄梦蝶4 小时前
宝塔使用nodejs管理器下载nodejs版本失败解决方式之一
linux·运维·前端
8125035334 小时前
DNS监控:生产实践
linux·网络·网络协议·tcp/ip·计算机网络
Unpredictable2224 小时前
Ubuntu 22.04 ROS2 Humble 源码编译安装 teb_local_planner 踩坑记录
linux·ubuntu·teb·自主导航·局部路径规划