【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);

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

相关推荐
Peter·Pan爱编程15 分钟前
Docker在Linux中安装与使用教程
linux·docker·eureka
kunge20131 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5491 小时前
DVWA中级
linux
m0_748254091 小时前
2025最新华为云国际版注册图文流程-不用绑定海外信用卡注册
服务器·数据库·华为云
MUY09901 小时前
应用控制技术、内容审计技术、AAA服务器技术
运维·服务器
楠奕1 小时前
elasticsearch8.12.0安装分词
运维·jenkins
Sadsvit2 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok2 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux
java资料站2 小时前
Jenkins
运维·jenkins
苦学编程的谢3 小时前
Linux
linux·运维·服务器