Linux信号

发布于:2025-09-11 ⋅ 阅读:(19) ⋅ 点赞:(0)

基本概念

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

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

信号产生:

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

系统调用

向目标发送信号:

向自己发送信号:

向自己发送6号信号:

软件条件:

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

异常产生信号:

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

查看信号:

kill -l

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

信号处理:

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

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

查看默认动作:

man 7 signal

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

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

测试代码:

#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号信号:

kill -2 pid

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

#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号信号:

#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信号:

#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改一下大小,允许形成异常文件,保存进程退出时候的镜像数据(核心转储)

运行下面代码:

#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操作系统给用户的数据类型。

信号集操作函数

#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可以读取或更改进程的信号屏蔽字(阻塞信号集)。

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

how:

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

sigpending

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

屏蔽2号信号代码

#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捕捉信号的方式,还有一个:

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

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

捕捉2号信号例子:

#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表示在处理特定型号时,需要对其他信号也屏蔽,如对三号信号屏蔽:

#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

来看一个现象:

#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关键字:

volatile int flag = 0;

SIGCHLD信号

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

#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表只改了一次,因此,应该这么改:

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,这个忽略和列表中原始的忽略不同,它会自动回收资源)。


网站公告

今日签到

点亮在社区的每一天
去签到