Linux:理解中断

一、硬件中断

问题:操作系统是怎么知道外设来信息了(比如:键盘输入字,网卡收到数据,......)

答:操作系统对外设进行任何周期性的检测或者轮询, 或者外部设备告诉操作系统,"我来了"

从宏观到细节理解中断:

CPU上有许多针脚,针脚可以感受到高低电平,比如说:从键盘上输入字时,CPU上的一个针脚感受到了高电平(因为键盘上有信息要来),这时候,操作系统会产生硬件中断,硬件上下文数据先被保存起来,然后执行中断服务。

但是外设有很多 ,针脚的数量是有限的,全部用硬件来实现,成本真的是太大了,所以, 就有了中断控制器,详细过程看下图

从图中可以看到,存在一个中断向量表,每一个外部设备在产生中断的时候,会有对应的中断号,通过中断向量表(可以理解为数组和数组下标的关系),找到对应的中断服务,执行对应的操作,最后执行完毕后,恢复现场,刚刚被保护的进程继续执行。

细节:

1.中断向量表 是操作系统的一部分,启动就加载到内存中了

2.通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询

3.由外部设备触发的中断系统运行流程,叫做硬件中断

下面看一下Linux内核0.11核心中断初始化代码

复制代码
//Linux内核0.11源码
void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_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,&parallel_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,&divide_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,&parallel_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(); // 暂停
	}
}

运行结果:

相关推荐
北山有鸟2 小时前
解析 Linux 内核驱动中的“换行美学”
linux·运维·服务器
Run_Teenage2 小时前
Linux:信号保存与捕捉
运维·服务器
龙侠九重天2 小时前
可视化自动化工具实现
运维·自动化·openclaw
水冗水孚2 小时前
以Vultr供应商的VPS为例、十分钟自建一个自己的VPN(图文并茂)
运维·服务器
巨大八爪鱼2 小时前
【方法】Tomcat网站添加用户名密码弹窗认证
运维·服务器·tomcat·jsp·mod_jk
unDl IONA2 小时前
Linux安装RabbitMQ
linux·运维·rabbitmq
米高梅狮子2 小时前
Ubuntu和Containerd
linux·运维·ubuntu
片酷2 小时前
【IsaacLab报错】C++ 标准库版本过低
linux·运维·服务器
小宋0012 小时前
Ubuntu 22.04 + ROS 2 Humble 下 SLAM 建图 + 导航(Nav2 + slam_toolbox) 的完整教程
linux·ubuntu·机器人