【Linux】信号

目录

信号的基本概念

信号的产生(5种方法)

(1)kill命令,向指定进程发送指定的信号

(2)键盘产生信号

(3)系统调用

(4)软件条件产生信号

(5)硬件异常产生信号硬件异常产生信号)

总结

信号的保存

sigprocmask

sigpending

总结

信号的处理

信号的捕捉(自定义动作)

signal

sigaction

用户态VS内核态

可重入函数

SIGCHLD信号


信号的基本概念

信号:Linux系统提供的一种,向指定进程发送特定事件的方式,能做识别和处理;

信号是异步产生的;

信号是进程之间事件异步通知的一种方式,属于软中断;

用kill -l命令可以察看系统定义的信号列表:

每个信号都有一个编号和一个宏定义名称 ,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2

一个信号要先产生,产生后不一定会立即执行这个信号,有时候会先等待,等到时机合适的时候才会执行,所以对于产生的信号我们还要进行保存,时机合适的时候我们再对信号进程处理;

所以接下来我们将对信号进行三个主要方面的解释:信号的产生,信号的保存,信号的处理;

信号的产生(5种方法)

(1)kill命令,向指定进程发送指定的信号

我们写一个while(true)循环,一直打印;运行后,再另外一个窗口可以对这个进程用kill直接发送信号;

kill 信号 进程的pid

(2)键盘产生信号

我们平时终止一个进程时经常会使用ctrl+c,这其实也是向进程发送一个信号,进程收到这个信号,就会终止;

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump;

(3)系统调用

kill函数:可以给任意进程发送任意的信号;

其实kill命令是调用kill函数实现的

代码:

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

//./mykill 2 1234
int main(int argc ,char *argv[])
{
    if(argc!=3)
    {
        cout<<"Usage: "<<argv[0]<<"signum pid"<<endl;
        return 1;
    }
    pid_t pid=stoi(argv[2]);
    int sig =stoi(argv[1]);
    kill(pid,sig);
    return 0;
}

运行结果:

rasie函数:给当前进程发送指定的信号(自己给自己发信号)

    while(true)
    {
        cout<<"hello world,pid:"<<getpid()<<endl;
        raise(2);
        sleep(1);
    }

运行:运行到raise函数时,会向进程发送2号信号;

abort函数:abort函数使当前进程接收到信号而异常终止;

就像exit函数一样,abort函数总是会成功的,所以没有返回值

    while(true)
    {
        cout<<"hello world,pid:"<<getpid()<<endl;
        abort();
        sleep(1);
    }

运行:

(4)软件条件产生信号

alarm函数:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

返回值:函数的返回值是0或者是以前设定的闹钟时间还余下的秒数

int main()
{
    alarm(1);
    while(true)
    {
         cout<<"get a pid:"<<getpid()<<endl;
    }
    return 0;
}

运行:

(5)硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

默认终止进程有两种:core,term;

term:异常终止;

core:异常终止,但会帮我们形成debug文件,进程退出时镜像数据(核心转储);

云服务器默认是关闭核心转储的,因为不断的核心转储,不断的dump,会使磁盘打满;

可以用ulimit -a来查询core文件是否开启核心转储;

没有的话可以用ulimit -c 10240来开启;

为什么要有core?

可以协助我们进行debug调试

我们在编译代码时:g++ ...... -g;

加上-g的话可以允许程序被调试:gdb;

core -file core文件名----->用gdb可以直接定位出错

这时我们就要解决一下历史遗留问题了;

进程有运行完正常终止,也有异常状态,在被信号所杀的时候,会有core dump标志;

总结

信号的有OS发出,上面的5种产生情况,都是操作心态发出信号的触发条件;

(1)上面所有信号的产生都会需要操作心态的参与,为什么?

因为操作系统是进程的管理者,只要操作系统能管理进程。

(2)OS是怎么向进程发送信号的?

普通信号有31个,剩余的是实时信号,进程PCB中有数据结构,位图(只需要一个整个即可),来记录当前进程是否收到对应位置的信号,OS向进程发送信号时,将对应位置置1即可;

信号的保存

  • 执行信号的处理动作:信号递达;
  • 产生到递达之间的状态:信号未决;
  • 进程可选择阻塞某个信号;阻塞一个信号,对应的信号一旦产生,永不递达,一直未决,直到主动接触阻塞;

在内核中信号在内存中的表示:示意图

关于block和pending位图:普通信号是1~31,只要一个整数(32位)就可以记录所有普通信号;

block位图:位图的位置表示信号的编号,位图的内容表示信号是否阻塞

pending位图:位图的位置表示信号的编号,位图的内容表示信号是否收到

handle:函数指针数组,是指定信号递达的方式(信号处理);信号编号就是数组下标,可采用信号编号;

怎么让系统快速对block和pending位图进行操作呢?

用sigset_t(信号集);--->Linux给用户提供的一个用户级的数据类型(禁止用户直接设置);

既然禁用户直接设置,那如何操作修改位图呢?

可以调用信号集操作函数:

#include <signal.h>

  • int sigemptyset(sigset_t *set);初始化,使其中所有信号的对应bit清零
  • int sigfillset(sigset_t *set);初始化,其中所有信号的对应bit置位;
  • int sigaddset (sigset_t *set, int signo);在该信号集中添加某种有效信号
  • int sigdelset(sigset_t *set, int signo);在该信号集中删除某种有效信号
  • int sigismember(const sigset_t *set, int signo);布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

返回值:若成功则为0,若出错则为-1

how参数:

set:输入型

oldset:输出型

sigpending

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

总结

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.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<<endl;
}
void handler(int sig)
{
    cout<<"get a pid:"<<getpid()<<endl;
}
int main()
{
    signal(2,handler);
    //1、屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);//初始化
    sigemptyset(&old_set);//初始化
    sigaddset(&block_set,2);//添加2号信号屏蔽
    sigprocmask(SIG_BLOCK,&block_set,&old_set);//完成真正的修改
    int cnt=10;
    cout<<"get a pid:"<<getpid()<<endl;
    while(true)
    {
        sleep(1);
        //2、获取当前进程的pending进程
        sigset_t pending;
        sigpending(&pending);

        //3、打印pending信号集
        PrintPending(&pending);
    }
    return 0;
}

上述代码,我们将2号信号进行屏蔽后,在去发送2号信号,2号信号不会被执行,观察pending位图,在发送完2号信号后,2号信号处于了未决状态;

注意:阻塞和忽略信号不同,阻塞信号,信号处于未决状态,并不是达到信号,忽略是在递达信号;

解除2号信号的屏蔽:

int main()
{
    signal(2, handler);
    // 1、屏蔽2号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);                      // 初始化
    sigemptyset(&old_set);                        // 初始化
    sigaddset(&block_set, 2);                     // 添加2号信号屏蔽
    sigprocmask(SIG_BLOCK, &block_set, &old_set); // 完成真正的修改
    int cnt = 10;
    cout << "get a pid:" << getpid() << endl;
    while (true)
    {
        sleep(1);
        // 2、获取当前进程的pending进程
        sigset_t pending;
        sigpending(&pending);

        // 3、打印pending信号集
        PrintPending(&pending);
        cnt--;
        // 4、解除对2号信号的屏蔽
        if (cnt == 0)
        {
            cout << "2号信号解除屏蔽" << endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
    }
    return 0;
}

通过运行结果我们会发现,解除屏蔽后,一般会立即处理当前被解除的信号;

pending位图对应的信号被清零是在递达之前;

信号的处理

信号处理动作有三种:

  • 忽略动作(SIG_IGN)
  • 默认动作(SIG_DFL)
  • 自定义动作 (handler)

信号的捕捉(自定义动作)

信号可能不会被立即处理,而是在合适的时候处理;这个合适的时候就是进程从内核态返回到用户态的时候,检测并处理信号;

注意: 9号信号是不会被捕捉,不会被屏蔽的;

signal
void handler(int sig)
{
    cout << "get a pid:" << getpid() << endl;
}
int main()
{
    signal(2, handler);
    ......
}
sigaction

act:输入型

oldact:输出型

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.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 << endl;
}
void handle(int sig)
{
    cout<<"get a pid"<<getpid()<<endl;
}
int main()
{
    struct sigaction act;
    struct sigaction oldact;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    act.sa_handler=handle;
    sigaction(2,&act,&oldact);//自定义2号信号

    sigset_t pending;
    sigset_t block_set;
    sigset_t old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,2);
    sigprocmask(SIG_SETMASK,&block_set,&old_set);//将2号信号阻塞
    int cnt=10;
    while(true)
    {
        sleep(1);
        sigemptyset(&pending);
        sigpending(&pending);
        PrintPending(&pending);
        cnt--;
        if(cnt==0)
        {
            sigprocmask(SIG_SETMASK,&old_set,&block_set);
            cout<<"2号信号阻塞清除"<<endl;
        }
    }
    return 0;
}

运行结果:

用户态VS内核态

计算机在运行程序时,会有两种状态,用户态和内核态。

当程序运行的是用户自己编写的代码,并没有涉及中断,异常会在系统调用时,计算机会处于用户态。

当程序运行到中断,异常或者系统调用时,计算机会处于内核态。内核态就相当于是操作系统。

但一个程序在运行时,可能在不断进行内核态和用户态的切换。

计算机中怎么能实现用户态和内核态的互相切换?

因为在虚拟地址空间有两个区域,一个是用户区,一个是内核区。其中,用户区映射的是当计算机处于用户态时,要执行的代码和数据。内核区映射的是计算机处于内核态时,要执行的代码和数据。

当计算机处于用户态时,在虚拟地址空间的用户区,通过用户级页表,找到代码和数据执行;

当计算机处于内核态时,在虚拟地址空间的内核区,通过内核级页表,找到代码和数据执行;

怎么知道计算机现在处于用户态还是内核态??

CPU中有一个及存储CR0,里面有标志位记录了计算机处于内核态还是用户态;

注意:OS不相信任何人,所有用户访问【3,4】GB的地址空间时,受到一定的约束----->只能通过系统调用来访问;

可重入函数

可重入函数就是当一个执行流进入函数,会发生信号的捕捉处理,而处理的还是执行这个函数,如果不会出现错误就是可重入函数;如果发生错误就是不可重入函数;

比如链表的插入:

此时node2就找不到了,造成了内存泄漏,所以是不可重入函数;

SIGCHLD信号

父进程创建子进程需要以调用wait或者waitpid来等待子进程退出,不然子进程会变成僵尸进程。

子进程退出时,并不是静悄悄的退出,而是会给父进程发送信号 ------>SIGCHLD

怎么证明呢?------>通过捕捉信号

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

using namespace std;

void handle(int sig)
{
    cout<<"SIGCHLD : pid: "<<getpid()<<endl;
}
int main()
{
    signal(SIGCHLD,handle);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int cnt = 10;
        while (true)
        {
            sleep(1);
            cout << "get a child process,pid:" << getpid() << endl;
            cnt--;
            if (cnt == 0)
            {
                sleep(10);
                exit(1);
            }
        }
    }
    // father
    sleep(200);
    waitpid(-1, nullptr, 0);
    return 0;
}

父进程对SIGCHLD处理方式如果为SIG_ING忽略,子进程不会进入僵尸状态,会自动清理子进程的空间和数据结构

signal(SIGCHLD,SIG_IGN);

以上就是信号的全部内容,希望有所帮助!!!

相关推荐
麻花20134 分钟前
WPF里面的C1FlexGrid表格控件添加RadioButton单选
java·服务器·前端
韦德斯9 分钟前
嵌入式Linux的RTC读写操作应用
linux·运维·c语言·arm开发·实时音视频
程序员JerrySUN13 分钟前
熟悉的 Docker,陌生的 Podman
linux·docker·容器·系统架构·podman
a_安徒生13 分钟前
window系统改为Linux系统
linux·windows·centos·系统安全
C++忠实粉丝19 分钟前
计算机网络socket编程(2)_UDP网络编程实现网络字典
linux·网络·c++·网络协议·计算机网络·udp
哎呦喂-ll25 分钟前
Linux进阶:常用操作
linux·运维·服务器
m0_6446973327 分钟前
DNS域名解析服务器
linux·运维·服务器
byte轻骑兵43 分钟前
嵌入式 ARM Linux 系统构成全解:从硬件到应用层层剖析
linux·arm开发·arm·嵌入式开发
Oliver_LaVine1 小时前
linux搭建Gray
运维
gobeyye1 小时前
Docker 用法详解
运维·docker·容器