Linux 进程信号【信号产生】

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:Linux知识分享

🚚代码仓库:Linux代码练习🚚

🌹关注我🫵带你学习更多Linux知识

🔝

目录

前言

信号概念

[1. 生活角度的信号](#1. 生活角度的信号)

[2. 技术应用角度的信号](#2. 技术应用角度的信号)

ctrl+c如何转化成终止进程的信号

信号的产生

组合键

kill命令

系统调用

raise函数

[abort 函数](#abort 函数)

异常

除0异常

野指针异常

页表属性

核心转储

查看核心转储

​编辑

核心转储的作用

软件条件

alarm函数


前言

在前面的进程控制篇章里,父进程是怎么知道子进程退出了?并且回收子进程。 以及我们之前写的代码报错之后,进程就终止了。这背后的一切都是基于信号,那进程中信号是什么?

信号概念

**1.**生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"

  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成"在合适的时候去取"。

  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"

  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)

  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

**2.**技术应用角度的信号

从生活中还有许多的信号比如红绿灯,这里就有个问题了,你是怎么识别这些信号的?肯定是有人教我,我才知道这些信号是什么意思,并且识别这些信号并做出相应的动作处理。即使有些信号没有发生在我面前,我也知道那些没有发生的信号,我也知道该做什么。

那对于进程而言,也是需要像人一样知道什么信号,接受到信号知道自己该做什么!也会像人一样有信号并不会马上去做。那么也会时间窗口,这就需要进程需要具备保存已经发生信号的能力。

下面我们用一段简单的代码来演示进程是如何接收到信号,并作出相应的动作。

cpp 复制代码
#include <iostream>
#include <unistd.h>

using namespace std;


int main()
{
    while(true)
    {
        cout << "I am process...." << endl;
        sleep(1);
    }
    return 0;
}

代码运行起来变成进程,这时我按ctrl+c这个组合键。你会发现进程终止了。

这里有人就会问了,ctrl+c为什么能够在linux中杀掉进程?

要搞清楚,先要明白两个概念:前台进程和后台进程。

键盘输入首先是前台进程获取的,在linux中,一次登陆中,一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,但是可以允许多个后台进程。
如何切换成后台进程?
指令:./进程名 &

切换之后我们在ctrl+c发现终止不了进程了。
这时候我们需要用 kill -9 pid 杀掉进程。

从前后进程我们知道信号是OS发给前台进程的,那OS又是如何知道键盘上有数据?键盘的数据如何写入到内核中?以及ctrl+c如何转化成终止进程的信号?

这里就要谈谈硬件了,我们只有从硬件层面才能理解这里面的原理。

  1. 硬件层面:当用户按下键盘上的一个键时,键盘的硬件会检测到这个动作,并将相应的信号发送给计算机。

  2. 中断处理:键盘作为一个输入设备,会通过中断的方式通知CPU,告诉它有输入事件发生了。

  3. 驱动程序:操作系统中有一个专门的键盘驱动程序,负责处理这些中断。当驱动程序接收到中断信号时,它会读取键盘的输入状态,并将其转换为一个扫描码(scan code)。

  4. 转换为键码:扫描码是键盘上每个键的唯一标识符。驱动程序会将扫描码转换为键码(key code),键码通常是与特定字符或命令相关联的。

  5. 输入子系统:键码随后被传递给操作系统的输入子系统,该子系统负责将键码转换为实际的字符或命令。

  6. 字符编码:如果键码对应于一个字符,输入子系统会将其转换为相应的字符编码(如ASCII或Unicode)。

  7. 内核处理:内核会接收到这些字符编码,并根据当前的进程和线程的上下文,将它们传递给相应的应用程序。

  8. 应用程序接收:最终,应用程序的事件循环会捕获到这些字符,应用程序可以据此更新其状态或执行相应的操作。

  9. 缓冲区管理:内核通常会维护一个缓冲区来存储键盘输入,直到应用程序准备好读取它们。

  10. 用户空间与内核空间的交互:在许多操作系统中,用户空间(用户程序运行的地方)和内核空间(操作系统内核运行的地方)是分开的。键盘输入数据需要通过系统调用从用户空间传递到内核空间。

ctrl+c如何转化成终止进程的信号

在Linux操作系统中,当用户按下Ctrl+C组合键时,会触发一个名为SIGINT(信号中断)的信号。这个信号是由终端驱动程序生成的,然后传递给前台进程组的领导者。以下是Ctrl+C如何转化成进程终止信号的详细步骤:

  1. 用户按下组合键 :用户在终端或控制台中按下Ctrl+C

  2. 终端驱动程序识别 :终端驱动程序识别到这个特定的组合键,并将其转换为SIGINT信号。

  3. 信号发送 :终端驱动程序将SIGINT信号发送给当前在前台运行的进程组的领导者。

这里解释一下终端驱动程序,说直白的点就是一个函数指针数组。也是中断向量表,里面存放的就是各种方法地址。

信号的产生

前面我们演示了可以用组合键和kill命令产生信号。那还有没有其他的方法可以产生信号?

有的 系统调用、异常、软件条件

组合键

ctrl+c ctrl+\

kill命令

用kill命令可以查看系统定义的信号列表

指令:kill -l

上图红色框起来的是普通信号,绿色框起来的是实时信号。关于为什么没有32和33那是历史问题。这里我们重点讲解普通信号,实时信号只有很少一部分场景能用到,比如车祸时的安全气囊弹出。

在Linux中,signal()函数用于为程序设置信号处理函数。当一个进程接收到一个信号时,它可以选择忽略该信号、执行默认操作,或者通过signal()函数设置一个自定义的处理函数。

函数原型如下:

cpp 复制代码
#include <signal.h>

void (*signal(int signum, void (*action)(int)))(int);

参数说明:

  • signum:指定要处理的信号,常见的信号有SIGINT(中断,通常由Ctrl+C产生)、SIGTERM(终止)、SIGKILL(立即终止,不能被捕获或忽略)等。
  • action:指向信号处理函数的指针。如果设置为SIG_IGN,则忽略该信号;如果设置为SIG_DFL,则执行默认操作。

返回值:

  • 成功时,返回先前的信号处理函数的指针;失败时,返回SIG_ERR,并设置errno以指示错误。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

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

int main()
{
    for(int i = 1; i<=31 ; i++)
    {
        signal(i,myhandler);
    }
    while(true)
    {
        cout << "I am process.... pid: "  << getpid() << endl;
        sleep(1);
    }
    return 0;
}

这里我就演示了1到9的信号,我们自定义的handler方法对9号信号不起作用。大家下去可以用试试其他的,这里除了9、19这两个信号,其他的信号都可以用自定义的方法来处理信号。有人就会问了为什么9和19不行,你想想如果是一个恶意程序,永不退出。OS不就挂了吗?为了自己的安全,OS的生杀大权还要牢牢掌握在自己手中。
这里还有一个问题从代码来说,你的handler函数是在cout函数前面的,按照执行流程它是不会执行的。它是怎么执行的?就像前面说的你正在打游戏和你等下拿快递是两个流程,是异步的。

系统调用

在Linux中,kill函数用于发送信号给进程。每个信号都有一个特定的编号,并且可以用于不同的目的,比如终止进程、暂停进程等。

函数原型如下:

cpp 复制代码
#include <signal.h> 
int kill(pid_t pid, int sig);

参数说明:

  • pid:要发送信号的目标进程的进程标识符(PID)。如果pid是负数,则信号会被发送到与pid的绝对值相同的进程组中的所有进程。
  • sig:要发送的信号。可以是如SIGKILLSIGTERMSIGSTOP等预定义的信号常量。

SIGKILL

  • 定义SIGKILL是一个强制终止进程的信号,它不能被忽略或被捕获。当一个进程接收到SIGKILL信号时,操作系统会立即结束该进程,不会给进程清理资源或保存状态的机会。
  • 用途 :通常用于在进程不响应其他终止信号时强制终止进程。由于SIGKILL不能被捕获或忽略,它是一个强有力的工具,但也应该谨慎使用,因为它不允许进程正常关闭。
  • 发送方式 :可以使用kill命令或kill()函数发送SIGKILL信号。

SIGTERM

  • 定义SIGTERM(终止信号)是一种更为温和的终止信号,它允许进程进行清理操作并优雅地关闭。进程可以捕获SIGTERM信号,并执行一些必要的清理工作,如保存数据、释放资源、关闭网络连接等,然后再终止。
  • 用途SIGTERM通常用于请求进程终止,它给进程一个机会来正确地结束。例如,许多服务和守护进程在接收到SIGTERM时会尝试优雅地关闭。
  • 发送方式 :可以使用kill命令或kill()函数发送SIGTERM信号。

SIGSTOP

  • 定义SIGSTOP是一个暂停信号,它会使进程停止执行。当进程接收到SIGSTOP信号时,它会立即暂停,直到它接收到SIGCONT(继续信号)才会继续执行。
  • 用途SIGSTOP通常用于调试或控制进程的执行流。它可以暂停一个正在运行的进程,而不会导致进程终止,之后可以使用SIGCONT信号来恢复进程的执行。

返回值:

  • 如果成功,返回0。
  • 如果失败,返回-1,并设置全局变量errno以指示错误。

代码示例

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " signum pid\n\n";
}


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

    if(argc != 3) //提示用户怎么用kill命令
    {
        Usage(argv[0]);
        exit(1);
    }
    int signum = stoi(argv[1]); //stoi将字符转换成数字
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);
    if(n == -1) //差错处理
    {
        perror("kill");
        exit(2);
    }

    return 0;
}

提示用户怎么使用,这时我们没有进程,直接创建一个进程。试试!

raise函数

函数原型

cpp 复制代码
#include <signal.h>

int raise(int sig);

raise() 函数用于向调用进程或线程发送一个信号。在单线程程序中,它等同于:

cpp 复制代码
kill(getpid(), sig);

而在多线程程序中,它等同于:

cpp 复制代码
pthread_kill(pthread_self(), sig);

如果发送的信号导致调用了信号处理函数,raise() 将在信号处理函数返回后才返回。

返回值(RETURN VALUE)

  • 成功时,raise() 返回0。
  • 出错时,返回非零值。
cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    // exit(1);
}

int main(int argc, char *argv[])
{
    signal(2, myhandler);
    int cnt = 0;
    while (true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if (cnt % 2 == 0)
        {
            // kill(getpid(), 2);
            raise(2);
            
        
        }
    }
    return 0;
}

raise(2)相当于 kill(getpid(),2)

abort 函数

abortC 语言提供的一个函数,它的作用是 给自己发送 6SIGABRT 信号

abort - 导致进程异常终止。

函数原型

cpp 复制代码
#include <stdlib.h> void abort(void);

描述

abort 函数首先解除对 SIGABRT 信号的阻塞,然后为调用进程引发该信号。这将导致进程异常终止,除非 SIGABRT 信号被捕获,并且信号处理函数没有返回(参见 longjmp(3))。

如果 abort() 函数导致进程终止,所有打开的流都会被关闭并刷新。

如果 SIGABRT 信号被忽略,或者被一个返回的处理器捕获,abort() 函数仍将终止进程。它通过恢复 SIGABRT 的默认处理方式,然后再次引发该信号来实现这一点。

返回值

abort() 函数从不返回。

代码示例

cpp 复制代码
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    // exit(1);
}

int main(int argc, char *argv[])
{
    //signal(2, myhandler);
    signal(SIGABRT, myhandler);
    int cnt = 0;
    while (true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if (cnt % 2 == 0)
        {
            // kill(getpid(), 2);
            //raise(2);
            abort();
            //  kill(getpid(), 6);
        }
    }
    return 0;
}

异常

除0异常

所谓 硬件异常 其实就是我们在写程序最常遇到的各种报错,比如 除 0、野指针

先来看一段简单的代码

cpp 复制代码
int main()
{
    int a = 10;
    a /= 0;
    return 0;
}

点击运行出现错误,我们可以根据报错 用 man 7 signal查看是几号信号。

发现是8号信号,也就是传说中的浮点溢出!!!

我们自定义捕捉函数,看是不是8号信号

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    // exit(1);
}
int main()
{
    signal(SIGFPE,myhandler);
    int a = 10;
    a /= 0;
    return 0;
}

结果:一直在死循环似的发送信号,明明只发生了一次 除 0 行为

这时有人就会问了 为什么会一直死循环

当FPU触发内部异常时,状态寄存器中的相应位会被设置,以指示发生了哪种类型的异常

  1. 一旦检测到异常,处理器的异常处理机制会被激活。这通常涉及到以下几个步骤:

    将当前的状态和一些寄存器的值(如程序计数器、状态寄存器、浮点状态寄存器等)保存到一个安全的地方,通常是堆栈。如上图将状态寄存器的比特位设置为1

  2. 将程序计数器更新为指向异常处理程序的地址。在大多数操作系统中,这个地址是在操作系统的内存中的一个固定位置。

操作系统会识别这个异常,并将其转换为一个信号(如 SIGFPE)。操作系统会查找该信号的当前处理函数。

如果程序已经注册了一个信号处理函数,操作系统会将控制权传递给这个函数。在信号处理函数执行期间,相关的寄存器和状态可能会被进一步修改。

如果信号处理函数返回到主程序,而主程序中的异常状态没有得到妥善处理,程序可能会重新尝试执行导致异常的指令,从而陷入死循环。

野指针异常

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int* ptr = nullptr;
    *ptr = 10;

    return 0;
}

Segmentation fault 段错误 出现段错误问题时,操作系统会发送 11SIGSEGV 信号终止进程

那么 野指针 问题是如何引发的呢?

看下图


野指针两类问题:

  1. 内存访问越界

2.权限不匹配,只读的区域,进行写入操作

在执行 *ptr = 10 这句代码时,首先会进行 虚拟地址 -> 真实(物理)地址 之间的转换

指向不该指向的空间:这很好理解,就是页表没有将 这块虚拟地址空间 与 真实(物理)地址空间 建立映射关系,此时进行访问时 MMU 识别到异常,于是 MMU 直接报错,操作系统识别到 MMU 异常后,向对应的进程发出终止信号

权限不匹配:页表中除了保存映射关系外,还会保存该区域的权限情况,比如 是否命中 / RW 等权限,当发生操作与权限不匹配时,比如 nullptr 只允许读取,并不允许其他行为,此时解引用就会触发 MMU 异常,操作系统识别到后,同样会对对应的进程发出终止信号


页表属性

在操作系统中,页表(Page Table)是用于实现虚拟内存管理的关键数据结构,它将虚拟地址空间映射到物理地址空间。页表中的每个表项(通常称为页表条目或PTE, Page Table Entry)包含了多种属性,这些属性定义了虚拟页和物理页之间的关系以及内存访问的权限和特性。以下是一些常见的页表属性:

  1. 有效位(Valid/Present Bit): 这个位指示页表条目是否有效。如果有效,表示该条目包含指向物理内存的合法地址。

  2. 脏位(Dirty Bit): 当页从物理内存写回磁盘时,脏位用于标记该页自从被加载到物理内存后是否被修改过。

  3. 访问位(Access Bit): 这个位记录了页被访问的记录,可以用来跟踪内存使用模式,对某些调度算法是有用的。

  4. 修改位(Modified Bit): 与脏位类似,修改位也用于指示页内容是否被修改。某些系统可能会使用这个位来优化页面置换算法。

  5. 引用位(Reference Bit): 引用位记录了页是否被访问过,可以用来辅助页面置换算法决定哪些页面应该被换出到磁盘。

  6. 读/写权限(Read/Write Permissions): 这些位定义了页的访问权限,决定了是否可以读取或写入该页。在某些系统中,如果只允许读取,试图写入会触发一个异常。

  7. 用户/超级用户权限(User/Supervisor Bit): 这个位定义了页是否只对操作系统内核模式(超级用户)可用,或者对用户模式也可用。

  8. 物理地址(Physical Address): 页表条目中通常包含指向物理内存帧的地址。

  9. 全局位(Global Bit): 在某些处理器架构中,全局位用于指示该页表条目在所有进程的页表中都是有效的,如在x86的分页机制中。

  10. 锁定位(Locked Bit): 锁定位用于指示页表条目是否应该被锁定在物理内存中,以防止被页面置换算法换出。

  11. 缓存禁用(Cache-Disable Bit): 这个位可以禁止CPU缓存对该页的缓存,直接从物理内存中读取数据。

  12. 写穿/写合并(Write-Through/Write-Combine Bit): 这些位控制内存的写入策略,如是否将写入操作直接反映到物理内存中(写穿),或者先缓存起来再合并写入(写合并)。

页表属性的具体实现和使用会根据操作系统和处理器架构的不同而有所差异。操作系统的内存管理单元(MMU)使用这些属性来控制内存访问和优化性能。


核心转储

核心转储(Core Dump)是操作系统在进程异常终止时,如遇到段错误(Segmentation Fault)或其他严重错误时,保存的该进程的内存映像文件。这个文件包含了程序的内存、寄存器、程序计数器等状态信息,可以用于后续的调试和分析,以确定程序崩溃的原因。

  1. 内容: 核心转储文件通常包含以下内容:

    • 程序的代码和数据的内存映像。
    • 寄存器的状态,包括程序计数器、堆栈指针等。
    • 程序的堆栈跟踪,包括函数调用序列。
    • 打开的文件描述符。
    • 以及其他一些可能有助于调试的信息。
  2. 目的: 核心转储用于后续的调试分析。开发者可以使用调试器(如gdb)加载核心转储文件,检查崩溃时的程序状态,包括变量的值、调用栈、程序计数器等。

  3. 生成条件: 核心转储的生成可能由以下因素决定:

    • 操作系统的配置。
    • 进程的权限。
    • 系统的资源限制(如磁盘空间)。
  4. 配置: 在Linux系统中,可以通过以下方式配置核心转储的生成:

    • 使用 ulimit 命令设置核心转储的大小限制。
    • 使用 /proc/sys/kernel/core_pattern 配置核心转储文件的命名规则和存储位置。
  5. 安全性: 核心转储文件可能包含敏感信息,如密码、密钥等。因此,出于安全考虑,应该谨慎处理核心转储文件,避免未经授权的访问。

  6. 调试: 使用调试器加载核心转储文件进行调试的基本命令如下:

    gdb /path/to/program /path/to/core
    

    在gdb中,可以使用 bt(backtrace)命令来查看崩溃时的调用栈。

  7. 限制: 核心转储文件并不总是包含所有需要的信息。例如,如果程序使用了动态内存分配,一些内存可能已经被操作系统回收。此外,复杂的程序可能需要结合多个核心转储文件和源代码来定位问题。

  8. 性能影响: 生成核心转储文件可能会对系统性能产生一定影响,尤其是在资源受限的系统中。


在Linux系统中,当进程由于接收到某些信号而异常终止时,操作系统可以生成核心转储(Core Dump)。可以设置为核心转储的信号通常包括但不限于以下几种:

  1. SIGABRT : 由 abort() 函数引发,通常用于指示程序中有严重错误。

  2. SIGFPE: 浮点异常,如除以零、溢出等。

  3. SIGILL: 非法指令,当程序尝试执行非法、格式错误、或不被支持的机器语言指令时触发。

  4. SIGSEGV: 段错误,是最常见导致核心转储的信号,通常发生在程序试图访问未分配或不允许的内存区域时。

  5. SIGBUS: 总线错误,类似于段错误,但由硬件总线错误触发。

  6. SIGXCPU: 超过CPU时间限制,当进程超过其CPU时间配额时触发。

  7. SIGXFSZ: 超过文件大小限制,当进程尝试扩大文件超出其文件大小配额时触发。

这上面用一句话总结

  • Trem -> 单纯终止进程
  • Core -> 先发生核心转储,生成核心转储文件(前提是此功能已打开),再终止进程

我使用的是云服务器,默认是关闭了核心转储。原因等下看演示

查看核心转储

指令:ulimit -a

核心转储的资源大小为0,需要我们手动设置。

指令:ulimit -c 4096 // 大小这个随意

设置好了后,我们再运行之前的代码的得到下图

可以发现核心转储的文件的是很大的,而有很多信号都会产生核心转储文件,所以云服务器一般默认是关闭的

如果打开了核心转储,一旦程序 不断挂掉、又不断重启,那么必然会产生大量的核心转储文件,当文件足够多时,磁盘被挤满,导致系统 IO 异常,最终会导致整个服务器挂掉的

还有一个重要问题是 core 文件中可能包含用户密码等敏感信息,不安全

所以这就是云服务器默认关闭的原因

关闭非常简单 设置为0就行

ulimit -c 0

核心转储的作用

刚才演示核心转储的感觉没上面了不起的,那它存在的意义是什么?

调试!!!

之前在 进程创建、控制、等待 中,我们谈到了 当进程异常退出时(被信号终止),不再设置退出码,而是设置 core dump 位 及 终止信号

也就是说,父进程可以借此判断子进程是否产生了 核心转储 文件

被杀的第8位位图就是我们需要关注的

软件条件

其实这种方式我们之前就接触过了:管道读写时,如果读端关闭,那么操作系统会发送信号终止写端,这个就是 软件条件 引发的信号发送,发出的是 13SIGPIPE 信号 ​​​​​​​Linux 进程间通信之匿名管道

对此有疑问的小伙伴可以去看这篇博客!!!

alarm函数

在Linux中,alarm 函数用于设置一个定时器,当定时器超时后,会向调用进程发送一个 SIGALRM 信号。这个函数是POSIX标准的一部分,并且是UNIX系统中用于进程间同步的最基本的定时器之一。

函数原型

cpp 复制代码
#include <unistd.h> 
unsigned int alarm(unsigned int seconds);

参数

  • seconds:设置定时器超时的时间,单位为秒。

返回值

  • alarm 函数返回在调用前已经安排的剩余时间(秒),如果之前没有设置闹钟或者闹钟已经过期,则返回0。

行为

  • 当定时器超时,如果程序没有处理 SIGALRM 信号,缺省行为是终止程序。
  • 如果已经为 SIGALRM 设置了信号处理函数,那么在定时器超时时会调用该函数。

示例用法

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handle_alarm(int sig) {
    printf("Alarm clock!\n");
}

int main() {
    signal(SIGALRM, handle_alarm); // 设置信号处理函数

    printf("Sleeping for 5 seconds...\n");
    alarm(5); // 设置5秒的定时器

    pause(); // 挂起,直到一个信号到达

    return 0;
}

在这个示例中,我们首先为 SIGALRM 信号设置了自定义的信号处理函数 handle_alarm。然后在 main 函数中,我们使用 alarm(5) 设置了一个5秒的定时器。程序随后调用 pause() 挂起,直到接收到一个信号。当5秒定时器超时后,会发送 SIGALRM 信号给进程,调用 handle_alarm 函数,并打印出 "Alarm clock!"。

注意事项

  • alarm 函数设置的定时器是实时的,它不会暂停即使进程不在运行状态。
  • 如果在定时器超时前再次调用 alarm,可以重置定时器。
  • alarm 函数提供的定时精度较低,不适合需要高精度计时的场合。
  • 在多线程程序中,使用 alarm 可能不是最佳选择,因为它只对整个进程有效,而不是单个线程。

alarm 函数是单次定时器,一旦超时就会失效,如果需要周期性定时,需要在信号处理函数中重新设置。

如何理解返回上一次的的剩余时间?

代码示例

cpp 复制代码
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    int n = alarm(10);
    cout << "上一个闹钟剩余时间: " << n << endl;
    // exit(1);
}


int main()
{
    signal(SIGALRM, myhandler);
    alarm(10);   //设定一个十秒后的闹钟

    while(true)
    {
         cout << "I am a crazy process " << getpid() << endl;
         sleep(1);
    };

    return 0;
}

本篇是信号开篇之作,前面的生活案例打游戏,收到快递信号没有及时去拿快递,那么在系统中又是如何保存这个信号,然后处理?后篇就讲信号是如何保存的!!!

相关推荐
热爱嵌入式的小许24 分钟前
Linux基础项目开发1:量产工具——显示系统
linux·运维·服务器·韦东山量产工具
韩楚风4 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
陈苏同学4 小时前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm
Ambition_LAO4 小时前
解决:进入 WSL(Windows Subsystem for Linux)以及将 PyCharm 2024 连接到 WSL
linux·pycharm
Pythonliu75 小时前
茴香豆 + Qwen-7B-Chat-Int8
linux·运维·服务器
你疯了抱抱我5 小时前
【RockyLinux 9.4】安装 NVIDIA 驱动,改变分辨率,避坑版本。(CentOS 系列也能用)
linux·运维·centos
追风赶月、5 小时前
【Linux】进程地址空间(初步了解)
linux
栎栎学编程5 小时前
Linux中环境变量
linux
我是哈哈hh5 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
郭二哈5 小时前
C++——模板进阶、继承
java·服务器·c++