【Linux】信号(1)认识、记录和产生信号

认识信号:

        当进程发生错误时,OS 会中止进程,它是如何做到的?这就需要了解一个重要的元素:信号。OS 会发出信号使得进程状态改变,比如我们常用的 kill -9、ctrl + c 等等,那么 OS 是怎么识别这些信号的,下面我们来认识一下信号。

        当信号还没产生时,我们都知道信号的结果,为什么,因为我们学习了,那操作系统怎么学习呢,在产生时我们得先识别出来。且信号产生的种类有很多种情况,进程的运行和信号的产生是属于一种异步关系,也就是说,信号产生的同时不会打搅进程的运行。

        当信号产生时,不一定立马区处理信号,因为可能有更高优先级的事。当信号已经到来,暂时没有处理,一定要有某种方式记下来这个信号,等到“合适”的时候处理。

准备处理信号有3种接收方式:

        1、默认行为(正常记录)

        2、自定义行为(handler)

        3、忽略信号(但忽略也是一种接收)

        那么进程内部一定能识别信号,程序员设计进程时,已经内置了处理方案,信号属于进程内部特有的特征。

        当输入指令 kill -l 时,我们会看到一个有 62 个信号,没有0、32、33,所以是 62 种,其中普通信号是 1 - 31,实时信号是 34 - 64。常用的是普通信号。

如何记录信号:

        信号的记录是在进程的 PCB 中的结构体变量,本质更多是为了记录信号是否产生,细心的可以观察到 1 - 31 相对于一个整型的 32 个比特位,所以通常采用位图来记录,是一个 unsigned int 无符号整形。可以通过比特位的位置对应信号的编号,假设从右往左,第一位不算,下标是一的比特位对应的是一号信号,而比特位的内容,1 或者 0,代表是否收到信号。

        所以,进程收到信号,本质是进程内的位图被修改了!只有谁资格修改进程的数据呢?OS!因为操作系统是进程的管理者,是软硬件系统的管理者。

        信号是如何发送的:

        本质是 OS 直接修改目标进程 task_struct 中的成员信号位图,信号只有 OS 有这发送,别的进程无法修改其他进程。

        我们经常遇到的信号就算 ctrl + c,它是代表着 2 号信号,那么我们在键盘上敲出 ctrl + c 时,系统怎么识别呢?这里我们需要介绍两个函数:

signal(signo, handler);
// 捕捉信号函数,signo 代表几号信号,handler 是一个函数指针,是一个回调函数;
typedef void (*sighandler_t)(int)

void handler(int signo)
// 会工具 signal 函数自动传递 signo 的值

        handler 函数是一种捕捉方法,通过映射方式,但有的信号不一定产生,它就像妈妈教孩子遇到小偷时,要大喊“捉小偷!”,相对于自定义了信号,当进程接收到改自定义信号时,就输出自定义后的内容。

        而我们在 ctrl + c 后,就怕被产生一个硬件中断,被 OS 获取,包装成信号发送给进程,进程收到信号后退出。而后台进程,就是 ./exe文件 + & 让当前进程变为后台进程,后台进程无法通过键盘杀掉,只能通过指令。这里除了 ps axj 外还可以使用指令 jobs,可以查看后台进程,随后 fg + 进程号,可以把进程放到前台,就可以杀掉了。

        需要注意的是,前提进程在运行过程中,用户随时可能按下 ctrl + c 而产生信号,就算说可以在如何地方获取,所以信号相对于进程的控制流是异步的。信号是进程之间时间异步通知的方式,属于软中断,键盘属于硬中断。

        我们可以通过捕捉信号来查看哪些信号的可以捕捉的,哪些信号是不能捕捉的,比如 9 号信号,如果所有的信号都能被捕捉,那这个进程岂不是无敌了!!

void handler(int signo){
    printf("got a signo : %d\n", signo);
    signal(signo, handler);
}

int main()
{
    int i = 1;
    for (; i < 32; i++){
        signal(i, handler);
    }
 

    waitpid(-1, NULL, 0);


    while (1){
         ;
    } 

    return 0;
}

        所以,为什么要有信号?因为有许多突发事情,要具有应对事件的能力。

产生信号:

代码运行时出错,我们该如何判断?

        第一种:调试;

        第二种:核心转储。

        核心转储:是把进程在内存中的核心数据,转储到磁盘上,记录具体运行到哪,哪一行出错了等信息,一般叫 core.pid 核心转储文件,它的目的是为了更好的调式,定位到错误行。一般云服务器这类线上生产环境,默认关闭,虚拟机有。

        怎么打开呢?输入命令:

ulimit -a
// 显示系统目前资源的限定

        其中 core file size 默认是 0,表示关闭状态,我们可以将核心转储打开,输入指令:

ulimit -c 10240

        这时 core 会发生改变,完成打开。当我们运行含有错误的进程时,会显示(core dump),意思是生成 core 文件,这时我们就可以看到目录下多了一个 core.pid 文件,这个文件叫做核心转储文件。

        当返回 core 文件后,我们可以用 gdb + 可执行程序调试,再输入 core-file core.pid 后,会自动定位到位置错误行上。这种调试方式叫做事后调试,代码实在找不到 bug 时可以打开 core。

 

         还记得当时我们在讲进程中止时的 status 信息吗,其中第 8 位就算 core dump,它表示是否有核心转储。我们可以通过 status & 0x80,看到 core dump 位是 1。

发送信号的方式:

发送信号一共有四种方式:

        1、通过键盘产生硬件信号

        2、进程异常,通过软硬件产生信号

        3、通过系统调用接口产生信号

        4、软件条件产生信号

        前两种在前面已经讲了,下面先讲第三种,系统调用。通过系统调用接口,也可以产生信号,通常使用的接口是:

int kill (int pid, int signo)
// 自成一体,形成进程调用

int main(int argc, int *argv[])
// 注意的是,这里调用 kill 需要带上main参数。
// 通常用法是 ./myproc pid 2

// 可以采用辅助函数,提醒接口
viod usage (const char *proc)

raise(int signo) 
// 给自己发信号

abort() 
// 自己调用6号信号,随后会中止进程,没有返回值,和exit()不同,abort总是成功

alarmc (second)
// 闹钟信号,设定时间后返回14号信号

        这里的 alarmc() 属于第四种软件条件,还包括下面要讲的,阻塞信号!

阻塞信号:

下面先来了解一下理论:

        实际执行信号的处理动作称为信号递达;信号从产生到递达之间的状态叫未决;忽略也是一种处理方式,而阻塞与忽略不同,它是不要递达,知道解除阻塞,不是处理方式。

        在接收信号时,不一定立即执行,所以的需要一个地方来保存信号,所以有了保存信号的数据结构,是一个叫做 pending 的位图,是 unsigned int 类型,发几就标记哪个位置,是接收未处理的信号的数据结构。

        还有一个是 handler,在上面我们认识到 handler 是自定义信号,在这里我们可以看作是一个函数数组,每一个下标都是函数指针。其下标中对应了 pending 的处理方法,也可以自定义。

        SIG_DFL:代表默认信号;

        SIG_IGN:代表信号忽略;

        最后一种就算自定义 handler;

 还有一种位图叫 block,每个比特位代表是否阻塞改信号,和 pending、handler 都是相对应的。

递达信号的过程:

        发送信号 -> 修改 pending 位 -> 时间合适时处理 -> 检查 block (-> 有阻塞 -> 不可被递达,直到解除 )-> 没有阻塞 ->  开始递达 -> 抵达后查看 handler -> 执行对应的处理方式。

        这2个位图和一个函数数组都在 task_struct 中,这些位图我们一般叫信号集:sigset_t,为何不当作无符号整形看待是有原因的,sigset_t 是系统级的,不要直接修改,要用函数修改,不同系统 sigset_t 作用不同。

        sigset_t 相关的接口有:

int sigemptyset(sigset_t *set);
// 对所有信号比特位清零

int sigfillset(sigset_t *set);
// 设置位全部有效,置1

int sigaddset(sigset_t *set, signo);
// 某一个信号添加到集合当中

int sigdelset(sigset_t *set, signo);
// 将一个信号从信号集中删掉

int sigismember(const sigset_t *set, signo);
// 判断一个信号是否在集合中
// 其实这里的int是一个bool

        上述接口成功返回0,失败返回-1。

进程识别信号的必经之路 -- OS:

        为什么进程会崩溃,因为收到了信号,那信号是怎么传递进来的,OS怎么知道出错了?

举两个例子,第一个是除 0 错误:

        当 cpu 计算时,寄存器计算数值返回,cpu 发现有除 0 异常,就会给 cpu 中的状态寄存器发送信号,状态寄存器更改对应的标志位的值,因为 cpu 属于硬件,所以这是一种硬件错误。OS 发现了 cpu 中的状态寄存器发生变化,随后将这个硬件错误包装成信号发送给目标进程,这里本质是找到对应的 PCB 中信号的位图,随后置 1,进程就可以接收到异常信号了。

第二种情况:空指针等

        进程通过页表映射到物理内存当中,当发现有人对 NULL 解引用时,会返回错误信息,页表是一种软件,还有一个硬件叫 MMU:是硬件单元 + 页表,做映射工作,在 MMU 中也有状态信息,OS 会看到这个信息发生变化,随后发送给进程。

        总的来说,这些错误最终一定会在硬件层面上有所表现,进而被 OS 识别到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值