【Linux详解】——进程信号

📖 前言:本期介绍进程信号。


目录

  • 🕒 1. 生活角度的信号
  • 🕒 2. 技术角度的信号
    • 🕘 2.1 Linux中的信号
    • 🕘 2.2 进程对信号的处理
  • 🕒 3. 信号的产生方式
    • 🕘 3.1 键盘产生
    • 🕘 3.2 通过系统调用
      • 🕤 3.2.1 kill信号
      • 🕤 3.2.2 raise信号
      • 🕤 3.2.3 abort信号
    • 🕘 3.3 硬件异常产生信号
      • 🕤 3.3.1 溢出错误
      • 🕤 3.3.2 段错误
    • 🕘 3.4 软件条件产生异常
  • 🕒 4. 信号的Term终止和Core终止
  • 🕒 5. 信号的保存
    • 🕘 5.1 PCB的具体内容
    • 🕘 5.2 sigset_t
    • 🕘 5.3 sigprocmask
    • 🕘 5.4 sigpending
    • 🕘 5.5 案例
  • 🕒 6. 信号的递达
    • 🕘 6.1 用户态和内核态
    • 🕘 6.2 信号捕捉
      • 🕤 6.2.1 signal、sigaction
  • 🕒 7. 可重入函数
  • 🕒 8. volatile关键字
  • 🕒 9. SIGCHLD信号

🕒 1. 生活角度的信号

你在网上买了很多件商品,在等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递,也就是你能“识别快递”。快递到来的整个过程,对你来讲是异步的,你不能准确断定快递什么时候到。

当快递送到菜鸟驿站后,你可以选择不立刻去取,等合适时间再去,并且你知道并记住了有一个快递要去取。当你时间合适,顺利拿到快递之后,就要开始处理快递了。
而处理快递一般方式有三种

  1. 执行默认动作(打开快递,使用商品)
  2. 执行自定义动作(快递是礼物,你要送给别人)
  3. 忽略快递(快递拿上来之后,做其他事情)

进程就是你,操作系统就是快递员,信号就是快递

总结说,首先你能识别信号,其次即使信号没有产生,你也有处理信号的能力,至于处理,就是在你觉得合适的时候再去,处理有三种情况,一是默认,而是忽略,三是自定义。

🕒 2. 技术角度的信号

🕘 2.1 Linux中的信号

使用kill -l查看所有信号。使用信号时,可使用信号编号或它的宏。

# 查看信号
[hins@vm-centos7 testLinux]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

1、Linux中信号共有61个,没有0、32、33号信号。
2、【1,31】号信号称为普通信号,【34,64】号信号称为实时信号

以普通信号为例,进程task_struct结构体中存在unsigned int signal变量用以存放普通信号。(32个比特位中使用0/1存储、区分31个信号——位图结构

那么发送信号就是修改进程task_struct结构体中的信号位图。当然,有权限改动进程PCB的,也只有操作系统了。

🕘 2.2 进程对信号的处理

  1. 进程本身是程序员编写的属性和逻辑的集合;
  2. 信号可以随时产生(异步)。但是进程当前可能正在处理更为重要的事情,当信号到来时,进程不一定会马上处理这个信号;信号是进程之间事件异步通知的一种方式,属于软中断,本质也是数据。
  3. 所以进程自身必须要有对信号的保存能力;
  4. 进程在处理信号时(信号被捕捉),一般有三种动作:默认、自定义、忽略

🕒 3. 信号的产生方式

🕘 3.1 键盘产生

以前,我们在使用 Ctrl+C 结束进程时,本质是向指定进程发送2号信号。

man 7 signal          # 查看详细信号手册

在这里插入图片描述

signal()函数:在这里插入图片描述

如果想让Ctrl+C热键组合不做信号的默认动作,我们可以使用signal系统调用来自定义信号的行为。

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

void handler(int signo)
{
    std::cout << "进程捕捉到了一个信号,信号编号是: " << signo << std::endl;
    // exit(0);
}

int main()
{
    // 这里是signal函数的调用,并不是handler的调用
    /// 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
    // 一般这个方法不会执行,除非收到对应的信号!
    signal(2, handler);

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

在这里插入图片描述

运行代码,从上图可以看到,无论是使用 Ctrl+C ,还是kill -2 [pid]尝试终止进程,都无效。是因为我们改变了2号信号的处理方式,这里不会终止进程,只能通过其他信号终止。键盘可以产生信号,键盘产生的信号只能用来终止前台进程,后台进程可以使用命令 kill -9 [pid] 杀掉。9号信号不可被捕捉(自定义)。

🕘 3.2 通过系统调用

🕤 3.2.1 kill信号

kill:发送一个信号给其他进程

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);	// pid:目标进程的pid。sig:几号信号
// 成功时(至少发送了一个信号) ,返回零。出现错误时,返回 -1设置errno

例子:

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

int main()
{
    while(true)
    {
        std::cout << "这是一个正在运行的进程,pid: " << getpid() << std::endl;
        sleep(1);
    }
}
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <string>

void Usage(const std::string& proc)     // 类似手册
{
	std::cout << "\nUsige: " << proc << " Pid Signo\n" << std::endl;
}

// ./myprocess pid signo
int main(int argc, char* argv[])    // 运行main函数时,需要先进行传参
{
	if (argc != 3)  // 如果传入main函数的参数个数不为3
	{
		Usage(argv[0]);
		exit(1);
	}
	pid_t pid = atoi(argv[1]);  // 获取第一个命令行参数,作为pid
	int signo = atoi(argv[2]);  // 获取第二个命令行参数,作为signo
	int n = kill(pid, signo);   // 需要发送信号的进程/发送几号信号
	if (n == -1)                // kill()失败返回-1
	{
		perror("kill");
	}
}

在这里插入图片描述

🕤 3.2.2 raise信号

raise:给自己发送任意信号

#include <signal.h>
int raise(int sig);	// sig:信号编号
// raise()在成功时返回0,在失败时返回非0

例子:

int main(int argc, char* argv[])    // 运行main函数时,需要先进行传参
{
	//当计数器运行到5时,进程会因3号进程退出

    int cnt = 0;
    while (cnt <= 10)
    {
        std::cout << cnt++ << std::endl;
        sleep(1);
        if(cnt >= 5)  raise(3);
    }
    return 0;
}
[hins@vm-centos7 signal]$ ./mysignal
cnt: 0
cnt: 1
cnt: 2
cnt: 3
cnt: 4
Quit

🕤 3.2.3 abort信号

abort:给自己发送指定信号SIGABRT

#include <stdlib.h>
void abort(void);  // 永远不会返回

例子:

int main(int argc, char* argv[])   
{
    int cnt = 0;
    while (cnt <= 10)
    {
        std::cout << cnt++ << std::endl;
        sleep(1);
        if(cnt >= 5)  abort();
    }
    return 0;
}
[hins@vm-centos7 signal]$ ./mysignal
cnt: 0
cnt: 1
cnt: 2
cnt: 3
cnt: 4
Aborted

🕘 3.3 硬件异常产生信号

硬件异常指非人为调用系统接口等行为,因软件问题造成的硬件发生异常。操作系统通过获知对应硬件的状态,即可向对应进程发送指定信号。

🕤 3.3.1 溢出错误

例子:

int main(int argc, char* argv[])   
{
	while(true)
    {
        std::cout << "运行中..." << std::endl;
        sleep(1);
        int a = 10;
		a /= 0;
    }
}
[hins@vm-centos7 signal]$ ./mysignal
运行中...
Floating point exception

当我们写一段程序,有除0操作。我们知道计算的过程是交给cpu的,那么操作系统为什么能够知道我们除0?还能给进程发送一个终止信号?

在这里插入图片描述

其原因在于:cpu内部有一个状态寄存器,用来衡量某次运算的结果。这个寄存器有一个溢出标记位,当标记位溢出,则表示当前计算结果们没有意义,不需要被采纳。也就是说,除0会产生一个无穷大的值,从而导致状态寄存器的标记位溢出,然后发生cpu运算异常。而操作系统是要对所有硬件管理的,所以操作系统自然而然地知道了cpu发生了异常,然后操作系统又能对软件做管理,很轻松地找到了是谁导致地异常,然后再给导致异常的进程发送一个8号信号(我们看到的错误信息就是8号信号导致的)。

我们对上面的代码进行改造如下:

void catchSig(int signo)
{
	std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
	sleep(1);
}

int main(int argc, char* argv[])   
{
	signal(SIGFPE, catchSig);
	while(true)
    {
        std::cout << "运行中..." << std::endl;
        sleep(1);
        int a = 10;
		a /= 0;
    }
}
[hins@vm-centos7 signal]$ ./mysignal
运行中...
获取到一个信号,信号编号是: 8
获取到一个信号,信号编号是: 8
......

可以看到在循环进行我们的自定义动作catchSig,即把8号信号的终止行为改成了catchSig,而里面又没有终止相关代码,自然在死循环。

此处就产生一个问题,操作系统发现除0操作后发送8号信号,不应该只会提示1次吗?为什么会在循环提示?我们把除0操作拿出循环,再观察一下。

void catchSig(int signo)
{
	std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
	sleep(1);
}

int main(int argc, char* argv[])   
{
	signal(SIGFPE, catchSig);
    int a = 10;
	a /= 0;
	while(true)
    {
        std::cout << "运行中..." << std::endl;
        sleep(1);
    }
}
[hins@vm-centos7 signal]$ ./mysignal
获取到一个信号,信号编号是: 8
获取到一个信号,信号编号是: 8
......

可以看到,即使只进行了一次除0操作,其依旧在循环打印。

其原因在于:当cpu发生运算异常后,操作系统给进程发送一个8号信号,进程收到信号后执行自定的行为,并没有退出该进程。我们自定义的行为执行完后, 这个进程将会继续被调度,那么cpu的寄存器是保存了当前进程的上下文的,也就是说cpu的状态寄存器还没有清空,所以操作系统一直看到cpu发出运算异常,从而一直给进程发送8号信号。

🕤 3.3.2 段错误

还有一种便是野指针。我们来看代码:

int main()
{
    int* p = nullptr;
    *p = 100;
    return 0;
}
[hins@vm-centos7 signal]$ ./mysignal
Segmentation fault

在这里插入图片描述

编译运行此程序将会引发段错误,其对应的信号为11号信号。一定是操作系统向进程发送了该信号。其原因在于:用户层看到的指针(地址)是虚拟地址,虚拟地址通过页表+MMU转换到物理地址(MMU集成在cpu上,通过读取页表的内容形成物理地址),MMU会检测到越界访问而发生异常,然后操作系统立马识别到MMU发生异常,然后找到导致异常的进程,然后给该进程发送信号。

所以大部分信号的行为是导致进程终止,事实上进程可以不被终止,但是没有意义。

🕘 3.4 软件条件产生异常

在进程间通信中,通信双方的进程打开同一个管道,这个管道就是一个软件。当某个进程关闭读端时,写端就没有存在的意义了,此时操作系统便会向写端进程发送一个SIGPIPE信号。管道的某端关闭导致操作系统向进程发送信号,是一种软件条件。

还有一种软件条件便是系统调用alarm,其作用是设定闹钟(单位为秒),时间到后调用该系统调用的进程将会收到SIGALRM信号(14号信号),进而终止进程。时间到达(或者超时)导致操作系统向进程发送一个信号,也是一种软件条件。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回值为0或还剩多长时间闹钟结束

🕒 4. 信号的Term终止和Core终止

在这里插入图片描述

Term:正常结束;Core:异常退出,可以使用核心转储功能定位错误;Ign:内核级忽略。
核心转储:当进程出现异常的时候,我们将进程对应的时刻,在内存中的有效数据转储到磁盘中

我们通过以下指令查看各种数据的上限:

在这里插入图片描述

当操作系统向进程发送了Core终止的信号时,应该会产生一个文件,但由于我的测试环境在云服务器上,可以看到其配置文件大小为0,是因为云服务器默认关闭了core file选项。

如果需要设置,可以使用以下命令改变core file size的大小:

ulimit -c [字节数] 

在这里插入图片描述

我们以上面野指针的代码为例,进行测试,发现改变core file size的大小后,在本目录下会发现多了一个文件:

在这里插入图片描述

[hins@vm-centos7 signal]$ ll
total 328
-rw------- 1 hins hins 557056 Mar 19 17:27 core.18418

我们对其进行gdb调试,即可看到核心转储文件导出的错误,这种调试方式称为事后调试

在这里插入图片描述

不一定所有的退出信号都会被core dump 例如:9号信号。

🕒 5. 信号的保存

在开始内容之前,先介绍一些信号的专业名词:

  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)(就是收到信号,但没有执行信号对应的动作)
  3. 进程可以选择阻塞(Block)某个信号,阻塞的信号就是收到信号,但是一直处于未决状态。
  4. 忽略信号也是一种递达动作。
  5. 未决就是未决,阻塞就是阻塞。没有收到信号时,依然可以对没有收到的信号阻塞(收到信号后直接就是未决信号)

🕘 5.1 PCB的具体内容

操作系统描述信号也是需要用对应的数据结构的。其中,描述未决状态的信号使用一个pending位图,存放在进程的pcb中(信号对应比特位为0,表示没收到该信号;为1表示收到该信号)。由此可见,发送信号不如称为"写信号"。再次强调一点,只有操作系统才能改写进程pcb中的pending位图,也就是说发送信号的载体必须是操作系统。

pcb还有一个描述阻塞状态信号的位图,称为block位图。比特位的位置表示信号编号,内容表示信号是否被阻塞。如何确实某一信号被阻塞了,那么进程收到该信号时,永远不会递达该信号,除非在未来解除阻塞。

pcb除了拥有以上的两个位图,还有一个指向handler_t handler数组的数组指针,这个数组是一个函数指针数组,其内容指向了对应信号的递达动作。由此可见,signal捕捉信号的本质就是在数组当中设置新的函数指针。

在这里插入图片描述

小结:

  1. 如果一个信号没有产生,并不会妨碍它被进程阻塞(也就是不妨碍操作系统设置block位图)

  2. 进程为何能够识别信号?不仅仅是通过程序员编码完成,在pcb中操作系统已经设置了上述三种数据结构(最主要的便是数组记录了信号对应的递达动作)

  3. 普通信号(1~31号信号)只能被收到一次(位图只有0或1两种状态),也就是说就算我们向某一信号发送N次M号信号,进程只会收到一次M号信号

🕘 5.2 sigset_t

这是操作系统设置的类型,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态。

sigset_t类型的变量不能单独使用,必须要配合特定的系统调用接口使用。

信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set); // 将信号集所有的位清0
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); // 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

🕘 5.3 sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 返回值:若成功则为0,若出错则为-1
// set:输入型参数
// oset:输出型参数
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
  • 如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

how参数的可选值 :
SIG_BLOCK:将set中的信号添加到信号屏蔽字中
SIG_UNBLOCK:将set中的信号从信号屏蔽字中解除阻塞
SIG_SETMASK:将信号屏蔽字设置为set

🕘 5.4 sigpending

该系统调用不对pending表修改,而仅仅是获取进程的pending位图。

#include <signal.h>
int sigpending(sigset_t *set); // 参数为输出型参数

🕘 5.5 案例

现在需要把2、3号信号屏蔽,并观察其位图

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

#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

using namespace std;

static vector<int> sigarr = {2,3};

static void show_pending(const sigset_t &pending)
{
    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else cout << "0";
    }
    cout << "\n";
}

int main()
{
    // 先尝试屏蔽指定的信号
    sigset_t block, oblock, pending;
    // 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 添加要屏蔽的信号
    for(const auto &sig : sigarr) sigaddset(&block, sig);
    // 开始屏蔽,设置进内核(进程)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 遍历打印pengding信号集
    int cnt = 10;
    while(true)
    {
        sigemptyset(&pending);  // 初始化
        sigpending(&pending);   // 获取
        show_pending(pending);  // 打印
        sleep(1);
    }
}

在这里插入图片描述

可以看到,在输入Ctrl+C(2号信号)时,位图变成10(从右起第2个0变成1);输入Ctrl+\(3号信号),位图变成110(从右起第3个0变成1)

如果解除信号屏蔽,那信号就要递达,我们应该能观察到由1变0的情况

......

static void myhandler(int signo)
{
    cout << signo << " 号信号已经被递达!!" << endl;
}

int main()
{
    for(const auto &sig : sigarr) signal(sig, myhandler);

    // 先尝试屏蔽指定的信号
    sigset_t block, oblock, pending;
    // 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 添加要屏蔽的信号
    for(const auto &sig : sigarr) sigaddset(&block, sig);
    // 开始屏蔽,设置进内核(进程)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 遍历打印pengding信号集
    int cnt = 10;
    while(true)
    {
        sigemptyset(&pending);  // 初始化
        sigpending(&pending);   // 获取
        show_pending(pending);  // 打印
        sleep(1);
        if(cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!
            cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
        }
    }
}

在这里插入图片描述

🕒 6. 信号的递达

信号什么时候被处理?

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

🕘 6.1 用户态和内核态

用户态:用户代码和数据被访问或者执行的时候,所处的状态。自己写的代码全部都是在用户态执行。

内核态:执行OS的代码和数据时,进程所处的状态。OS的代码的执行全部都是在内核态执行(例如系统调用)。

主要区别:权限大小,内核态权限远远大于用户态。

用户态使用的是用户级页表,只能访问用户数据和代码;内核态使用的是内核级页表,只能访问内核数据和代码。

在这里插入图片描述

需要强调一点:内核级页表只存在一张。然后要执行的操作系统的代码的时候,只需要在自己的地址空间随意跳转即可。

无论进程如何切换,都不改变更改内核级页表。

CPU内有寄存器保存了当前进程的状态。

所谓系统调用:就是进程的身份转化成为内核,然后根据内核页表找到对应函数执行。

🕘 6.2 信号捕捉

信号捕捉本质是修改handler表中的内容。

内核实现信号捕捉的过程大致是下图这样:
在这里插入图片描述

上图可简化抽象为:
在这里插入图片描述

为什么返回第一次陷入内核的下一处指令需要再次返回到内核态?其原因在于:用户态执行自定义捕捉动作的时候没有权限返回第一次陷入内核的下一处指令(即使有上下文数据),所以必须再次返回内核态。

🕤 6.2.1 signal、sigaction

sigaction这个接口与signal的动作一模一样,都是信号的捕捉。不过sigaction的信号更加复杂,并且在运行时可以屏蔽除当前信号之外的信号。

#include <signal.h>
 
typedef void (*sighandler_t)(int); // 函数指针
 
sighandler_t signal(int signum, sighandler_t handler);
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act:新的处理动作
// oldact:原来的处理动作
 
// act 结构体
struct sigaction 
{
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
// 当某个信号的处理函数被调用时, 内核自动将当前信号加入进程的信号屏蔽字, 当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时, 如果这种信号再次产生, 那么它会被阻塞到当前处理结束为止(即同一个信号不能被嵌套使用)。
 
// 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号, 则用sa_mask字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 
 
// sa_flags字段包含一些选项, 本章的代码都把sa_flags设为0, sa_sigaction是实时信号的处理函数, 本章不详细解释这两个字段

可以看到sigaction的接口是两个结构体。我们在使用sigaction时需要设置好指定的结构体对象,结构体对象当中有三个我么需要操作的参数:第一个箭头很明显,是自定义捕捉动作;第二个是一个用户层信号级,用来设置要阻塞的信号;第三个箭头指向的flags置0即可。

下面我们要完成一套动作:向进程连续发送N个2号信号,然后屏蔽3号信号,然后再发送N个3号信号,观察现象。

下面是实现的代码:

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

using namespace std;

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

void handler(int signo)
{
    cout << "get a signo: " << signo << "正在处理中..." << endl;
    Count(10);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask); // 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
    sigaddset(&act.sa_mask, 3);
    sigaction(SIGINT, &act, &oact);

    while(true) sleep(1);

    return 0;
}

在这里插入图片描述

可以看到,我们发送了一堆2号信号,由此可以证明:当某个信号正在处理的时候,同类型信号无法被递达,其原因在于当当前信号正在被捕捉时,系统会自动将当当前信号加入到进程的信号屏蔽字,当信号完成捕捉动作后,自动解除对该信号的屏蔽。一般一个信号被解除屏蔽的时候,自动进行递达当前屏蔽的信号(信号已经被加入pending的话)。再次重申:信号检测之后,递达信号之前,pending信号对应的比特位由1置0,所以可以接收第二次信号。这就是我们为什么能看到2号信号出现了两次的原因(最多也只能出现两次)。

正因为同类型的信号只能递达两次,当这两次递达完成之后,系统将会取消上层信号级sa_mask中设置的屏蔽信号

由此可以得出结论:在上述的案例中,如果没有对3号信号屏蔽,那么当2号信号正在处理的时候就会直接退出进程。并且需要注意,上面我们一直强调某一信号递达时将会阻塞该信号,一定是同类型信号,非同类型信号将会直接递达(如果没有手动屏蔽的话)。

🕒 7. 可重入函数

一般而言,我们认为main执行流和自定义捕捉动作是两个执行流。所以不可避免两个执行流调用相同的函数。那么就可能发生这么一种情况:假设函数func会执行malloc动作,而free的动作在main执行流存在,而自定义捕捉动作没有free动作,如果main和自定捕捉动作都执行了这个func函数,那么将会发生内存泄漏。所以这个函数被称为不可重入函数(因为一旦重复调用,就会引发错误)。

一般的,我们可以由下面一条规则判断一个函数是否是可重入函数:

当函数调用了malloc接口或者标准IO函数,这个函数就是不可重入函数

大部分函数都是不可重入函数!可重入和不可重入函数并不是一个BUG,而是特性!

🕒 8. volatile关键字

volatile这个关键字是C语言的关键字,在解释这个关键字之前先看看其他问题。

编译器是存在优化的,例如STL中发生的连续拷贝构造会优化成一个,debug和release版本的效率不同等等。那么在LInux的gcc编译器中,也存在编译器的优化级别,分别为:O0、O1、O2、O3等等,编译器默认的优化级别不会超过O2。下面以一个案例来探讨一些问题:

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

int quit = 0;
 
void handler(int signo)
{
    printf("%d 号信号,正在被捕捉!\n", signo);
    printf("quit: %d", quit);
    quit = 1;
    printf("-> %d\n", quit);
}
 
int main()
{
    signal(2, handler);
    while(!quit);
    printf("正常退出程序!\n");
    return 0;
}
[hins@vm-centos7 signal]$ gcc mysignal.c -o mysignal

在这里插入图片描述

在这里插入图片描述

可看到,发送2号信号后,myHandler执行流把全局变量quit由0置1,回到main执行流时不进入while循环,就会退出程序。这是编译器默认优化情况。

现在手动将gcc优化级别提高到O3:

[hins@vm-centos7 signal]$ gcc -O3 mysignal.c -o mysignal

在这里插入图片描述
可以看到quit已经由0置1了,但是main执行流中的while循环似乎仍在继续,原因如下:

当编译器没有优化时,cpu需要的数据确确实实从内存拿,所以可以看到一个很正常的现象。但是当编译器优化之后,编译器会认为main执行流中的quit仅仅用作判断,并没有改变值,所以在cpu需要quit时,就会直接从内存拿quit装入寄存器,在以后需要quit的时候直接读取寄存器的值,即使内存中的值已经发生更新。

所以,为了避免上述的错误,可以使用关键字volatile,其作用是保持内存可见性。也就是说,当全局变量quit被valitle修饰之后,编译器不会把它当作优化对象,在以后需要quit的时候,会正常的从内存里读取数据。

volatile int quit = 0;

在这里插入图片描述

🕒 9. SIGCHLD信号

这个信号和进程等待有关系。子进程死亡时会告诉父进程自己已经是"僵尸"了,这个告诉的过程便是发送一个SIGCHLD信号(17号信号)。但是这个信号与其他信号不同,它的默认处理动作是忽略,但是是内核级别的忽略动作

在这里插入图片描述

如何证明子进程退出时,会向父进程发17号信号?我们以下面这段代码证明:

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

using namespace std;
 
void myHandler(int signo)
{
    cout << "get signo:" << signo << endl;
}

int main()
{
    signal(SIGCHLD,myHandler);
    pid_t id = fork();
    if(id == 0)
    {
        cout << "I am a child..." << endl;
        sleep(1);
        exit(0);
    }
    waitpid(id,nullptr,0);
    return 0;
}
[hins@vm-centos7 signal]$ ./mysignal
I am a child...
get signo:17

上面的代码仅仅是创建了一个子进程,如果我们有多个子进程同时退出,就不能单独waitpid了,必须循环等待。那么如何知道要回收哪些子进程呢?可以将waitpid的第一个参数设为-1,它会自动地检测并回收所有子进程。同时也带出了一个新的问题:如果一批子进程只有部分退出,如何处理?很显然,必须使用非阻塞式等待,否则将会影响父进程的执行流。

我们可以对SIGCHLD信号设置一个忽略动作SIG_IGN,此动作是让操作系统去负责子进程的回收,也就是对父进程来说,子进程不会变成僵尸进程:

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

using namespace std;

int main()
{
    signal(SIGCHLD,SIG_IGN);
    pid_t id = fork();
    if(id == 0)
    {
        cout << "I am a child..." << endl;
        sleep(3);
        exit(1);
    }

    while(1);   // 让父进程不退出,否则父进程一定先比子进程退出

    return 0;
}

在这里插入图片描述

需要注意的是,内核级的默认动作Ign与用户层的SIG_IGN忽略动作不一样,内核级的忽略动作就是要让父进程waitpid,而用SIG_IGN是让操作系统接管子进程。


OK,以上就是本期知识点“进程信号”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
💫如果有错误❌,欢迎批评指正呀👀~让我们一起相互进步🚀
🎉如果觉得收获满满,可以点点赞👍支持一下哟~

❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/489436.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

svn如何合并代码以及解决合并冲突的问题(把分支代码合并到主版本)

1.选择主版本的文件夹。 ​​​​​​​ 2.选择合并一个不同的分支 3.选择主分支的路径和要合并的代码范围 4.点解next 选择这两个选项 5.然后点击Test merge&#xff0c;查看能否和并成功 有红色的提示&#xff0c;说明是有冲突的&#xff0c; 都是黑色说明能够合并成功 …

【无标题】如何使用 MuLogin 设置代理

如何使用 MuLogin 设置代理 使用 MuLogin 浏览器设置我们的代理&#xff0c;轻松管理多个社交媒体或电子商务帐户。 什么是MuLogin&#xff1f; MuLogin 是一款虚拟反检测浏览器&#xff0c;使用户能够管理多个电子商务、社交媒体和广告帐户&#xff0c;而无需验证码或 IP 禁…

星巴克终止Odyssey Beta NFT计划

日前&#xff0c;咖啡品牌星巴克宣布将于3月31日终止其NFT产品Odyssey Beta客户忠诚度计划。这意味着&#xff0c;曾经旨在改进会员忠诚度的Web3 产品Starbucks Odyssey将终止&#xff0c;构筑多年的Web2会员系统“星享俱乐部”脱去了Web3外衣&#xff0c;回到了本来的面貌。 至…

体验分低导致闭店!抖音小店体验分是什么?教你如何提高体验分!

哈喽~我是电商月月 相信开抖音小店的伙伴们对体验分这个东西都不陌生&#xff0c;但如何有效的提高体验分你们知道吗&#xff1f; 今天&#xff0c;我就来讲讲抖音小店体验分低有什么后果&#xff0c;同时在后面说一下体验分降低如何提高&#xff01; 大家可根据情况不同自行…

羊大师羊奶靠谱么?品质保障深度解析

羊大师羊奶靠谱么&#xff1f;品质保障深度解析 羊大师羊奶&#xff0c;作为市场上的知名品牌&#xff0c;其靠谱性一直备受消费者关注。那么&#xff0c;羊大师羊奶究竟靠谱不靠谱呢&#xff1f;这就需要从品质保障和消费者信赖两个方面进行深入解析。 从品质保障的角度来看&…

【JAVA】数据类型与变量(主要学习与c语言不同之处)

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;橘橙黄又青-CSDN博客 目标&#xff1a; 1. 字面常量 2. 数据类型 3. 变量 1.字面常量 在上节课 Hello…

Mysql的高级语句2

目录 引言&#xff1a; 一、按关键字进行排序 1、语句以及用法 2、先创建一个新的数据库以及数据表&#xff0c;并且导入内容 二、关键字排序操作 1、单个字段排序 ①按照分数进行排序&#xff0c;默认不指定就是升序排列 ②按照分数降序排列 ③结合where进行条件过滤…

C# LINQ笔记

C# LINQ笔记 from子句 foreach语句命令式指定了按顺序一个个访问集合中的项。from子句只是声明式地规定集合中的每个项都要访问&#xff0c;并没有指定顺序。foreach在遇到代码时就执行其主体。from子句什么也不执行&#xff0c;只有在遇到访问查询变量的语句时才会执行。 u…

【zlm】问题记录:chrome更新引起的拉不出webrtc; 证书校验引起的放几秒中断

目录 chrome更新引起的拉不出webrtc 证书校验引起的放几秒中断 chrome更新引起的拉不出webrtc 【zlm】最新的chrome版本中的报错&#xff1a; 我有个问题event.js:8 [RTCPusherPlayer] DOMException: Failed to execute setRemoteDescription on RTCPeerConnection: Failed …

太牛逼了!视频号下载器手机版(工具+方法)绝了

在众多的视频号下载中&#xff0c;可以说这个工具真的是很牛逼了&#xff01;这里问大家一个问题&#xff01; 你使用视频号下载工具以及视频号下载器都会不会因时间导致而失效呢&#xff1f; 自从小编使用了这款工具后&#xff0c;就不会因为视频失效而烦恼。 很多人免费推荐…

互斥锁与信号量的区别

信号量与互斥锁都是用于多线程编程中&#xff0c;以实现资源共享和线程同步的机制&#xff0c;但它们在应用场景、实现方式和性能特点上有所不同。以下是详细介绍&#xff1a; 应用场景。信号量主要用于线程同步&#xff0c;其核心思想是控制对共享资源的访问许可&#xff0c;…

javaWeb项目-快捷酒店信息管理系统功能介绍

开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 项目关键技术 1、JSP技术 JSP(Java…

Windows虚拟主机上的网站如何来设置默认首页

近期有网友咨询想要知道Windows虚拟主机上的网站如何来设置默认首页,以便后期他需要时可以自行处理。这边了解到他当前使用的是Hostease 的Windows 虚拟主机&#xff0c;而设置默认首页的操作步骤如下&#xff1a; 1.Hostease的Windows虚拟主机都是带Plesk面板的,因此需要先进入…

智慧公厕的先进技术应用

公共厕所一直以来都是城市管理中一个重要的工作&#xff0c;但设施老化、环境脏乱、服务质量低下等问题一直困扰着城市居民。然而&#xff0c;随着科技的进步和数字技术的应用&#xff0c;智慧公厕的建设正在改变这一现状。 智慧公厕通过对所在辖区内所有公共厕所的全域感知、…

面试经典150题【91-100】

文章目录 面试经典150题【91-100】70.爬楼梯198.打家劫舍139.单词拆分322.零钱兑换300.递增最长子序列77.组合46.全排列39.组合总和&#xff08;※&#xff09;22.括号生成79.单词搜索 面试经典150题【91-100】 五道一维dp题五道回溯题。 70.爬楼梯 从递归到动态规划 public …

详解Java 中的 Lambda 表达式

引言&#xff1a; Lambda 表达式是 Java 8 中引入的一个重要特性&#xff0c;它可以使代码更加简洁、易读&#xff0c;并且更加具有函数式编程风格。Lambda 表达式本质上是一个匿名函数&#xff0c;它可以作为方法参数传递&#xff0c;也可以直接赋值给一个变量。 一、Lambda 表…

Day20:LeedCode 654.最大二叉树 617.合并二叉树 700.二叉搜索树中的搜索 98.验证二叉搜索树

654. 最大二叉树 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。 返回 nums …

【Rust】——提取函数消除重复代码和泛型

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

Java项目:75 springboot房产销售系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 使用房产销售系统分为管理员和用户、销售经理三个角色的权限子模块。 管理员所能使用的功能主要有&#xff1a;首页、个人中心、用户管理、销售经理管…

OpenCV4.9在iOS中安装

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;使用CUDA 为Tegra构建OpenCV-CSDN博客 下一篇&#xff1a; 警告&#xff01; 本教程可以包含过时的信息。 所需软件包 CMake 2.8.8 或更高版本Xcode 4.2 或更高版本 从 G…