【Linux】进程信号

目录

预备知识

一、信号的产生

二、信号的保存

三、信号的处理

四、内核如何实现信号的捕捉

可重入函数

volatile

SIGCHLD信号


 

预备知识

信号和信号量没有任何关系

1、进程怎么认识信号的? 因为有人教,记住了。

可以识别信号   知道信号的处理方法

进程必须能识别并能够处理信号,即使信号没有产生,也要具备处理信号的能力---信号处理能力,属于进程内置功能的一部分

2、即使现在没有产生信号,但是我知道信号产生之后,我该干什么。

3、信号产生了,我们并不立即处理这个信号,因为我们现在有更重要的事。所以在信号产生之后,在信号到来和处理信号之间的时间窗口内,在时间窗口内,你必须记住信号到来。

当进程真的收到了一个具体的信号的时候,进程可能并不不会立即处理这个信号,等到合适的时候才会处理

一个进程必须当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力。

./myprocess进程运行起来 为什么ctrl+c就能杀掉前台进程呢?
./myprocess & 这样进程就会变成后台进程,ctrl+c就不能杀掉了

Linux中,一次登录中,一个终端会配一个bash,每一个登录只允许一个进程是前台进程,允许多个进程是后台进程。谁是前台谁就能拿到键盘的输入。键盘输入首先是被前台进程收到的。   ctrl+c 的本质是被进程解释为收到信号---2号信号。

进程收到2号信号的默认动作就是终止自己。

没有0号信号 没有32、33号信号 一共有62个信号  1到31号信号称作普通信号  34到64称作实时信号。实时信号一旦产生了就必须立即处理。(我们一般不考虑)

信号的处理方式:

1、默认动作 (内置的动作)  2、忽略   3、自定义动作(信号的捕捉)

#include <signal.h>

typedef void(*sighandler_t)(int);参数类型是int 返回值是void 的函数指针类型

sighandler_t signal(int signum,  sighandler_t  handler); 设置特定进程的自定义属性  第二个参数就是上面的指针类型

只有后面遇到了signum信号,才会触发handler函数(函数的方法才会被调用)

第一个参数signum-----信号编号

第二个参数handler-----当前进程对信号执行自定义捕捉动作(修改特定进程对信号的处理动作)

总结:这个函数可以对指定信号采取捕捉动作,singal函数就像一个注册方法,设置对2号信号进行自定义捕捉,并不是调signal的时候该方法(handler)就被调了,只是告诉系统和进程我要更改2号进程的处理方法,以前是默认现在是自定义(内置方法),如果说我们调用了signal但是代码或者进程后续并没有收到信号,那么handler方法是不会被调用的。

当捕捉到信号之后,将默认动作变为了自定义动作。(ctrl+c就不会退出了)

键盘数据是如何输入给内核的,ctrl+c又是如何变成信号的

键盘被按下,肯定是OS先知道的! OS怎么知道键盘上有数据了??

linux下一切皆文件,因此键盘也是文件。读取键盘数据就是将键盘数据读取到键盘文件的缓冲区中。那么OS如何知道键盘中有数据了??

数据层面上,CPU是不和硬件打交道的。但是在控制层面,CPU是要能读取外设的。 cpu上有很多针脚,针脚是集成在主板上的,而每一种设备也是插在主板上的,键盘在物理上是可以直接和cpu相连的,cpu虽然不从键盘读数据但是键盘可以给cpu发送一个硬件中断(每一种中断都有中断号) 设备向cpu发送中断,cpu就要记录中断号。cpu内有寄存器,寄存器可以保存数据,但是这个寄存器凭什么可以保存数据呢?  给针脚发送中断的过程就是给cpu发送高低电平的过程,保存数据其实就是充电放电的过程。 OS内部会维护一张中断向量表(其实是一个数组) 里面存的是方法(直接访问外设的方法(主要是磁盘、显示器、磁盘))和地址。

中断号到cpu当中(cpu拿到中断号),触发了中断,OS就会立马识别到这个中断号(,就知道有设备就绪了),OS以中断号为索引在中断向量表中去找对应的方法,找到之后执行这个方法,这个方法才是将数据从外设拷贝到内存当中的方法。

当键盘的数据是ctrl+c的组合键呢? OS拿到数据会先对数据进行判断,控制(ctrl+c 转化成2号信号发送给进程

键盘是基于硬件中断来进行工作的。

OS在内存里,OS怎么知道哪个外设好了、或者数据已经读完了

OS必须得随时知道每一种设备的状态,因为OS是硬件的管理者

系统是通过软硬件结合的方式来做的,硬件设备会在就绪之后通过中断单元向cpu的针脚发送硬件中断,这样cpu就能获取对应设备的中断号,cpu就会去OS提供的中断向量表中找终端号对应的中断处理方法。 这样就不需要OS不断对外设进行检测。

一、信号的产生

信号的产生和我们自己的代码运行是异步的。  (我们执行自己的代码信号可能随时会到来)

信号属于软中断(信号是仿照硬件中断实现的软件中断)

1、键盘组合键产生信号:

ctrl + c:二号信号   xtrl+\ : 三号信号

进程要接收键盘信号,进程首先得是前台进程

不是所有的信号都可以被signal捕捉的,eg:9号信号(杀掉进程信号)   19号信号(暂停信号)    2、kill命令:kill -signo pid         

3、系统调用

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,  int  sig)  //给指定进程发送信号

第一个参数:pid
第二个参数:sig----信号编号

4、异常

进程出异常本质上时收到了信号。那么,进程收到异常信号一定会退出吗??答案是:不一定!!因为异常可以被我们捕捉,所以不一定会退出,但是异常退出了,肯定是执行了信号异常处理方法。

为什么进行除零或者野指针问题会让进程崩溃? 因为给进程发信号了  为什么会给进程发信号??因为OS识别到了系统中有除零这些错误,就会给进程发信号。 OS是如何知道发生了除零操作的???  当有一个进程,进程的代码中有一行是除0操作,cpu中有一个eip寄存器,可以知道代码运行到哪一行了,还有一个状态寄存器,这个状态寄存器有一个溢出标志位,当识别到除零错误之后,状态寄存器由0变为1。因为OS是硬件的管理者所以它必须要知道cpu的状态,它就知道了发生了除零错误。(cpu寄存器中的数据,是进程的上下文,虽然因为这个进程中的错误我们修改了CPU内部的寄存器状态,但是这并不影响其他进程,因为这些cpu中的寄存器数据是我这个进程的上下文) OS是如何知道发生了野指针问题???  MMU内存管理单元, 集成在cpu内部,cpu读到的都是虚拟地址,会结合页表、虚拟地址、MMU三个,就可以将虚拟转化为物理。野指针其实就是虚拟到物理地址转化失败,任何的寻址都先得转化成功之后再进行。 cpu中有一个寄存器,是当虚拟地址到物理地址转化失败之后,将虚拟地址存在寄存器中。 转化失败之后也会出现硬件报错,报错也会被OS识别到。

5、软件条件产生信号

异常只会由硬件产生吗??

不一定。 比如存在一个管道,管道有读端和写端,当读端关闭,写端还一直往管道写数据时,OS就会发送13号信号SIGPIPE,这就属于软件异常,软件问题引起的异常。

闹钟

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

参数是设定的时间  返回值是  如果在设定时间之前反应,返回值就是剩余的时间

int main()
{
    int n = alarm(5);//设定5秒  如果进程的运行时间不足5s 那么也就不会收到这个信号
    while(1)
    {
        cout << "proc is running..." << endl;
        sleep(1);
    }
    return 0;
}

void handler(int signo)
{
    cout << "...get a sig, number: " << signo << endl;
    alarm(5); //设置闹钟 但现在不会响  函数直接返回 从下面函数的while开始执行
//等5s之后,闹钟响 被捕捉 再设置 
}

int main()
{
    signal(SIGALRM, handler);//捕捉信号 自定义行为
    int n = alarm(5);//设定5秒  如果进程的运行时间不足5s 那么也就不会收到这个信号

    while(1)
    {
        cout << "proc is running..." << endl;
        sleep(1);
    }
    return 0;
}

 

闹钟每隔5s响一次

void work()
{
    cout << "print log..." << endl;
}
void handler(int signo)
{
    work();
    //cout << "...get a sig, number: " << signo << endl;
    alarm(5);
}

int main()
{
    signal(SIGALRM, handler);//捕捉信号 自定义行为
    int n = alarm(5);//设定5秒  如果进程的运行时间不足5s 那么也就不会收到这个信号
    while(1)
    {
        cout << "proc is running..." << endl;
        sleep(1);
    }
    return 0;
}

定时执行任务

core dump标志位表示信号终止的方式:是以term方式还是core方式

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

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 50;
        while(cnt)
        {
            cout << "i am a child process, pid: " << getpid() << " cnt: "<< cnt << endl;
            sleep(1);
            cnt--;
        }
        exit(0);

    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        //rid 退出状态(次低八位) 终止信号(低7位) core dump标志位(低八位)---将右移七位,将终止信号移走,将其放到最低位
        cout << "child quit info, rid: " << rid << " exit code: " << 
        ((status>>8)&0xFF) << " exit signal:  " << (status&0x7F) << " core dump: " << ((status>>7)&1) << endl;//(status>>7)&1 1是整数,32个比特位,最后一位是1,按位与之后将除过最后一位的全都清零只保留最后一位(最后一位是1就是1是0就是0)
    }

}

现象:

 

打开系统的core dump 功能,一旦进程出异常,OS会将进程在内存中的运行信息,给dump(转储)到进程的当前目录(磁盘)形成core dump文件:核心转储(core dump)

为什么要进行核心转储:运行时错误,代码是在哪行出错,为什么出错。所以core dump是方便事后调试的(先运行,再调试)

为什么在云服务器上这个core dump 是被关闭的:因为服务器是在挂掉之后立即重启的,这样就会core dump 就会保存core dump 文件,但是如果有刚启动就挂掉的,就会立马形成该文件,服务器又被重新启动 又形成文件,每core dump 一次就会形成一个临时文件,这样磁盘就会被占满,可能导致OS挂掉,因此一般默认是被关掉的。

二、信号的保存

1、信号的发送与保存

什么是信号的发送

对于普通信号而言,对于进程而言,自己有没有收到哪一个信号是给进程的PCB发。

task_struct{

int signal; //0000 0000 0000 0000 0000 0000 0000 0010 32个比特位 普通信号,位图管理信号

}

1、比特位的内容是0还是1表示是否收到

2、比特位的位置(第几个),表示信号的编号 例如上面改的是第一个(我们可以理解为下标从0开始)

3、所谓的“发信号”,本质上就是OS去修改task_struct的信号位图对应的比特位,其实可理解为写信号。 一个os要给一个进程发信号,只需要找到pcb,找到对应的字段,将对应的字段由0置1,信号发送完成。(OS是进程的管理者,只有他有资格去修改task_struct内部的属性!) 但是!OS系统不能将进程直接杀掉,只能发信号,因为OS害怕进程正在执行重要的事情,万一直接杀掉有数据没有保存就会出问题。

信号的保存:

进程收到信号可能并不会立即处理。信号不会被处理就要有一个时间窗口。在这个时间窗口以内信号已经产生但还没有被处理。 (普通信号比较简单,因此采用位图来保存。)

普通信号(1~31号信号) 比如发了10次2号信号,就只会保存一次,执行一次。

34~64号信号被称为实时信号。实时信号发过来了就必须立即处理。实时信号发了10次就需要处理10次。每产生一个信号就有一个信号队列(实时信号用对列来进行管理)

1、信号要被处理:忽略 默认 自定义   实际执行信号处理的动作被称作信号的递达

在内核中表示:

2、信号从产生到递达之间的状态(信号在被保存),称为信号未决(pending)。----和位图相关

每一个信号都要有一个hander表,这个表的元素是函数指针类型

typedef void(*handler_t)(int);

hander_t handler[31]; (函数指针数组)

每个指针指向一个方法(OS提供的默认的方法)

3、进程可以选择阻塞(block)某个信号

系统会收到各种各样的信号,我们可以选择屏蔽某种信号,只有解除屏蔽了才能递达。

屏蔽是一种状态,和是否产生没有关系。没有产生也可以屏蔽。

block也是位图表和pending一样。比特位的位置决定信号的编号,比特位的内容为0表示不屏蔽,为1表示屏蔽。

block表用来记录特定信号是否被屏蔽

pending表记录当前进程是否收到了信号包括收到了哪些信号(没有被处理的信号)

handler表表示每种信号对应的处理方式

4、被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。(当某个信号被阻塞,还是可以给进程发送信号的,只不过信号被屏蔽了罢了。)

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

2、sigset_t

pending表、block表、handler表都是OS的内核数据结构,OS不相信用户,他不会让用户直接去修改这三张表,因此用户要修改这三张表时就需要系统调用接口。要获取pending表和block表就注定了要在用户空间和内核空间、内核空间和用户空间进行来回的数据拷贝,就要在接口的设计上设计输入输出型参数,

3、signprocmask

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

#include<signal.h>

int sigprocmask(int how, const sigset_t *set,sigset_t *oset);

我们可以通过三个参数达到去信号屏蔽字(block信号集)的设置

若成功返回1,出错返回-1

how参数的可选值:

SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask | set

或运算相当于在原来的基础上新增。(将两个不同的比特位进行按位或就是比如2号和3号都置1的效果)

SIG_UNBLOCK:mask = mask&~set 先把传入的信号集按位取反,为0的变为1,为1的变0,例如当二号信号2号位为1其他全为0,取反之后2号位为0,其他为1 然后&之后就是去掉在set中设置的信号(在block表里去掉我们传入的所有信号)eg:我们已经对1、2、3号做屏蔽了,我们不想对2号再屏蔽就把2号信号利用这个接口传进去就可以被屏蔽。

SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set。

将set传入的参数设置进内核的进程pcb当中的block表里。相当于覆盖式的重新设置对信号进行屏蔽的字段

const sigset_t *set:

输入型参数,自己写代码时可以自己提前对位图结构做设置,然后将设置好的信号集通过sigprocmask传进来,哪个进程调这个函数 就会将这个set按照how对这个集合里添加的信号要么新增要么去掉要么重新设置。

sigset_t *oset:

输出型参数,可以保存被修改之前的block表,即保存的是上一次的。

4、sigpending

#include <signal.h>

int sigpending(sigset_t *set)

参数是输出型参数,是把调用进程对应的pending表以位图的形式带出来。方便未来做检查。

返回值 0成功 -1失败 

三、信号的处理

信号是在什么时候被处理的??

前提是已经知道自己收到了信号,那么进程就要在合适的时候查一查自己对应的pending位图、block位图和handler表,而这三个都属于内核结构,其他人无法查阅。那么进程就必须处在内核状态才可以对信号做处理。

当进程从内核态返回到用户态的时候,进行信号的检测和处理!!!

可以理解为:返回意味着已经将重要的事情做完了,顺手将信号进行处理

我们自己的代码有自己写的、库的、操作系统的代码 代码组成比较复杂

一般执行自己写的代码和库的代码是在用户态    调用系统调用的时候就会将身份由用户转化为内核,返回时再将身份做变换。

9和19号信号不可被捕捉,不可被处理。

重谈地址空间:

task_struct指向当前进程所对应的地址空间 

页表进行虚拟到物理的转化

用户级页表有几份?有几个进程就有几份用户级页表-----因为进程具有独立性
内核级页表有几份?只有1份,每个进程看到的3~4GB的东西都是一样的!在整个系统中,进程再怎么切换,3~4GB的空间内容是不变的!!

进程视角:我们调用系统中的方法,就是在自己的地址空间中进行执行的

操作系统视角:任何一个时刻,都有进程在执行。我们想执行操作系统的代码,就可以随时执行。

操作系统的本质:基于时钟中断的死循环。

CPU中有一个ecs寄存器,它的低两位 00(0 代表内核态) 01 10 11(3 代表用户态)

要想实现用户态和内核态之间的转变就需要改变寄存器的值

cr3寄存器指向页表

内核态:允许访问操作系统的代码和数据

用户态: 只能访问用户自己的代码和数据

由上图可以看出进行了四次内核态与用户态的转化

当信号的处理动作是自定义动作时,就需要回到用户态去执行代码。(因为OS不允许,害怕你进行危险操作。)

从用户态到内核态:陷入内核。 不是只有当调用系统调用的时候才会发生。

当我们的代码里面没有系统调用,我们ctrl+c也可以杀掉进程,那它是怎么陷入内核的???

进程是会被调度的  只要进程一直在跑,进程是会被cpu调度的,当时间片消耗完毕,进程就会被从cpu上剥离下来,也必然会将进程二次拿到cpu上去运行,那么就是OS将pcb、地址空间、页表恢复到cpu上,这肯定是在内核态的,然后执行我们的代码肯定是在用户态上执行的,因此肯定就会存在内核态到用户态的转变。 不要认为只有系统调用才会有用户到内核,在代码的周期里会有无数次的内核与用户态的转化

信号的检测和处理是在由内核态返回用户态时进行的。

四、内核如何实现信号的捕捉

1、signal

2、sigaction

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

struct sigaction{

        void  (*sa_handler)(int); -------处理方法

        void (*。。。。。。

        sigset_t  sa_mask;

        。。。

        。。。

}

第二个参数:输入型参数 捕捉一个信号,传入的是一个结构体 结构体名和函数名是一样的

第三个参数:输出型参数

 sa_mask字段:

正在处理2号信号,2号会自动被屏蔽。如果我还想屏蔽更多信号呢??

  sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,1);//在屏蔽2号的同时将1、3、4同时屏蔽
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);

//问题一:pending位图,什么时候从1->0

//我们知道当收到信号,信号在pending表里就被置1了

问题二:信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

void PrintPending()
{
    sigset_t set;
    sigpending(&set); //获取
    for(int signo = 1; signo <= 31; signo++)
    {
        if(sigismember(&set, signo)) cout << "1"; //打印
        else cout << "0";
    }
    cout << "\n";
}
void handler(int signo)
{
    PrintPending();
    cout << "catch a signal, signal number : " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));
    act.sa_handler = handler;
    sigaction(2, &act, &oact);//修改handler表 的2号下标的函数指针,该向指向handler方法就好了

    while(true) //当没有2号信号产生的时候 进程一直运行 当产生2号信号,我们对2号信号进行自定义捕捉
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

实验现象: 先将pending位图由1变为0 再调用的handler处理方法

执行信号捕捉方法之前,先清0,再调用

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字(添加到block表里) , 当信号处理函数返回时自动恢复原来的信号屏蔽字 
当信号正在被处理,要处理2号信号肯定是先收到了2号信号,所以pending位图一定是1,当递达2号信号把1改0再递达,再递达2号信号之前 将block(2号信号的阻塞位图)的比特位置1  这就叫自动的把当前信号加入到当前信号的屏蔽字里,当我们把2号信号的处理全部做完,  2号信号正在被捕捉期间,2号信号是不可在被递达的 只能等当前处理做完了才进行下一次处理。

可重入函数

头插新节点核心代码:

node1->next = head;

head = node1;

 

现象:insert函数被main和handler执行流重复进入

造成问题:节点丢失,内存泄漏

如果一个函数,被重复进入的情况下,出错了,或者可能出错,这就叫作不可重入函数!否则叫做可重入函数。目前,我们学到的大部分函数都是不可重入函数。因为,c++中的stl容器里面有大量的new或则malloc这样的操作,用容器会自动扩容,这些容器在stl库中是使用链表来管理的。容器里面有链式结构,即只要内存块上扩容,就会用大量的指针的变化,所以stl容器使用的时候都是不可重入的。

volatile

在优化的条件下,会在cpu和内存之间形成一个寄存器屏障。  把内存的值放到cpu中之后,就只会一直从cpu中取值,对内存的值进行修改并不会影响之前放入到cpu中的值。(即因为优化,导致内存不可见了)

volatile int flag = 0;   核心作用:防止编译器的过度优化,保持内存的可见性!!(告诉编译器,好好的从内存里面读,不要进行过度优化)

mysignal:mysignal.cc
	g++ -o $@ $^ -O3 -std=c++11 //-O3是最高级别的优化
.PHOONY:clean
	rm -f mysignal
volatile int flag = 0;  //添加volatile关键字 防止flag被过度优化

void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    flag = 1;
}
int main()
{
    signal(2, handler);
    //在优化条件下,flag变量可能被直接优化到cpu内的寄存器中
    while(!flag); //flag 0, !flag 真  //while条件 是一种运算  有两种运算 算数运算、逻辑运算 flag没有被修改


    cout << "process quit normal" << endl; //当捕捉到信号之后这句就会被打印
    return 0;
}

SIGCHLD信号

子进程退出的时候,不是静悄悄的退出。子进程在退出的时候,会主动的向父进程发送SIGCHLD(17)号信号。

子进程在进行等待的时候,我们可以采用基于信号的方式进行等待。等待子进程的好处:1、获取子进程的退出状态,释放子进程的僵尸 2、虽然不知道父子谁先运行,但我们清楚,一定是父进程最后退出!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值