1、信号的产生

  • 键盘产生信号:Ctrl+C → 2号信号(SIGINT) → 终止进程Ctrl+\ → 3号信号(SIGQUIT) → 终止进程(可生成核心转储文件),Ctrl+Z → 19号信号(SIGSTOP) → 暂停进程,转入后台
  • 硬件异常产生信号:CPU运算异常(除0错误),MMU内存转换异常(野指针),硬件报错触发中断,OS向current进程发送信号
  • 命令行指令产生信号:kill命令
  • 系统调用/库函数产生信号:kill、raise、abort,用户代码主动调用API触发信号
  • 软件条件产生信号:内核检测软件逻辑异常,管道通信中,写端正常,读端关闭,操作系统会发送13号信号。alarm闹钟

无论哪种产生方式,最后发送信号时操作系统修改目标进程PCB内部信号位图完成

2、硬件异常产生信号

代码遇到除0、野指针访问,程序崩溃

除0错误:CPU 运算单元报错 → 收到 SIGFPE 8 号信号

野指针非法访问内存:MMU 内存管理硬件报错 → 收到 SIGSEGV 11 号信号

发送信号本质:操作系统修改目标进程task_structPCB 内部的信号位图。只有操作系统能修改 PCB,所以无论信号来源是键盘、硬件异常,最终写位图一定是操作系统完成,因此发送信号必须依赖系统调用

问题1:除0为什么会让CPU硬件报错?

  1. 用户代码的变量存放在内存,计算时CPU必须把数据从内存读到CPU内部的通用寄存器,如eax、ebx等
  2. CPU内部有ALU算逻运算单元,负责算术运算、逻辑运算
  3. x86 CPU有标志寄存器EFLAGS,寄存器每一个bit代表CPU运算后的硬件状态
  4. CPU硬件异常后,触发中断,通知操作系统,当前CPU运行出错

问题2:内核如何知道是哪个进程出现的异常?

内核全局指针current,类型为struct task_struct* ,永远指向当前CPU正在运行的进程,操作系统通过全局指针current识别当前正在运行的进程,并向该进程的PCB中的信号位图写入对应的信号

问题3:为社么不调用exit退出,程序就会死循环打印信号?

由于进程上下文(包括错误状态)在调度时被保存和恢复,若信号处理函数未终止进程,进程会反复触发相同异常,导致"死循环"现象

问题3:野指针为什么硬件报错?

CPU内部控制寄存器CR3,保存当前进程页表物理地址,MMU硬件单元集成在 CPU 内部:负责虚拟地址→物理地址转换,依靠页表,野指针是非法虚拟地址,MMU查页表时硬件报错,触发中断通知操作系统;OS 识别是内存访问异常,给当前进程发送 11 号 SIGSEGV 段错误信号

3、库/系统调用产生信号

3.1 kill系统调用

头文件<signal.h>

int kill(pid_t pid, int sig);

功能:给指定pid的进程发送sig信号

返回:成功返回0,失败返回-1

模拟实现kill:

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

void Usage(const std::string &cmd)
{
    std::cout << "Usage: " << cmd << " signumber pid" << std::endl;
    std::cout << "example: ./mykill 2 12345  send SIGINT to 12345\n";
}

int main()
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo = std::stoi(argv[1]);
    pid_t pid = std::stoi(argv[2]);

    // 核心系统调用:给pid发送signo信号
    int ret = kill(pid, signo);
    if(ret == -1)
    {
        perror("kill"); // 打印错误,比如pid不存在、权限不足
        exit(2);
    }
    return 0;
}

用户调用kill只是发起请求,真正发送信号、修改PCB位图是操作系统

3.2 raise库函数

int raise(int sig);

在这里插入图片描述

raise(sig)等价于kill(getpid(), sig)功能:给自己当前进程发送信号,自己给自己发

演示:

#include <iostream>
#include <signal.h>
#include <unistd.h>   // getpid sleep
#include <string>

int cnt = 1;
void Usage(const std::string &cmd)
{
    std::cout << "Usage: " << cmd << " signumber who" << std::endl;
}
void handler(int sig)
{
    std::cout << "进程捕捉到信号: " << sig << " pid: "
              << getpid() << " cnt: " << cnt << std::endl;
    cnt++;
}

int main()
{
    // 注册捕捉2号信号 SIGINT
    signal(SIGINT, handler);

    std::cout << "进程pid = " << getpid() << std::endl;
    while (true)
    {
        sleep(2);
        raise(2);   // raise:给自己进程发送 SIGINT(2)
    }
    return 0;
}

在这里插入图片描述

3.3 abort库函数

void abort(void);

在这里插入图片描述

底层等价kill(getpid(), SIGABRT); 6 号信号

4、软件条件产生信号

不是硬件报错,是内核检测软件层面异常条件,主动发送信号

例子1:管道读端关闭,子进程继续往管道里写,内核检测这个软件异常,发送SIGPIPE杀死写端进程,这就是软件条件产生信号

例子2:alarm闹钟系统调用

unsigned int alarm(unsigned int seconds);

在这里插入图片描述

功能:给调用进程设置闹钟,seconds秒之后,操作系统给本进程发送SIGALRM14号闹钟信号

alarm是一次性闹钟,触发后就失效,不会重复发信号

默认SIGALRM处理动作TERM终止进程

返回值:上一个未触发闹钟的剩余秒数,没有旧闹钟返回0

如果想实现周期性定时,需要在 SIGALRM 信号处理函数内部,再次调用 alarm () 重新设置闹钟

设置闹钟:

#include <iostream>
#include <signal.h>
#include <unistd.h>   // getpid sleep alarm

int cnt = 1;

int main()
{
    int n = alarm(3);       // 设置3秒闹钟

    std::cout << "进程正在运行: " << cnt << " pid: " << getpid()
              << " n: " << n << std::endl;

    while (true)
    {
        std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n: " << n << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

捕捉闹钟信号:

#include <iostream>
#include <signal.h>
#include <unistd.h>   // getpid sleep alarm

int cnt = 1;

void handler(int sig)
{
    std::cout << "进程捕捉到信号: " << sig << " pid: "
              << getpid() << " cnt: " << cnt << std::endl;

    int n = alarm(2);
    std::cout << "上一个闹钟剩余的时间: " << n << std::endl;
    cnt++;
}

int main()
{
    // 注册捕捉闹钟信号 SIGALRM(14)
    signal(SIGALRM, handler);

    alarm(3);   // 设置3秒后发送SIGALRM

    while (true)
    {
        std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

alarm返回值n:上一次闹钟还剩下多少秒,没有旧闹钟返回0

注意:不要在信号处理函数中写死循环,信号处理函数依然属于进程上下文

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐