操作系统对定时器的应用
tips:这是一篇系列文章,总目录在这里哟~
到了在操作系统层面,可以依靠硬件产生的定时器中断做很多事情,同时,操作系统的定时器怎么实现呢?我们来分析一下。
1. 硬件定时器
现在的Linux对时间的管理是很复杂的,大体可以分为高精度时钟和低精度时钟。两者互不兼容。
在 Linux 2.6.16 之前,内核只支持低精度时钟。内核围绕着 tick 时钟来实现所有的时间相关功能。tick 是一个定期触发的中断,一般由 PIT 提供,大概 10ms 触发一次 (100HZ),精度比较低。如果频率设置的太高,就会严重影响系统性能。
(1) tick
以 x86 为例,系统初始化时会配置定时器中断。当硬件设备初始化完成后,便开始定期地产生中断,这便是 tick 了。需要强调的是 tick 中断是由硬件直接产生的真实中断。
那么,这个tick操作系统会拿它来做什么呢?
- Linux内核依赖 tick来进行分时,这是分时操作系统的硬件基础,也是多任务实现的基础。
- 维护系统时间。Linux 系统初始化时,读取 RTC,得到当前时间值。此后直到下次重新启动,Linux 不会再读取硬件 RTC 了。Linux通过tick来更新系统时间。
可以说tick和其他的中断就是Linux内核的驱动力。Linux内核并非是一堆持续运行的程序,内核的各个模块基本都是由中断驱动的。而中断可以包括硬件中断,可以是软件中断。
(2)进程调度
每个时钟中断(timer interrupt)发生时,需要由3个函数协同工作,共同完成进程的选择和切换:schedule()
、do_timer()
及ret_form_sys_call()
。
- schedule():进程调度函数,由它来完成进程的选择(调度)。
- do_timer():暂且称之为时钟函数,该函数在时钟中断服务程序中被调用,是时钟中断服务程序的主要组成部分,该函数被调用的频率就是时钟中断的频率即每秒钟100 次(简称100 赫兹或100Hz);由这个函数完成系统时间的更新、进程时间片的更新等工作,更新后的进程时间片counter 作为调度的主要依据。
- ret_from_sys_call():系统调用、异常及中断返回函数。当一个系统调用或中断完成时,该函数被调用,用于处理一些收尾工作,例如信号处理、核心任务等。函数检测need_resched 标志,如果此标志为非0,那么就调用调度程序schedule()进行进程的选择。调度程序schedule()会根据具体的标准在运行队列中选择下一个应该运行的进程。当从调度程序返回时,如果发现又有调度标志被设置,则又调用调度程序,直到调度标志为0,这时,从调度程序返回时由RESTORE_ALL恢复被选定进程的环境,返回到被选定进程的用户空间,使之得到运行。
(3)核心代码浅析
我们重点关注:硬件-->硬件控制-->进程控制
这部分内容。
Linux是如何控制8259芯片的?
在计算机的定时器章节中,我们知道了计算机有一个8259中断控制芯片,同时内存中会维护一个中断向量表。
硬盘的第一个扇区为主引导扇区,计算机加电后,执行BIOS程序初始化,然后从硬盘启动时,就会读取主引导扇区的程序执行。Linux系统会在主引导扇区写入一个bootsect.s
的程序,然后bootsect.s
通过调用到setup.s
的内容,进行一些系统初始化的设置。在setup.s
中会重新初始化8259A芯片,并且在header.s
中重新设置一张中断向量表。
bootsect.s
bootsect.s是引导程序,BIOS会把bootsect加载到0x7c00处开始执行。
bootsect首先会把自己搬移到0x90000处,然后继续执行,把setups加载到0x90200处。setups在硬盘中也有固定的位置,它位于第二个扇区开始的4个扇区内。然后利用BIOS中断0x13取磁盘参数表中当前引导盘的参数,并在屏幕上显示“Loading system…”。
bootsect继续执行,会把磁盘上挨着setups的system模块加载到0x10000的地方。随后是确定根文件系统的设备号。
最后长跳转到setup的开始处,开始执行setup程序。
汇编的程序大家读起来都比较费劲,就不全贴了。
! SYS_SIZE是要加载的系统模块长度,单位是节,16个字节为1节。0x3000就是196个字节
SYSSIZE = 0x3000.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.textSETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! 程序本来在这里
INITSEG = 0x9000 ! 我们把bootloader移到这里
SETUPSEG = 0x9020 ! setup程序从这里开始
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading! .........省略
! 这句就是段间跳转指令,跳转执行setup程序jmpi 0,SETUPSEG! .........省略sectors:.word 0msg1:.byte 13,10.ascii "Loading system ...".byte 13,10,13,10.org 508
root_dev:.word ROOT_DEV
boot_flag:.word 0xAA55.text
endtext:
.data
enddata:
.bss
endbss:
setup.s
setup顾名思义会做一些系统的初始设置。它会从BIOS的ROM中读取一些设置参数,并保存在对应的内存位置。
会重新设置两个中断控制芯片8259A,重新设置硬件中断号为0x20~0x2f。最后会转到system模块下开头部分的head.s继续执行。
! ...掐头! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.mov al,#0x11 ! initialization sequenceout #0x20,al ! send it to 8259A-1.word 0x00eb,0x00eb ! jmp $+2, jmp $+2out #0xA0,al ! and to 8259A-2.word 0x00eb,0x00ebmov al,#0x20 ! start of hardware int's (0x20)out #0x21,al.word 0x00eb,0x00ebmov al,#0x28 ! start of hardware int's 2 (0x28)out #0xA1,al.word 0x00eb,0x00ebmov al,#0x04 ! 8259-1 is masterout #0x21,al.word 0x00eb,0x00ebmov al,#0x02 ! 8259-2 is slaveout #0xA1,al.word 0x00eb,0x00ebmov al,#0x01 ! 8086 mode for bothout #0x21,al.word 0x00eb,0x00ebout #0xA1,al.word 0x00eb,0x00ebmov al,#0xFF ! mask off all interrupts for nowout #0x21,al.word 0x00eb,0x00ebout #0xA1,al! ...去尾
header.s
header会和其他程序一起链接成system模块,并位于system的头部。这段程序首先重新设置中段描述符表idt,使各项均指向一个只报错误的哑中断子程序ignore_int。中间还有一些处理,最后head.s程序利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行main()程序。
main.c
main程序是整个内核初始化的过程。这里面做了很多工作,大部分都是调用其他的xxx.c
程序去处理。我们只关注定时器中断是怎么处理的。在上面headers中,所有的中断向量都是指向了ignore_int。在headers中,陆续的就会在需要的位置重新初始化中断向量。
在main.c的开头,我们可以看到这样一段逻辑:
// 内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void)
{// ......省略一些代码// 以下是内核进行所有方面的初始化工作mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.ctrap_init(); // 陷阱门(硬件中断向量)初始化,kernel/traps.cblk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.cchr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.ctty_init(); // tty初始化, kernel/chr_drv/tty_io.ctime_init(); // 设置开机启动时间 startup_timesched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)// 缓冲管理初始化,建内存链表等。(fs/buffer.c)buffer_init(buffer_memory_end);hd_init(); // 硬盘初始化,kernel/blk_drv/hd.cfloppy_init(); // 软驱初始化,kernel/blk_drv/floppy.csti(); // 所有初始化工作都做完了,开启中断// 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。move_to_user_mode(); // 移到用户模式下执行if (!fork()) { /* we count on this going ok */init(); // 在新建的子进程(任务1)中执行。}// pause系统调用会把任务0转换成可中断等待状态,再执行调度函数。但是调度函数只要发现系统中// 没有其他任务可以运行是就会切换到任务0,而不依赖于任务0的状态。for(;;) pause();
}
就是各种初始化,其中有一个sched_init()
调度器的初始化。
sched.c
直接看调度器初始化的代码:
// 内核调度程序的初始化子程序
void sched_init(void)
{// ......省略部分代码// 下面代码用于初始化8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的// 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始// 定时计数值。outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */outb_p(LATCH & 0xff , 0x40); /* LSB */outb(LATCH >> 8 , 0x40); /* MSB */// 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。// 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符在宏定义在文件// include/asm/system.h中。set_intr_gate(0x20,&timer_interrupt);outb(inb_p(0x21)&~0x01,0x21);set_system_gate(0x80,&system_call);
}
set_intr_gate(0x20,&timer_interrupt);
这一句,就是我们要找的关键,现在是把timer_interrupt
作为了中断处理程序了。timer_interrupt是一段汇编代码,我们一起来看看:
### int32 - (int 0x20)时钟中断处理程序。中断频率被设置为100Hz。
# 定时芯片8253/8254是在kernel/sched.c中初始化的。因此这里jiffies每10 ms加1.
# 这段代码将jiffies增1,发送结束中断指令给8259控制器,然后用当前特权级作为
# 参数调用C函数do_timer(long CPL).当调用返回时转去检测并处理信号。
.align 2
timer_interrupt:push %ds # save ds,es and put kernel data spacepush %es # into them. %fs is used by _system_callpush %fspushl %edx # we save %eax,%ecx,%edx as gcc doesn'tpushl %ecx # save those across function calls. %ebxpushl %ebx # is saved as we use that in ret_sys_callpushl %eaxmovl $0x10,%eaxmov %ax,%dsmov %ax,%esmovl $0x17,%eaxmov %ax,%fsincl jiffies
# 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。movb $0x20,%al # EOI to interrupt controller #1outb %al,$0x20 # 操作命令字OCW2送0x20端口
# 下面从堆栈镇南关取出执行系统调用代码的选择符(CS段寄存器值)中的当前特权级别(0或3)
# 并压入堆栈,作为do_timer的参数。do_timer函数执行任务切换、计时等工作。movl CS(%esp),%eaxandl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)pushl %eaxcall do_timer # 'do_timer(long CPL)' does everything fromaddl $4,%esp # task switching to accounting ...jmp ret_from_sys_call
do_timer又做了什么呢:
/// 时钟中断C函数处理程序,在system_call.s中timer_interrupt被调用。
// 参数cpl是当前特权级0或3,是时钟中断发生时正在被执行的代码选择符中的特权级。
// cpl=0时表示中断发生时正在执行内核代码;cpl=3表示中断发生时正在执行用户代码。
// 对于一个进程由于执行时间片用完时,则进城任务切换。并执行一个计时更新工作。
void do_timer(long cpl)
{extern int beepcount; // 扬声器发声滴答数extern void sysbeepstop(void); // 关闭扬声器。// 如果发声计数次数到,则关闭发声。(向0x61口发送命令,复位位0和1,位0// 控制8253计数器2的工作,位1控制扬声器)if (beepcount)if (!--beepcount)sysbeepstop();// 如果当前特权级(cpl)为0,则将内核代码运行时间stime递增;if (cpl)current->utime++;elsecurrent->stime++;// 如果有定时器存在,则将链表第1个定时器的值减1.如果已等于0,则调用相应的// 处理程序,并将该处理程序指针置空。然后去掉该项定时器。next_timer是定时器// 链表的头指针。if (next_timer) {next_timer->jiffies--;while (next_timer && next_timer->jiffies <= 0) {void (*fn)(void); // 这里插入了一个函数指针定义!!!! o(︶︿︶)o fn = next_timer->fn;next_timer->fn = NULL;next_timer = next_timer->next;(fn)(); // 调用处理函数}}// 如果当前软盘控制器FDC的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序if (current_DOR & 0xf0)do_floppy_timer();// 如果进程运行时间还没完,则退出。否则置当前任务计数值为0.并且若发生时钟中断// 正在内核代码中运行则返回,否则调用执行调度函数。if ((--current->counter)>0) return;current->counter=0;if (!cpl) return; // 内核态程序不依赖counter值进行调度schedule();
}
所以,所谓的时间片轮转法,知道是怎么回事了吧。所以,可以说操作系统的进程调度是时间驱动力的第三层体现。
2. 软件定时器
能够提供可编程定时中断的硬件电路都有一个缺点,即同时可以配置的定时器个数有限。但现代 Linux 系统中需要大量的定时器:内核自己需要使用 timer,比如内核驱动的某些操作需要等待一段给定的时间,或者 TCP 网络协议栈代码会需要大量 timer;内核还需要提供系统调用来支持 setitimer 和 POSIX timer。这意味着软件定时器的需求数量将大于硬件能够提供的 timer 个数,内核必须依靠软件 timer。
timer的软件实现:
- 通过timer链表实现,早起Linux就是这种方式。每次tick来临时,遍历链表,触发所有到期timer即可。但是遍历链表需要花费的时间不可控。
- 时间轮算法,Linux从2.4开始采用这种算法,时间负责度恒为O(1)。
3. 参考资料
- Linux内核时钟系统和定时器实现,by Walker
- 浅析 Linux 中的时间编程和实现原理,by kyle
- Linux下定时器的设计与实现,by Baixiangcpp
- 试谈Linux下的线程调度,by Gunjianpan
- 时间系统、进程的调度与切换,by s1mba
- 《Linux内核完全注释》,by 赵炯
操作系统对定时器的应用相关推荐
- linux系统支持多种硬件平台吗,linux操作系统对硬件的要求是多少
你们知道在Linux中操作系统对硬件的要求多吗,是多少?下面是学习啦小编带来的关于linux操作系统对硬件的要求是多少的内容,欢迎阅读! linux操作系统对硬件的要求是多少? Linux操作系统对硬 ...
- 发现好文!51单片机特殊功能寄存器 /I/O口操作 /中断/ 定时器/ 串口通信/ ---位寻址解释由来--以及程序例程
51单片机特殊功能寄存器有哪些_功能是什么 最近学习中对寄存器的概念理解很迷惑,I/O口操作/中断/定时器/串口通信四大模块的寄存器应用不太明白,这篇文章,解释的不错,希望帮到各位! 1.21个寄存器 ...
- 计算机操作系统对文件进行管理的体现,计算机操作系统复习之文件管理
第五章 文件系统 操作系统对系统的软件资源的管理都以文件方式进行,承担着部分功能的操作系统称为文件系统. 本章介绍文件的逻辑组织和在文件存储器上的物理组织:实现"按名存取"和文件共 ...
- 鸿蒙系统的战略意义,方正证券:鸿蒙操作系统对华为的意义
金融界网5月25日消息,方正证券认为鸿蒙操作系统对华为的意义主要在于以下三点: 1.是华为汽车的战略支点: 2.是华为手机+IoT的延续: 3.是战略升华的落脚点. 第一层:是华为汽车的战略支点. 华 ...
- golang基础-chan的select操作、定时器操作、超时控制、goroutine中使用recover
chan的只读和只写 a.只读chan的声明 Var 变量的名字 <-chan int Var readChan <- chan int b. 只写chan的声明 Var 变量的名字 ch ...
- Javascript——进阶(事件、数组操作、字符串操作、定时器)
目录 事件属性 数组 字符串操作 定时器 变量的作用域 封闭函数 弹框接收数据 事件属性 参数 描述 onclick 鼠标点击事件 onmouseover 鼠标移入标签,触发行为 onmouseout ...
- 物联网操作系统软件定时器
软件定时器的定义和作用 FreeRTOS软件定时器 FreeRTOS软件定时器工作原理 软件定时器函数应用 功能需求 使用软件定时器功能完成闹钟功能设计 当闹钟到达时,可根据执行动作,触发相关的led ...
- 【ROS程序】--- 1.基本时间操作和定时器
"琅琊少年诸葛恪!" I.准备工作 II.demo01_time/src/time_01.cpp中的主要代码 1. 获取时刻 2. 设置时刻 3. 设置时间段 4. 时间与时刻的运 ...
- linux系统最大支持多大硬盘容量,LINUX操作系统对硬件支持有上限么?最大多少内存?多大硬盘容量?...
32位的Linux的内存最大支持到4GB,64位的Linux的最大支持内存在TB级别上. (实际上最大支持多大的内容跟操作系统的种类无关,而是跟操作系统是几位的.还有CPU是几位的有关.) DOS是1 ...
最新文章
- 如何获得images.xcassets 中图片的路径?
- Subversion快速入门教程
- codeforces D Santa Claus and a Palindrome(hash+贪心)
- GPU/DRM 简介
- 微信公众号管理系统 RhaPHP1.2.5更新啦!
- 比特币经历价格过山车 理财还是乐金所、ppmoney网贷靠谱
- [SCOI 2010]传送带
- Safari 14.0 的功臣 Webp?
- sharding-jdbc整合mybatis
- Oracle 配置监听和本地网络服务
- WebApi开启CORS支持跨域POST
- python 函数参数self_Python类中self参数用法详解
- C#.Net实现AutoCAD块属性提取
- 小甲鱼Python3学习笔记之第十讲(仅记录学习)
- iOS内存管理——alloc/release/dealloc方法的GNUstep实现与Apple的实现
- 2022.04.15【单细胞】|Seurat安装,C++ compiler supports the long long type... no解决方法
- 大学生用计算机,大学生计算机科学基础
- mysql decimal、numeric数据类型
- map和multimap的用法详解
- 搜狗 linux 五笔输入法,Ubuntu下安装搜狗、谷歌、五笔等输入法
热门文章
- 简历重点stat法则
- pomelo + vscode + typescript搭建可约束可调试的游戏服务端框架
- python2爬取虎扑NBA的新闻标题和内容发送到QQ邮箱
- 计算机控制菜单在哪里,电脑菜单在哪里
- mybatis-plus分页插件配置与使用(springboot)
- sas 读取mysql数据类型_SAS | 格式规范数据读取
- Java实现凑硬币或者最少硬币数
- linux系统安全优化策略
- 定义一个基类Shape,在此基础上派生出Rectangle和Circle,二者都有getArea( )函数计算对象的面积,再使用Rectangle类创建一个派生类Square。
- 西南医科大学口腔医学院•瑞泰口腔奖学金设立