[5]开发一个Linux系统吧——IO模型与中断
IO模式
提及到了IO就一定包含了同步、异步、阻塞和非阻塞四大金刚,这些概念都是宏观概念,所以完全可以从字面上分析,接下来举例就可以了。
同步与异步就是并发的过程,这里需要使用到新的进程或者线程。阻塞就是系统发送信号进行锁住状态,同时也会产生相应的回调过程。在C语言中,scanf
就是一个阻塞的过程。非阻塞IO就是在进行内核数据交换中,内核还可以进行其他处理,比如与其他进程2继续交换数据。
IO是可以出现多个的,这样的操作叫多路复用,在数据交互中,准备好的数据既可以继续向下传递,也可以现用现读。如果IO结合信号机制的话,也会有一种信号式IO。
这里罗列以下几种IO的操作顺序:
进程调度
这是内核负责管理那些程序需要投入使用的方法,进程调度其实是对进程文件的资源分配。
进程在运行的时候,用户空间需要传递很多变量,这些就是所谓的进程上下文,可以看作是用户空间的数据传递。一个进程包含三个上下文,分别为:
- 用户上下文:数据、栈区
- 寄存器上下文:通用寄存器,特殊寄存器(程序寄存器)、栈指针
- 系统上下文:task_struct进程描述符,mm_struct(内存描述符)
上下文切换就是对上面的信息进行切换。
在硬件触发中断的时候,内核优先处理中断,这些就是中断上下文,可以看作是内核运行的其他环境。中断分为两个部分,一个是接受中断的部分,另一部分就是善后工作。
Linux的进程调度一共有三种策略,分别为:
- SCHED_OTHER,分时调度(CFS),按优先级运行
- SCHED_RR,实时调度,时间轮转
- SCHED_FIFO,先到先服务
为了衡量时间,Linux定义了时间片的概念,他表示一个进程抢占的时间可以运行多久。
分时调度时Linux默认的程调度策略,默认的优先级都是0,在策略表上就是据此进行的。事实上,如果标记值越大运行的优先级越低,这样标记值小的可以获得更多的运行时间。标记值是会随着是按进行动态改变的,这样能确保更公平的占有处理器。同时这种方法摒弃了时间片,而是采用了优先级的概念,这样可以获得更为合理的处理时间。默认最少获得时间为1ms。
NICE是一个表达优先级的数值,我们可以使用ps -el
进行查看,NI就是优先级修正的值,它可以更改优先级。
1 |
|
这里也显示了一些信息,包括:F表示程序的用户组,S表示状态,UID表示执行者的身份,PID表示进程的ID号,C表示CPU的资源占比,PRI表示优先权限,NI就是NICE值,ADDR表示内核函数,SA表示内存大小,WCHAN表示运行状态,TTY表示bash的位置,TIME就是消耗CPU的时间,CMD值下达的命令。
PRI:进程优先级,值越小,优先级越高
NI:变化值,PR = PRI + NI
在调度的过程中需要完成四个功能:
- 运行时间记录
调度器必须记录进程的运行时间,因为CFS没有了时间片的概念,所以就需要统计时间以保证公平分配时间。
- 进程选择
有限选择运行时间小的进程,这个查找使用的是红黑树,速度很快。红黑树也可以组织可运行进程队列,并利用它来迅速找到最小vruntime值的进程。
- 调度器
进程调度的主要入口点函数是schedule(),他会调用pick_next_task(),pick_nexttask()会以优先级为序,从高到低依次检查每一个调度类,并且从最高优先级的调度类中选择最高优先级的进程,pick_next_task()会返回指向下一个可运行进程的指针,没有的话返回NULL。
- 睡眠苏醒
休眠就是将进程移除进程的btree(Linux的红黑树),放入等待的队列里,然后选择一个复苏的进程或者其他新加入的进程加入红黑树。苏醒状态就是从队列移出到红黑树。
在用户抢占进程的过程中,内核从need_resched获取进程是否需要被调度,检查完成后如果表明这个进程需要被运行,那就尽快调用调度功能,再返回到用户空间一节从中断返回时,内核依然会检查need_resched。
内核即将返回用户空间的时候,如果需要设置need_resched就会发生用户抢占,在进入用户空间的时候,他就可以继续执行,或者作为一个新进程运行。当然这些都是使用汇编实现的。
在内核运行中,如果进程没有锁,那么内核就可以抢占。换言之就是只要调度时安全的,那么内核就可以抢占。内核抢占会发生以下事情:
- 在进入内核空间前执行中断程序
- 内核的抢占机会过多的时候
- 显示更改need_resched标志的时候
- 任务阻塞期间
中断与异常
中断指CPU的运行期间收到了外部的干扰或者程序预先安排的中断时间引起了CPU停止运行程序,转去运行其他程序的过程,在运行之后再重新执行被中断的程序。中断分为两种,一种是硬件中断,一种是异步中断:
- 硬件中断是CPU产生的,因为只有执行完相应的操作后才会引发中断,而不是代码执行期间。
- 异步中断指其他设备或者程序按照CPU的始终产生的,意味着中断可以再指令之间异步发生。
同时异常产生的中断是程序的错误产生的,或者是内核必须处理的一场条件产生的。第一种是通过信号处理,第二种是恢复异常需要的步骤。
这里注意,中断和进程切换不同,进程切换产生了新的进程,中断处理只是一个方法。同时中断也是CPU最敏感的操作,需要满足如下条件:
- 内核需要做其他事情的时候就会触发中断,中断存在的目的就是推迟处理。因此内核最后需要划分两部分,关键的部分和退出的部分
- 中断的产生具有随机性,而且是可以堆叠的,这样可以让更多的IO设备处于运行状态,所以必须要嵌套运行,这样才可以完成所有的进程。
- 中断存在临界区,如果中断处于这些区域内,中断就会被限制,其次中断必须处于可以随时被相应的状态(开中断)
当然如果你学过STM32,一定会知道NVIC的中断管理方式,其实等同于Linux内核的向量中断。
- 中断信号在传递的过程就是中断号,其中中断控制器把中断信号发送给CPU就会产生一个中断向量,中断向量与中断号存在一一对应的关系。
- 存在一种可以屏蔽的中断信号,中断控制器可以忽略他
- 存在一种不可以被忽略的中断信号,一般是非常紧急的时间,区分它需要使用CPU
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
中断处理器在把中断号变成中断向量值得过程时,就会在内存中建立一个向量表,其中包含了所有中断向量,中断向量表明了中断程序的地址。
为了处理中断,每个硬件都会于一条IRQ线连接,每个IRQ都和一个可编程的中断向量控制器连接,控制器负责将收到的信号转化为对应的向量,这些向量存储于控制器内,CPU可以按照需求进行读取,一旦产生中断,COU的NTR引脚就会被激活。当一切结束后,NTR引脚重置。
中断向量控制器也是一种APIC,可以更好的发挥并行能力。
异常的种类非常多,为了处理这些异常,内核必须提供所有异常的处理方法,在执行某些方法时只需要返回一个错误码即可。
为了能将信号与对应的中断向量建立联系,内核制定了一套中断描述符表(IDT)来存储中断和异常。
- 任务门:中断信号发生后,把目标进程移到此区域内,用户不可以访问到,同时所有中断处理程序都是通过中断门激活的
- 系统门:用户可以访问到的区域,可以使用对应的汇编指令进行操作
- 系统中断门:可以被用户访问到的中断,异常处理程序从这里激活
- 陷阱门:用户不可以访问的门,大部分异常处理程序都是通过此门激活
- 任务门:不能被用户访问的门,处理浮点异常等
为了能让CPU获得更多的工作时间,中断控制器会确定中断和关联的中断向量:
- 如果触发了中断,首先读取IDT的值,确定触发中断程序的地址
- 确定中断发生源,并且判断中断的等级,这样可以避免进入了错的门
- 检查等级的变化,读取tr寄存器,将中断的处理部分放入栈里,然后运行
- 如果出现问题就需要保存在cs和cip寄存器内,并将引发故障的指令放入栈。
- 如果差生了硬件码,则放入栈内
确定异常、中断向量。
权限、特权检查;针对不同类型的异常、中断,保存不同的内容。
将从异常、中断向量得到的中断或异常处理程序地址装入 cs、eip 寄存器。
最有意思的一点就是:中断可以触发中断,或者一个中断噎死另一个中断。
这样我们就知道了,异常的实现就是中断,既然如此我们就发现了java的try catch
的语法就是一个中断信号的响应。那么中断的处理过程是怎样的呢?
中断机制包含三个部分:中断系统的初始化,中断的处理,中断的API
初始化
首先分配IDT一定的内存空间,我们知道中断向量每个都是八字节,而且有256个中断向量,所以内存空间为2048b,也就是2kb。第二部就是初始化保留的中断向量,完成其余的中断向量初始化。
这里区分一下向量中断和非向量中断。向量中断时中断有不同的入口,非向量中断仅有一个入口地址,然后进行判断是哪种中断。
- 向量中断模式用于RESET、NMI(非屏蔽中断)、异常处理。当向量中断产生时,控制器直接将PC赋值
- 非向量中断模式,有一个寄存器标识位,跳转到统一的函数地址,此函数通过判别寄存器标识位和优先级关系进行中断处理。
中断的处理
设备产生中断后,通过信号发送给中断控制器,然后变化为中断向量给CPU的NTR口,然后执行相应的程序。
中断控制器执行:处理中断请求,比较中断的优先级,返回中断向量给CPU。
CPU执行:使用中断向量在IDT内查询,等级变化进入栈,进入中断处理程序。
API
1 |
|
1 |
|
中断分为两部分,上半部分在收到信号后就会离开了执行,主要完成一些必要操作,在运行上半部分时可以被其他中断扼杀。下半部分完成比较耗时的工作。
上半部分:
下半部分:
软中断(可以被加塞):
软中断拥有中断上文的全部性质,同时他有自己定义的32个接口,软中断不能同类打断,只能被硬中断打断,同时软中断时并发的,也就意味着软中断需要锁保护。
1 |
|
tasklet
这是一种延迟型的软中断,一般的软中断都是内核分配的,而tasklet时可以进行动态分配,不过tasklet绑定CPU,性能就会有所下降
1 |
|
因为tasklet使用较为轻松,所以一般都会使用它完成中断操作。
为了简化内核的线程创建,Linux引入了workqueue(工作队列),它可以根据内核数创建线程。
- work :工作。
- workqueue :工作的池子
- worker :工人。
- worker_pool:工人的池子
- pwq(pool_workqueue):建立工人队列与工人池子的关系
work_struct负责描述work,初始化后会被加入到哦工作队列,然后传递到合适的内核进行处理。
workqueue可以绑定worker到CPU上,也可以在处理器间来回运行
每个worker包含了一个内核线程,可以根据工作状态添加worker_pool内
worker_pool就是一个线程池,可以管理线程。
pool_workqueue用于将workqueue和worker_pool关联起来
使用的API如下:
1 |
|
中断的使用需要在使用Linux内核的单片机,同时编译内核时也需要开启GPIO,具体可以参考第一期编译的过程。
总结
过年了,休息一下,暂停更新
本期博文主要介绍了中断和IO模型,通过阅读本期博文,读者可以更好的理解包括异常处理、线程池等,读者可以使用已有的API完成C对系统的更好的使用。