Linux 信号

信号基础

中断

中断是计算机系统中硬件和操作系统之间进行通信的一种基础机制,它使得系统能够响应紧急事件或需要立即处理的情况。中断分为两大类:

  1. 硬件中断:由硬件设备触发,例如键盘、鼠标、磁盘驱动器等。当这些设备需要 CPU 注意时,它们会发送一个信号给 CPU,请求进行某些操作,如数据传输或状态变更。
  2. 软件中断:由软件程序触发,通常是通过执行特定的中断指令来实现。软件中断用于实现系统调用,允许用户态程序请求操作系统提供的服务。

中断机制允许硬件设备或软件程序在需要时通知 CPU,从而确保了系统能够及时响应外部事件或程序请求。CPU 接收到中断信号后,会保存当前状态,转而执行相应的中断处理程序,处理完毕后再返回到中断前的工作状态。

例子:键盘输入操作

键盘输入操作触发的中断处理流程是计算机系统中硬件中断的一个典型实例。以下是该流程的详细步骤:

  1. 按键动作:当用户按下键盘上的一个键时,键盘硬件电路检测到这一动作并转换为电信号。

  2. 硬件中断请求:键盘的内置控制器接收电信号并生成一个硬件中断请求,随后发送给中央处理器(CPU)。

  3. 中断信号接收:CPU 在接收到来自键盘的中断信号时,如果当前没有处理更高优先级的中断,它会暂停正在执行的任务,并保存当前任务的状态,以便之后能够恢复执行。

  4. 执行中断服务:CPU 根据中断信息访问中断向量表,找到与键盘中断相对应的中断服务程序(ISR)的入口地址,并开始执行该程序。ISR 读取键盘硬件寄存器中的数据,并进行初步处理。

  5. 操作系统处理:键盘的 ISR 将初步处理后的键盘事件传递给操作系统,操作系统负责进一步处理这个事件。

  6. 事件传递给应用程序:操作系统将键盘事件以消息的形式发送给当前活跃的或具有焦点的应用程序。应用程序通过其事件处理机制接收并响应这些消息,例如,在文本编辑器中按下字符键时,编辑器会在光标位置显示相应的字符。

  7. 恢复执行:应用程序处理完键盘事件后,操作系统将控制权返回给之前被中断的任务。CPU 随后恢复执行之前的操作上下文,继续执行,直到遇到下一个中断。

这一流程不仅展示了硬件中断的工作原理,也体现了现代操作系统如何高效地处理用户输入和任务切换。

例子:网络数据包的接收

网络数据包的接收是计算机网络通信中硬件中断的一个关键环节。以下是通过网卡接收网络数据包触发中断的大致流程:

  1. 数据到达:当网络上的数据包到达时,网卡(NIC)利用其硬件逻辑和配置(如 MAC 地址)来确定该数据包是否应由本设备接收。
  2. 硬件中断请求:若数据包是针对本机,网卡将生成硬件中断请求信号,并将其发送给 CPU,以通知有待接收的网络数据。
  3. 中断信号接收:CPU 在接收到网卡的中断信号后,将根据中断的优先级暂时挂起当前任务,并保存任务的运行状态,以便之后可以恢复执行。
  4. 执行中断服务例程(ISR)
    1. CPU 根据中断信号查询中断向量表,定位到与网络中断相关的 ISR,并执行该程序。
    2. ISR 负责与网卡进行交互,从网卡的缓冲区中提取数据包,并进行初步处理,包括数据包的完整性检查和目的地验证。
  5. 数据包处理:ISR 将数据包从网卡复制到操作系统的内存中,完成初步的数据传输工作。
  6. 操作系统处理:数据包复制到系统内存后,操作系统接管控制权,进行进一步处理:
    1. 解析网络协议头部,如 IP、TCP 或 UDP。
    2. 根据数据包类型和目标,将其传递至相应的网络协议栈层或服务。
  7. 应用程序处理:若数据包针对特定应用程序,操作系统将其转发至该应用程序。应用程序根据数据包的内容执行相应操作,如服务器响应 HTTP 请求。
  8. 恢复执行:应用程序处理完毕后,操作系统将控制权交还给之前被中断的任务,CPU 继续执行,直至下一个中断发生。

这一流程不仅展示了硬件中断在网络通信中的作用,也反映了现代操作系统如何处理外部事件和任务调度。

例子:软件中断

软件中断是操作系统提供给应用程序的一种服务请求手段,通常通过特定的系统调用指令来实现。以下是应用程序通过软件中断进行系统调用的流程:

  1. 应用程序请求:例如,应用程序需要从文件中读取数据,会在代码中调用如 read() 这样的系统函数。

  2. 触发软件中断:当应用程序执行 read() 函数时,该函数内部会触发一个软件中断。这个中断指令通知 CPU 从用户模式切换到内核模式,以处理即将到来的系统调用。

  3. 系统调用:CPU 响应软件中断,保存当前任务的状态,以便之后可以恢复执行,并跳转到相应的内核函数。

  4. 执行系统调用:操作系统内核接管控制权,识别出这是一个 read() 系统调用请求。内核检查调用参数的有效性,并执行读取文件数据所需的操作,这可能包括文件系统的查找和磁盘 I/O 操作。

  5. 返回结果:操作系统完成数据读取后,将数据复制到应用程序指定的缓冲区,并将控制权及系统调用的结果返回给应用程序。

  6. 恢复应用程序执行:系统调用完成后,CPU 返回到用户模式,应用程序从中断点继续执行。

  7. 继续执行:应用程序继续执行其余的程序代码,继续其逻辑和任务流程。

软件中断机制是操作系统内核与用户空间程序交互的桥梁,它允许应用程序请求操作系统提供的服务,同时确保系统的安全性和多任务处理能力。

信号概念

在操作系统中,信号 是一种用于通知进程发生了某些事情的 进程间通信机制

在 Linux 系统中,信号可以在多种情况下产生,用于通知进程系统状态的变化或用户交互。每个信号用一个整型常量宏表示,以 SIG 开头。

常见信号

以下是一些常见的信号及其含义:

  • 用户交互信号
    • SIGINT:当用户在终端中按下 Ctrl+C 时,前台进程组会收到此信号,通常用于请求中断正在运行的程序。
    • SIGQUIT:当用户在终端中按下 Ctrl+\时,前台进程组会收到此信号,通常会导致进程终止并生成核心转储文件,用于调试。
    • SIGTSTP:当用户在终端中按下 Ctrl+Z 时,前台进程组会收到此信号,通常用于暂停正在运行的程序。
  • 错误或异常条件信号
    • SIGSEGV:当进程试图访问其内存地址空间之外的内存时,会收到此信号。
    • SIGFPE:当发生异常的算术运算,如除以零或其他算术错误时,进程会收到此信号。
    • SIGBUS:当进程因为硬件故障(例如对齐问题或无法解析的物理地址)尝试进行非法的内存访问时,会收到此信号。
  • 系统状态变化信号
    • SIGCHLD:当子进程终止、停止(由于信号)或继续(由于 SIGCONT)时,父进程会收到此信号。
    • SIGHUP:当控制终端关闭或父进程终止时,与终端关联的进程会收到此信号。
  • 显式的信号发送
    • SIGKILL:可以通过 kill 命令显式发送给进程,以请求立即终止。

POSIX.1-1990 标准信号

Signal Value Action Comment
SIGHUP 1 Term 链接断开
SIGINT 2 Term 键盘中断(Ctrl+C 触发) (默认行为: 终止进程)
SIGQUIT 3 Core 键盘退出(Ctrl+\触发) (默认行为: 终止进程)
SIGILL 4 Core CPU 指令译码阶段无法识别
SIGABRT 6 Core 异常终止
SIGFPE 8 Core 浮点异常
SIGKILL 9 Term 终止进程
SIGSEGV 11 Core 异常内存访问
SIGPIPE 13 Term 写入无读端的管道
SIGALRM 14 Term 定时器超时
SIGTERM 15 Term 终止
SIGUSR1 30, 10, 16 Term 自定义信号 1
SIGUSR2 31, 12, 17 Term 自定义信号 2
SIGCHLD 20, 17, 18 Ign 子进程终止或者暂停
SIGCONT 19, 18, 25 Cont 暂停后恢复
SIGSTOP 17, 19, 23 Stop 暂停进程(可通过 Ctrl+Z 触发)(SIGCONT 或者 fg 恢复)
SIGTSTP 18, 20, 24 Stop 终端输入的暂停
SIGTTIN 21, 22, 26 Stop 后台进程控制终端读
SIGTTOU 22, 22, 27 Stop 后台进程控制终端写

常见信号触发方式

SIGINT 信号:

  • 可通过键盘操作 “Ctrl+C” 触发。
  • 可通过命令 kill -2 pid 触发。

SIGQUIT 信号:

  • 可通过键盘操作 “Ctrl+” 触发。
  • 可通过命令 kill -3 pid 触发。
  • 与 SIGINT 信号的区别在于,SIGQUIT 会导致进程终止并生成核心转储文件(core dumped)。如果 ulimit 设置中的 core file size 为 0,则不会生成转储文件。可以通过 ulimit -a 查看当前的 ulimit 设置。

SIGKILL 信号:

  • 可通过命令 kill -9 pid 触发。
  • 注意:SIGKILL 信号的默认行为是不可更改的,即不能被进程捕获、忽略或阻塞。

SIGSTOP 信号:

  • 可通过键盘操作 “Ctrl+Z” 触发。
  • 可通过命令 kill -19 pid 触发。
  • 信号的默认行为是暂停进程,且这一行为不可更改。
  • 可以通过 SIGCONT 信号使暂停的进程继续运行,或使用 fg 命令恢复。

SIGCONT 信号:

  • 可通过命令 kill -18 pid 触发。
  • 信号的默认行为是恢复之前被 SIGSTOP 暂停的进程。

进程状态查看:

  • 可以通过 ps -elf 命令查看进程的状态,包括它们是否处于暂停状态。

信号列表查看:

  • 使用 kill -l 命令可以列出所有可用的信号及其对应的编号。

信号的处理机制

当进程接收到信号时,它有几种不同的处理机制可以选择:

接收默认处理

进程可以按照操作系统预设的默认行为来响应信号。例如,当用户在终端按下 Ctrl+C 时,内核会向前台进程发送 SIGINT 信号。如果进程没有特别指定如何处理这个信号,它将采用默认行为,如终止进程。

默认处理行为包括:
Term:终止进程。
Ign:忽略信号。
Core:终止进程并生成核心转储文件。
Stop:暂停进程。
Cont:继续被暂停的进程。

忽略信号

进程可以选择忽略某些信号。例如,通过设置信号处理函数 signal(SIGSEGV, SIG_IGN),可以忽略 SIGSEGV 信号,这样程序在访问空指针时就不会触发异常。

然而,有些信号是不允许被忽略的,如 SIGKILL 和 SIGSTOP。

捕捉信号并处理

进程可以注册自定义的信号处理函数。当信号发生时,这些函数会被自动调用来处理信号。这允许进程以编程方式响应各种信号,执行如资源清理、状态保存等操作。

特殊信号处理

有两个信号,SIGKILL 和 SIGSTOP,它们既不能被忽略也不能被捕捉。当进程接收到 SIGKILL 时,它会被立即终止。而 SIGSTOP 会导致进程暂停,只有通过 SIGCONT 信号或特定的命令(如 fg)才能恢复。

信号和中断

信号和中断是操作系统中两种不同的机制,它们在处理紧急事件和进程调度方面各自扮演着独特的角色。

中断

定义:中断是 CPU 对紧急事件的处理和响应机制,涉及到进程的调度和上下文切换。当硬件设备需要 CPU 注意时,它会发送一个中断信号,CPU 响应这个信号并执行相应的中断处理程序。

键盘输入示例:用户按下键盘上的 Ctrl+C 时,键盘硬件生成一个中断请求。CPU 接收到请求后,会暂停当前任务,保存状态,并执行中断处理程序来响应这个事件。

信号

定义:信号是由某些行为产生的,用于修改对应进程的状态,并触发进程做出相应的反应。信号处理机制与进程的生命周期紧密关联,通过 task_struct 结构体来管理。

信号处理:当信号被发送给进程时,Linux 内核会更新进程的 task_struct 中的信号字段,包括待处理信号、信号屏蔽和信号处理程序。待处理信号指示哪些信号已被发送但尚未处理,信号屏蔽指示当前哪些信号被进程阻塞,信号处理程序是指向将在接收信号时执行的函数的指针。

信号和中断的协同

信号和中断在操作系统中经常协作,使得系统和应用程序能够响应各种事件。例如,当用户按下 Ctrl+C 时,键盘硬件生成中断请求,CPU 响应并执行中断处理程序。中断处理程序识别出这是一个请求发送 SIGINT 信号的事件,操作系统确定哪个进程或进程组接收信号,并将信号添加到目标进程的待处理信号队列中。

当目标进程下一次被调度执行时,如果它没有阻塞 SIGINT 信号,操作系统会安排它执行与 SIGINT 信号相关联的处理程序。默认情况下,SIGINT 会导致进程终止,但进程可以自定义信号处理程序来执行不同的操作。

通过这种协同机制,操作系统能够有效地处理外部事件和内部状态变化,确保系统的稳定性和响应性。

信号递送的流程

在 Linux 系统中,信号的递送流程是一个涉及多个步骤的复杂机制,包括信号的生成、传递和处理。

信号生成

信号可能由多种事件触发:

  • 外部中断,例如用户按下键盘上的 Ctrl+C。
  • 系统调用,如使用 kill 函数显式发送信号给某个进程。
  • 软件条件,如执行了除以零的操作。

信号传递

当信号生成后,内核会处理并传递信号给目标进程。这一过程涉及到修改目标进程的 task_struct 结构体中的相关字段,特别是 struct sigpending 类型的 pending 字段,它用于存储和管理当前进程的未决信号。

// /include/linux/sched.h
struct sigpending {
    sigset_t signal;  // 待处理信号的集合
};

pending 字段中包含一个链表 list 和一个 sigset_t 类型的位图 signal。链表 list 存储可排队信号的信息,每个节点是一个 sigqueue 结构体,表示一个具体的未决信号。位图 signal 则标记了当前进程有哪些信号等待处理。

// /include/linux/signal_types.h
struct sigpending { 
    struct list_head list;// 专门存储, 可排队信号的   eg: 40信号,  再来40号
    sigset_t signal;      // 位图 -> 存储当前进程有那些信号等待处理
};
  • 可排队信号:对于某些信号,操作系统允许它们具有多个实例状态,即信号种类相同但携带信息不同。这些信号即使前面存在未处理的信号,也允许它们排队。
  • 不可排队信号:对于不可排队的信号,如果进程中已经存在同一类型的未决信号,后续同类型的信号可能会被视为重复,实际上只是简单地忽略。

信号排队

信号被传递后,内核会为该信号创建一个队列项,并将其加入到目标进程的信号队列中。这个队列可能是一个位图或者一个更复杂的数据结构,用于管理多个信号。在某些情况下,如果信号队列已满,新信号可能会覆盖旧的信号或者被丢弃。

信号处理

当进程下一次被调度执行时,如果存在未被屏蔽的待处理信号,内核将根据信号的类型和进程的信号处理设置,决定是否立即执行信号处理程序。如果进程没有注册信号处理程序,或者信号被设置为默认处理,内核将执行相应的默认操作,如终止进程或忽略信号。

信号与中断的关系

信号的递送流程与中断处理密切相关,但它们是两个独立的概念。中断是硬件或软件触发的紧急事件,需要立即处理;而信号是一种更通用的通信机制,用于通知进程发生了某些事件。

信号的阻塞与解除阻塞

Linux 系统中的进程可以通过修改其 信号掩码 来实现对特定信号的阻塞或解除阻塞。信号掩码是一个数据结构,用于定义哪些信号在当前时刻对进程是阻塞的。

  • 阻塞信号:当信号被阻塞时,它虽然可以被递送到进程,但不会立即得到处理。这些信号会处于待处理状态,直到它们被解除阻塞。
  • 解除阻塞:当信号解除阻塞后,如果该信号的待处理集合中存在,它将可以被进程接收和处理。
信号掩码(Signal Mask)
  • 信号掩码是每个进程都有的一个数据结构,它决定了哪些信号当前被阻塞。信号掩码中的每个位对应一个特定的信号,如果位被设置为 1,则表示相应的信号被阻塞。
  • 进程启动时,默认的信号掩码通常不阻塞任何信号,即信号掩码的所有位都是 0。
信号掩码的设置和改变
  • 进程可以通过系统调用改变其信号掩码,从而控制哪些信号被阻塞或解除阻塞。

  • 例如,如果用户按下 Ctrl+C 触发 SIGINT 信号(信号值为 2),并且进程的信号掩码中对应的位被设置为 1,那么 SIGINT 信号将被阻塞。只有当该位变为 0 时,信号才会变为非阻塞状态,并等待被处理。

信号的接收与处理

  • 检查待处理信号:当进程从内核态切换回用户态时,内核会检查是否存在未被阻塞的待处理信号。
  • 执行信号处理程序:如果有待处理信号,内核将在进程继续执行用户态代码之前,先执行相应的信号处理程序。

信号处理程序的执行

  • 自定义处理函数:如果进程为某个信号注册了自定义的处理函数,当该信号发生时,该函数将被调用执行。
  • 默认行为:如果进程没有为信号注册自定义处理函数,或者信号被设置为使用默认处理,那么将采取信号的默认行为,例如终止进程或暂停进程。
  • 忽略信号:对于某些可以被忽略的信号,如果进程选择忽略它们,那么这些信号将不会产生任何影响。

尽管信号可能源自多种事件,但对于接收信号的进程而言,信号的产生实际上是内核对 task_struct 结构体中信号相关参数的修改。

一些状态

信号的接收与处理

当进程处于可以接受信号的状态,称为 响应时机,它会根据信号执行默认行为、忽略或执行自定义的信号处理函数。

信号的产生与递送

  • 信号产生 意味着内核已经识别到信号的发生。
  • 信号递送 涉及将内核生成的信号添加到目标进程的待处理信号集中,这通常通过修改 task_struct 来实现。有时,它也指信号从产生到执行的完整过程。

信号递送与处理的区别

信号递送 侧重于描述信号从内核传递给进程的过程,而 信号处理 则侧重于进程对接收到的信号所采取的具体动作。

挂起信号与未决信号

已经递送但尚未执行的信号称为 挂起信号未决信号。这可能是因为信号被阻塞,或进程暂时无法处理信号。需要注意的是,信号未决 并不意味着信号阻塞;信号未决表示信号尚未被执行,而信号阻塞需要进程进行相应的解除阻塞操作。

信号阻塞

信号的阻塞由进程的 信号掩码 控制,信号掩码定义了当前被阻塞的信号集合。

同步信号与异步信号

由进程的操作产生的信号称为 同步信号,例如代码中的除以零操作。

由进程外部事件产生的信号称为 异步信号,如用户击键产生的信号。

信号和函数

注册信号: signal

进程在 Linux 系统中对信号的处理有三种基本方式:

  1. 预设处理机制:如果未对信号进行特别设置或操作,进程将按照操作系统提供的预设机制来处理信号。
  2. 忽略信号:进程可以选择忽略特定的信号,使用 signal(SIGINT, SIG_IGN) 来设置。需要注意的是,并非所有信号都可以被忽略。
  3. 设置信号的默认行为:通过 signal(SIGINT, SIG_DFL),进程可以为信号设置其在操作系统中的默认行为。

以下是一个 C 语言示例,展示如何使用 signal 函数来设置信号的处理方式:

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

int main(int argc, char* argv[]) {
    // 忽略SIGINT信号(2, Ctrl+C)
    signal(SIGINT, SIG_IGN);
    printf("SIGINT signal has been ignored.\n");
    sleep(10); // 睡眠10秒,以便观察信号效果

    // 恢复SIGINT信号的默认行为
    signal(SIGINT, SIG_DFL);
    printf("SIGINT signal has default behavior now.\n");
    sleep(10); // 再次睡眠10秒,以便观察信号效果

    return 0;
}

在这个示例中,程序首先忽略 SIGINT 信号,然后通过 sleep 函数暂停 10 秒,之后恢复 SIGINT 信号的默认行为,并再次暂停 10 秒。

signal 设置进程信号处理方式

signal 函数是 POSIX 标准中用于设置进程的信号处理方式的系统调用。它允许程序定义或修改当进程接收到特定信号时所采取的行动。

函数原型:

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

参数:

  1. signum:指定要设置处理方式的信号的编号,例如 SIGINT(中断信号,通常由 Ctrl+C 触发)或 SIGTERM(终止信号)。

  2. action:指向一个函数的指针,该函数定义了当信号 signum 被触发时进程应执行的操作。这个函数应该具有以下原型:

    void handler(int signum);
    

    其中 signum 是被触发的信号的编号。

返回值:

  • 成功时,signal 返回指向之前信号处理函数的指针,如果之前未设置或为默认处理,则返回 SIG_ERR
  • 如果调用失败,返回 SIG_ERR 并设置全局变量 errno 以指示错误。

signal 函数的行为是设置当进程接收到某个信号时所采取的默认行为、忽略该信号或执行一个用户定义的信号处理函数。

以下是使用 signal 函数的一个例子:

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

void sigint_handler(int signum) {
    printf("Received SIGINT, exiting gracefully.\n");
    // 清理工作...
    _exit(EXIT_SUCCESS);
}

int main() {
    // 设置 SIGINT 信号的处理函数
    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
        perror("signal");
        return EXIT_FAILURE;
    }

    printf("Press Ctrl+C to trigger SIGINT and exit gracefully.\n");
    while (1) {
        // 程序主循环
    }

    return EXIT_SUCCESS;
}

在上面的例子中,我们定义了一个 sigint_handler 函数来处理 SIGINT 信号。在 main 函数中,我们使用 signal 设置当接收到 SIGINT 信号时调用 sigint_handler 函数。这样,当用户在终端按下 Ctrl+C 时,程序将捕获 SIGINT 信号,并调用处理函数,从而优雅地退出。

使用 signal 时需要注意的事项:

  • signal 函数的行为在不同平台上可能不一致,特别是在信号处理函数中调用不是异步信号安全(async-signal-safe)的函数时。
  • 在某些系统中,signal 已被 sigaction 函数取代,后者提供了更多的控制,但 signal 仍然广泛用于兼容性。
  • 信号处理函数应该尽可能快速执行并避免调用不是异步信号安全的函数,以避免潜在的竞态条件和不可预测的行为。

同时注册多个信号处理函数

在 Linux 系统中,signal 函数允许同时为多个信号注册处理函数。甚至可以将不同的信号设置为调用同一个处理函数

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

void signal_handler(int sig_value) {
    printf("Signal received with value: %d\n", sig_value);
    // 注意:exit(0); 应避免在信号处理函数中使用,因为它可能会导致未定义的行为
}

int main(int argc, char* argv[]) {
    // 为SIGINT和SIGQUIT信号注册相同的处理函数
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);

    // 无限循环,使程序持续运行以等待信号
    while (1) {
        // 此处的代码将一直运行,直到接收到信号
    }
    return 0;
}

在进程执行过程中,可以重新指定信号的处理函数,用新的行为覆盖旧的行为。

sleep 函数可能会因信号的到达而提前终止,并返回剩余的睡眠时间。

信号处理函数执行完毕后,被中断的 read 函数可能会自动重启。

void fun1(int sig_value) {
    printf("***1*** \n");
}

void fun2(int sig_value) {
    printf("***2*** \n");
}

int main(int argc, char* argv[]) {
    // 注册SIGINT信号的第一个处理函数
    signal(SIGINT, fun1);
    unsigned int retime = sleep(10); // SIGINT信号可能使sleep提前返回
    printf("Sleep was interrupted, remaining time: %u seconds\n", retime);

    // 重新指定SIGINT信号的处理函数
    signal(SIGINT, fun2);
    sleep(10); // 如果再次接收到SIGINT,将调用fun2
    return 0;
}

在这个示例中,SIGINT 信号(通常由 Ctrl+C 触发)的处理函数首先被设置为 fun1,然后在 sleep 调用后被重新指定为 fun2

多个信号同时触发时的行为

在使用 signal 函数处理信号时,进程可能会在处理一个信号的过程中接收到另一个信号。这种情况下,信号处理的行为如下:

  • 不同类型信号:如果进程在处理一个信号时接收到另一个不同类型的信号,当前的信号处理流程将被中断,CPU 将转移执行新到来的信号的处理流程。一旦新信号的处理完成,CPU 将恢复原来信号的处理流程。
  • 相同类型信号:如果进程在处理某个信号时再次接收到相同类型的信号,当前的信号处理流程不会被中断。CPU 将继续执行原来的信号处理流程,待该流程完成后,再响应新到来的相同类型信号。
  • 连续重复的相同类型信号:如果连续接收到重复的相同类型的信号,后续的重复信号将被忽略。这意味着信号处理流程不会因为重复信号而多次执行。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int sig_value) {
    printf("*** %d ***\n", sig_value);
    sleep(10); // 假设信号处理需要一些时间
    printf("*** %d ***\n", sig_value);
    printf("----------- \n");
}

int main(int argc, char* argv[]) {
    // 为SIGINT和SIGQUIT信号注册相同的处理函数
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);

    // 无限循环,使程序持续运行以等待信号
    while (1) {
        // 此处的代码将一直运行,直到接收到信号
    }
    return 0;
}

在这个示例中,SIGINTSIGQUIT 信号共享同一个处理函数 signal_handler。如果在处理 SIGINT 信号的过程中进程接收到 SIGQUIT 信号,当前的 SIGINT 处理流程将被中断,CPU 将执行 SIGQUIT 的处理流程,然后恢复 SIGINT 的处理。

QA

信号处理函数的重新注册

问题:在通过 signal 注册一个信号处理函数后,处理完毕一个信号,是否需要重新注册以捕捉下一个信号?

答案:不需要重新注册。一旦信号处理函数被注册,它将用于该信号的所有后续发生,直到被显式地更改或进程终止。

同类型信号的连续触发

问题:如果信号处理函数正在处理信号,但尚未完成,此时又产生了一个同类型的信号,系统将如何响应?

答案:系统将依次处理这些信号。如果信号处理函数尚未完成,新的同类型信号将被加入待处理队列。然而,根据具体的系统实现,可能会忽略超过一定数量的连续同类型信号,以避免潜在的无限循环。

不同类型信号的连续触发

问题:如果信号处理函数正在处理信号,但尚未完成,此时产生了一个不同类型的信号,系统将如何响应?

答案:当前信号处理流程将被中断,CPU 将转而执行新到来的不同类型的信号的处理流程。一旦新信号处理完毕,如果原始信号处理函数没有被修改或进程没有终止,CPU 将恢复原始信号的处理流程。

注册信号:sigaction

sigaction 检查修改信号处理

sigaction 函数是 POSIX 标准中用于检查或修改信号处理的系统调用。与较旧的 signal 函数相比,sigaction 提供了更多的灵活性和控制能力,包括对信号处理行为的精细控制,以及对信号的屏蔽集的访问。

函数原型:

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

参数:

  1. signum:指定要操作的信号的编号,例如 SIGINTSIGTERM 等。
  2. act:指向 struct sigaction 结构的指针,该结构定义了信号的处理方式。如果这个参数是 NULL,则 sigaction 只查询当前的信号处理设置,不进行修改。
  3. oldact:指向 struct sigaction 结构的指针,用于存储信号的旧处理方式。如果不需要旧信息,可以设置为 NULL

struct sigaction 结构定义了信号的处理方式,其成员包括:

  • sa_handler:指向信号处理函数的指针,用于 SIG_DFLSIG_IGN 之外的信号处理。
  • sa_sigaction:指向一个函数的指针,该函数类似于 sa_handler,但可以处理信号的发送参数,并且可以指定恢复的上下文。
  • sa_mask:信号掩码,定义了在信号处理函数执行期间需要屏蔽的信号集。
  • sa_flags:指定信号处理的各种选项。
    • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
    • SA_RESETHAND: 处理完捕获的信号以后,信号处理回归到默认( 一次注册只生效一次)
    • SA_NODEFER: 在信号处理函数执行期间,同一个信号设置可以再次被触发

    • SA_RESTART:使被信号打断的系统调用自动重新调用。

返回值:

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

以下是使用 sigaction 函数的一个例子:

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

void sigint_handler(int signum) {
    printf("Received SIGINT, exiting gracefully.\n");
    // 清理工作...
    _exit(EXIT_SUCCESS);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;    // 设置信号处理函数
    sigemptyset(&sa.sa_mask);          // 初始化信号掩码集合
    sa.sa_flags = 0;                   // 无特殊标志

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

    printf("Press Ctrl+C to trigger SIGINT and exit gracefully.\n");
    while (1) {
        // 程序主循环
    }

    return EXIT_SUCCESS;
}

在上面的例子中,我们首先设置了 sigaction 结构,指定了 SIGINT 信号的处理函数、信号掩码和标志。然后,我们使用 sigaction 函数来设置 SIGINT 信号的处理方式。当用户在终端按下 Ctrl+C 时,程序将捕获 SIGINT 信号,并调用指定的处理函数,从而优雅地退出。

使用 sigaction 时需要注意的事项:

  • sigaction 提供了比 signal 更灵活的信号处理方式,允许设置信号掩码和特定的处理标志。
  • 信号处理函数应该尽可能快速执行,并避免调用不是异步信号安全的函数。
  • 在编写信号处理函数时,要注意避免使用非可重入的函数,以防止与主程序中的其他部分产生竞态条件。
  • sigaction 是现代 POSIX 系统上推荐使用的信号处理函数,相比 signal,它在多线程环境中更加可靠。

sa_mask

struct sigaction 中的 sa_mask 成员是一个信号掩码,用于定义在信号处理函数执行期间需要被屏蔽的信号集。这是为了在信号处理函数执行时防止这些信号再次发生,从而避免潜在的竞态条件或意外行为。

信号掩码是一个位图(bitmap),其中的每一位对应一个特定的信号。如果位图中的某一位被设置(即值为 1),则表示相应的信号在信号处理函数执行期间将被屏蔽。

以下是 struct sigaction 结构中 sa_mask 的定义示例:

struct sigaction {
    void     (*sa_handler)(int);       // 信号处理函数
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 另一种信号处理函数,可以接收额外信息
    sigset_t sa_mask;                 // 信号掩码,定义了信号处理期间需要屏蔽的信号集
    int      sa_flags;                // 信号处理选项
};

sigset_t 是一个能够表示所有信号的位集合,通常是一个足够大的数组或结构,能够为系统中的每个信号分配一个位。

POSIX 定义了一系列的函数来操作 sigset_t 类型的变量 (man sigemptyset)

sigemptyset(sigset_t *set)                  // 初始化信号集,清除所有信号。
sigfillset(sigset_t *set)                   // 添加所有信号到信号集中。
sigaddset(sigset_t *set, int signo)         // 向信号集添加一个信号。
sigdelset(sigset_t *set, int signo)         // 从信号集中删除一个信号。
sigismember(const sigset_t *set, int signo) // 检查一个特定信号是否在信号集中。

在操作系统中,尤其是 Linux 系统中,信号的阻塞屏蔽(Blocking)和忽略(Ignoring)是两种不同的信号处理策略:

  • 阻塞屏蔽信号:当进程选择阻塞某个信号时,意味着该信号被暂时搁置,不会立即执行。内核会维护一个位图来跟踪所有未决(Pending)信号的状态。如果信号被阻塞,它可能会在进程解除阻塞后,在适当的时机被执行。
  • 忽略信号:与阻塞不同,当进程选择忽略某个信号时,该信号将被系统直接丢弃,不会对进程产生任何影响。即使信号被发送到进程,它也不会被加入到未决信号的位图中。

内核使用位图来跟踪所有信号的状态,包括哪些信号是未决的,哪些是被阻塞的。位图中的每个位对应一个特定的信号,如果位被设置,表示相应的信号处于未决状态。

  • 对于 被阻塞的信号,进程可以在稍后的时间点通过修改信号掩码(Signal Mask)来解除阻塞,使得信号得以处理。
  • 对于 被忽略的信号,由于它们被系统丢弃,因此不会留下任何未决状态,进程也无法对其进行处理。

sigpending 访问待处理信号集

sigpending 是一个在 POSIX 系统(如 Linux)中使用的宏,用于访问和检查进程的待处理信号集。待处理信号集是一组尚未被进程处理的信号。这些信号可能是因为它们被阻塞了,或者进程尚未有机会处理它们。

在 POSIX 标准中,待处理信号集通常通过 sigpending 宏和 sigset_t 数据结构来访问。sigpending 宏定义在 <signal.h> 头文件中,并且通常与 sigprocmask 函数一起使用来检查或修改信号的待处理集。

以下是 sigpending 宏的用法示例:

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

int main() {
    sigset_t pending_set;

    // 获取当前进程的待处理信号集
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        return 1;
    }

    // 检查特定信号是否在待处理信号集中
    if (sigismember(&pending_set, SIGINT)) {
        printf("SIGINT is pending.\n");
    }

    // 处理待处理信号...
    // ...

    return 0;
}

在上面的例子中,我们使用 sigpending 宏获取当前进程的待处理信号集,并存储在 pending_set 变量中。然后,我们使用 sigismember 函数检查 SIGINT(通常由 Ctrl+C 触发)是否在待处理信号集中。

使用 sigpending 时需要注意的事项:

  • sigpending 宏返回的信号集表示了当前进程中所有待处理的信号。
  • 待处理信号集与进程的当前信号掩码(通过 sigprocmask 设置)是分开的。信号可能因为被阻塞而未被处理,即使它们不在待处理信号集中。
  • 信号处理函数应该尽可能快速执行,以避免长时间阻塞信号。
  • 在多线程环境中,待处理信号集是进程级的,而不是线程级的,这意味着所有线程共享相同的待处理信号集。

sigpending 是进程信号管理的重要组成部分,它允许程序检查哪些信号正在等待处理,并据此采取适当的行动。

sigprocmask 控制进程的信号屏蔽字

sigprocmask 是一个在 POSIX 兼容的操作系统中使用的系统调用,它允许用户级程序来控制或修改进程的信号屏蔽字(signal mask)。信号屏蔽字是一个特殊的数据结构,它定义了哪些信号在发送给进程时会被忽略。

信号屏蔽字的作用是暂时阻止某些信号的传递,直到进程准备好处理它们。这在多线程编程中尤其有用,因为它可以防止信号在不适当的时间被传递给进程。

函数的原型如下:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • int how: 这是一个整数参数,用于指定对信号屏蔽字进行何种操作。how 参数可以是以下宏之一:
    • SIG_BLOCK: 将 set 参数指定的信号添加到当前的信号屏蔽字中,即这些信号将被阻塞。
    • SIG_UNBLOCK: 从当前的信号屏蔽字中移除 set 参数指定的信号,即这些信号不再被阻塞。
    • SIG_SETMASK: 将当前的信号屏蔽字替换为 set 参数指定的信号集合。
  • const sigset_t *set: 这是一个指向 sigset_t 类型的指针,sigset_t 是一个结构体,用于表示一组信号。set 参数指定了一组信号,这些信号将根据 how 参数的值被添加到、从或设置为当前的信号屏蔽字。
  • sigset_t *oldset: 这是一个可选的输出参数,也是一个指向 sigset_t 类型的指针。如果提供了这个参数,sigprocmask 会在调用成功时,将调用前的信号屏蔽字复制到 oldset 指向的 sigset_t 结构中。如果不需要这个信息,可以将这个参数设置为 NULL

函数的返回值:

  • 如果 sigprocmask 调用成功,它将返回 0。
  • 如果调用失败,它将返回 -1,并且会设置全局变量 errno 来指示错误类型。可能的错误包括但不限于:
    • EFAULT: 如果 setoldset 指针指向无效的地址空间。
    • EINTR: 如果系统调用被中断。

sigprocmask 函数的使用示例:

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

int main() {
    sigset_t new_set, old_set, pending_set;

    // 初始化信号屏蔽字,包括SIGINT和SIGQUIT
    sigemptyset(&new_set);
    sigaddset(&new_set, SIGINT);
    sigaddset(&new_set, SIGQUIT);

    // 阻塞SIGINT和SIGQUIT
    if (sigprocmask(SIG_BLOCK, &new_set, &old_set) < 0) {
        perror("sigprocmask");
        return -1;
    }

    // ... 执行其他任务 ...

    // 检查是否有被阻塞的信号待处理
    sigpending(&pending_set);
    if (sigismember(&pending_set, SIGINT) || sigismember(&pending_set, SIGQUIT)) {
        printf("SIGINT or SIGQUIT is pending.\n");
    }

    // 恢复原始的信号屏蔽字
    if (sigprocmask(SIG_SETMASK, &old_set, NULL) < 0) {
        perror("sigprocmask");
        return -1;
    }

    return 0;
}

在这个示例中,我们首先创建了一个新的信号屏蔽字 new_set,然后使用 sigprocmask 来阻塞 SIGINTSIGQUIT 信号。之后,我们检查是否有这些信号待处理,最后恢复原始的信号屏蔽字。

kill 向进程发送信号

kill 函数是 POSIX 标准定义的一个系统调用,用于向进程发送信号。信号是进程间通信的一种简单机制,可以用来通知进程发生某些事件或需要执行某些操作。

函数原型如下:

#include <signal.h>
int kill(pid_t pid, int sig);

参数说明:

  1. pid_t pid: 这是要接收信号的进程的进程 ID(PID)。有几种特殊值可以传递给这个参数:
    • pid 为正数时,表示发送信号给具有该 PID 的进程。
    • pid 为负数时,表示发送信号给组 ID 等于 pid 绝对值的所有进程(组内所有进程)。
    • pid 为 0 时,表示发送信号给调用进程所在的进程组中的所有进程,但不包括调用进程本身。
  2. int sig: 这是要发送的信号的编号。信号可以是标准信号,如 SIGKILL(立即终止进程),SIGSTOP(暂停进程),SIGCONT(继续暂停的进程),SIGTERM(终止进程),等等。也可以是自定义信号。

返回值:

  • 如果 kill 调用成功,返回 0。
  • 如果调用失败,返回-1,并设置全局变量 errno 来指示错误类型。可能的错误包括:
    • ESRCH: 没有找到指定的进程或进程组。
    • EPERM: 进程没有权限向目标进程发送信号。

使用 kill 函数时,需要注意以下几点:

  1. 权限: 调用进程必须具有相应的权限才能向目标进程发送信号。例如,通常只有进程的所有者或超级用户(root)才能向进程发送 SIGKILL 信号。
  2. 信号处理: 目标进程可以捕获、忽略或处理信号。进程通过设置信号处理函数来定义对信号的响应。
  3. 信号的发送和接收: kill 函数只负责发送信号,信号的接收和处理由目标进程负责。如果进程没有处理某个信号,系统将采取默认行为。

下面是一个简单的 kill 函数使用示例:

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

int main() {
    pid_t pid = getpid();  // 获取当前进程的PID
    int sig = SIGTERM;    // 定义要发送的信号,这里是SIGTERM

    // 向当前进程发送SIGTERM信号
    if (kill(pid, sig) == -1) {
        perror("kill");
        exit(EXIT_FAILURE);
    }

    printf("SIGTERM signal sent to process %d\n", pid);
    return 0;
}

在这个示例中,我们使用 kill 函数向当前进程发送 SIGTERM 信号。如果进程没有捕获这个信号,它将被默认行为终止。

pause 挂起进程

pause 函数是 POSIX 标准定义的一个系统调用,它使调用进程挂起,直到它接收到一个信号。简单来说,pause 函数让进程进入休眠状态,直到某个信号到来,然后根据信号的类型决定是否继续执行。

函数原型如下:

#include <unistd.h>
unsigned int pause(void);

参数说明:该函数不接受任何参数。

返回值:pause 函数永远不会正常返回。如果进程被信号中断,它会返回一个非零值,并且全局变量 errno 会被设置为 EINTR(中断的系统调用)。

使用 pause 函数的典型场景是在等待某些事件发生时,比如等待信号的到来。这在多线程编程中尤其有用,当一个线程需要等待另一个线程发送信号时,可以使用 pause 来挂起当前线程。

下面是一个简单的 pause 函数使用示例:

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

int main() {
    printf("Process is sleeping...\n");
    pause();  // 进程将在这里休眠,直到接收到信号

    printf("This line will not be printed until a signal is received.\n");
    return 0;
}

在这个示例中,进程使用 pause 函数进入休眠状态。由于 pause 永远不会正常返回,所以 “This line will not be printed until a signal is received.” 永远不会被打印,除非进程接收到信号并且信号处理函数中调用了 return 或者 _exit 函数。

需要注意的是,pause 函数在多线程环境中的行为是未定义的。如果在多线程程序中使用 pause,可能会导致不可预测的行为,因为它可能会影响到整个进程的所有线程。在多线程环境中,通常使用其他同步机制,如互斥锁、条件变量等,来实现线程间的等待和通知。

sigsuspend 挂起进程

sigsuspend 函数是 POSIX 标准定义的一个系统调用,它允许进程挂起直到接收到一个信号。与 pause 函数类似,sigsuspend 也用于等待信号,但它提供了更多的控制,允许进程指定一个信号屏蔽字,从而决定哪些信号应该被屏蔽,哪些信号应该被立即处理。

函数原型如下:

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

参数说明:const sigset_t *sigmask: 这是一个指向 sigset_t 类型的指针,sigset_t 是一个结构体,用于表示一组信号。sigmask 参数指定了一组信号,这些信号在 sigsuspend 调用期间应该被屏蔽,即这些信号在进程挂起时不会被立即处理。

返回值:sigsuspend 函数永远不会正常返回。如果进程被信号中断,它会返回一个错误码,并且全局变量 errno 会被设置为 EINTR(中断的系统调用)。

使用 sigsuspend 函数的典型场景是在等待特定信号的到来时,同时屏蔽其他不需要立即处理的信号。这在多线程编程中尤其有用,当一个线程需要等待特定的信号,同时忽略其他信号时,可以使用 sigsuspend 来实现。

下面是一个简单的 sigsuspend 函数使用示例:

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

void signal_handler(int sig) {
    // 信号处理函数,可以在这里执行一些清理工作
}

int main() {
    sigset_t mask, oldmask;

    // 初始化信号屏蔽字,屏蔽SIGINT和SIGTERM
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);

    // 设置信号处理函数
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);

    printf("Process is waiting for a signal...\n");
    // 进程将在这里挂起,直到接收到SIGINT或SIGTERM信号
    sigsuspend(&mask);

    printf("This line will be printed after a signal is received.\n");
    return 0;
}

在这个示例中,我们首先创建了一个信号屏蔽字 mask,并添加了 SIGINTSIGTERM 信号。然后,我们为这两个信号设置了信号处理函数。使用 sigsuspend 函数时,进程将挂起,直到接收到 SIGINTSIGTERM 信号。由于这两个信号被屏蔽,进程不会立即处理它们,而是继续等待。当信号到达时,sigsuspend 被中断,进程恢复执行,并打印出信号处理后的消息。

需要注意的是,sigsuspend 函数在多线程环境中的行为是未定义的。如果你在多线程程序中使用 sigsuspend,可能会导致不可预测的行为,因为它可能会影响到整个进程的所有线程。在多线程环境中,通常使用其他同步机制,如互斥锁、条件变量等,来实现线程间的等待和通知。

alarm 设置定时器

alarm 是一个 POSIX 标准定义的系统调用,它用于设置一个定时器,当定时器到期时,将向调用进程发送 SIGALRM 信号。这个函数主要用于实现简单的定时功能。

函数原型如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数说明:unsigned int seconds: 这是一个无符号整型参数,表示定时器的超时时间,单位是秒。

返回值:

  • alarm 返回在调用之前剩余的秒数(如果有的话),如果之前没有设置定时器,则返回 0。
  • 如果函数调用失败,它将返回一个非零值,并设置全局变量 errno 来指示错误。

使用 alarm 函数时,需要注意以下几点:

  1. 重复定时: alarm 函数每次调用都会重置定时器,即使之前已经设置了一个定时器。如果你想要重复定时,需要在信号处理函数中再次调用 alarm
  2. 信号处理: 进程必须设置一个信号处理函数来处理 SIGALRM 信号。如果进程没有设置信号处理函数,SIGALRM 信号的默认行为是终止进程。
  3. 多进程和多线程: alarm 只影响调用它的进程。在多线程环境中,alarm 只影响调用它的线程,对其他线程没有影响。

下面是一个简单的 alarm 函数使用示例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void handle_alarm(int sig) {
    printf("Timer expired!\n");
    // 可以选择在这里退出程序或者重新设置定时器
    exit(0);
}

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

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

    // 主线程将在这里等待,直到SIGALRM信号到来
    pause();

    return 0;
}

在这个示例中,我们首先为 SIGALRM 信号设置了一个信号处理函数 handle_alarm。然后,我们使用 alarm 函数设置了一个 5 秒的定时器。pause 函数使主线程挂起,直到 SIGALRM 信号到来。当定时器到期时,SIGALRM 信号被发送给进程,handle_alarm 函数被调用,并打印出 “Timer expired!” 消息,然后退出程序。

alarm 函数提供了一个简单的定时机制,但它的精度可能受到系统调度和信号处理延迟的影响。对于需要更高精度定时的应用程序,可能需要使用其他定时器功能。

setitimer 设置定时器

setitimer 函数是 POSIX 标准定义的一个系统调用,用于设置进程的间隔定时器。与 alarm 函数不同,setitimer 提供了更灵活的定时功能,允许设置一个定时器,它可以在指定的时间间隔后触发,并且可以选择性地周期性地重复触发。

函数原型如下:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

参数说明:

  1. int which: 这个参数指定了要设置的定时器类型,可以是以下三种之一:
    • ITIMER_REAL: 真实的时间定时器,它在实际时间(墙上时钟时间)中计时。
    • ITIMER_VIRTUAL: 虚拟时间定时器,它只在进程执行时计时,当进程被阻塞或在睡眠状态时,定时器不会减少。
    • ITIMER_PROF: 用于性能分析的时间定时器,它在进程执行以及在系统调用中花费的时间中计时,通常用于性能分析。
  2. const struct itimerval *value: 这是一个指向 itimerval 结构的指针,该结构定义了定时器的值。itimerval 结构包含两个字段:
    • it_value: 定时器的初始值,即定时器设置后第一次到期的时间。
    • it_interval: 定时器的间隔值,即定时器到期后,如果需要重复触发,每次触发之间的时间间隔。
  3. struct itimerval *ovalue: 这是一个可选的输出参数,如果提供了这个参数,setitimer 会将定时器的旧值复制到 ovalue 指向的 itimerval 结构中。

返回值:

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

使用 setitimer 函数时,需要注意以下几点:

  • 信号处理: 当定时器到期时,会向进程发送 SIGALRM 信号。进程必须设置一个信号处理函数来处理这个信号。
  • 定时器重置: 当 it_value 到期后,如果 it_interval 设置了非零值,定时器将被重置为 it_interval 的值,并再次开始计时。
  • 精度问题: 定时器的精度可能受到系统调度和信号处理延迟的影响。

下面是一个简单的 setitimer 函数使用示例:

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void handle_alarm(int sig) {
    printf("Timer expired!\n");
}

int main() {
    struct itimerval timer;
    signal(SIGALRM, handle_alarm);

    // 设置定时器,5秒后第一次触发,之后每隔3秒触发
    timer.it_value.tv_sec = 5;
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_sec = 3;
    timer.it_interval.tv_usec = 0;

    if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
        perror("setitimer failed");
        return 1;
    }

    // 主线程将在这里等待,直到SIGALRM信号到来
    pause();

    return 0;
}

在这个示例中,我们首先为 SIGALRM 信号设置了一个信号处理函数 handle_alarm。然后,我们使用 setitimer 函数设置了一个定时器,它将在 5 秒后第一次触发,之后每隔 3 秒触发。当定时器到期时,SIGALRM 信号被发送给进程,handle_alarm 函数被调用,并打印出 “Timer expired!” 消息。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇