<Linux系统复习>信号

一、本章重点

1、什么是信号?

2、查看信号列表

3、信号捕捉

4、信号产生的5种方式

5、介绍CoreDump

6、信号处理的方式

7、如何理解信号产生到处理的过程

8、sigpending、sigprocmask、sigaction函数的使用

9、信号处理的时机

10、SIGCHLD信号

11、可重入函数

01 什么是信号?

生活中的信号:红绿灯、下课铃声、闹钟铃声等等。

当信号出现的时候,我们之前就知道信号该如何处理,并且不被要求立即处理该信号,因为有时我们正在处理重要的事情。(类比闹钟铃声)

技术应用角度的信号:当进程执行除0代码的时候是如何被操作系统终止的?本质是因为进程收到了信号,在合适的时机处理该信号的时候被操作系统终止。

02 查看信号列表

如何查看Linux中信号列表?

使用命令:kill -l

 需要知道的是没有32、33号信号,总共就62个信号。其中1-31是普通信号,34-64是实时信号。

03 信号捕捉

1、什么是信号捕捉?

简单来说就是改变信号的默认处理方式

介绍signal函数

 功能:对特定信号进行捕捉

使用方式:signal(要捕捉的信号编号,要执行的处理方式)

04 信号产生的5种方式

1、代码异常(如:除零错误、对野指针解引用)

2、命令行产生(如:kill -信号编号 pidkillall -信号编号 进程名

3、键盘组合键产生(如:ctrl + c、ctrl + z、ctrl + \ ,分别对应2,20,3号信号)

4、系统调用

5、软件条件

代码演示

1、异常

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

using namespace std;


int main()
{
    int cnt = 5;
    while(true)
    {
        cout<<"I am a process"<<endl;
        if(cnt == 0)
        {
            int num = 3/0;//除零错误
        }
        cnt--;
        sleep(1);
    }
    return 0;
}

 错误信息和信号列表对比大概可以看出除零错误属于8号信号

用signal函数对8号信号进行捕捉,执行我们的自定义的处理方式

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

using namespace std;

void handler(int signo)
{
    cout<<"收到了8号信号"<<endl;
    while(1)
    {
        cout<<"I am a process"<<endl;
        sleep(1);
    }
}

int main()
{
    signal(8,handler);
    int cnt = 5;
    while(true)
    {
        cout<<"I am a process"<<endl;
        if(cnt == 0)
        {
            int num = 3/0;
        }
        cnt--;
        sleep(1);
    }
    return 0;
}

 

2、命令行产生

①kill -信号编号 pid

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

using namespace std;


int main()
{
    int cnt = 5;
    while(true)
    {
        cout<<"I am a process"<<endl;
        sleep(1);
    }
    return 0;
}

②killall -信号编号 进程名

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

using namespace std;


int main()
{
    int cnt = 5;
    while(true)
    {
        cout<<"I am a process"<<endl;
        sleep(1);
    }
    return 0;
}

3、键盘组合键产生

①ctrl + c

②ctrl + z(强制当前进程转为后台,并使之停止)

jobs可以显示后台进程,fg + 后台编号可以将进程从后台放置到前台运行。

 

 ./test是前台运行该进程,./test &代表的是后台运行该进程

在后台运行进程时,解释器还能够解释命令,但由于都向同一个显示器打印,会造成互相干扰的情况,键盘组合键只能对前台进程有效,后台进程无效。

要想杀掉后台进程有两种方式:1、先fg 1 再 ctrl +c       2、kill -2 pid

③ctrl + \

如何知道ctrl + c、ctrl + z、ctrl + \分别给进程发的是什么信号?

答:对所有信号捕捉,然后再发送信号,信号捕捉函数就可以打印发送的信号。

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

using namespace std;

void handler(int signo)
{
    cout<<"进程收到了"<<signo<<"号信号"<<endl;
    sleep(1);
}
int main()
{
    for(int i = 1; i <= 31; i++)
    {
        signal(i,handler);
    }
    while(true)
    {
        cout<<"I am a process pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

 ctrl + c:向进程发送2号信号

ctrl + z:向进程发送20号信号

ctrl + \:向进程发送3号信号(注:有的朋友可能ctrl + \没有反映,具体原因可以百度解决

上述代码,还证明了并不上所有信号都能够被捕捉,比如9号信号就不能被捕捉,因为所有信号都能够被捕捉,那么这个进程真就“刀枪不入”了,连操作系统也拿它没办法了。

4、系统调用产生

①kill

功能:向任意进程发送任意信号

 写一个kill命令 

#include<iostream>
#include<sys/types.h>
#include<signal.h>

using namespace std;

int main(int argc,char** argv)
{
    if(argc!=3)
    {
        cout<<"Usage: kill -信号编号 pid"<<endl;
        return 1;
    }
    int signo = argv[1][1] - '0';
    //cout<<signo<<endl;
    int pid = atoi(argv[2]);
    //cout<<pid<<endl;
    kill(pid,signo);
    return 0;
}

②raise

功能:向当前进程发送任意信号

 

③abort

功能:向当前进程发送abort信号

 

 这个函数有点特殊,因为你就算捕捉了该信号还是会终止进程。

5、软件条件产生

1、管道的读端关闭,写端会收到SIGPIPE信号,处理该信号时被操作系统杀掉。

common.h

#include<iostream>
#include<stdio.h>
#include<cstdlib>
#include<sys/types.h>
#include<sys/stat.h>
#include<cstring>
#include<fcntl.h>
#include<unistd.h>
#include<signal.h>
#define PATH "./.fifo"
using namespace std;

 server.cc

#include "common.h"
 
int main()
{
    umask(0);
    if(mkfifo(PATH,0666)!=0)
    {
        cerr<<"mkfifo error"<<endl;
        return 1;
    }
    
    int fd = open(PATH,O_RDONLY);
    if(fd < 0)
    {
        cerr<<"open error"<<endl;
        return 2;
    }
 
 
#define SIZE 1024
    char buf[1024] = {0};
    while(true)
    {
        ssize_t s = read(fd,buf,sizeof(buf)-1);
        if(s==0)
        {
            cout<<"写端关闭"<<endl;
            unlink(".fifo");
            close(fd);
            exit(0);
        }
        else if(s>0)
        {
            buf[s] = '\0';
            cout<<"client->server: "<<buf<<endl;
        }
        else
        {
            cerr<<"read error"<<endl;
            close(fd);
            exit(3);
        }
    }
    return 0;
}

client.cc

#include "common.h"
 
 void handler(int signo)
 {
    cout<<"写端收到了"<<signo<<"号信号"<<endl;
    exit(1);
 }

int main()
{
    signal(SIGPIPE,handler);
    int fd = open(PATH,O_WRONLY | O_APPEND);
    if(fd<0)
    {
        cerr<<"open error"<<endl;
    }
 
    while(true)
    {
        char msg[1024] = {0};
        cout<<"请输入信息# ";
        fflush(stdout);
        if(fgets(msg,sizeof(char)*1024,stdin)!=nullptr)
        {
            //msg为 xxxx\n\0
            //下面的write我只传了xxxx
            write(fd,msg,strlen(msg)-1);
        }
        else
        {
            exit(0);
        }
    }
    return 0;
}

2、alarm函数

功能:自定义多少秒之后向进程发送一个alarm信号,该信号的默认处理方式是终止进程。

测试1秒钟服务器能够对cnt++多少次

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

using namespace std;

int cnt = 0;
void handler(int signo)
{
    cout<<cnt<<endl;
    exit(1);
}
int main()
{
    alarm(1);
    signal(SIGALRM,handler);
    while(true)
    {
        cnt++;
    }
    return 0;
}

05 介绍CoreDump

1、什么是CoreDump?(核心转储)

coredump是指当程序出错而异常中断时,OS会把程序工作的当前状态存储成一个coredump文件。然后我们可以通过该文件来定位异常的地方。

2、一般线上生产环境都不会自动打开CoreDump,需要我们手动打开。

ulimit -c 文件大小(一般1024的整数倍)

 

 3、试验一下CoreDump

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
using namespace std;

int main()
{
    cout<<"my pid is "<<getpid()<<endl;
    int* p = nullptr;
    *p = 3;
    return 0;
}

发生段错误,生成了core.25647文件,这个25647是什么意思呢?

本质其实就是异常进程的pid

生成了core.25647,我们可以通过gdb来分析CoreDump文件,定位异常。

 使用core-file core.26293可以导入coredump文件,然后gdb就可对该文件进行分析。

分析结果:进程终止是因为收到了11号信号,异常的地方是在test.cc文件的第10行。

06 信号处理的方式

1、默认处理方式

一般是终止该进程,可通过man 7 signal查看各个信号的默认处理方式

2、忽略

什么都不处理,只是将该信号从收到设置为未收到(ignore的缩写)

3、自定义

可通过signal函数或者sigaction函数自定义处理方式。

07 信号屏蔽字&&信号未决表&&信号处理函数

task_struct中存在三张表,分别是block表、pending表、handler表。

 block是一个32位的位图结构,从上到下0代表该信号没被阻塞,1代表该信号被阻塞。

pedding也是一个32位的位图结构,从上到下0代表该信号没被收到,1代表该信号被收到。

handler是一个函数指针数组,从上到下依次为每个信号对应的处理方式,SIG_DFL代表默认处理方式,SIG_IGN代表忽略。

pedding表说白了就是记录进程收到了哪些信号。

block表为了防止信号被处理而设计的,当某个信号被阻塞后,该信号就是递达了它都不会被处理,直到它被解除阻塞才能够被处理。

解释一下,给进程test使用组合键ctrl + c产生信号,操作系统做了什么?

首先,ctrl + c产生的是2号信号,因此操作系统会修改test进程控制块中的pending位图,将2号信号由0置为1,然后在合适时机处理该信号,当合适时机到来时,进程test执行2号信号的处理函数,由于该信号的处理函数是默认处理方式,那么操作系统会将test进程改为z状态并释放部分资源,最后被父进程回收test进程。

08 sigpending、sigprocmask、sigaction函数的使用

1、sigprocmask

功能:可以读取或更改进程的信号屏蔽字

成功返回0,出错返回-1

 其中how选项为SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK

set为输入型参数,设置新的阻塞信号集。

oldset为输出型参数,获取老的阻塞信号集。

在此之前需要介绍一下sigset_t 类型

sigset_t是一个32位的位图结构

对它进行操作的常用函数有:

①sigemptyset(sigset_t* set)

②sigfillset(sigset_t* set)

③sigaddset(sigset_t* set , signo)

④sigdelset(sigset_t* set , signo)

⑤sigismember(const sigset_t* set , signo)

现在我们使用sigprocmask函数对所有信号进行屏蔽

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

using namespace std;

int main()
{
    sigset_t new_block;
    sigset_t old_block;
    sigfillset(&new_block);
    sigemptyset(&old_block);

    sigprocmask(SIG_SETMASK,&new_block,&old_block);
    while(true)
    {
        cout<<"I am process"<<endl;
        sleep(1);
    }
    return 0;
}

 可以发现,绝大多数信号可以被屏蔽,但9号信号是无法被屏蔽的

使用一下sigprocmask中老的信号集

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

using namespace std;

int main()
{
    sigset_t new_block;
    sigset_t old_block;
    sigfillset(&new_block);
    sigemptyset(&old_block);

    sigprocmask(SIG_SETMASK,&new_block,&old_block);

    int cnt = 0;
    while(true)
    {
        cout<<"I am process"<<endl;
        sleep(1);
        if(cnt == 5)
        {
            cout<<"信号屏蔽字已经恢复到原来"<<endl;
            sigprocmask(SIG_SETMASK,&old_block,nullptr);
        }
        cnt++;
    }
    return 0;
}

 从上述现象可以看出,当有多个信号同时到来的时候,先处理信号编号小的。

2、sigpending

成功返回0,失败返回-1。

功能:读取当前进程的未决信号集

代码测试:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void ShowPending(const sigset_t& pending)
{
    for(int i = 1; i<=31; i++)
    {
        if(sigismember(&pending,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}

int main()
{
    sigset_t block;
    sigfillset(&block);
    sigprocmask(SIG_SETMASK,&block,nullptr);//阻塞所有信号

    sigset_t pending;
    sigemptyset(&pending);

    while(true)
    {
        sigpending(&pending);
        ShowPending(pending);
        sleep(1);   
    }
    return 0;
}

3、sigaction

成功返回0,失败返回-1

功能:可以读取和修改与指定信号相关联的处理动作

 sa_handler:设置普通信号的处理方式

 sa_sigaction:实时信号的处理方式,这里我们不管。

 sa_mask:在信号处理时,设置额外需要屏蔽的信号

 sa_flags:用来处理普通信号,设置为0就行。

 sa_restirer:不管。

代码测试

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

using namespace std;

void handler(int signo)
{
    cout<<"收到"<<signo<<"号信号"<<endl;
}

int main()
{
    //signal(2,handler);
    
    struct sigaction act;

    act.sa_handler = handler;
    act.sa_flags = 0;

    sigaction(2,&act,nullptr);//相当于signal(2,handler);

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

09 信号处理的时机

信号处理的时机:从内核态切换回用户态时会做信号检测,检测到有信号就会处理。

 当检测到有信号要处理时,就要处理信号。

对默认处理方式是终止进程,修改pending位图,然后返回用户态。

对忽略处理方式是修改pending位图,返回返回用户态。

对自定义的处理方式则需返回到用户态模式下处理该自定义函数,然后在返回用户态,在进行信号检测,如果没有信号,则返回用户态继续进行向下执行。

1、为什么要返回用户态默认执行自定义函数?

因为自定义函数是用户写的,必须要防止用户冒用操作系统的身份执行非法行为。

2、处理完自定义函数,为什么不能直接返回用户态继续执行剩下的代码?

因为你需要返回到内核态完成返回工作,比如你需要恢复上下文或者系统调用的返回值,这些都需要更高的权限来做,必须返回到内核态再回到用户态。

10 SIGCHLD信号

子进程退出时会给父进程发送SIGCHLD信号,对该信号的默认处理方式是忽略。

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

using namespace std;


void handler(int signo)
{
    cout<<getpid()<<"收到了"<<signo<<"号信号"<<endl;
}

int main()
{
    signal(SIGCHLD,handler);
    if(fork() == 0)
    {
        int cnt = 0;
        while(true)
        {
            cout<<"I am a child pid: "<<getpid()<<endl;
            if(cnt == 5)
            {
                exit(1);
            }
            cnt++;
            sleep(1);
        }
    }
    else
    {
        while(true)
        {
            cout<<"I am a father pid: "<<getpid()<<endl;
            sleep(1);
        }
    }
    return 0;
}

 但这种对SIGCHLD信号的处理方式在多个子进程同时退出则不能很好的处理。因为对于普通信号而言只有一个比特位来记录信号是否被收到(实时信号会被链表链接起来)。

多个子进程同时退出的现象

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>
#include<wait.h>
using namespace std;


void handler(int signo)
{
    cout<<getpid()<<"收到了"<<signo<<"号信号"<<endl;
    waitpid(-1,nullptr,0);
}

int main()
{
    signal(SIGCHLD,handler);
    for(int i = 0;i <= 5; i++)
    {
        if(fork() == 0)
        {
            int cnt = 0;
            while(true)
            {
                cout<<"I am a child pid: "<<getpid()<<endl;
                if(cnt == 5)
                {
                    exit(1);
                }
                cnt++;
                sleep(1);
            }
        }
    }
    
    while(true)
    {
        cout<<"I am a father pid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

这个代码的弊端:

①无法处理多个子进程同时退出的情况

②当子进程永远不退出时,则会一直阻塞住,因为waitpid使用的是0,代表阻塞等待。

改进后

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>
#include<wait.h>
using namespace std;


void handler(int signo)
{
    while(true)
    {
        int ret = waitpid(-1,nullptr,WNOHANG);
        if(ret > 0)
        {
            cout<<"等待成功->"<<ret<<endl;
        }
        else if(ret == 0)
        {
            cout<<"有的进程还未退出"<<endl;
            break;
        }
        else if(ret < 0)
        {
            cout<<"进程已全部退出"<<endl;
            break;
        }
    }
}

int main()
{
    signal(SIGCHLD,handler);
    for(int i = 0;i <= 5; i++)
    {
        if(fork() == 0)
        {
            int cnt = 0;
            while(true)
            {
                cout<<"I am a child pid: "<<getpid()<<endl;
                if(cnt == 5)
                {
                    exit(1);
                }
                cnt++;
                sleep(1);
            }
        }
    }
    
    while(true)
    {
        cout<<"I am a father pid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

最常用的做法是忽略SIGCHLD信号,当它退出时自动被操作系统回收。

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>
#include<wait.h>
using namespace std;



int main()
{
    signal(SIGCHLD,SIG_IGN);
    for(int i = 0;i <= 5; i++)
    {
        if(fork() == 0)
        {
            int cnt = 0;
            while(true)
            {
                cout<<"I am a child pid: "<<getpid()<<endl;
                if(cnt == 5)
                {
                    exit(1);
                }
                cnt++;
                sleep(1);
            }
        }
    }
    
    while(true)
    {
        cout<<"I am a father pid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

 有人或许有疑惑,SIGCHLD信号的默认处理方式不是忽略吗?你使用SIG_IGN处理方式不还是忽略吗?为啥手动写就能让子进程自动回收呢?

可以把这里当成一种特殊的情况,暂且只能记住了,或许之后会找到更好的答案。

11 可重入函数

要理解重入函数,先得理解重入的概念。

重入:一个执行流还未执行完毕,另一个执行就开始执行。

可重入函数:当执行流重入函数时,该函数不会出现不同的现象或者任何问题,则称这个函数是可重入的,否则是不可重入的。

大多数函数都是不可重入的,比如链表的头插函数。

 

所有的STL容器都是不可重入的,要想变得可重入得加锁保护,但加锁会损害STL容器的效率,这对以效率著称的STL是破坏性的,因此STL容器没有加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李逢溪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值