linux-进程信号的产生

Linux中的进程信号(signal)是一种用于进程间通信或向进程传递异步事件通知的机制 。信号是一种软中断,用于通知进程某个事件的发生,如错误、终止请求、计时器到期等。

1. 信号的基本概念

- 信号(Signal) :是一种异步通知机制,当内核或其他进程需要通知某个进程发生了某种事件时,会向该进程发送一个信号。进程接收到信号后,可以根据预设的处理方式进行响应。

- 默认处理:每种信号都有默认的处理动作,比如终止、忽略、停止或继续执行。

- 捕捉信号:进程可以通过注册信号处理函数(signal handler)来捕捉信号,从而自定义对信号的响应。
怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
信号产生之后,进程知道怎么处理吗?知道,信号的处理方法,在信号产生之前,已经准备好了。
处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理。什么时候处理?合
适的时候。

查看信号 kill -l

1~31为普通信号。34~64为实时信号。

实时信号需要立即处理,可以不立即处理的是普通信号。

怎么进行信号处理?a.默认处理 b.忽略处理 c.自定义处理, 都叫做信号捕捉。
示例:

cpp 复制代码
#include <iostream>
#include <unistd.h>
int main()
{
    while(true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}


用户输⼊命令,在Shell下启动⼀个前台进程, 按下 Ctrl+C , 这个键盘输入产生⼀个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
而其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下。这里需要引入一

个系统调用函数。

signal:用于设置对特定信号的处理方式

signum:要处理的信号编号只需要知道是数字即可

handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
    std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<
    std::endl;
}
int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGINT/*2*/, handler);
    while(true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}

这里重定义了二号信号的处理方式,二号信号默认处理是终止进程


这里可以使用ctrl+\来终止进程。

2. 信号的产生

在 Linux 中,信号(Signal)是进程间通信(IPC)和异常处理的重要机制。信号的产生方式主要包括 硬件事件 、软件命令 和 内核机制 触发。

2.1 硬件事件触发信号

当发生硬件异常时,操作系统会向相应的进程发送信号,例如:

非法操作 :程序执行非法指令,如除零 (SIGFPE)、访问非法内存 (SIGSEGV)。

键盘输入 :用户在终端输入 Ctrl+C 或 Ctrl+Z,分别产生 SIGINT(中断进程)和 SIGTSTP(暂停进程)。

硬件事件 触发的信号
除零错误 SIGFPE
非法内存访问 SIGSEGV
非法指令 SIGILL
总线错误 SIGBUS
用户按 Ctrl+C SIGINT
用户按 Ctrl+Z SIGTSTP

进程访问非法地址触发 SIGSEGV

访问 NULL 指针或越界访问内存,会触发 SIGSEGV(段错误)

遇到除0错误

触发八号信号SIGFPE

2.2 系统调用触发信号

发送信号的本质是相进程写信号,通过进程的pid和信号编号修改位图(本质是OS修改内核的数据)。

用户或进程可以使用 命令 或 系统调用 产生信号。

2.2.1 使用 kill 命令

kill 可用于向指定进程发送信号。例如:

复制代码
kill -SIGTERM 1234   # 向进程 1234 发送 SIGTERM(终止进程)
kill -9 1234         # 等同于 kill -SIGKILL 1234,强制终止进程
kill -STOP 1234      # 暂停进程
kill -CONT 1234      # 继续运行被暂停的进程

其中:

cpp 复制代码
SIGTERM(15):请求终止进程,进程可捕获并决定是否退出(默认 kill 发送的信号)。
SIGKILL(9):强制终止进程,进程无法捕获,立即终止。
SIGSTOP(19):暂停进程,类似 Ctrl+Z,进程无法忽略。
SIGCONT(18):恢复暂停的进程。

2.2.2 使用 kill() 系统调用

在 C 语言中,kill() 可以向指定进程发送信号:

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

int main() {
    pid_t pid = 1234;  // 目标进程的 PID
    kill(pid, SIGTERM);  // 发送 SIGTERM 终止进程
    return 0;
}
cpp 复制代码
//mykill.c
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout << "./mykill signum pid" << std::endl;
        return 1;
    }
    pid_t target = std::stoi(argv[2]);
    int signum = std::stoi(argv[1]);

    int n = kill(target, signum);
    if(n == 0)
    {
        std::cout << "Send " << signum << " to " << target << std::endl;
    }
    return 0;
}
cpp 复制代码
//testsig.cc
#include <iostream>
#include <signal.h>


void handler(int signum)
{
    std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{
    signal(SIGINT, handler);
    while(true)
    {
        std::cout << "i am a process, pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

2.2.3 使用 raise() 让当前进程向自己发送信号

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


void handler(int signum)
{
    std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{
    signal(SIGINT, handler);
    //捕捉信号
    for(int i = 1; i < 32; i++)
        signal(i, handler);
    for(int i = 1; i < 32; i++)
    {
        sleep(1);
        raise(i);
    }
    return 0;
}

信号 9 也就是 SIGKILL 信号,它是一个强制终止信号,并且不可被捕获、阻塞或忽略的,也就无法对其进行修改和自定义。

2.2.4 abort

abort用于异常终止进程,并生成核心转储(core dump),以便调试程序崩溃的原因。

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

void abort(void);

无参数,直接终止当前进程
不会返回,进程立即结束
默认产生 SIGABRT 信号,导致进程终止并生成 core dump(如果系统允许)

abort() 与 exit() 的区别

abort() exit()
终止方式 发送 SIGABRT,可能生成 core dump 正常终止
释放资源 不执行 atexit() 注册的函数 执行 atexit() 注册的清理函数
可捕获 可通过 signal(SIGABRT, handler) 处理 不发送信号
适用场景 程序遇到致命错误时终止 正常退出,返回状态码

捕获SIGABRT

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

void handler(int signum)
{
    std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{

    signal(SIGABRT, handler);

    printf("before pause\n");
    abort();
    printf("after pause\n");
    return 0;
}

2.3 软件命令触发信号

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时 (如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

2.3.1 使用 alarm() 触发 SIGALRM

alarm() 是一个用于设置定时器的系统调用,它会在指定的秒数后向进程发送 SIGALRM 信号,从而触发相应的信号处理函数或终止进程。

函数原型:

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

seconds:设置的定时秒数。
返回值:返回上一个 alarm() 调用设置的剩余时间(如果没有,则返回 0)。

alarm(0):取消闹钟

alarm() 只能设置一个定时器,如果在定时器未触发前再次调用 alarm(),则前一个定时器会被覆盖。

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


void handler(int signum)
{
    std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{
    signal(SIGINT, handler);
    alarm(3);
    int cnt = 0;
    while(true)
    {
        std::cout << "i am a process " << cnt++ << " pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

2.3.2 pause()

pause 是一个系统调用,它使进程挂起(阻塞),直到接收到信号(且该信号的处理方式不是忽略)。它通常与 signal() 或 sigaction() 结合使用,以等待某个特定信号的到来。

函数原型:

cpp 复制代码
#include <unistd.h> int pause(void);

返回值:通常不返回,除非被信号中断,此时返回 -1,并设置 errno 为 EINTR(被信号中断的错误)。
cpp 复制代码
#include <iostream>
#include <signal.h>


void handler(int signum)
{
    std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{

    signal(SIGALRM, handler);

    alarm(3);
    printf("before pause\n");
    pause();
    printf("after pause\n");
    return 0;
}

2.3.3 设置重复闹钟

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

using func_t = std::function<void()>;
std::vector<func_t> funcs;

void Schel()
{
    std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{
    std::cout << "我是周期性的内存管理, 正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
    std::cout << "我是刷新程序,定期刷新内存数据" << std::endl;
}
void handler(int signum)
{
    gcount++;
    std::cout << "###################" << std::endl;
    for(auto &f : funcs)
        f();
    std::cout << "###################" << std::endl;
    int n = alarm(1);
    std::cout << gcount << std::endl;
}
int main()
{
    funcs.push_back(Schel);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);
    signal(SIGALRM, handler);
    alarm(1);

    while(true)
        pause();
    return 0;
}

alarm内核数据结构

cpp 复制代码
struct timer_list {
    struct list_head entry; //将 timer_list 结构体组织成链表
    unsigned long expires;  //表示定时器的超时时间
    void (*function)(unsigned long); //指向回调函数的指针,当定时器超时后,内核会调用该函数。
    unsigned long data; //作为 function 回调函数的参数,通常用于传递自定义数据。
    struct tvec_t_base_s *base;
};

2.4 内核触发信号

Linux 内核在特定情况下会向进程发送信号,例如:

2.4.1 进程终止时,父进程收到 SIGCHLD

子进程终止后,父进程会收到 SIGCHLD,可用于回收子进程资源:

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

void child_handler(int signum) {
    printf("Child process exited.\n");
}

int main() {
    signal(SIGCHLD, child_handler);
    
    if (fork() == 0) {
        printf("Child process running...\n");
        sleep(2);
        exit(0);
    }

    pause();  // 等待信号
    return 0;
}

(2)磁盘 I/O 错误触发 SIGBUS

访问未映射的内存或硬件错误会触发 SIGBUS。

(3)后台进程写入终端触发 SIGHUP

后台进程尝试写入终端时,可能收到 SIGHUP,表示挂起(通常用于会话管理)。

2.4 信号的分类

Linux 信号可分为 终止信号 、忽略信号 、暂停信号 和 核心转储信号。

类别 常见信号
终止信号 SIGTERM、SIGKILL、SIGINT、SIGHUP
暂停信号 SIGSTOP、SIGTSTP、SIGCONT
核心转储信号 SIGSEGV、SIGILL、SIGABRT
忽略信号 SIGCHLD、SIGURG
cpp 复制代码
SIGTERM (15):终止信号,用于请求进程正常终止,允许进程进行清理工作后退出。
SIGKILL (9):强制杀死进程的信号,无法被捕捉或忽略,立即终止进程。
SIGSTOP:暂停进程的执行,无法被捕捉或忽略。
SIGCONT:使处于暂停状态的进程继续运行。
SIGHUP (1):挂起信号,常用于通知进程重新读取配置文件或重启。
SIGALRM:定时器信号,定时器到期时发出,用于处理超时操作。

man 7 siganl

Core(终止),Term(终止),Cont(继续),Stop(暂停),Ign(忽略)

信号 vs 通信IPC:

(1)信号是用户和OS,IPC是用户之间

(2)信号是OS修改内核数据结构,IPC是写到缓冲区中

3.目标进程

前台进程是指 直接与终端交互 的进程,用户可以通过 键盘输入 来控制它。

后台进程指的是 不直接与终端交互,在后台运行的进程,用户可以继续在终端执行其他操作。

假如有一个可执行程序code:

./code -> 前台进程

./code & -> 后台进程

命令行shell进程是前台进程。

  • 后台进程无法从标准输入获取内容 ,前台可以。但是都可以向标准输出打印内容。

  • 前台进程只能有一个,后台进程可以有多个。

在上图中,testcode 进程成为了前台进程,因此当我们在终端输入 ls 命令时,并未在屏幕上看到输出 。这是因为此时命令行 Shell 进程已切换到后台,而 testcode 进程本身 并未提供执行 ls 等命令的接口,导致输入的命令无法被正确解析和执行。

在上述代码中,testcode 进程虽然在标准输出上打印内容,但它是 后台进程 ,而 前台进程仍然是 Shell 进程 。因此,当用户在终端输入 ls 等命令时,Shell 进程能够正常接收输入并执行相应命令,输出也会正确显示在终端上。

几个命令:

bash 复制代码
jobs查看所有的后台任务
fg(frontground)任务号,将特定的进程提到前台
ctrl + z:将进程暂停。前台进不能被暂停,如果对前台进程使用ctrl+z,该进程会被自动提到后台。
bg:让后台进程回复运行
相关推荐
GeW16 小时前
新质生产力路线图Linux、数据库、国产化替代一次讲透
linux
aningx18 小时前
cachyos 从 GRUB 切换到 rEFInd
linux
第404号工位20 小时前
40块钱二手瘦客户端,如何跑满家庭物联网中枢?(拒绝Docker爆盘实录)
linux·架构·node.js
对象存储与RustFS20 小时前
网盘限速逼你开会员?用 RustFS 自己搭一个不限速的“私人网盘
服务器·后端·docker
骑着蜗牛撵大象3271 天前
Linux 进程间通信之管道:从 pipe() 的内核语义到 FIFO 落地排错
android·java·linux·系统编程·fifo·管道·进程间通信
众壹新能源科技1 天前
逆变器自带无功不够用,加装5MVar SVG后功率因数从0.85回到0.99
运维·人工智能·自动化
..Dauntless..1 天前
【Linux】一切皆文件和缓冲区
linux·运维·服务器
裕晟资质规划1 天前
三类资质变更报告时限差在哪?军工 15 日、集成印制 10 日
大数据·服务器·网络
QMCY_jason1 天前
x86 ubuntu2204 无gui 安装 openeuler 2203 aarch64版本
linux·运维·服务器
重生之小比特1 天前
【Linux系统】12 进程程序替换 自定义Shell 命令行解释器
linux·运维·服务器