Linux进程信号

信号入门

信号是进程之间事件异步通知的一种方式,属于软中断。
kill -l命令可以察看系统定义的信号列表:
在这里插入图片描述
其中1号信号到31号信号叫做普通信号,从34到64号信号每一个信号都带了RT称为实时信号。
在这里插入图片描述

信号的本质

进程收到信号其实不是立即处理的,而是选择在合适的时候。因为信号的产生是在进程运行的任何时间点都可以产生的,有可能进程正在做更重要的事情。
因为信号不是立即处理的,所以信号一定要先被保存起来。
在哪里保存?
进程的PCB,进程控制块task_struct。
如何保存?
对进程而言,核心要保存的是"是否有信号"+“是哪个信号”。信号编号1-31一共31个,所以使用位图表示。
比特位的位置代表是哪个信号,比特位的内容(0 or 1)代表是否有信号。
00000000 00000000 00000000 00000000
二进制序列全0证明没有收到任何信号。
00000000 00000000 00000000 00000010
代表收到了2号信号。
谁发的,如何发?
发送信号的本质,就相当于写对应进程的task_struct信号位图。
因为OS是进程的管理者,有能力和义务对进程数据做修改。
信号的本质:信号是OS发送的,通过修改对应进程的信号位图(0->1),完成信号的发送。

信号的处理

1.执行该信号的默认处理动作。(部分是终止进程,部分有特定的功能)
2.忽略此信号。
3.自定义方式捕捉信号。提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

产生信号

通过终端按键产生信号

有一个mysignal.cc

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

int main()
{
    while(1)
    {
        std::cout << "i am a process: " << getpid() << std::endl;
        sleep(1);
    }
}

在这里插入图片描述
当在键盘中Ctrl+c其实就是向进程发送2号信号。
./mysignal进程在前台跑起来,当Ctrl+c的时候其实是向前台进程发送二号信号SIGINT
进程在收到信号的时候,一共31个信号,对于相当一部分信号而言,当进程收到信号的时候,默认的处理动作就是终止当前进程。
注意
1.Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2.Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
3.前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
另外还可以使用Ctrl+\ 来终止进程
在这里插入图片描述Ctrl+\实际上是向进程发送3号信号SIGQUIT。
也可以通过kill命令的方式向进程发送信号:kill -信号 进程pid
在这里插入图片描述

调用系统函数向进程发信号

abort函数

#include <stdlib.h>
void abort(void);

abort函数使当前进程接收到信号SIGABRT(6号)信号而异常终止,就像exit函数一样,abort函数总是会成功的,所以没有返回值。
raise函数

#include <signal.h>
int raise(int sig);

raise函数给当前进程发送指定信号,参数传几号就发几号。成功返回0,失败返回非0值。
kill函数

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

第一个参数表示想给哪个进程发信号,第二个参数表示想发几号信号。
返回值成功返回0,失败返回-1。

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

// kill pid signo
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
        exit(1);
    }

    kill(atoi(argv[2]), atoi(argv[1]));
    return 0;
}

有一个运行的进程,向该进程发送9号信号:
在这里插入图片描述
实际上Linux中的kill命令也是通过kill系统调用接口实现的。

由软件条件产生信号

进程间通信使用管道的时候,读端不光不读管道,而且把自己的读文件描述符还关了,写端此时一直在写的时候,写端会收到SIGPIPE(13号)信号,进而写进程退出。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM(14号)信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。

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

int main()
{
    alarm(1);	// 1s之后,会给目标进程发送SIGALRM信号
    int count = 0;
    while(1)
    {
        std::cout << count++ << std::endl;
    }
    return 0;
}

设置一个闹钟,时间为1s。
在这里插入图片描述

硬件异常产生信号

Core Dump
在这里插入图片描述
进程等待时有两个函数wait和waitpid,这两个函数都有一个输出型参数status,这个参数是一个整型变量,由操作系统填充,获取进程的退出信息返回给父进程。
在status的低16位中,高8位表示进程的退出状态即退出码,若进程被信号杀掉,低7七位表示终止信号,第8位是core dump标志。一旦发生核心转储,core dump标志位就会被设置为1。
在Linux下使用ulimit -a查看系统的各种资源内容。
在这里插入图片描述
ulimit -c 1024把core file size的大小调整成1024。
在这里插入图片描述
一旦放开core dump后,进程运行的时候收到信号,就会在当前目录形成一个core文件。core后面跟个进程pid。
在这里插入图片描述

core dump叫做核心转储,当一个进程在运行时突然崩溃了,OS将进程运行时的核心数据dump到磁盘上,方便用户进行调试使用。
一般而言,线上环境核心转储是被关闭的。因为程序崩一次,就要dump一次,占用内存资源。
现在有这样一份代码,其中第13行有除0错误:

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

int main()
{
    while(1)
    {
        std::cout << "i am a process: " << getpid() << std::endl;
        sleep(1);
        int a = 10;
        int b = 0;
        a /= b;
    }
    return 0;
}

程序在运行1s之后core dump了。
在这里插入图片描述
使用gdb调试程序,用core-file把生成的core文件导入,这时gdb就会定位错误。
在这里插入图片描述
terminated with signal 8表示进程收到8号信号,in main () at mysignal.cc:13表示错误的代码行数是在mysignal中main函数的第13行,并且给出了出错的语句。这种调试方案叫做事后调试。
硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE(8号)信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV(11号)信号发送给进程。
当程序出现问题时,例如越界,除0,野指针等,当运行程序时,站在语言的角度叫程序崩溃了,站在系统的角度,叫做进程收到了信号。

阻塞信号

信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。信号递达的方式其实就三种:默认、忽略、自定义捕捉。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号在内核中的表示

信号在内核中的表示示意图:
在这里插入图片描述
pending位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否收到信号。OS发送信号本质是修改task_struct中pending位图的内容。
handler数组:用信号的编号作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)。
block位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否阻塞该信号。
如果没有收到对应的信号,照样可以阻塞特定信号。阻塞更准确的理解,可以理解成为一种“状态”。
所以实际上检测信号递达的时候,第一步看pending中看是否收到信号,发现有这个信号,第二步发现对应block位图中是0,也就是不会被阻塞,然后才会执行hander数组中对应的处理方法,否则block中如果为1,即便pending位图中为1也不会被递达。
检测信号是否会被递达,是否被阻塞,都是OS的任务。

sigset_t

pending位图中每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数解释:
sigemptyset:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
sigfillset:初始化set所指向的信号集,使其中所有信号的对应bit位置为1。
sigaddset:把特定的信号设置到信号集当中,指定位设置为1。
sigdelset:指定位设置为0。
这四个函数都是成功返回0,出错返回-1。
sigismember:判断信号集中是否包含特定信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数解释:
第一个参数表示想怎样调用这个函数,有三个选项:

选项功能
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。
返回值调用成功返回0,出错返回-1。

sigpending

sigpending函数读取当前进程的未决信号集,并通过set参数传出。

#include <signal.h>
int sigpending(sigset_t *set);

参数set是一个输出型参数,获取进程的pending信号位图。
返回值调用成功返回0,出错返回-1。


下面做一个简单的实验,实验步骤:
1.屏蔽(阻塞)2号信号。
2.不断的获取pending信号集并打印。
3.发送2号信号给进程。
4.过一段时间恢复对2号信号的block(取消2号的block)。
5.2号信号立马会被递达
6.依旧打印pending位图。

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

using namespace std;

void handler(int signo)     // 自定义捕捉
{
    cout << "get a signo: " << signo << endl;
}

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

int main()
{
    signal(2, handler);
    sigset_t in, out;
    sigemptyset(&in);
    sigemptyset(&out);

    // 设置2号信号被阻塞,这里仅在用户栈上设置,并不影响内核 
    sigaddset(&in, 2);      
    // 在内核中阻塞2号信号
    sigprocmask(SIG_SETMASK, &in, &out);  

    int count = 0;
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);   // 获取pending信号集
        show_pending(&pending);     // 打印pending信号集
        sleep(1);

        if(count == 10)
        {
            // 恢复之后2号信号立马递达并且执行默认动作就会终止进程
            // 所以想看到2号信号被递达pending位图的变化就要进行自定义捕捉
            sigprocmask(SIG_SETMASK, &out, &in); 
            cout << "my: ";
            show_pending(&in);
            cout << "recover default: ";
            show_pending(&out);
        }
        ++count;
    }
    return 0;
}

实验结果:
在这里插入图片描述
实验结果解释:
在这里插入图片描述

捕捉信号

signal系统调用

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal() 将信号的处置设置为处理程序,它可以是 SIG_IGN、SIG_DFL 或程序员定义的函数(“信号处理程序”)的地址。
第一个参数填各种信号的编号。
第二个参数:
如果设置为 SIG_IGN,则忽略该信号。
如果设置为 SIG_DFL,则与信号关联的默认操作发生。
如果设置为函数,则首先第二个参数被重置为 SIG_DFL,或者信号被阻塞,然后使用参数 signum 调用处理程序。 如果处理程序的调用导致信号被阻塞,则信号在从处理程序返回时被解除阻塞。


信号捕捉的过程

信号被捕捉的时间点:内核态->用户态的时候:
在这里插入图片描述
在这里插入图片描述
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

sigaction

sigaction函数的作用和signal函数是一样的,都可以进行信号捕捉。

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

参数说明:
signum表示想要捕捉的信号。
第二个参数和第三个参数是一个结构体:

struct sigaction {
	void(*sa_handler)(int);
	void(*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void(*sa_restorer)(void);
};

第二个参数act表示想怎么处理这个信号。
其中sa_handler就是捕捉信号函数的函数指针。
其中sa_flags这个参数通常设置为0,执行默认的动作就可以了。
sa_mask:默认情况下,当系统正在处理某个信号的时候,当前该信号会被短暂的block,直到当前信号处理完毕。
在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
第三个参数oldact可以把老的信号捕捉方式返回,如果不需要设置为null。
返回值成功返回0,失败返回-1。


下面有一段程序,我们发送二号信号,同时将3号信号也加入信号集,看看有什么现象。

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

// 信号捕捉函数执行死循环
void handler(int signo)
{
    while(1)
    {
        cout << "get a signo: " << signo << endl;
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3); // 将3号信号block
    // act.sa_restorer = nullptr;
    // act.sa_sigaction = nullptr;
    
    sigaction(SIGINT, &act, &oact);
    while(1)
    {
        cout << "process is running...\n" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
结果解释:
在这里插入图片描述
这个实验说明了正在处理某个信号的时候,这个信号自动会被block。处理完后才会去掉block,然后才可以触发第二次该信号,可以把这个理解为OS为了防止大量信号产生时导致进程频繁处理信号的一种策略。

可重入函数

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

void show(int signo)
{
    int i = 0;
    while(i < 5)
    {
        cout << "get a signo: " << signo << endl;
        sleep(1);
        ++i;
    }
}

void handler(int signo)
{
    show(signo);
}

int main()
{
    struct sigaction act, oact;	// 注册信号的捕捉动作
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    
    sigaction(SIGINT, &act, &oact);	// 注册对二号信号的捕捉
    show(9999);
}

main函数前部分设置信号捕捉处理动作,接下来main函数执行show方法,需要5s钟,在这5s内发送2号信号,此时主流程就跑过去执行handler,而handler内部又调了show方法,又进到show函数中。相当于mian函数正在执行show,而信号处理函数又执行了show。
运行结果:
在这里插入图片描述
一个函数有可能被多个执行流同时进入,函数被多个执行流同时进入的情况叫做重入。
下面看一个例子:
在这里插入图片描述
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步(node1->next = head; head = node1;),刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步(head = node1;)。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
一旦多个执行流同时进入时,这个函数的访问是不安全的,就叫做不可重入函数,如果访问的时候不会出任何问题,就叫做可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

volatile是C语言当中的一个关键字,这个关键字的作用是保持内存的可见性。

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

int flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("handler signo: %d, set flag == %d\n", signo, flag);
}

int main()
{
    signal(2, handler);
    while(!flag);
    printf("process end...\n");
}

有一个全局变量flag是0,在main函数中以flag做循环条件一直死循环。当发送2号信号的时候,信号递达执行自定义捕捉函数把flag置为1,循环条件!flag就是0应该退出循环,打印process end,然后进程退出。
运行结果:
在这里插入图片描述
疯狂的向进程发2号信号,通过运行结果可以发现,flag的值确实被置为1了,但是循环没退出,进程没结束,非1是0啊,但是还在循环,这是为什么呢?
在这里插入图片描述
在main函数执行流中没有发现任何一个对flag变量做修改的操作,所以编译器在优化的情况下极有可能会将flag变量优化成为寄存器变量。
这个全局变量在内存中肯定是存在的,只不过在编译的时候同步的把这个flag也优化成寄存器变量,也就是当进程启动的时候默认的把flag变量加载到寄存器中,这时候寄存器拿到的值就是0。
然后while循环就开始不断检测flag,它检测的是寄存器当中的flag值,当信号产生时对flag变量进行写入,这个写入动作不会修改寄存器的值,而只是把内存中的flag值由0改成了1。这个flag确实变成了1,但while循环它依旧只检测ebx寄存器中的值,内存的值变为1了,但是寄存器中的值依旧是0。
所以就出现了内存的数据和寄存器的数据不一致,好像把flag的值改了,但while循环一直不退出的原因就在这里,因为while循环检测的flag照样是0值。


编译器在优化的时候,要么是代码的体积不要形成那么大的程序,要么是提高效率。编译器发现main函数执行流中没有修改flag所以直接把flag优化成寄存器变量,可是我们发现此时这个优化就出问题了。如果我们在不知道编译器优化的前提下,运行代码ctrl+c发现它不终止,我们会发现这个问题特别难调试,基本上再怎么看代码,都会认为代码是没问题的。
怎么解决呢?
使用volatile关键字,在flag前加volatile关键字。

volatile int flag = 0;

在这里插入图片描述
再次运行程序发现,while死循环,2号信号递达执行自定义捕捉函数flag被置为1,循环立即结束,进程退出。
volatile关键字叫做易变关键字,告诉编译器这个变量不要认为它是不变的,它是易变的,所以不要做优化,对flag变量的任何操作都必须真实的在内存中进行,保持了内存的可见性。

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

北川_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值