Linux:信号初识上(信号一)

linux中,信号也是一个非常重要的知识点,接下来的博客我们将仔细探讨信号的知识,话不多说,现在就开始啦~~

1.信号是什么

我们举几个例子,比如上课铃,上课铃一响我们就知道要回去上课,这就是信号,它提醒我们该干什么事情了,再比如点外卖,外卖小哥送外卖敲门的声音,就是一个信号,他表示外卖到了,我们可以取外卖啦,在古代战争时,点狼烟可以告诉战士敌人来了,同理,狼烟也是一种信号,告诉我们敌人来临

所以什么是信号?

答:信号是一种给进程发送的,用来进行事件异步通知的机制!

那什么时异步通知呢,举个例子,在上课的时候老师发现张三还不在教室,如果此时继续上课而不等待张三,就可以说是异步,如果老师等张三回来在上课,那么就是同步

那么根据上面的描述,我们可以得到几个关于信号的基本结论:

1.信号处理,进程在信号没有产生的时候,就知道信号该如何处理了(你在外卖员未敲门前就知道敲门是因为你的外卖到了)

2.信号的处理,不是立即处理,而是可以等一会儿处理,合适的时候,进行信号处理(你的外卖到了,而此时你正在忙,你可以选择让外卖小哥把外卖放在门口,等你忙完了再去取,而不是必须他一敲门你就必须去取外卖)

3.进程早已经内置了对于信号的识别与处理方式

4.信号源非常多->给进程产生信号的信号源也非常多

2.信号的产生

信号需要先产生,然后保存,然后处理,下面我们就探讨产生

产生信号的方法非常多,这里使用键盘产生信号等来讲解

1.键盘产生信号

SignTest.cc

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

int main()
{
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world : " << cnt++ << std::endl;
        sleep(1);
    }
    return 0;
}

我们在键盘上输入的ctrl + c就是一种信号,是让该进程结束的信号

其实信号在linux里面只不过是数字,不过为了更好的表示,使用了宏替代了数字,1~31是普通信号,后面的是实时信号,不需要掌握太多,只需要了解即可,而我们的ctrl + c正是触发了2信号,其实相当一部分信号处理动作,就是让自己终止!

进程收到信号->处理信号

1.默认处理动作(信号给什么就干什么)

2.自定义信号处理动作(自己定义接下来的行为)

3.忽略处理(继续干自己的事情,不管信号)

我们依旧用外卖的例子来解释一下:

此时我们拿到了外卖,接下来该干什么呢

1.我们可以选择吃外卖(我们点外卖就是为了吃,合情合理)-> 默认处理动作

2.我们自己不吃,而是给女朋友吃(我们自己决定干接下来的事情)-> 自定义信号处理动作

3.不管外卖,继续做自己的事情(忽略处理)

那我们怎么证明进程默认处理是ctrl + c(2信号)呢,下面提供一种可以更改进程的默认信号处理的方法 -- signal

1.signal替换信号

其中第一个参数是你想改变的信号值,第二个参数是你改变的这个信号值之后他表示哪个函数(比如2->A, 你现在传入2, B,现在2->B)

SignTest.cc

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

void handlerSign(int sign)
{
    std::cout << "获得一个信号:" << sign << std::endl;
}

int main()
{
    signal(2, handlerSign);
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world : " << cnt++ << std::endl;
        sleep(1);
    }
    return 0;
}

此时发现ctrl + c对于的就是信号2

那么我们怎么关掉这个进程呢,可以使用ctrl + \

2.前后台

在 Linux 终端里,我们运行的程序分 "前台进程" 和 "后台进程",它们和键盘的交互规则,藏着终端的 "专属逻辑":

比如你在命令行敲./XXX启动程序,这个./XXX就是前台进程 ;要是加个&写成./YYY &./YYY就会以后台进程的形式运行 ------ 而此刻你正在操作的命令行 shell,本身也是一个前台进程(所以终端里同一时间,前台进程只能有一个)

这时候键盘的 "专属权限" 就体现出来了:像 Ctrl+C 这类由键盘触发的信号,只会发给当前的前台进程。后台进程是收不到的 ------ 毕竟键盘只有一个,终端得确保 "输入的信号 / 数据,能精准给到一个确定的进程",要是前台进程能有多个,键盘信号就不知道该发给谁了

输入输出的规则也有区别:前台进程能直接从键盘获取输入(比如你运行一个需要输入密码的程序,前台才能正常读键盘);但后台进程不行,它没法从标准输入里拿内容。不过不管前后台,程序都能把内容输出到屏幕上(比如后台进程的日志,照样能打印到终端)

简单说:前台进程是终端的 "当前交互对象",占着键盘的交互权;后台进程是 "后台默默跑的",不抢键盘的交互资源 ------ 这是终端为了避免输入混乱,定好的 "单前台" 规则

就拿刚刚的程序举例子

此时我们的进程不会读取键盘内容,要想杀死这个进程,就需要启动kill -9 pid指令了

3.补充前后台命令

命令 / 操作 功能描述 用法示例
jobs 查看当前终端所有后台任务(显示任务号、状态、命令) 直接输入jobs,输出如[1]+ Stopped ./XXX
ctrl+z 当前前台进程暂停,并切换到后台 前台进程运行时,按下ctrl+z组合键
fg 任务号 将指定任务号的后台任务,切换到前台运行 jobs显示任务号为 1,输入fg 1
bg 任务号 让后台暂停的任务恢复运行(仍保持后台状态) jobs显示任务号为 1,输入bg 1
3.进程发送信号本质

给进程发送信号,并非直接向进程传递数据,而是:

  1. 通过目标进程的PID,找到该进程在内核中的管理结构;
  2. 修改该结构中的 "信号位图"(某一位对应一个信号);
  3. 这一操作必须由操作系统完成(用户态进程无法直接修改内核数据),因此发送信号的本质是调用系统调用(如kill)让内核修改目标进程的信号状

信号的记录方式(内核层面)

内核中每个进程对应一个struct task_struct(进程控制块,PCB),其中包含unsigned int sigs字段,该字段是信号位图结构

  • 位图的每一位对应一个信号(比如第 2 位对应 SIGINT 信号);
  • 当某信号被发送给进程时,内核会将sig中对应位置 1,以此记录 "进程收到了该信号"。

信号的处理逻辑

  1. 非立即处理 :信号产生后,进程不会立刻执行信号处理函数,而是先将信号记录在sig位图中;
  2. 合适时机处理 :进程会在 "从内核态返回用户态" 等安全时机,检查sig位图的状态,执行对应信号的处理逻辑。

这一机制的核心是 "内核统一管理进程的信号状态"------ 因为进程的运行状态、资源都由内核维护,信号作为进程的 "事件通知",必须通过内核完成记录与触发

2.系统调用产生信号

1.kill
Signtest.cc
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handlerSign(int sign)
{
    std::cout << "获得一个信号:" << sign << std::endl;
}

int main()
{
    signal(2, handlerSign);
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
mykill.cc
cpp 复制代码
#include <iostream>
#include <signal.h>

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cout << "./mykill sig pid" << std::endl;
        return 1;
    }
    int sig = std::stoi(argv[1]);
    int pid = std::stoi(argv[2]);
    int n = kill(pid, sig);
    if (n == 0)
    {
        std::cout << "kill -" << sig << " " << pid << std::endl;
    }
    return 0;
}
运行结果
2.raise

raise() 是标准 C 库函数(非系统调用,但封装了kill系统调用),功能是向调用该函数的进程自身发送指定信号 ,等价于 kill(getpid(), sig)getpid()获取当前进程 ID)

代码如下:

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

void handlerSign(int sign)
{
    std::cout << "获得一个信号:" << sign << std::endl;
}

int main()
{
    //signal(2, handlerSign);
    for (int i = 1; i < 32; i++)
    {
        signal(i, handlerSign);
    }
    for (int i = 1; i < 32; i++)
    {
        raise(i);
    }
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
3.abord

abort() 是标准 C 库函数,核心功能是强制让当前进程异常终止 :它会向进程自身发送 SIGABRT 信号(6 号信号),且这个终止行为是 "不可阻挡" 的(即便你捕获了 SIGABRT,默认仍会终止进程)

复制代码
#include <stdlib.h>  // 必须包含的头文件(注意不是signal.h)
void abort(void);

3.产生异常

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

void handlerSign(int sign)
{
    std::cout << "获得一个信号:" << sign << std::endl;    
    //exit(13);
}

int main()
{
    //signal(2, handlerSign);
    for (int i = 1; i < 32; i++)
    {
        signal(i, handlerSign);
    }
    /*for (int i = 1; i < 32; i++)
    {
        if (i != 9)
            raise(i);
        sleep(1);
    }*/
    int cnt = 0;
    while (true)
    {
        std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
        sleep(1);

        //除零错误
        /*int a = 10;
        a /= 0;
        */

        //野指针
        /*
        int* a = nullptr;
        *a = 10;
        */
    }
    return 0;
}

好啦,今天的博客就到这里啦,我们下篇博客见~~

相关推荐
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 433 题:最小基因变化
数据结构·c++·算法·哈希算法
移幻漂流2 小时前
C/C++内存掌控之道:从内存泄漏到零开销抽象的进阶之路
java·c语言·c++
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 1926 题:迷宫中离入口最近的出口
c++·算法·结构与算法
衫水2 小时前
Docker 常用指令大全(完整整合版)
运维·docker·容器
余衫马2 小时前
Qt for Python:PySide6 入门指南(下篇)
c++·python·qt
HellowAmy2 小时前
我的C++规范 - 请转移到文件
开发语言·c++·代码规范
2501_941982052 小时前
RPA 自动化推送中的多任务调度与并发控制
运维·自动化
FreeBuf_2 小时前
Cloudflare 0Day漏洞可绕过防护直接访问任意主机服务器
运维·服务器
米高梅狮子2 小时前
04. iSCSI 服务器
linux·运维·服务器