一、硬件中断
问题:操作系统是怎么知道外设来信息了(比如:键盘输入字,网卡收到数据,......)
答:操作系统对外设进行任何周期性的检测或者轮询, 或者外部设备告诉操作系统,"我来了"
从宏观到细节理解中断:
CPU上有许多针脚,针脚可以感受到高低电平,比如说:从键盘上输入字时,CPU上的一个针脚感受到了高电平(因为键盘上有信息要来),这时候,操作系统会产生硬件中断,硬件上下文数据先被保存起来,然后执行中断服务。
但是 ,外设有很多 ,针脚的数量是有限的,全部用硬件来实现,成本真的是太大了,所以, 就有了中断控制器,详细过程看下图


从图中可以看到,存在一个中断向量表,每一个外部设备在产生中断的时候,会有对应的中断号,通过中断向量表(可以理解为数组和数组下标的关系),找到对应的中断服务,执行对应的操作,最后执行完毕后,恢复现场,刚刚被保护的进程继续执行。
细节:
1.中断向量表 是操作系统的一部分,启动就加载到内存中了
2.通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
3.由外部设备触发的中断系统运行流程,叫做硬件中断
下面看一下Linux内核0.11核心中断初始化代码
//Linux内核0.11源码
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13); // 设置协处理器的陷阱门。
outb_p(inb_p(0x21)&0xfb,0x21); // 允许主8259A 芯片的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1); // 允许从8259A 芯片的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt); // 设置并行口的陷阱门。
}
void rs_init (void) // 串口初始化
{
set_intr_gate (0x24, rs1_interrupt); // 设置串行口1 的中断门向量(硬件IRQ4 信号)。
set_intr_gate (0x23, rs2_interrupt); // 设置串行口2 的中断门向量(硬件IRQ3 信号)。
init (tty_table[1].read_q.data); // 初始化串行口1(.data 是端口号)。
init (tty_table[2].read_q.data); // 初始化串行口2。
outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯片的IRQ3,IRQ4 中断信号请求。
}
二、时钟中断
- 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
- 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
所有就有了时钟源(晶振),时钟源可以以固定的频率给CPU发送中断,操作系统进行时钟中断(进行进程调度,包括时间片检测,切换,调度等动作)。在每一个进程中,都会有一个计数器counter,当计数器减到 <0 时,时钟中断进来发现,这个进程的counter小于0了,就进行进程调度,切换到下一个进程,同时上一个进程会重新计算counter放到进程调度队列中。这个的计数器counter就是时间片。
所以,有了时钟中断,操作系统就可以在硬件的推动下,进行自动调度了


细节:电脑为什么断电后可以自动计时?
因为有晶振 存在,当电脑断电后,电脑中会有一个纽扣电池,给晶振供能,晶振可以继续震动,电脑可以把晶振震动的次数记录下来,当电脑再次启动时,通过这个震动次数和电脑的起始时间,就可以计算出当前的时间了。
细节:时钟源的组成时钟发生器 + 时钟计数器 == 时钟源
时钟发生器 :一秒可以产生多少次
时钟计数器:多少次的计数触发一次中断
有了时钟计数器,也就是说触发时钟中断的时间间隔可以用户自己设置了,但一般我们是不会去设置的,内核开发人员会给我们设置好
在我们的电脑属性中,可以看到处理器的频率 ,就是时钟发生器的频率,这就是CPU的主频,也就解释了CPU的主频越快,CPU越快

三、死循环
操作系统的本质就是一个死循环,操作系统,在硬件时钟的推动下,自动调度
操作系统本身就是有许多进程的,进程只要可以调度起来,操作系统就可以正常跑起来。
cpp
void main(void) /* 这里确实是void,并没有错。 */
{
/* 在startup 程序(head.s)中就是这样假设的。 */
...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
* 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任
* 务0 在任何空闲时间里都会被激活(当没有其它任务在运行时),
* 因此对于任务0 'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没
* 有的话我们就回到这里,一直循环执行'pause()'。
*/
for (;;)
pause();
} // end main
四、软中断
上述外部硬件中断,需要硬件设备触发。
有没有可能,因为软件原因,也触发上面的逻辑?是有的!
为了让操作系统支持进行系统调用,CPU 也设计了对应的汇编指令 (int 0x80 或者 syscall), 可以让 CPU 内部触发中断逻辑。
所以就有了软中断

在内核层面,每一个系统调用都有一个系统调用号---->数组下标,当进行系统调用的时候,会触发软中断,把对应的寄存器设置好,拿到系统调用号,进行软中断,执行对应的函数。
细节:是谁把对应的寄存器设置好,然后进行系统调用的???
答:是C语言标准库做的!你没有看错,就是C语言,只是不过是用汇编来写的。这样就封装好了系统调用,不然还需要用户手动设置寄存器,太麻烦了。Linux的gnuC标准库,给我们把几乎所有的系统调用全部封装了。


cpp
// sys.h
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c, 71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读文件。 (fs/read_write.c, 55)
extern int sys_write (); // 写文件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开文件。 (fs/open.c, 138)
extern int sys_close (); // 关闭文件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终止。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建文件。 (fs/open.c, 187)
extern int sys_link (); // 创建一个文件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除一个文件名(或删除文件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执行程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前目录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建立块/字符特殊文件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改文件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改文件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使用路径名取文件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写文件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装文件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载文件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程用户id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程用户id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间日期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使用文件句柄取文件的状态信息。 (fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运行。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变文件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端行设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端行设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查用户对一个文件的访问权限。 (fs/open.c, 47)
extern int sys_nice (); // 设置进程执行优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取日期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步高速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终止一个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改文件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建目录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除目录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制文件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运行时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执行时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段长度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效用户id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // 文件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显示系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认文件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取文件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制文件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取父进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id, 等于getpgid(0)。 (kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运行程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效用户id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
// 调度程序的初始化子程序。
void sched_init(void)
{
...
// 设置系统调用中断门。
set_system_gate(0x80, &system_call);
}
_system_call:
cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
push ds ; // 保存原段寄存器值。
push es
push fs
push edx ; // ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
push ecx ; // push %ebx,%ecx,%edx as parameters
push ebx ; // to the system call
mov edx,10h ; // set up ds,es to kernel space
mov ds, dx ; ds,es 指向内核数据段(全局描述符表中数据段描述符)
mov es, dx
mov edx, 17h ; fs points to local data space
mov fs, dx ; fs 指向局部数据段(局部描述符表中数据段描述符)
; 下面这句操作数的含义是: 调用地址 = _sys_call_table + %eax * 4
; 对应的C程序中的sys_call_table在include/linux/sys.h中,定义了72个系统调用C处理函数地址数组
call [_sys_call_table+eax*4]
push eax ; 把系统调用号入栈
mov eax, _current ; 取当前任务(进程)数据结构地址→eax
; 查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序
; 如果该任务在就绪状态但counter值等于0, 则也去执行调度程序
cmp dword ptr [state+eax], 0 ; state
jne reschedule
cmp dword ptr [counter+eax], 0 ; counter
je reschedule
; 以下这段代码执行从系统调用C 函数返回后, 对信号量进行识别处理
ret_from_sys_call:
五、理解其他中断
缺页中断?内存碎片处理?除零野指针错误?
这些问题,全部都会被转换成为 CPU 内部的软中断,然后走中断处理流程,完成所有处理。
有的是进行申请内存,填充页表,进行映射的。
有的是用来处理内存碎片的,
有的是用来给目标进行发送信号,杀掉进程
等等。
cpp
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error); // 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_trap_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13); // 设置协处理器的陷阱门。
outb_p(inb_p(0x21)&0xfb,0x21); // 允许主8259A 芯片的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1); // 允许从8259A 芯片的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt); // 设置并行口的陷阱门。
}
所以:
- 操作系统就是躺在中断处理例程上的代码块!
- CPU 内部的软中断,比如 int 0x80 或者 syscall,我们叫做 陷阱
- CPU 内部的软中断,比如除零 / 野指针等,我们叫做 异常。(所以也就解释了"缺页异常" 为什么这么叫了)
六、实现一个简易版的OS
细节:信号的处理就是模拟的操作系统的中断的!!!!
下面用信号实现一个简易版的OS
cpp
#include <iostream>
#include <vector>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int current=0;
class task_struct
{
private:
int pid;
int status;
int counter;
public:
task_struct(int p):pid(p), counter(5)
{}
void desc()
{
counter--;
}
void ResetCounter()
{
counter = 5;
}
int Pid()
{
return pid;
}
bool Expired()
{
return counter<=0;
}
void run()
{
std::cout << "process " << pid << "running" << std::endl;
}
~task_struct(){}
};
std::vector<task_struct> tasks;
void do_timer(int signo)
{
tasks[current].desc();
if(tasks[current].Expired())
{
std::cout << tasks[current].Pid() << "过期了,重新选择进行调度" << std::endl;
//选择一个进程运行
current = rand()%tasks.size();
tasks[current].ResetCounter();
}
else{
tasks[current].run();
}
//reset
alarm(1);
}
int main()
{
alarm(1);
signal(SIGALRM, do_timer);
srand(time(nullptr));
tasks.emplace_back(1);
tasks.emplace_back(2);
tasks.emplace_back(3);
tasks.emplace_back(4);
tasks.emplace_back(5);
for(;;)
{
// printf("OS 被中断唤醒\n");
pause(); // 暂停
}
}
运行结果:
