中断
CPU 在运行的过程中,也会被各种“异常”打断,如图,中断也是“异常”的一种。区别在于中断也是程序正常运行中的一部分,而异常则是在设计程序时不想发生的,要避免的。
1、中断处理流程
硬件如下:
首先,要初始化中断。1)设置可以产生中断的中断源 2)设置中断控制器,使它可以屏蔽某些中断,设定中断优先级 3)设置CPU总开关,使能要用的中断
其次,在程序执行的过程中产生中断后,中断处理器接收中断并告诉CPU,CPU会停下当前的工作,转而由内核调用中断服务程序。(中断控制器发送中断给处理器的时候,处理器根据中断号查找中断向量表,找到中断服务程序的入口地址,才能去执行中断服务程序)CPU每执行完一条指令都会检查有无中断/异常的产生。
软件如下:
软件中断是由软件程序主动发起的 中断,它通常通过系统调用或软中断指令来向CPU发起中断请求。
系统调用是一种从用户空间程序向操作系统内核发出服务请求的机制。当用户程序需要访问操作系统提供的服务或资源时,如文件系统、进程管理、网络通信等,它会通过系统调用指令(如int 0x80)进入内核态,并将请求参数及系统调用号传递给内核程序进行处理。内核程序处理也即是根据中断号,此处是系统调用号来调用中断服务程序,内核程序处理完请求后,将结果返回给用户程序,并通过软件中断指令将CPU控制权返回到用户程序中。
(注:软件中断通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断并执行中断处理程序。
由于中断号是有限的,操作系统不舍得每一个系统调用对应一个中断号,而更倾向于用一个或少数几个中断号来对应所有的系统调用。Linux则使用int 0x80来触发所有系统调用。每个系统调用对应一份系统调用号,这个系统调用号在执行int 0x80指令前会放置在某个固定的寄存器里(eax),对应的中断代码会取得这个系统调用号,并且调用正确的函数)
2、异常向量表
![](https://i-blog.csdnimg.cn/blog_migrate/c2605277719ae0dce404e1bfacf32b57.png)
如图,这就是异常向量表,写在U-BOOT或内核中,每一条指令对应一种异常。这些指令都是跳转指令,发生异常时,CPU就会停下现在的工作转而执行指令,跳转至其他地址执行更复杂的函数。
这么看来,异常向量表就像是一本异常问题手册,供CPU查阅使用,哪里不对点哪里。
Linux系统对中断的处理
1、进程、线程、中断的核心:栈
ARM 芯片属于精简指令集计算机(RISC:
Reduced Instruction Set Computing),它所用的指令比较简单。对内存的数据只能进行读或写的操作,对于数据运算只能在CPU内部运算逻辑单元(也就是ALU)中进行,对读到的值和运算结果都放入CPU的寄存器中(保存现场)。由此可知CPU中寄存器的值相当重要,当要暂停某个程序时,就需要将寄存器中的值保存下来,保存到内存中的栈中。(注:每个线程都有自己的栈)
2、符合中断的情况有:
1)函数的调用
a.
在函数
A
里调用函数
B
,实际就是中断函数
A
的执行
b.
那么需要把函数
A
调用
B
之前瞬间的
CPU
寄存器的值,保存到栈里
c.再执行函数B
d.函数 B 返回之后,就从栈中恢复函数 A 对应的 CPU 寄存器值,继续执行
(注:这也就解释了局部变量的生命周期短的原因,函数B返回之后,存储在栈上的局部变量的空间就被释放了。
那么函数B是如何返回的?当调用函数时,主程序代码下一条指令的地址保存写入到栈中;当函数返回时,程序就会从栈中获取该地址,并从那一点继续向下执行。在函数调用了其它函数的情况下,将每一个返回地址都放到栈中;当函数结束时,就可以找到它们在栈中的地址。)
2)中断处理
a. 进程A执行时发生中断
b. CPU保存现场并执行中断向量表的跳转指令
c.可以保存在进程 A 的内核态栈,也可以保存在进程 A 的内核结构体中
d.
中断处理完毕,要继续运行进程
A
之前,恢复这些值
3)进程切换
进程并不是同时运行的,而是微秒纳秒级别的快速切换,每个程序都执行一小段时间。CPU同一时间只能执行一个进程。
Linux 对中断的扩展:硬件中断、软件中断
硬件中断是由硬件设备触发的中断,如时钟中断、串口接收中断、外部中断等。当硬件设备有数据或事件需要处理时,中断处理器会接收中断并会向CPU提供中断号,CPU在收到中断请求后,会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。硬件中断具有实时性强、可靠性高、处理速度快等特点。
软件中断是由软件程序触发的中断,如系统调用、软中断、异常等。软件中断不是由硬件设备触发的,而是由软件程序主动发起的,一般用于系统调用、进程切换、异常处理等任务。软件中断需要在程序中进行调用,其响应速度和实时性相对较差,但是具有灵活性和可控性高的特点。
1、硬、软中断的区别
- 硬件中断是由外设引发的, 软中断是执行中断指令产生的.
- 硬件中断的中断号是由中断控制器提供的, 软中断的中断号由指令直接指出, 无需使用中断控制器.(在软件中断中,也有中断号,它是由操作系统内核和应用程序约定的。在Linux操作系统中,软件中断号被称为信号(Signa,每个信号都有对应的编号,如SIGINT表示中断信号,编号为2;SIGTERM表示终止信号,编号为15等。)
- 硬件中断是可屏蔽的, 软中断不可屏蔽.
- 硬件中断处理程序要确保它能快速地完成任务, 这样程序执行时才不会等待较长时间, 称为上半部.
- 软中断处理硬中断未完成的工作, 是一种推后执行的机制, 属于下半部.
进程和线程
一个进程就是一套公寓,多个线程“合租”一套公寓,线程都有自己的卧室(栈空间),但厨房(代码区,存放的是代码编译后的机器指令)和卫生间(数据区)要共享。
![](https://i-blog.csdnimg.cn/blog_migrate/8958a6324db0d46217fad2eed33f0f2c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/89d037a329285f5fe8872fa7ea87fa13.png)
(注:由于线程运行的本质就是函数运行,函数运行时信息是保存在栈帧中的,因此每个线程都有自己独立的、私有的栈区。另外,程序计数器(记录线程的现场)、栈指针以及函数运行使用的寄存器是线程私有的。)
在Linux中,资源分配的最小单位是进程,调度的最小单位是线程。(进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束。)在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、 全局变量等等。同一进程中的不同线程也是依靠全局变量传递数据,比较高效,进程间的通信慢(七种IPC:InterProcess Communication:管道pipe、命名管道FIFO、消息队列、共享存储、信号量、套接字Socket、信号)且进程的创建、销毁、切换存在很大的时空开销。
进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据。由此就引出了一个问题,同一进程中的不同线程共享进程资源的话,如何保证线程间的内存使用不冲突呢?毕竟当一个线程使用共享内存时,其他线程只能干看着,等上一个用完了自己再用。这就需要引入“互斥锁”、“信号量”的概念。那就了解一下多线程编程的相关知识。
多线程编程
对于进程而言,每一个进程都有一个唯一对应的 PID
号来表示该进程,而对于线程而言,也有一个“类似于进程的PID
号”,名为
tid
,其本质是一个 pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。
互斥量:
为了解决对线程间对共享资源/临界资源的争夺,pthread线程引出了互斥量(pthread_mutex)来解决,一般定义为全局变量,以便所有线程都能访问。初始化互斥量之后就可以对互斥量进行加锁(pthread_mutex_lock阻塞方式),若有线程加锁成功,则可以访问临界区,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。或者用
pthread_mutex_trylock(非阻塞方式)该函数是非阻塞模式通过返回值来 判断是否加锁成功,用法与上述阻塞加锁函数一致。值得注意的是当拿到锁的线程执行完之后要解锁后者销毁,否则资源会一直被占用,出现
死锁情况。
信号量:
解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程都是无序执行。因此引入信号量的概念。 (注:互斥量和信号量不一样,互斥量是一把锁,谁把这把锁锁住了谁就去访问。信号量是线程各自有一个信号灯,谁的灯亮着就代表谁在访问,执行完自己的就灭灯换下一个线程的灯亮。)
条件变量:
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥量结合在一起。
当条件满足的时候,线程通常解锁并等待该条件发生变化,一旦另一个线程修改了条件变量,就会通知相应的条件变量唤醒一个或者多个被这个条件变量阻塞的线程。这些被唤醒的线程将重新上锁,并测试条件是否满足。一般来说
条件变量被用于线程间的同步;当条件不满足的时候,允许其中的一个执行流挂起和等待。
也就是给申请加锁的进程加了条件,只有满足条件的进程才能加锁成功。达到更好的协调线程调度的作用。避免出现不想执行的线程也在竞争申请加锁的情况。
休眠与唤醒
休眠-唤醒机制是应用程序访问硬件的一种方式,其他还包括查询方式、poll方式、异步通知方式。休眠唤醒流程框图如下图
![](https://i-blog.csdnimg.cn/blog_migrate/ae929527e939cc88b7bafba36b18916f.png)
以按键操作为输入为例,当应用程序执行read函数时,会调用内核态中的read_drv函数,在read_drv函数中有wait函数,wait函数会判断是否有按键输入,若有则返回数据,若没有就执行休眠,以上过程在一进程/线程中进行。
在进程休眠后,若有按键操作会触发中断,中断处理器接收中断并告诉CPU,CPU会停下当前的工作(保存现场),转而由内核调用(在驱动程序中注册的)中断服务程序。中断服务程序会将按键信息记录下来并唤醒休眠中的进程/线程(休眠中的线程被放在队列wq中),然后将按键信息拷贝到应用程序。(注:硬件中断不可嵌套,不可休眠,所以要尽可能快的执行完中断服务程序,因此耗时间的函数比如IIC的操作函数就不能放在中断中)
总结:妈妈(应用程序)想知道孩子醒没醒(硬件信息),孩子没有哭(无硬件中断),妈妈就休眠,孩子哭了就唤醒妈妈。
驱动框架如下:
![](https://i-blog.csdnimg.cn/blog_migrate/1293db7cf022464cbae521e53d43328e.png)
- APP 调用 read 等函数试图读取数据,比如读取按键;
- APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用空间(copy_to_user)并马上返回;
- 如果 APP 在内核态,也就是在驱动程序中发现没有数据,则 APP 休眠,休眠时除了把程序状态改为非 RUNNING 之外,还要把进程/进程放入wq 中,以后中断服务程序要从 wq 中把它取出来唤醒;
- 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记 录数据、唤醒 APP;
- APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户 空间并马上返回。
POLL机制
妈妈进入房间时,会先看小孩醒没醒,闹钟响之后走出房间之前又会再看小
孩醒没醒。
此机制与休眠唤醒的区别在于,在poll函数查询小孩状态后(第一次看小孩),若没醒就会设定超时时间,在超时时间内有数据就唤醒,若没有就休眠,时间到了也唤醒,再次查看小孩状态(第二次看小孩)
若过程中有数据:
③
PP
调用
poll
之后,进入内核态;
④致驱动程序的
drv_poll
被调用:
注意
,
drv_poll
要把自己这个线程挂入等待队列
wq
中,drv_poll 还会判断一下:有没有数据啊?返回这个状态。
⑤当前没有数据,则休眠一会;
⑥过程中,按下了按键,发生了中断:
◼
在中断服务程序里记录了按键值,并且从
wq
中把线程唤醒了。
⑦从休眠中被唤醒,继续执行
for
循环,再次调用
drv_poll
:
◼
drv_poll
返回数据状态
⑧哦,你有数据,那从内核态返回到应用态吧
⑨
APP
调用
read
函数读数据
若过程中一直没有数据:
③
APP
调用
poll
之后,进入内核态;
④ 导致驱动程序的
drv_poll
被调用:
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,一直没有按下了按键,超时时间到:内核把这个线程唤醒;
⑦
线程从休眠中被唤醒,继续执行
for
循环,再次调用
drv_poll
:
drv_poll
返回数据状态
⑧ 哦,你还是没有数据,但是超时时间到了,那从内核态返回到应用态吧
⑨
APP
不能
调用
read
函数读数据
注意几点:
⚫
drv_poll
要把线程挂入队列
wq
,但是并不是在
drv_poll
中进入休眠,而是在调用 drv_poll
之后无数据,在sys_poll中休眠
⚫
drv_poll
要返回数据状态
⚫
APP
调用一次
poll
,有可能会导致
drv_poll
被调用
2
次(休眠时遇到中断 | 超时自动唤醒)
⚫
线程被唤醒的原因有
2
:中断发生了去队列
wq
中把它唤醒,超时时间到了内核把它唤醒
⚫
APP
要判断
poll
返回的原因:有数据,还是超时。有数据时再去调用
read 函数。
驱动编程:
使用
poll
机制时,驱动程序的核心就是提供对应的
drv_poll
函数。在drv_poll 函数中要做
2
件事:
把当前线程挂入队列 wq:poll_wait
a) APP
调用一次
poll
,可能导致
drv_poll
被调用
2
次,但是我们并不需要把当前线程挂入队列 2
次。
b)
可以使用内核的函数
poll_wait
把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
返回设备状态:
APP
调用
poll
函数时,有可能是查询“有没有数据可以读”:
POLLIN
,也有 可能是查询“你有没有空间给我写数据”:POLLOUT
。所以
drv_poll
要
返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
唤醒函数:
APP
调用
poll
后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。
驱动程序中
poll
的代码如下:
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
应用编程:
APP
可以调用
poll
或
select
函数,这
2
个函数的作用是一样的。
在调用
poll
函数时,要指明:
⚫
你要监测哪一个文件:哪一个
fd
⚫
你想监测这个文件的哪种事件:是
POLLIN
、还是
POLLOUT 最后,在 poll
函数返回时,要判断状态。
应用程序代码如下:
int timeout_ms = 5000;
int ret;
fds[0].fd = fd;
fds[0].events = POLLIN;
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
异步通知
妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈 ,妈妈、小孩互不耽误。
所谓异步就是独立、互不干预的。 接收到中断,驱动就告诉APP
驱动程序怎么通知
APP
:
发信号
,这只有
3
个字,却可以引发很多问题:
谁发:驱动程序发
发什么:信号
发什么信号:
SIGIO
怎么发:内核里提供有函数
发给谁:
APP
,
APP
要把自己告诉驱动
APP
收到后做什么:执行信号处理函数
信号处理函数和信号,之间怎么挂钩:
APP
注册信号处理函数
框架:
重点从②开始:
②
APP
给
SIGIO
这个信号注册信号处理函数
func
,以后
APP
收到
SIGIO 信号时,这个函数会被自动调用;
③ 把
APP
的
PID(
进程
ID)
告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录 PID
;
④ 读取驱动程序文件
Flag
;
⑤ 设置
Flag
里面的
FASYNC
位为
1
:当
FASYNC
位发生变化时,会导致驱动程序的 fasync
被调用;
⑥⑦ 调 用
faync_helper
, 它 会 根 据
FAYSNC
的值决定是否设置button_async->fa_file=驱动文件
filp
:
驱动文件
filp
结构体里面含有之前设置的
PID
。
⑧
APP
可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用 kill_fasync 发信号;
⑪⑫⑬
APP
收到信号后,它的信号处理函数被自动调用,可以在里面调用read 函数读取按键。
阻塞与非阻塞
所谓阻塞,就是等待某件事情发生。比如调用
read
读取按键时,如果没有按键数据则 read
函数不会返回,它会让线程休眠等待。
阻塞就是慢性子,有反馈就返回,得不到反馈就等。
非阻塞就是急性子,有没有反馈都不等,即刻返回。
使用 poll
时,如果传入的超时时间不为
0
,这种访问方法也是阻塞的。
使用 poll
时,可以设置超时时间为
0
,这样即使没有数据它也会立刻返回, 这就是非阻塞方式。能不能让 read
函数既能工作于阻塞方式,也可以工作于非 阻塞方式?
可以
!
APP
调用
open
函数时,传入
O_NONBLOCK
,就表示要使用非阻塞方式;默认是阻塞方式。
注意
:对于普通文件、块设备文件,
O_NONBLOCK
不起作用。
注意
:对于字符设备文件,
O_NONBLOCK
起作用的前提是驱动程序针对O_NONBLOCK 做了处理。
定时器
也就是闹钟,赋予Linux系统时间的概念与标准。
可以使用定时器处理按键抖动,如下图
![](https://i-blog.csdnimg.cn/blog_migrate/2273326cbc63b5d6f73962abe6108a8a.png)
核心在于:在
GPIO
中断中并不立刻记录按键值,而是修改定时器超时时间,10ms 后再处理。
◼
如果
10ms
内又发生了
GPIO
中断,那就认为是抖动,这时再次修改超时时间为 10ms
。
◼
只有
10ms
之内再无
GPIO
中断发生,那么定时器的函数才会被调用。 在定时器函数中记录按键值。
setup_timer(timer, fn, data)
:
◼
设置定时器,主要是初始化
timer_list
结构体,设置其中的函数、参数。
⚫
void add_timer(struct timer_list *timer)
:
◼
向内核添加定时器。
timer->expires
表示超时时间。
◼
当 超 时 时 间 到 达 , 内 核 就 会 调 用 这 个 函 数 :
timer->function(timer->data)
。
⚫
int mod_timer(struct timer_list *timer, unsigned long expires):
◼
修改定时器的超时时间,
◼
它等同于:
del_timer(timer); timer->expires = expires;
add_timer(timer);
◼
但是更加高效。
⚫
int del_timer(struct timer_list *timer)
:
◼
删除定时器。
参考原文链接:https://blog.csdn.net/qq_36822217/article/details/107237670
参考原文链接:https://blog.csdn.net/qq_20880415/article/details/81387186