进程信号

目录

1.认识信号

信号是什么

为什么要有信号

2.信号的产生

[kill 命令](#kill 命令)

键盘产生

系统调用

[kill 函数](#kill 函数)

raise函数

abort函数

软件条件

硬件异常

3.信号的保存(信号集)

认识信号集

信号集操作函数

操作未决信号集的函数

[sigemptyset 函数](#sigemptyset 函数)

[sigfillset 函数](#sigfillset 函数)

[sigaddset 函数](#sigaddset 函数)

[sigdelset 函数](#sigdelset 函数)

[sigismember 函数](#sigismember 函数)

[sigpending 函数](#sigpending 函数)

操作阻塞信号集的函数

sigprocmask函数

4.信号的处理

signal函数

sigaction函数

内核态和用户态

信号的捕捉流程


1.认识信号

信号是什么

在Linux操作系统中,信号是Linux系统提供的,让一个进程给其他进程发送异步信息的一种方式

我们可以从生活的角度来理解一下Linux系统中的信号,以红绿灯为例:

我们在等待红绿灯的时候,我们默认就知道 "红灯停、绿灯行、黄灯亮了等一等",红灯、绿灯、黄灯其实就是交通信号灯给行人发送的一种信号。

  • 我想说的是:进程能识别并处理信号

假如有一个人在等红绿灯,绿灯亮了,但是他收到了一条要及时回复的信息,于是他回复完信息之后再通过斑马线。

  • 我想说的是:信号产生的时候可以不处理,在合适的时候再处理(不处理的时候需要对信号进行保存)

再假如,某一天一个路口的红绿灯只会显示颜色而不显示倒计时,这个时候有的行人就可能会通过玩玩手机或者聊聊天等方式来打发一下时间,等到绿灯亮了再通过斑马线。

  • 我想说的是:信号的产生是随时的,进程无法预料,所以信号是异步发送的

在Linux操作系统中具体的信号是一批具有编号的宏,我们可以使用 kill -l 命令查看:

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 头文件中找到。
  • 编号34及以上的是实时信号,我们只学习编号34以下的信号,不讨论实时信号。
  • 注意:没有0号信号,也没有32、33号信号。

每一个信号都有特定的产生方式和默认的处理方法,我们可以使用 man 7 signal 命令进行查看:

为什么要有信号

信号被设计出来可不是为了玩的,而是有特定用途的,信号的用途主要有以下几个方面:

  • 进程控制 :通过信号可以控制进程的行为,例如终止进程(SIGKILL)、暂停进程(SIGSTOP)等等......
  • 异常处理:当进程发生异常(如段错误、除零错误)时,内核会给发生异常的进程发送信号,进程可以捕获并处理这些信号。
  • 事件通知 :信号可以用于通知进程某些事件的发生,例如定时器到期(SIGALRM)、子进程状态改变(SIGCHLD)等......
  • 用户交互 :用户可以通过键盘输入(如Ctrl+C 终止一个进程)向进程发送信号。

正是因为有这么多的需求,操作系统的开发者才设计了信号这么一种进程异步通信的方式。

2.信号的产生

信号的产生方式可以概括为五种,分别为:kill命令、键盘、系统调用、软件条件、硬件异常。

kill 命令

我们可以使用 kill 信号编号 进程pid命令给一个特定进程发送特定的信号。

我们给特定进程发送9号信号:

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

using namespace std;

int main()
{
	while(1)
	{
		cout << "my pid is: " << getpid() << endl; 
		sleep(1);
	}

	return 0;
}

运行结果:

  • kill 命令通过给特定进程发送特定信号的方式终止了当前进程。

键盘产生

我们可以在键盘上按下特定的组合键来给当前进程发送信号,常用的有:

  • Ctrl+c: 终止当前前台进程。
  • **Ctrl+\ :**终止当前进程并生成核心转储文件。
  • **Ctrl+z:**暂停当前进程并将其放入后台。

以Ctrl+c为例,终止一个进程:

系统调用

kill 函数

功能:用于向指定进程发送信号。

头文件:<sys/types.h>、<signal.h>

函数原型:int kill(pid_t pid, int sig)

参数:

  • pid_t pid:接收信号进程的pid,表明向哪个进程发送信号。
  • int sig:要发送的信号。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置错误码指示错误类型。
  • kill 命令其实就是通过kill函数实现的。

raise函数

功能:用于向当前进程发送信号(自己给自己发送信号)。

头文件:<signal.h>

函数原型:int raise(int sig)

参数:

  • int sig:要发送的信号

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置错误码指示错误类型。

使用示例:让进程给自己发送9号信号,自己终止自己。

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

using namespace std;

int main()
{
	int cnt = 0;
	while(1)
	{
		cout << "I am runnig happly" << endl; 
		sleep(1);
		cnt++;
		if(cnt == 5)
		{
			cout << "I want to die !!!\n" << endl;
			raise(9); // 自己给自己发送9号信号
		}
	}

	return 0;
}

运行结果:

abort函数

功能:用于异常终止当前进程 ,并生成一个核心转储文件(core dump)。这个函数通常用于在程序检测到无法恢复的错误时,强制终止程序并生成调试信息。

头文件:<stdlib.h>

函数原型:void abort(void)

参数:无参数

返回值:无返回值

使用起来有点类似于 exit 函数。

使用示例:

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

using namespace std;

int main()
{
	int cnt = 0;
	while(1)
	{
		cout << "I am runnig happly" << endl; 
		sleep(1);
		cnt++;
		if(cnt == 5)
			abort();
	}

	return 0;
}

运行结果:

软件条件

SIGPIPE就是一种由软件条件产生的信号。当一个进程通过管道向另一个进程发送数据时,如果读端(read end)的进程关闭了管道,而写端(write end)的进程仍然尝试写入数据,操作系统会发送 **SIGPIPE**信号。

今天我们学习另一种由软件条件产生的信号 ------ SIGALRM。SIGALRM是一个由操作系统发送给进程的信号,通常与alarm函数 有关;alarm函数可以设置一个闹钟 ,当闹钟设定的时间结束之后,操作系统就会给调用 alarm 函数 的进程发送SIGALRM信号,该信号的默认动作是终止当前进程

alarm函数

功能:设定一个闹钟。 也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号 ,该信号的默认处理动作是终止当前进程

头文件: <unistd.h>

函数原型:unsigned int alarm(unsigned int seconds)

参数:

  • 指定定时器的时间,单位为秒。

返回值:

  • 返回之前设置的定时器的剩余时间(以秒为单位)。

  • 如果之前没有设置定时器,则返回 0。

使用示例:我们设置一个5秒的定时器。

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

using namespace std;

int main()
{
	alarm(5);

	while(1)
	{
		cout << "I am running happily" << endl;
		sleep(1);
	}

	return 0;
}

运行结果:

  • 最后显示 Alarm clock,表示闹钟响起后,进程收到了SIGALRM信号而退出。

硬件异常

在代码中,常见的异常有两种:

  • 除零错误引发异常,操作系统会给进程发送 SIGFPE 信号。
  • 访问空指针引发异常,操作系统会给进程发送 SIGSEGV信号。

这两个信号的默认行为相同,都是终止进程,在系统配置允许的情况下,会生成核心转储文件,用于事后调试。

我们编写如下代码验证一下:

具有除0错误的代码:

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

int main()
{
	int a = 1;
	int b = 0;
	int c = a/b;
	cout << "c: " << c << endl;

	return 0;
}

运行结果:

访问空指针的代码:

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

int main()
{
	int* ptr = nullptr;
	*ptr = 10;
	printf("%d\n",*ptr);
	
	return 0;
}

运行结果:

**硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。**例如第一份代码中执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如第二份代码中访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3.信号的保存(信号集)

先介绍两个概念:

  • 信号递达:实际执行信号的处理动作称为信号递达。
  • 信号未决:信号从产生到递达之间的状态叫做信号未决。

认识信号集

当进程收到信号之后,可能不立即处理信号,而是先将信号进行保存,等到合适的时候再处理信号,那么,保存信号是将信号保存在哪的呢?Linux操作系统提供了一个 sigset_t类型 的数据结构用来存储信号,这个数据结构里面用一个比特位来表示一个信号,这个存储信号的数据结构类型被称为信号集

我们可以把信号集简单的理解成位图结构,操作系统为了维护好信号机制,会使用两个信号集位图共同来维护信号机制,这两个位图分别是:

  • pending位图:该位图通常被称为未决信号集 。pinding位图中比特位的位置表示信号的编号,比特位的内容表示进程是否收到该信号(1表示收到信号,0表示未收到信号)
  • block位图:该位图通常被称为阻塞信号集 。block位图中比特位的位置表示信号的编号,比特位的内容表示信号是否被阻塞(1表示被阻塞,0表示未被阻塞)。如果一个信号被屏蔽,那么该信号永远不会被抵达处理,除非接触阻塞。

大概示意图如下:

  • 每个信号都有自己的默认处理方法,这个方法也可以是自定义的。

我们可以看出,Linux系统中通过两个比特位共同决定是否对信号做处理,对与某一个信号来说,具体情况有以下几种:

  • block为1,pending为1:这种情况表示进程收到了一个信号,但是这个信号被阻塞了,只有结束阻塞才能处理该信号。
  • block为0,pending为1:这种情况表示进程收到了一个信号,并且这个信号没有被阻塞,进程会在合适的之后处理该信号。
  • block为1,pending为0:这种情况表示进程没有收到该信号,但是有一个信号被阻塞了,一旦收到该信号就会阻塞该信号。
  • block为0,pending为0:这种情况表示进程没有收到该信号,并且该信号也没有被阻塞。

信号集操作函数

为了灵活的控制进程中的信号,Linux操作系统只吃了一批用于操作 sigset_t类型变量的函数。

操作未决信号集的函数

sigemptyset 函数

功能:初始化set所指向的信号集,使其中所有信号的对应比特位清零,表示该信号集不包含 任何有效信号(相当于清空信号集)。

头文件:<signal.h>

函数原型:int sigemptyset(sigset_t *set)

参数:

  • sigset_t* set:指向 sigset_t 类型变量的指针,该变量用于表示信号集。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

sigfillset 函数

功能:初始化set所指向的信号集,使其中所有信号的对应比特位置为1,表示该信号集的有效信号包括系统支持的所有信号。

头文件:<signal.h>

函数原型:int sigfillset(sigset_t *set)

参数:

  • sigset_t* set:指向 sigset_t 类型变量的指针,该变量用于表示信号集。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

sigaddset 函数

功能:用于向信号集中添加一个特定的信号。

头文件:<signal.h>

函数原型:int sigaddset(sigset_t *set, int signum)

参数:

  • sigset_t *set:指向 sigset_t 类型变量的指针,表示要操作的信号集。
  • int signum:要添加到信号集中的信号编号(例如 SIGINTSIGTERM 等)

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

sigdelset 函数

功能:用于从信号集中删除一个特定的信号。

头文件:<signal.h>

函数原型:int sigdelset(sigset_t *set, int signum)

参数:

  • sigset_t *set:指向 sigset_t 类型变量的指针,表示要操作的信号集。
  • int signum:要从信号集中删除的信号编号(例如 SIGINTSIGTERM 等)

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

sigismember 函数

功能:检查某个信号是否在指定的信号集中。

头文件:<signal.h>

函数原型:int sigismember(const sigset_t *set, int signum)

参数:

  • sigset_t *set:指向 sigset_t 类型变量的指针,表示要检查的信号集。
  • int signum:要检查的信号编号(例如 SIGINTSIGTERM 等)

返回值:

  • 如果信号 signum 在信号集 set 中,返回 1

  • 如果信号 signum 不在信号集 set 中,返回 0

  • 如果发生错误(例如 signum 无效),返回 -1,并设置 errno 以指示错误原因。

sigpending 函数

功能:函数用于获取当前进程中被阻塞且未决的信号集。未决信号是指已经发送给进程但尚未被处理的信号(通常是因为信号被阻塞)。

头文件:<signal.h>

函数原型:int sigpending(sigset_t *set)

参数:

  • sigset_t *set:输出型参数,指向 sigset_t 类型变量的指针,用于存储当前进程中被阻塞且未决的信号集。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

操作阻塞信号集的函数

sigprocmask函数

功能:读取更改进程的信号屏蔽字(阻塞信号集)。

头文件:<signal.h>

函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)

参数:

  • how:指定如何修改当前阻塞信号集,可以取以下值:

    • SIG_SETMASK :将当前阻塞信号集替换set 中的信号集。

    • SIG_UNBLOCK :将 set 中的信号从当前阻塞信号集中移除(解除阻塞这些信号)。

    • SIG_BLOCK :将 set 中的信号添加到当前阻塞信号集中(阻塞这些信号)。

  • set :指向 sigset_t 类型变量的指针,表示要修改的信号集。如果为 NULL,则忽略此参数。

  • oldset :指向 sigset_t 类型变量的指针,用于存储 修改前的阻塞信号集。如果为 NULL,则不保存旧的信号掩码。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

4.信号的处理

信号的处理方式有三种:

  • 执行信号的默认动作。
  • 自定义处理信号(对信号进行自定义捕捉)。
  • 忽略信号(忽略信号也是信号的一种处理方式)。

每一个信号都有自己的默认处理行为,我们也可以通过一些接口来自定义当前进程对信号的处理方式,常用的接口有signal 、sigaction

signal函数

功能:设置指定信号(signum)的处理函数 。当信号发生时,操作系统会调用指定的处理函数(handler)来处理信号。

头文件:<signal.h>

函数原型:void (*signal(int signum, void (*handler)(int)))(int)

参数:

  • signum :信号的编号(例如 SIGINTSIGTERM 等),表示要设置处理函数的信号。

  • handler:信号处理函数的指针。可以是一个用户定义的函数,也可以是以下特殊值:

    • SIG_DFL恢复信号的默认行为

    • SIG_IGN忽略信号

返回值:

  • 成功时返回之前的信号处理函数。

  • 失败时返回 SIG_ERR,并设置 errno 以指示错误原因

使用示例:使用signal函数重定义2号信号的处理行为。

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

void handler(int signo)
{
    std::cout << "我就不退" << std::endl;
}

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

    while (true)
    {
        std::cout << "I am runnig..., pid: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

运行结果:

sigaction函数

功能:用于设置信号处理行为。它比传统的 signal 函数更强大、更灵活,并且具有更好的可移植性。

头文件:<signal.h>

函数原型:int sigaction(int signo, const struct sigaction *act, struct sigaction *oact)

参数:

  • signum :信号的编号(例如 SIGINTSIGTERM 等),表示要设置处理函数的信号。

  • act :指向 struct sigaction 结构体的指针,用于指定新的信号处理行为。如果为 NULL,则忽略此参数。

  • oldact :指向 struct sigaction 结构体的指针,用于保存之前的信号处理行为。如果为 NULL,则忽略此参数。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

补充sigaction结构体说明

sigaction中的字段如下:

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_handler :指定信号处理函数,类似于 signal 函数的处理函数。可以设置为以下特殊值:

    • SIG_DFL:恢复信号的默认行为。

    • SIG_IGN:忽略信号。

  • sa_sigaction :指定一个更复杂的信号处理函数,可以接收额外的信号信息(siginfo_t)。当 sa_flags 包含 SA_SIGINFO 标志时,使用此字段而不是 sa_handler

  • sa_mask :如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

  • sa_flags:控制信号处理行为的标志。常用的标志包括:

    • SA_NOCLDSTOP:如果 signumSIGCHLD,则不接收子进程停止或继续的信号。

    • SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler 作为信号处理函数。

    • SA_RESTART:如果信号中断了系统调用,自动重启该系统调用。

  • sa_restorer:已废弃,不再使用。

使用示例:使用sigaction函数自定义2号信号的处理方式。

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

// 自定义信号处理函数
void handle_sigint(int signum) {
    printf("Received signal number is %d\n", signum);
}

int main() {
    struct sigaction sa;

    // 初始化 sa 结构体
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sigint; // 设置信号处理函数
    sigemptyset(&sa.sa_mask);      // 初始化信号掩码为空
    sa.sa_flags = 0;               // 不使用特殊标志

    // 设置 SIGINT 的信号处理行为
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 等待信号
    while (1) {
        printf("I am running happly and my pid is %d\n", getpid());
		sleep(1);
    }

    return 0;
}

运行结果:

当某个信号的处理函数被调用时,内核会自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止

内核态和用户态

我们前面一直说,信号是在合适的时候被处理的,那合适的时候是什么时候呢?进程从内核态切换回用户态的时候,信号会被检测并处理

那什么是内核态、什么是用户态呢?以文件操作为例,当我们调用fwrite不断写入数据,把文件缓冲区写满的时候,fwrite函数内部就会调用系统调用 write,一次性将文件缓冲区中的数据刷新到内核缓冲区;write是系统调用,其内部的代码肯定要访问操作系统内核的数据,比如文件描述符等等......也就是说,当CPU在执行write的代码的时候,需要较高的权限,此时CPU所处的状态就是内核态 。当执行完write的代码需要返回来执行我们自己的代码,此时,CPU所处的状态就是用户态

信号的捕捉流程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这就叫信号的捕捉。

信号捕捉流程如下图:

举个例子:用户程序设置了 SIGQUIT信号 的处理函数 sighandler。进程当前正在执行main函数,这时发生中断或异常切换到内核态,在中断处理完毕后要返回用户态的main函数之前,检查到有信号SIGQUIT递达,进程决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数(sighandler函数和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程)。 sighandler函数返回后自动执行特殊的系统调用 sigreturn,再次进入内核态查看是否有新的信号要递达,如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

相关推荐
网运少年21 分钟前
5G网络切片技术浅显易懂分析
服务器·网络·5g
邪恶的贝利亚28 分钟前
python的运行--命令行
linux·运维·服务器
sanggou34 分钟前
单体架构部署的缺陷:为什么现代应用需要转型?
运维
偏右右1 小时前
Linux常见命令
linux
weixin_519311741 小时前
通过多线程分别获取高分辨率和低分辨率的H264码流
linux·运维·算法
剑走偏锋o.O1 小时前
Linux常用指令学习笔记
linux·linux常用指令
道法自然,人法天1 小时前
微服务学习(4):RabbitMQ中交换机的应用
运维·学习·docker·微服务·rabbitmq
刘翔在线犯法1 小时前
虚拟机IP的配置,让它上网
linux·运维·服务器
m0_748230941 小时前
详解Nginx no live upstreams while connecting to upstream
运维·nginx
西域编娃2 小时前
CentOS 7 IP 地址设置保姆级教程
linux·运维·centos