Linux进程(七)—— 信号

什么是信号

比如红绿灯、闹钟、倒计时、鸡叫等。

  1. 红绿灯:红灯亮的时候,会有匹配的动作。你为什么会有这个动作?因为曾有人有事“培养”过你。所以信号没有产生,我们也知道如何处理它。

  2. 我们能够认识并处理一个信号 —— 我们是能够识别这个信号的

    进程就是我,信号就是一个数字,进程在没有收到信号的时候,其实它早就已经能够知道一个信号该怎么被处理了。也就是能够认识并处理一个信号。程序员在设计程序的时候,早就已经设计了对信号的识别能力。

  3. 因为信号可能随时产生,所以在信号产生前,我正在做优先级更高的事情,我们可能不能立马处理这个信号!要在后续合适的时候进行处理。

    信号产生<———时间窗口————>信号处理

    在这个过程中信号需要被保存起来。

    同理:进程收到信号的时候,如果没有立马处理这个信号,需要进程有记录信号的能力!—— 信号保存

  4. 信号的产生对于进程来说是异步的。

kill -l
#查看linux中所有的信号

1-31普通信号,34-64实时信号

普通信号只保存有无产生

  1. 进程如何记录对应的信号?记录在哪里?

    先描述,再组织。

    使用位图结构管理信号。

    task_struct 内部必定要存在一个位图结构,用int表示

  2. 所谓的发送信号,本质其实是写入信号,直接修改特定进程的信号位图中的特定的比特位,0→1。

    比特位的位置——信号的编号

    比特位的内容——是否收到该信号

  3. task_struct 数据内核结构,只能由OS进行修改 —— 无论画面有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程。

  4. 信号产生之后,不是立即处理的,是在合适的时候。

处理信号的方式:

  1. 默认动作
  2. 用户自定义捕捉动作
  3. 忽略信号

大部分进程的信号的默认动作都是中止进程,那么为什么要分那么多种类呢?

为了进一步确认进程是因为什么原因退出的,信号这么多类也可以表征进程异常时是因为什么原因异常的。

信号处理,可以不是立即处理的,而是”合适“的时候。

信号可以被立即处理吗?

如果一个信号之前被block了,当他被解除block的时候,对应的信号会被立即递达!

为什么大部分情况下信号不是立即处理的呢?

信号的产生是异步的,当前进程可能正在做更重要的事情!

什么时候是合适的时候呢?

当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的加测与处理!


信号的产生

1. 用户输入命令,在Shell下启动一个前台进程。

用户按ctrl+c,这个键盘输入产生一个键盘中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。

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

int main()
{
    while(true)
    {
        cout << "我是一个进程,PID:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

前台进程,无法运行其他程序,可使用ctrl+c中断。

后台进程,不影响其他程序的运行,无法使用ctrl+c中断,必须使用信号中断。

捕捉信号

signal()

#include <signal.h>

typedef void (*sighandler_t)(int);  //函数指针

sighandler_t signal(int signum, sighandler_t handler);
//int signum 信号编号

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

//自定义方法
//signo: 特定信号被发送给当前进程的时候,执行handler方法的时候,要自动填充对应的信号给handler方法
//甚至可以给所有信号设置同一个处理函数
void handler(int signo)
{
    cout << "   signal num:" << signo<< endl;
}

int main()
{
		//1. 2号信号,继承的默认处理动作是终止进程
		//2. signal 可以进行对指定的信号设定自定义处理动作
		//3. signal(2,handler);调用完这个函数的时候,handler方法被调用了吗
		//没有,只是更改了2号信号的处理动作,并没有调用handler函数
		//4. 那么handler方法何时被调用?2号信号产生的时候!
		//5. 默认我们对2号信号的处理动作:终止进程,我们用signal(2,handler), 我们在执行用户动作的自定义捕捉

    signal(2,handler);

    while(true)
    {
        cout << "我是一个进程,PID:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

不再中断,运行自己定义的函数

man 7 signal

9号信号,管理员信号,不可被自定义

平时在输入的时候,计算机怎么知道我们从键盘输入了数据呢?

键盘是通过硬件中断的方式通知系统 键盘已经被按下了。

2.通过系统调用向进程发信号

(1)kill

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
//pid 向哪个进程发信号
//sig 信号编号
.PHONY:all
all:mykill loop

mykill:mykill.cpp
	g++ -o $@ $^ -std=c++11
loop:loop.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f mykill loop
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void usage(char* proc)
{
    std::cout << "\\n Usage:\\n\\t" << proc << " 进程编号 信号编号\\n" << std::endl;
}

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(2);
    }

    int pid = std::stoi(argv[1]);
    int signo = std::stoi(argv[2]);
    int n = kill(pid, signo);
    if(n != 0)
    {
        std::cerr<< errno << " : " << strerror(errno);
    }

    return 0;
}
//loop.cpp
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    while (true)
    {
        cout << "我是一个进程,我的pid是:" << getpid() << endl;
        sleep(1);
    }
}

(2)raise

#include <signal.h>

int raise(int sig);
//哪个进程调用就给谁发sig 
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "get a signal: " << signo << std::endl;
}

int main(int argc, char *argv[])
{
    signal(2, handler);
    while (true)
    {
        raise(2);
        sleep(1);
    }
		return 0;
}

(3)abort

#include <stdlib.h>

void abort(void);
//给调用abort的进程发送指定的信号6

即便被自定义捕捉了,也要退出进程

3. 由软件条件产生信号

13)SIGPIPE,管道将读端fd关闭,这时写端就没意义了,OS就会将进程直接终止。向管道写入必须满足写入条件,也就是不关闭读端fd,这就是软件条件中的一种。

14)SIGALRM

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
//设定闹钟,告诉内核在seconds秒后为当前进程发送SIGALRM信号,该信号默认处理动作是终止该进程
//返回值为0或之前设定的闹钟时间余下的秒数
//如果seconds值设为0,则表示取消之前设定的闹钟,返回值依然是之前设定的闹钟时间还余下的秒数
//闹钟是一次性的
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "get a signal: " << signo << std::endl;
    alarm(1); //自己给自己设置闹钟,称为自举
}

int main(int argc, char *argv[])
{
    signal(14,handler);

    alarm(1);
    while(true)
    {
        sleep(1);
    }
		return 0;
}

使用alarm可以计算CPU一秒计算量级

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int count = 0;

void handler(int signo)
{
    std::cout << "get a signal: " << signo << "\\ncount: " << count << std::endl;
}

int main(int argc, char *argv[])
{
    signal(14,handler);
    alarm(1);

    while(true)
    {
        count++;
    }
		return 0;
}

如果每次计算都进行打印可以发现和直接计算最后打印差上千倍,可见IO的速度是很慢的。

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    alarm(1);

    while(true)
    {
        std::cout << "count: " << count++ << std::endl;
    }
		return 0;
}

4. 硬件异常产生信号

CPU

CPU中有一个状态寄存器,会检测本次计算是否有溢出问题,除0会导致溢出,OS检测到状态寄存器中记录了溢出错误时会向进程发送终止信号(8)SIGFPE),所以除0的本质就是出发硬件异常(CPU)

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{

    int a = 10;
    a /= 0;
    cout << "——除零错误——" << endl;

    return 0;
}

OS会向进程循环发送信号

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

using namespace std;

void handler(int signo)
{
    cout << "get a signal: " << signo << endl;
}

int main()
{
    signal(SIGFPE, handler);

    int a = 10;
    a /= 0;
    cout << "——除零错误——" << endl;

    return 0;
}

MMU

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

using namespace std;

int main()
{
    
    int *a = nullptr;
    *a = 10;
		
		cout << "——野指针——"
    return 0;

}

页表中不止有虚拟地址和物理地址,还有权限(rwx)。

查页表的动作由MMU完成,

虚拟地址到物理地址的转换采用软硬件结合。

*a = 10; 第一步并不是写入,而是首先进行虚拟到物理地址的转换。

没有映射,MMU硬件报错。

有映射但是没有权限,MMU硬件报错。

然后OS向进程发送信号11)SIGSEGV终止进程。

OS向进程循环发送11号信号,因为硬件报错并没有被修复

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

using namespace std;

void handler(int signo)
{
    cout << "get a signal: " << signo << endl;
}

int main()
{
    signal(SIGSEGV, handler);

    int *a = nullptr;
    *a = 10;

    return 0;
}

总结

信号的产生:

  1. 键盘
  2. 系统调用
  3. 软件条件
  4. 硬件异常

信号都需借助OS的手向目标进程发送信号,向目标进程的PCB信号位图。

根据上面的测试可以发现,进程信号的默认动作Term和Core都是终止进程,那么他们有什么区别呢?

Linux系统级别提供了一种能力,可以将一个进程在异常的时候,OS可以将该进程在异常时,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core.pid(核心转储文件)这样的二进制文件。

核心转储文件在云服务器上看不见,因为云服务器默认时关闭这个功能的。

ulimit -a    #查看系统对应资源的上限

核心转储文件默认大小被设置为0了所以功能没有打开。

ulimit -c size

设置一个大小即可打开

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

using namespace std;

int main()
{
    //signal(SIGSEGV, handler);

    cout << "——————野指针——————" << endl;
    int *a = nullptr;
    *a = 10;

    return 0;
}

如果给进程手动发送信号呢?

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

using namespace std;

int main()
{
    while (true)
    {
        cout << "我是一个进程,我的PID:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

Term:终止,没有多余的其他动作

Core:先进行核心转储,再进行终止

核心转储有什么用?

方便程序出现异常后进行调试

核心转储为什么一般都是被关闭的?

云服务器一般是开发环境、测试环境、生产环境为一体的。默认是生产环境

在公司上万个服务器,如果核心转储功能是打开的,在深夜大家都睡着了,如果某个服务出了一个段错误,服务器的OS会尝试重启这个服务,但是这个服务的问题并没有被解决,所以会不断的产生核心转储文件,直到占满服务器的存储器。

进程状态里面有一个core dump标志,他是什么呢?

core dump标志表示是否发生core dump

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

using namespace std;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        cout << "——————野指针——————" << endl;
        int *a = nullptr;
        *a = 10;
    }

    int status = 0;
    int a = waitpid(id, &status, 0);
    cout << "child exit code: " << ((status > 8) & 0xFF) << endl;
    cout << "child exit signal: " << (status & 0x7F) << endl;
    cout << "core dump: " << ((status > 7) & 0x01) << endl;

    return 0;
}

异常和中断

产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的。 中断是异步的,这意味着中断可能随时到来;而异常是CPU产生的,所以,它是时钟同步的。 当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中。

阻塞信号

  1. 信号其他相关常见概念

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

信号在内核中的表示

pending表:位图结构,比特位的位置表示哪个信号,比特位的内容,代表是否收到该信号。

block表:位图结构,比特位的位置,表示哪个信号,比特位的内容代表对应的信号是否该被阻塞。

handler表:函数指针数组——void (*sighandler_t)(int); 。该数组的下标,表示信号编号,数组的特定下标内容,表示该信号的递达任务。

SIG_DFL(0): 默认动作

SIG_IGN(1): 忽略

信号集

sigset_t 属于OS的应用层一种数据类型,位图结构。

这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);  
int sigfillset(sigset_t *set);   //位图结构全置1
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);  //判断信号是否在信号集中 

系统调用

sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//how :如何更改,三个选项SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
//oset: 输出型参数,用于备份

sigpending

读取pending信号集

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

测试代码

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t& pending)
{
    cout << "当前进程的Pending信号集: ";
    for(int i = 1;i <= 31;i++)
    {
        if(sigismember(&pending,i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

int main()
{
    //设置对2号信号的阻塞
    sigset_t set,oset;
    //初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    //2号信号添加进set
    sigaddset(&set, SIGINT);
    //更改阻塞信号集
    sigprocmask(SIG_BLOCK, &set, &oset);
    
    
    //循环获取、打印pending信号集
    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        //读取pending信号集
        int n = sigpending(&pending);
        assert(n == 0);
        (void) n;

        showPending(pending);
        sleep(1);
    }
    
}

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t &pending)
{
    cout << "当前进程的Pending信号集: ";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

int main()
{
    // 设置对2号信号的阻塞
    sigset_t set, oset;
    // 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    // 2号信号添加进set
    sigaddset(&set, SIGINT);
    // 更改阻塞信号集
    sigprocmask(SIG_BLOCK, &set, &oset);

    int count = 0;
    // 循环获取、打印pending信号集
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 读取pending信号集
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;

        showPending(pending);
        sleep(1);

        // 十秒后取消对2号信号的屏蔽
        if (count++ == 10)
        {
            cout << "已取消该进程对2号信号的屏蔽!" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
}

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t &pending)
{
    cout << "当前进程的Pending信号集: ";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "捕捉到信号: " << signo << endl;
}

int main()
{
    //自定义捕捉动作
    signal(SIGINT, handler);

    // 设置对2号信号的阻塞
    sigset_t set, oset;
    // 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    // 2号信号添加进set
    sigaddset(&set, SIGINT);
    // 更改阻塞信号集
    sigprocmask(SIG_BLOCK, &set, &oset);

    int count = 0;
    // 循环获取、打印pending信号集
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 读取pending信号集
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;

        showPending(pending);
        sleep(1);

        // 十秒后取消对2号信号的屏蔽
        if (count++ == 10)
        {
            cout << "已取消该进程对2号信号的屏蔽!" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
}


信号的处理

信号处理,可以不是立即处理的,而是”合适“的时候。

信号可以被立即处理吗?

如果一个信号之前被block了,当他被解除block的时候,对应的信号会被立即递达

为什么大部分情况下信号不是立即处理的呢?

信号的产生是异步的,当前进程可能正在做更重要的事情!

什么时候是合适的时候呢?

当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的检测与处理!

用户态:执行用户写的代码的时候,进程所处的状态。

内核态:执行OS的代码的时候,进程所处的状态。

进程怎么样会执行OS的代码?

1、 进程时间片到了,需要切换,就要执行进程切换逻辑。 2、 系统调用

再看地址空间

  1. 所有的进程[0,3]GB是不同的,每一个进程都要有自己的用户级页表

  2. 所有的进程[3,4]GB是一样的,每一个进程都可以看到同一张内核级页表,所有进程都可以通过统一的窗口,看到同一个OS!

  3. OS运行的本质:其实都是在进程的地址空间内运行的!!

    无论进程如何切换,[3,4]GB不变,看到OS的内容,与进程切换无关。

  4. 所以,所谓的系统调用的本质:其实就如同调用.so中的方法,在自己的地址空间中进行函数跳转并返回即可!

    那么我们不就可以通过虚拟地址任意访问OS的数据和代码了吗?

    不想看到这样的现象,所以有了用户态和内核态的概念。进程在地址空间中访问自己的代码[0,3]GB,此时的状态就是用户态。一旦进程要访问[3,4]GB的内容时,OS就会对进程的身份进行检测,如果不是内核态,CPU就会拒绝执行这部分代码,OS检测到硬件异常非法访问,向进程发送信号,终止进程。

    CPU中有一个CR3寄存器,寄存器中有对应的比特位,如果对应的比特位为

    • 3:表征正在运行的进程执行级别是用户态。
    • 0:表征正在运行的进程执行级别是内核态。

    谁来更改这些?

    用户无法直接更改,所以,OS提供的所有系统调用,内部在正式执行调用逻辑的时候,会去修改执行级别!

  5. 所以进程是如何被调度的?

    进程被调度,就是时间片到了,然后将进程对应的上下文等进行保存并切换,选择合适的进程,这些工作由系统函数schedule()来做

    OS的本质是什么?

    1. OS是软件,本质是一个死循环。
    2. OS时钟硬件,每隔很短的时间向OS发送时钟中断,OS就要执行对应大的中断处理方法——检测当前进程的时间片,时间片到了OS就让进程调用schedule()函数。

信号捕捉

下图为信号捕捉的全过程:

过程简图:

本文在前面介绍了一个捕捉信号的方法,第二个方法sigaction()

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

sigaction函数可以读取修改与指定信号相关联的处理动作。调用成功则返回 0 ,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t &pending)
{
    cout << "当前进程的Pending信号集: ";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "捕捉到信号: " << signo << endl;
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));

    act.sa_flags = 0;
    act.sa_handler = handler;

    sigaction(SIGINT, &act, &oldact);
    while (true)
    {
        sleep(1);
    }
}

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t &pending)
{
    cout << "当前进程的Pending信号集: ";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "捕捉到信号: " << signo << endl;
    int count = 10;
    while(--count)
    {
        sigset_t pending;
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n == 0);
        (void) n;

        showPending(pending);
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));

    act.sa_flags = 0;
    act.sa_handler = handler;

    sigaction(SIGINT, &act, &oldact);
    while (true)
    {
        sleep(1);
    }
}

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>

using namespace std;

void showPending(const sigset_t &pending)
{
    cout << "当前进程的Pending信号集: ";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&pending, i))
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "捕捉到信号: " << signo << endl;
    int count = 30;
    while(--count)
    {
        sigset_t pending;
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n == 0);
        (void) n;

        showPending(pending);
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));

    act.sa_flags = 0;
    act.sa_handler = handler;
		//阻塞3、4号信号
		sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);

    sigaction(SIGINT, &act, &oldact);
    while (true)
    {
        sleep(1);
    }
}

可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

在不同的执行流中,同一个函数被重复进入,就叫做重入。

如果重入后没有问题,则该函数被称为可重入函数。

如果重入后有问题,则该函数被称为不可重入函数。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

#makefile
test:test.cpp
	g++ -o $@ $^

.PHONY:clean
clean:
	rm -f test
#include <iostream>
#include <signal.h>

using namespace std;

int flag = 0;

void handler(int signo)
{
    cout << "change flag 0 to 1" << endl;
    cout << "flag = " << flag;
    flag = 1;
    cout << " -> flag = " << flag << endl;
}
int main()
{
    signal(2, handler);
    while(!flag);
    cout << "程序正常退出" << endl;
    return 0;
}

发现进程接受到信号后flag可以正常被修改,进程正常退出

但是如果在编译语句中加上优化呢

#makefile
test:test.cpp 
	**g++ -o $@ $^ -O1**

.PHONY:clean
clean:
	rm -f test

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。

解决方法就是使用volatile关键字,使flag不要让CPU优化。

#include <iostream>
#include <signal.h>

using namespace std;

volatile int flag = 0;

void handler(int signo)
{
    cout << "change flag 0 to 1" << endl;
    cout << "flag = " << flag;
    flag = 1;
    cout << " -> flag = " << flag << endl;
}
int main()
{
    signal(2, handler);
    while(!flag);
    
    cout << "程序正常退出" << endl;
    return 0;
}

volatile关键字保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

如何理解编译器的优化?

  1. 编译器的本质是更改代码
  2. CPU其实很笨,用户喂给他什么代码,它就执行什么代码。

子进程退出了,父进程如何得知的呢?

父进程阻塞式等待&&非阻塞 ——都需要父进程主动检测

子进程在退出的时候,会向父进程发送信号SIGCHLD,而父进程对这个信号的默认动作是SIG_DFL,就是什么也不做

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。只在linux下有效。

signal(17,SIG_IGN);

这里的手动设置的IGN和之前默认动作中的IGN是不一样的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值