Linux信号

基本概念

信号是linux系统提供的一种向指定进程发送特定事件的一种方式,信号产生式异步的;进程能识别信号,处理信号。

真正发送信号的事操作系统,本质是修改pcb内信号位图

信号产生:

通过kill命令发送信号或者键盘产生(如Ctrl+c),系统调用,软件条件(管道读端被关闭,写开着,SIGPIPE),异常产生信号。

系统调用

向目标发送信号:

向自己发送信号:

向自己发送6号信号:

软件条件:

指定时间后接受到14号SIGALRM信号。

异常产生信号:

除数为0,野指针都会出错发送信号,使进程中断。

查看信号:

bash 复制代码
kill -l

1-31 普通信号 32-64 实时信号

信号处理:

有三种:默认动作、忽略动作、自定义处理(信号的捕捉)

默认动作基本是:终止、暂停或者忽略。

查看默认动作:

bash 复制代码
man 7 signal

在后面(core和term基本是终止)

自定义捕捉(捕捉一次,后续一直有效,但是9号信号不允许自定义捕捉):

测试代码:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void hander(int sig)
{
    cout << "get!" << endl;
}
int main()
{
    signal(2, hander);
    while(1)
    {
        cout << "syx 666" << endl;
        sleep(1);
    }
    return 0;
}

发送2号信号:

bash 复制代码
kill -2 pid

使用kill系统调用发送信号:

cpp 复制代码
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<signal.h>
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        cerr << "error!" << endl;
        return 1;
    }
    int pid = stoi(argv[2]);
    int flag = stoi(argv[1]);
    kill(pid,flag);
}

./emitsig 2 pid就可以观察到同样的效果。

向自己发送6号信号:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
void hander(int sig)
{
    cout << "abort!" << endl;
}
int main()
{
    signal(6, hander);
    while(1)
    {
        cout << "syx 666" << endl;
        sleep(1);
        abort();
    }
    return 0;
}

结果(还是要终止):

发送alarm信号:

cpp 复制代码
#include<iostream>
using namespace std;
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int cnt = 0;
void hander(int sig)
{
    cout << cnt << endl;
    exit(1);
}
int main()
{
    signal(14, hander);
    alarm(1);
    while(1)
    {
        cnt++;
    }
    return 0;
}

在系统内部也会对闹钟做管理,当然也是一个结构体,按照未来的超时时间构建一个最小堆,alarm(0)取消闹钟,返回剩余时间。闹钟设置一次就会触发一次。

当出现错误,操作系统会向进程发送信号,一般都是终止进程,为了防止死循环,本质是释放进程的上下文数据,包括溢出标志数据和其他异常数据。

core和term

都叫做终止进程,区别是term是异常终止,core也是,但会帮我们形成一个debug文件。

查看限制;

复制代码
ulimit -a

把core file改一下大小,允许形成异常文件,保存进程退出时候的镜像数据(核心转储)

运行下面代码:

cpp 复制代码
#include<iostream>
int main()
{
    int a = 10;
    a /= 0;
    return 0;
}

就会发现生成了一个core文件:

可以协助我们进行调试。

阻塞信号

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

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

3.进程可以选择阻塞 (Block )某个信号。(阻塞和未决没关系)

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

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

在PCB中有一个pending位图,表示未决信号集,对应为为1的时候,表示该信号接收到。

signal函数就是用来信号函数的。

途中handler表式一个函数指针数组,用来处理对应信号(信号递达),对应信号就是对应下标。

block和pending一样是一个32位位图,比特位对应相应信号,表示信号是否阻塞,当block对应为1,pending对应位信号不能递达,只有阻塞被解除,才能递达。

sigset_t

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。

sigset_t就是linux操作系统给用户的数据类型。

信号集操作函数

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set); //清空
int sigfillset(sigset_t *set);  //全部置1
int sigaddset (sigset_t *set, int signo); //对应位置1
int sigdelset(sigset_t *set, int signo); //对应位置0
int sigismember(const sigset_t *set, int signo); //信号是否在集合中

sigprocmask

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

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

how:

第二个是输入型参数,第三个是输出型参数,修改前保存老的信号屏蔽字.

sigpending

获取pending位图,成功返回0,失败返回-1。

屏蔽2号信号代码

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void PrintPending(sigset_t& pending)
{
    for (int i = 31; i > 0;i--)
    {
        if(sigismember(&pending,i))
        {
            cout << "1";
        }
        else
        cout << "0";
    }
    cout << endl;
}
void hander(int sig)
{
    cout << "unblock" << endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
}
int main()
{
    signal(2, hander);
    // 屏蔽2号信号
    sigset_t old_set, block_set;
    sigemptyset(&old_set);
    sigemptyset(&block_set);
    sigaddset(&block_set, 2);//此处并没有修改pcb的block表
    //修改pcb的block表
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    int cnt = 10;
    while (1)
    {
        if(cnt==0)//解除屏蔽
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
        cnt--;
    }
    return 0;
}

最终现象是发送2号信号后,对应的比特位在屏蔽时是1,解除后为0(递达之前置0);解除信号屏蔽一般会立即处理当前被解除的型号。

信号处理

忽略信号:SIG_IGN

默认动作:SIG_DEF

信号被捕捉不会立即处理,而是在进程从内核态返回到用户态的时候,进行处理。

操作系统不能直接转过去执行用户提供的的方法,而是要切换用户心态去执行。信号捕捉要经历四次状态切换(自定义处理函数)。

内核级页表只有一张,多个进程共用一张,访问操作系统和访问库函数差不多,由于操作系统不信任用户,所以用户访问操作系统只能通过系统调用。

OS中断机制:

操作系统的本质就是一个死循环,时钟中断,不断调度系统任务;在操作系统源码中会有一个系统调用的函数指针数组,我们只需要找到特定数组下标,就能使用对应的系统调用。调用封装的系统调用的函数,会把对应系统调用数组的下标保存到寄存器中。由外部形成的中断叫外部中断,外部器件发送中断信号,内部直接形成的中断叫陷阱、缺陷(0x80)。

当系统调用的时候,需要把状态切换为内核态(由3置0),否则会被拦截。

信号捕捉

除了signal捕捉信号的方式,还有一个:

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

第二个参数是输入型参数,第三个参数是输入型参数。对特定信号做捕捉,oact是记录更改之前的act,以便于回复。

捕捉2号信号例子:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
    cout << "get " << sig << endl;
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(2, &act, &oact);
    while(1)
    {
        cout << "hello!" << endl;
        sleep(1);
    }
    return 0;
}

当我们正在处理特定信号的时候,特定信号会被屏蔽,处理完成会解除屏蔽,这是为了防止递归调用导致崩溃。

在sigaction中的sa_mask表示在处理特定型号时,需要对其他信号也屏蔽,如对三号信号屏蔽:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
    cout << "get " << sig << endl;
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    act.sa_flags = 0;
    sigaction(2, &act, &oact);
    while(1)
    {
        cout << "hello!" << endl;
        sleep(1);
    }
    return 0;
}

但是有一些信号是不能被屏蔽的,如9号信号。

可重入函数

main函数在执行insert的时候,head还没有改变就接收到信号,去头插入另一个节点,就造成了节点丢失,内存泄露,这里的insert就被重复进入了(被重入了)。因此,如果重复进入的函数会出问题了,该函数就叫做不可重入函数,我们使用的大部分函数都是不可重入函数(使用全局变量基本都是不可重入的)。

volatile

来看一个现象:

cpp 复制代码
#include<iostream>
using namespace std;
#include<signal.h>
int flag = 0;
void handle(int sig)
{
    cout << "get:" << sig << endl;
    flag = 1;
}
int main()
{
    signal(2, handle);
    while (!flag);
    return 0;
}

运行后发现程序竟然终止不掉了。

这是因为编译器优化,cpu发现多次访问flag,就把flag放在寄存器中了,while(!flag)访问寄存器中的数据,而handle修改的是内存,因此一直死循环。

对此,为了防止出现这种问题,我们要求保持内存可见性,每次从内存中读取,就使用volatile关键字:

cpp 复制代码
volatile int flag = 0;

SIGCHLD信号

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

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void notice(int sig)
{
    cout << "child quit!" << endl;
}
int main()
{
    signal(SIGCHLD, notice);
    pid_t id = fork();
    if(id==0)
    {
        cout << "i am child" << endl;
        sleep(1);
        exit(1);
    }
    int cnt = 10;
    while(cnt--)
    {
        cout << "father" << endl;
        sleep(1);
    }
    cout << "over!" << endl;
    return 0;
}

但是有个问题:多个子进程同时退出,都会向父进程发送信号,但是pending表只改了一次,因此,应该这么改:

cpp 复制代码
void notice(int sig)
{
    int flag = 0;
    while (!flag)
    {
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if(id>0)
        {
            cout <<id<< " child quit!" << endl;
        }
        else
            flag = 1;
    }
}

因此为了避免造成僵尸进程,就可以用这种方式(可以将处理函数设为SIG_IGN,这个忽略和列表中原始的忽略不同,它会自动回收资源)。

相关推荐
拾光Ծ11 小时前
【linux】环境变量(详解)
linux·运维·服务器
落羽的落羽11 小时前
【C++】并查集的原理与使用
linux·服务器·c++·人工智能·深度学习·随机森林·机器学习
虾..19 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙19 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
hkhkhkhkh12321 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen1 天前
Linux字符串处理
linux·string
张童瑶1 天前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功1231 天前
什么是SELinux
linux
石小千1 天前
Linux安装OpenProject
linux·运维
柏木乃一1 天前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程