产生信号的多种方法,本质,键盘输入(原理,中断机制),软件条件(匿名管道,alarm函数,定时器模拟实现),硬件异常的原理(除0,野指针),函数调用(kill,raise,abort),命令行kill

目录

产生信号 

引入 -- 信号怎样被管理

数据结构 -- 位图

产生信号的本质

键盘输入

介绍

原理

单个键

组合键

中断

分类(按来源)

硬中断

时钟中断

软中断

系统事件

软件条件

管道文件问题

alarm函数

函数原型

应用 -- 定时器

思路

代码

硬件异常 

除0

引入

思考 -- 信号产生原理

死循环问题

引入

 ​编辑

原理 

野指针

思考

函数调用

kill()系统调用

函数原型

pid

sig

模拟实现

示例

​编辑

raise()

abort()

命令行 -- kill指令

默认

指定信号


产生信号 

引入 -- 信号怎样被管理

我们先不去想是信号如何产生,而是来思考一个问题

  • 我们可以通过多种方式来产生信号,但是进程不一定会立即处理他们
  • 所以为了不遗漏这些产生的信号,就需要对应的进程去保存他们收到的信号
  • 一旦这些需要被保存的信号数量增加,os势必要去管理他们
  • 那么就是 -- 先描述,再组织

数据结构 -- 位图

  • 再想想信号的特点,每个信号都有自己的编号,且常用的就31个
  • 而且信号对应的结果只有两种 -- 要么收到了该信号,要么没收到(可以对应1 / 0)
  • 是不是和位图的原理很像?
  • 所以我们可以使用位图来作为信号的结构
  • 又因为发送信号就是发送给某个特定的进程的
  • 所以,这个结构就在进程的pcb里

产生信号的本质

  • 从上面的分析我们已经知道,信号结构是存储在内核数据结构中的(pcb由os管理)
  • 也就代表 -- 只有os才能改变信号的状态 
  • 因此,我们无论通过什么手段来产生信号,实际上最终都是由os将信号的改变写入到信号位图结构中

 接下来,就开始介绍产生信号的多种方法噜

键盘输入

介绍

  • 有些信号可以由用户在终端上输入特定的键序列来产生
  • 例如Ctrl+C(SIGINT)表示终止进程,Ctrl+Z(SIGTSTP)表示暂停进程
  • 像这样的信号只能发送给前台进程

原理

单个键
  • 当我们在键盘上敲击一个键时,该键的电路将产生一个按键事件,通常为电气信号或电平变化
  • 键盘控制器会将按键事件转换为对应的扫描码
  • 每个按键都有一个唯一的扫描码
  • 该扫描码标识了按下的键是哪个键,并且还包括信息是否是按下或释放动作
  • 然后通过中断机制,将扫描码发送给cpu
  • 当cpu收到键盘中断后,它会执行预定义的中断处理程序,读取和解释键盘的扫描码,并将其转换为对应的字符或功能
组合键
  • 既然cpu可以处理一个键的输入,自然也就可以处理组合键
  • 当我们输入特定的组合键,由于其自身的设置,某个组合键 就代表 会触发操作系统产生预定义的信号
  • 所以,信号就可以通过键盘输入来产生
  • os再通过查找当前进程列表,找到前台正在运行的进程,向其写入刚刚产生的信号(在该进程pcb内部对应的位图结构中)

中断

是一种硬件机制,用于处理外部设备的事件或异常情况

它会打断cpu当前的执行,使其立即处理与中断相关的处理程序

分类(按来源)
硬中断

由硬件设备发起的中断,例如计时器、磁盘控制器、网络接口等

时钟中断
  • 时钟中断是由系统时钟产生的一种硬件中断
  • 这种中断周期性地发生,以固定的时间间隔触发,通常以毫秒为单位,由系统时钟频率决定的,常见的频率是1000 Hz,即每秒触发1000次时钟中断
  • 用于维护系统时间、进程调度以及执行与时间有关的操作
软中断

由软件生成的中断,通常通过系统调用或异常触发,例如系统调用、陷阱指令等

eg:信号就属于软中断

系统事件

操作系统在发生某些特定事件时,会向进程发送相应的信号

软件条件

由软件条件不满足 / 触发条件时,os会向当前使用该软件的进程发送信号

管道文件问题

前面有说到,管道文件的一端关闭后,另一端也会随之关闭:

  • 其中,当读端关闭时,写端会收到SIGPIPE信号,从而退出
  • 这是因为当读端关闭时,写入操作已经没有意义了,那么os自然需要直接结束io过程,也就是给写端发送信号
alarm函数
函数原型

  • 调用alarm函数可以设定一个闹钟
  • 也就是告诉osseconds秒之后,给当前进程发SIGALRM信号
  • 该信号的默认处理动作是终止当前进程
  • 闹钟触发后,会自动移除
应用 -- 定时器
思路
  • 闹钟触发后,会让os给当前进程发送信号,也就会调用SIGALRM信号对应的信号处理方法
  • 那么我们可以通过自定义函数,让他每次触发闹钟后,继续设定一个闹钟
  • 这样就可以实现每过一定时间,都会自动调用函数,我们可以在该函数中,调用我们所需的功能函数
  • 也就是定时处理
代码
vector<function<void()>> callbacks;
void log(){
  cout<<"loging"<<endl;
}
void show_user(){
  if(fork()==0){
    execl("/usr/bin/who","who",nullptr);
    exit(1);
  }
  wait(nullptr);
}
void func_test(int signum){
  signum=0;
  for(auto& it:callbacks){
    it();
  }
  alarm(1);
}

void test5(){
  callbacks.push_back(log);
  callbacks.push_back(show_user);
  alarm(1);
  signal(SIGALRM,func_test);
  while(1){
    ;
  }
}

硬件异常 

异常以某种方式被硬件检测到并通知os,然后os向当前进程发送信号

但进程不一定会退出!!!

除0
引入

之前我们只知道,/0后会使程序异常退出,但不知道为什么:

void test2(){
  int a=1;
  a/=0;
  while(1){
   sleep(1);
  }
}

但现在我们可以知道,程序终止和信号息息相关,而程序异常终止后打印出的这句话,正是8号信号对应的错误信息:

思考 -- 信号产生原理

但是,os怎么知道哪个进程出现异常的?

  • 自然是某个元件计算的过程中,发现异常,然后报告给了os
  • os定位当前正在运行的进程,然后给该进程发送了信号

谁计算?

  • 当然是专门计算的元件 -- cpu的运算器(硬件嗷,也就对应了是硬件发现了异常)

如何报告?

  • 可以参考os发送信号的原理 -- pcb的信号位图中,哪一位bit是1,就发送对应的信号给该进程
  • 报告也是一样的道理,会有专门的状态位图,硬件修改里面的bit位,来表示当前运算出现异常
  • 而状态位图是由硬件的状态寄存器实现(几乎所有外设都有寄存器),其中一个bit位就代表当前运算是否溢出
  • os通过读取寄存器中的数据,来获取状态
  • 一旦某位为1,就会让os知道当前运行的进程出现了某个异常

如何定位当前运行的进程?

  • os中会有该进程的上下文数据,其中有一个指针current指向当前运行进程的pcb

然后就可以发送信号了,还是一样的,os向pcb中的信号位图写入

死循环问题
引入

当我们修改上面的代码,执行自定义的处理方法:

void func(int signum)
{
  cout << "im " << getpid() << "i got a signal : " << signum << endl;
}

void test2(){
  signal(SIGFPE,func);
  int a=1;
  a/=0;
  while(1){
   sleep(1);
  }
}
 

会发现我们的进程在不断收到8号信号,为什么呢?

原理 
  • 是因为我们自定义的处理函数中并没有终止进程
  • 也就是说,进程的上下文依然存在,会被os不断进行时间片切换
  • 硬件中的状态寄存器也是进程的上下文
  • 随着每一次该进程的上下文被加载,os就会检测到溢出状态位是1,就会给进程发送8号信号

 

野指针
思考

还是和前面一样,出现野指针问题后,进程有时会退出

  • 原因肯定和上面差不多,都是进程收到了os的信号
  • 那信号的源头是谁呢?

联系我们已经学过的知识:

野指针是什么?

  • 野指针也就是我们使用了某个地址访问了不该访问的空间
  • 而这个地址是虚拟地址,访问物理内存是需要经过页表映射

如何映射?

  • 映射的工作是由硬件和软件共同来完成的
  • 承担这个任务的硬件部分叫做存储管理单元MMU,软件部分是操作系统的内存管理模块
  • 并且为了能够快速完成虚拟->物理的转换,页表的格式直接灌给了硬件
  • 所以查询页表等操作,都是以二进制形式进行的

信号的源头?

  • 当我们进行非法访问时,mmu在进行地址转换时发现了非法操作,它就会报错
  • 和进行除0操作时,cpu会报错的原理一样,mmu内部也有寄存器
  • 所以!对应的状态寄存器中的某个bit位就被赋为1
  • 那么os通过读取这个寄存器,就可以知道发生了异常,并且也能拿到当前进程的pcb
  • 也就可以发送信号了

函数调用

kill()系统调用

函数原型

pid

sig

要发送的信号编号

它可以是预定义的信号宏,也可以是用户自定义的信号编号

模拟实现
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <stdlib.h>

using namespace std;

void usage(string arr)
{
    cout << arr << " "
         << "illegal";
}
int main(int argc, char *argv[]) // 模拟kill命令 -- ./mykill 2 pid
{
    if (argc != 3)
    {
        usage(argv[0]);
        cout << endl;
        exit(0);
    }
    int signum = atoi(argv[1]);
    int pid = atoi(argv[2]);
    kill(pid, signum);

    return 0;
}
示例

raise()

它允许进程在运行时发送信号给自己,触发信号处理操作 

abort()

用于引发程序异常终止(和exit()类似)

它在程序执行时发送一个信号SIGABRT:

并且会发生核心转储(6号信号):

命令行 -- kill指令

(实际上底层还是调用了上面的系统接口)

它不仅可以终止(杀死)进程,还可以向进程发送其他信号,以便进行不同的处理

默认
  • 如果没有指定信号,默认情况下会向目标进程发送SIGTERM信号,这会请求目标进程正常退出
指定信号
  • 指定发送的信号 ( -s选项 )  (信号可以使用名称 / 编号):
  • 通常,只有root用户或进程的所有者(或者有特定权限的用户)可以向其他进程发送信号
  • 不是所有的信号都可以通过kill指令发送给进程,有些信号可能只能由内核或其他特定进程发送

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值