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. 参考资料

  1. Linux内核时钟系统和定时器实现,by Walker
  2. 浅析 Linux 中的时间编程和实现原理,by kyle
  3. Linux下定时器的设计与实现,by Baixiangcpp
  4. 试谈Linux下的线程调度,by Gunjianpan
  5. 时间系统、进程的调度与切换,by s1mba
  6. 《Linux内核完全注释》,by 赵炯

操作系统对定时器的应用相关推荐

  1. linux系统支持多种硬件平台吗,linux操作系统对硬件的要求是多少

    你们知道在Linux中操作系统对硬件的要求多吗,是多少?下面是学习啦小编带来的关于linux操作系统对硬件的要求是多少的内容,欢迎阅读! linux操作系统对硬件的要求是多少? Linux操作系统对硬 ...

  2. 发现好文!51单片机特殊功能寄存器 /I/O口操作 /中断/ 定时器/ 串口通信/ ---位寻址解释由来--以及程序例程

    51单片机特殊功能寄存器有哪些_功能是什么 最近学习中对寄存器的概念理解很迷惑,I/O口操作/中断/定时器/串口通信四大模块的寄存器应用不太明白,这篇文章,解释的不错,希望帮到各位! 1.21个寄存器 ...

  3. 计算机操作系统对文件进行管理的体现,计算机操作系统复习之文件管理

    第五章 文件系统 操作系统对系统的软件资源的管理都以文件方式进行,承担着部分功能的操作系统称为文件系统. 本章介绍文件的逻辑组织和在文件存储器上的物理组织:实现"按名存取"和文件共 ...

  4. 鸿蒙系统的战略意义,方正证券:鸿蒙操作系统对华为的意义

    金融界网5月25日消息,方正证券认为鸿蒙操作系统对华为的意义主要在于以下三点: 1.是华为汽车的战略支点: 2.是华为手机+IoT的延续: 3.是战略升华的落脚点. 第一层:是华为汽车的战略支点. 华 ...

  5. golang基础-chan的select操作、定时器操作、超时控制、goroutine中使用recover

    chan的只读和只写 a.只读chan的声明 Var 变量的名字 <-chan int Var readChan <- chan int b. 只写chan的声明 Var 变量的名字 ch ...

  6. Javascript——进阶(事件、数组操作、字符串操作、定时器)

    目录 事件属性 数组 字符串操作 定时器 变量的作用域 封闭函数 弹框接收数据 事件属性 参数 描述 onclick 鼠标点击事件 onmouseover 鼠标移入标签,触发行为 onmouseout ...

  7. 物联网操作系统软件定时器

    软件定时器的定义和作用 FreeRTOS软件定时器 FreeRTOS软件定时器工作原理 软件定时器函数应用 功能需求 使用软件定时器功能完成闹钟功能设计 当闹钟到达时,可根据执行动作,触发相关的led ...

  8. 【ROS程序】--- 1.基本时间操作和定时器

    "琅琊少年诸葛恪!" I.准备工作 II.demo01_time/src/time_01.cpp中的主要代码 1. 获取时刻 2. 设置时刻 3. 设置时间段 4. 时间与时刻的运 ...

  9. linux系统最大支持多大硬盘容量,LINUX操作系统对硬件支持有上限么?最大多少内存?多大硬盘容量?...

    32位的Linux的内存最大支持到4GB,64位的Linux的最大支持内存在TB级别上. (实际上最大支持多大的内容跟操作系统的种类无关,而是跟操作系统是几位的.还有CPU是几位的有关.) DOS是1 ...

最新文章

  1. 如何获得images.xcassets 中图片的路径?
  2. Subversion快速入门教程
  3. codeforces D Santa Claus and a Palindrome(hash+贪心)
  4. GPU/DRM 简介
  5. 微信公众号管理系统 RhaPHP1.2.5更新啦!
  6. 比特币经历价格过山车 理财还是乐金所、ppmoney网贷靠谱
  7. [SCOI 2010]传送带
  8. Safari 14.0 的功臣 Webp?
  9. sharding-jdbc整合mybatis
  10. Oracle 配置监听和本地网络服务
  11. WebApi开启CORS支持跨域POST
  12. python 函数参数self_Python类中self参数用法详解
  13. C#.Net实现AutoCAD块属性提取
  14. 小甲鱼Python3学习笔记之第十讲(仅记录学习)
  15. iOS内存管理——alloc/release/dealloc方法的GNUstep实现与Apple的实现
  16. 2022.04.15【单细胞】|Seurat安装,C++ compiler supports the long long type... no解决方法
  17. 大学生用计算机,大学生计算机科学基础
  18. mysql decimal、numeric数据类型
  19. map和multimap的用法详解
  20. 搜狗 linux 五笔输入法,Ubuntu下安装搜狗、谷歌、五笔等输入法

热门文章

  1. 简历重点stat法则
  2. pomelo + vscode + typescript搭建可约束可调试的游戏服务端框架
  3. python2爬取虎扑NBA的新闻标题和内容发送到QQ邮箱
  4. 计算机控制菜单在哪里,电脑菜单在哪里
  5. mybatis-plus分页插件配置与使用(springboot)
  6. sas 读取mysql数据类型_SAS | 格式规范数据读取
  7. Java实现凑硬币或者最少硬币数
  8. linux系统安全优化策略
  9. 定义一个基类Shape,在此基础上派生出Rectangle和Circle,二者都有getArea( )函数计算对象的面积,再使用Rectangle类创建一个派生类Square。
  10. 西南医科大学口腔医学院•瑞泰口腔奖学金设立