嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十四)Linux系统对中断的处理(上)

简介: 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十四)Linux系统对中断的处理

1. Linux系统对中断的处理


1.1 进程、线程、中断的核心:栈


中断中断,中断谁?

中断当前正在运行的进程、线程。

进程、线程是什么?内核如何切换进程、线程、中断?

要理解这些概念,必须理解栈的作用。


1.1.1 ARM处理器程序运行的过程


ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

① 对内存只有读、写指令

② 对于数据的运算是在CPU内部实现

③ 使用RISC指令的CPU复杂度小一点,易于设计


比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:

1670922600753.jpg

细看这几个步骤,有些疑问:

① 读a,那么a的值读出来后保存在CPU里面哪里?

② 读b,那么b的值读出来后保存在CPU里面哪里?

③ a+b的结果又保存在哪里?


我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。

注意:如果想入理解ARM处理器架构,应该从裸机开始学习。我们即将写好近30个裸机程序的文档,估计还3月底发布。

注意:为了加快学习速度,建议先不看裸机。

1670922614641.jpg

CPU运行时,先去取得指令,再执行指令:

① 把内存a的值读入CPU寄存器R0

② 把内存b的值读入CPU寄存器R1

③ 把R0、R1累加,存入R0

④ 把R0的值写入内存a


1.1.2 程序被中断时,怎么保存现场


从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。

保存在哪里?内存,这块内存就称之为栈。

程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。

这个场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:

1670922629394.jpg

a. 函数调用:

在函数A里调用函数B,实际就是中断函数A的执行。

那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;

再去执行函数B;

函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。

b. 中断处理

进程A正在执行,这时候发生了中断。

CPU强制跳到中断异常向量地址去执行,

这时就需要保存进程A被中断瞬间的CPU寄存器值,

可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。

中断处理完毕,要继续运行进程A之前,恢复这些值。


c. 进程切换

在所谓的多任务操作系统中,我们以为多个程序是同时运行的。

如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。

怎么切换?

切换过程是发生在内核态里的,跟中断的处理类似。

进程A的被切换瞬间的CPU寄存器值保存在某个地方;

恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了。


所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。

进程的调度也是使用栈来保存、恢复现场:

1670922639914.jpg


1.1.3 进程、线程的概念


假设我们写一个音乐播放器,在播放音乐的同时会根据按键选择下一首歌。把事情简化为2件事:发送音频数据、读取按键。那可以这样写程序:

int main(int argc, char **argv)
{
  int key;
  while (1)
  {
  key = read_key();
  if (key != -1)
  {
    switch (key)
    {
    case NEXT:
      select_next_music(); // 在GUI选中下一首歌
      break;
    }
  }
  else
  {
    send_music();
  }
  }
  return 0;
}


这个程序只有一条主线,读按键、播放音乐都是顺序执行。

无论按键是否被按下,read_key函数必须马上返回,否则会使得后续的send_music受到阻滞导致音乐播放不流畅。

读取按键、播放音乐能否分为两个程序进行?可以,但是开销太大:读按键的程序,要把按键通知播放音乐的程序,进程间通信的效率没那么高。

这时可以用多线程之编程,读取按键是一个线程,播放音乐是另一个线程,它们之间可以通过全局变量传递数据,示意代码如下:

int g_key;
void key_thread_fn()
{
  while (1)
  {
  g_key = read_key();
  if (g_key != -1)
  {
    switch (g_key)
    {
    case NEXT:
      select_next_music(); // 在GUI选中下一首歌
      break;
    }
  }
  }
}
void music_fn()
{
  while (1)
  {
  if (g_key == STOP)
    stop_music();
  else
  {
        send_music();
  }
  }
}
int main(int argc, char **argv)
{
  int key;
  create_thread(key_thread_fn);
  create_thread(music_fn);
  while (1) 
  {
  sleep(10);
  }
  return 0;
}


这样,按键的读取及GUI显示、音乐的播放,可以分开来,不必混杂在一起。

按键线程可以使用阻塞方式读取按键,无按键时是休眠的,这可以节省CPU资源。

音乐线程专注于音乐的播放和控制,不用理会按键的具体读取工作。

并且这2个线程通过全局变量g_key传递数据,高效而简单。


在Linux中:资源分配的单位是进程,调度的单位是线程。

也就是说,在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。

而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。如下图示:

1670922689228.jpg


1.2 Linux系统对中断处理的演进


从2005年我接触Linux到现在15年了,Linux中断系统的变化并不大。比较重要的就是引入了threaded irq:使用内核线程来处理中断。

Linux系统中有硬件中断,也有软件中断。

对硬件中断的处理有2个原则:不能嵌套,越快越好。


参考资料:请点击


1.2.1 Linux对中断的扩展:硬件中断、软件中断


Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。

为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:

1670922720076.jpg

注意:上图是简化的,Linux中这个数组复杂多了。

当发生A中断时,对应的irq_function_A函数被调用。硬件导致该函数被调用。


相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:

1670922731095.jpg

注意:上图是简化的,Linux中这个数组复杂多了。

问题来了:

a. 软件中断何时生产?

由软件决定,对于X号软件中断,只需要把它的flag设置为1就表示发生了该中断。

b. 软件中断何时处理?

软件中断嘛,并不是那么十万火急,有空再处理它好了。

什么时候有空?不能让它一直等吧?

Linux系统中,各种硬件中断频繁发生,至少定时器中断每10ms发生一次,那取个巧?

在处理完硬件中断后,再去处理软件中断?就这么办!

有哪些软件中断?

查内核源码include/linux/interrupt.h

1670922740114.jpg

怎么触发软件中断?最核心的函数是raise_softirq,简单地理解就是设置softirq_veq[nr]的标记位:

1670922750721.jpg


怎么设置软件中断的处理函数:extern void open_softirq(int nr, void (action) (struct soft_action));

1670922758680.jpg

后面讲到的中断下半部tasklet就是使用软件中断实现的。


1.2.2 中断处理原则1:不能嵌套


官方资料:中断处理不能嵌套

请点击

中断处理函数需要调用C函数,这就需要用到栈。

中断A正在处理的过程中,假设又发生了中断B,那么在栈里要保存A的现场,然后处理B。

在处理B的过程中又发生了中断C,那么在栈里要保存B的现场,然后处理C。

如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。

所以,为了防止这种情况发生,也是为了简单化中断的处理,在Linux系统上中断无法嵌套:即当前中断A没处理完之前,不会响应另一个中断B(即使它的优先级更高)。


1.2.3 中断处理原则2:越快越好


妈妈在家中照顾小孩时,门铃响起,她开门取快递:这就是中断的处理。她取个快递敢花上半天吗?不怕小孩出意外吗?

同理,在Linux系统中,中断的处理也是越快越好。

在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。

在SMP系统中,假设中断处理很慢,那么正在处理这个中断的CPU上的其他线程也无法执行。

在中断的处理过程中,该CPU是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。

在Linux系统中使用中断是挺简单的,为某个中断irq注册中断处理函数handler,可以使用request_irq函数:

1670923115831.jpg

在handler函数中,代码尽可能高效。


但是,处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在handler中等待吗?对于计算机来说,这可是一个段很长的时间。

怎么办?


1.2.4 要处理的事情实在太多,拆分为:上半部、下半部


当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。换句话说,在这段时间内,系统是关中断的。

如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?

在handler函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。

1670923127950.jpg

中断下半部的实现有很多种方法,讲2种主要的:tasklet(小任务)、work queue(工作队列)。


1.2.5 下半部要做的事情耗时不是太长:tasklet


假设我们把中断分为上半部、下半部。发生中断时,上半部下半部的代码何时、如何被调用?

当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用tasklet来处理下半部。tasklet是使用软件中断来实现。

1670923160495.jpg


写字太多,不如贴代码,代码一目了然:

1670923170046.jpg

使用流程图简化一下:

1670923177753.jpg

假设硬件中断A的上半部函数为irq_top_half_A,下半部为irq_bottom_half_A。

使用情景化的分析,才能理解上述代码的精华。

a. 硬件中断A处理过程中,没有其他中断发生:

一开始,preempt_count = 0;

上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。


b. 硬件中断A处理过程中,又再次发生了中断A:

一开始,preempt_count = 0;

执行到第⑥时,一开中断后,中断A又再次使得CPU跳到中断向量表。

注意:这时preempt_count等于1,并且中断下半部的代码并未执行。

CPU又从①开始再次执行中断A的上半部代码:

在第①步preempt_count等于2;

在第③步preempt_count等于1;

在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;

注意:重点来了,第2次中断发生后,打断了第一次中断的第⑦步处理。当第2次中断处理完毕,CPU会继续去执行第⑦步。

可以看到,发生2次硬件中断A时,它的上半部代码执行了2次,但是下半部代码只执行了一次。

所以,同一个中断的上半部、下半部,在执行时是多对一的关系。


c. 硬件中断A处理过程中,又再次发生了中断B:

一开始,preempt_count = 0;

执行到第⑥时,一开中断后,中断B又再次使得CPU跳到中断向量表。

注意:这时preempt_count等于1,并且中断A下半部的代码并未执行。

CPU又从①开始再次执行中断B的上半部代码:

在第①步preempt_count等于2;

在第③步preempt_count等于1;

在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;

注意:重点来了,第2次中断发生后,打断了第一次中断A的第⑦步处理。当第2次中断B处理完毕,CPU会继续去执行第⑦步。

在第⑦步里,它会去执行中断A的下半部,也会去执行中断B的下半部。

所以,多个中断的下半部,是汇集在一起处理的。


总结:

a. 中断的处理可以分为上半部,下半部

b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的

c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的

d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断

e. 中断上半部执行完后,触发中断下半部的处理

f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

相关文章
|
9月前
|
Ubuntu Linux Anolis
Linux系统禁用swap
本文介绍了在新版本Linux系统(如Ubuntu 20.04+、CentOS Stream、openEuler等)中禁用swap的两种方法。传统通过注释/etc/fstab中swap行的方式已失效,现需使用systemd管理swap.target服务或在/etc/fstab中添加noauto参数实现禁用。方法1通过屏蔽swap.target适用于新版系统,方法2通过修改fstab挂载选项更通用,兼容所有系统。
819 3
Linux系统禁用swap
|
9月前
|
Linux
Linux系统修改网卡名为eth0、eth1
在Linux系统中,可通过修改GRUB配置和创建Udev规则或使用systemd链接文件,将网卡名改为`eth0`、`eth1`等传统命名方式,适用于多种发行版并支持多网卡配置。
1387 3
|
Ubuntu Linux 网络安全
Linux系统初始化脚本
一款支持Rocky、CentOS、Ubuntu、Debian、openEuler等主流Linux发行版的系统初始化Shell脚本,涵盖网络配置、主机名设置、镜像源更换、安全加固等多项功能,适配单/双网卡环境,支持UEFI引导,提供多版本下载与持续更新。
880 3
Linux系统初始化脚本
|
10月前
|
运维 Linux 开发者
Linux系统中使用Python的ping3库进行网络连通性测试
以上步骤展示了如何利用 Python 的 `ping3` 库来检测网络连通性,并且提供了基本错误处理方法以确保程序能够优雅地处理各种意外情形。通过简洁明快、易读易懂、实操性强等特点使得该方法非常适合开发者或系统管理员快速集成至自动化工具链之内进行日常运维任务之需求满足。
655 18
|
9月前
|
安全 Linux Shell
Linux系统提权方式全面总结:从基础到高级攻防技术
本文全面总结Linux系统提权技术,涵盖权限体系、配置错误、漏洞利用、密码攻击等方法,帮助安全研究人员掌握攻防技术,提升系统防护能力。
1036 1
|
9月前
|
监控 安全 Linux
Linux系统提权之计划任务(Cron Jobs)提权
在Linux系统中,计划任务(Cron Jobs)常用于定时执行脚本或命令。若配置不当,攻击者可利用其提权至root权限。常见漏洞包括可写的Cron脚本、目录、通配符注入及PATH变量劫持。攻击者通过修改脚本、创建恶意任务或注入命令实现提权。系统管理员应遵循最小权限原则、使用绝对路径、避免通配符、设置安全PATH并定期审计,以防范此类攻击。
1345 1
|
10月前
|
缓存 监控 Linux
Linux系统清理缓存(buff/cache)的有效方法。
总结而言,在大多数情形下你不必担心Linux中buffer与cache占用过多内存在影响到其他程序运行;因为当程序请求更多内存在没有足够可用资源时,Linux会自行调整其占有量。只有当你明确知道当前环境与需求并希望立即回收这部分资源给即将运行重负载任务之前才考虑上述方法去主动干预。
2505 10
|
存储 负载均衡 网络协议
X86 linux异常处理与Ipipe接管中断/异常
本文讲述了X86平台上Xenomai的ipipe如何接管中断处理。首先回顾了X86中断处理机制,包括IDT(中断描述符表)的工作原理和中断处理流程。接着详细介绍了Linux中中断门的初始化,包括门描述符的结构、中断门的定义和填充,以及IDT的加载。在异常处理部分,文章讲解了早期异常处理和start_kernel阶段的异常向量初始化。最后,讨论了APIC和SMP中断在IDT中的填充,以及剩余中断的统一处理。文章指出,ipipe通过在中断入口处插入`__ipipe_handle_irq()`函数,实现了对中断的拦截和优先处理,确保了实时性。
624 0
X86 linux异常处理与Ipipe接管中断/异常
|
存储 负载均衡 算法
Linux内核17-硬件如何处理中断和异常
Linux内核17-硬件如何处理中断和异常

热门文章

最新文章