LINUX内核时钟中断机制
- 格式:pdf
- 大小:500.35 KB
- 文档页数:77
时间篇之linux系统时间和RTC时间⼀、linux系统下包含两个时间:系统时间(刚启动时读取的是rtc时间)和RTC时间。
⼀般情况下都会选择芯⽚上最⾼精度的定时器作为系统时间的定时基准,以避免在系统运⾏较长时间后出现⼤的时间偏移。
特点是掉电后不保存。
所以⼀旦你重启机器后,那么系统需要重新从RTC上重新获取时间,保存到系统内核⽂件中。
RTC(real_time clock)驱动程序,可以在E:\linux内核\linux-2.6.0\linux-2.6.0\drivers\char\rtc.c中找到。
设备接⼝就是 /dev/rtc, 他负责跟rtc打交道,并读取rtc中维护的时间.它是⼀个从系统定时器中独⽴出来的虚拟设备,⽤于设置系统时钟,提供报警器或周期性的定时器.那么系统时间⼀直运⾏吗?显然在操作系统关闭或重启期间,服务器宕机期间,整个服务器的时间就依赖于RTC芯⽚。
从这我们看出linux系统时间和RTC时间是两套独⽴的计时体系,但它们之间⼜是相互依存的:1)刚安装操作系统后,若在安装过程不设置系统时间,那么默认的系统时间就是从服务器的RTC芯⽚中获取当前的硬件时间;2)在linux操作系统中,⼀旦修改系统时间后,⼜重启或关闭Linux系统,则OS通常会将系统时间更新到RTC;3)在操作系统再次启动的时候,Linux OS则会再次从RTC中获取当前的时间。
服务器异常下电后,待操作系统重新启动后,发现系统时间发⽣了跳变?其原因通常是:修改了操作系统时间,在服务器异常下电后,操作系统并未及时将修改后的时间更新到RTC,导致操作系统重新启动后,就会从RTC芯⽚中加载了之前“⽼”的时间,从⽽在操作系统层⾯体现为“时间跳变”⼆、关于jiffies⼀次中断时间间隔叫⼀个tick,即每个触发周期的时间叫做tick,⼀秒内时钟中断的次数(每个时间间隔就是⼀次)等于Hzhz别名就是tick rate(HZ)linux系统查看hz:[root@k3master ~]# cat /boot/config-`uname -r` | grep 'CONFIG_HZ='CONFIG_HZ=10001hz就是每秒1000次中断每次时钟中断处理程序即每发⽣⼀次tick都会增加jiffies该变量的值,jiffies⼀秒内增加的值也就是Hz(频率),⽐如:linux下默认是 1000次时钟中断次数/秒系统运⾏时间以秒为单位,换算⽅法等于jiffies/Hz。
时钟子系统理解
时钟子系统是Linux内核中用于管理时钟的子系统。
它提供了一组API,用于创建、管理和使用时钟。
时钟子系统对于Linux系统的正常运行至关重要,因为它用于跟踪系统时间、调度进程和计时器等。
一、时钟子系统的基本概念
时钟子系统有以下几个基本概念:
1.时钟:时钟是提供计时功能的硬件或软件模块。
2.时钟源:时钟源是提供时钟信号的硬件或软件模块。
常见的时钟源包括晶振、PLL等。
3.时钟中断:时钟中断是每隔一定时间由时钟硬件产生的中断。
4.时钟滴答:时钟滴答是时钟中断的最小单位。
5.时钟频率:时钟频率是指时钟滴答的速率。
二、时钟子系统的架构
时钟子系统由以下几个模块组成:
1.时钟源:时钟源模块提供时钟信号。
2.时钟控制器:时钟控制器模块负责管理时钟源和时钟中断。
3.时钟提供程序:时钟提供程序模块为用户空间应用程序提供API来访问时钟。
4.时钟用户:时钟用户模块使用时钟来跟踪系统时间、调度进程和计时器等。
三、时钟子系统的功能
时钟子系统提供以下功能:
1.创建时钟:时钟子系统可以创建各种类型的时钟,包括实时时钟、定时器等。
2.管理时钟:时钟子系统可以管理时钟的频率、状态等。
3.使用时钟:时钟子系统可以为用户空间应用程序提供API来访问时钟。
hrtimer及内核clock/timer子系统[嵌入式]发布时间:2010-06-30 09:54:39转过来,研究android过程中发现linux kernel内核定时器这块较以前2.4版本改动非常大,在网上收到这篇,粗看了下,但时间紧张没仔细看,等忙完android的这个分析,回头仔细看。
kernel-2.6.22中的arm arch加入了对dynticks, clocksource/event支持. 找了些kernelclock及timer子系统近来的变化, 总结一下.一般来说Soft-Timer (timer wheel / hrtimer)都是由Hardware-Timer(时钟中断之类)以及相关的clock source(e.g GPT in Soc)驱动,所以我打算先从clock这层开始介绍, 接着是soft-timer, kernel timekeeping,最后来看一些应用.Clock Sourceclock source定义了一个clock device的基本属性及行为, 这些clock device一般都有计数,定时, 产生中断能力, 比如GPT. 结构定义如下:struct clocksource {char *name;struct list_head list;int rating;cycle_t (*read)(void);cycle_t mask;u32 mult; /* cycle -> xtime interval, maybe two clock cy cle trigger oneinterrupt (one xtime interval) */u32 shift;unsigned long flags;cycle_t (*vread)(void);void (*resume)(void);/* timekeeping specific data, ignore */cycle_t cycle_interval; /* just the rate of GPT count p er OS HZ */u64 xtime_interval; /* xtime_interval = cycle_interv al * mult. */cycle_t cycle_last ____cacheline_aligned_in_smp; /* las t cycle in rate count */u64 xtime_nsec; /* cycle count, remain from _ns ec* now nsec rate count offset = xtime_nsec +* _nsec << shift */s64 error;};最重要的成员是read(), cycle_last和cycle_interval. 分别定义了读取clock device count寄存器当前计数值接口, 保存上一次周期计数值和每个tick周期间隔值. 这个结构内的值,无论是cycle_t, 还是u64类型(实际cycle_t就是u64)都是计数值(cycle), 而不是nsec,sec和jiffies. read()是整个kernel读取精确的单调时间计数的接口,kernel会用它来计算其他时间, 比如:jiffies, xtime.clocksource的引入, 解决了之前kernel各个arch都有自己的clock device的管理方式,基本都隐藏在MSL层, kernel core 及driver很难访问的问题. 它导出了以下接口:1) clocksource_register() 注册clocksource2) clocksource_get_next() 获取当前clocksource设备3) clocksource_read() 读取clock, 实际跑到clocksource->read()当driver处理的时间精度比较高的时, 可以通过上面的接口, 直接拿clock device来读.当然目前ticker时钟中断源也会以clocksource的形式存在.Clock EventClock event的主要作用是分发clock事件及设置下一次触发条件. 在没有clock event之前,时钟中断都是周期性地产生, 也就是熟知的jiffies和HZ.Clock Event device主要的结构:struct clock_event_device {const char *name;unsigned int features;unsigned long max_delta_ns;unsigned long min_delta_ns;unsigned long mult;int shift;int rating;int irq;cpumask_t cpumask;int (*set_next_event)(unsigned long evt,struct clock_event_device *);void (*set_mode)(enum clock_event_mode mode,struct clock_event_device *);void (*event_handler)(struct clock_event_devi ce *);void (*broadcast)(cpumask_t mask);struct list_head list;enum clock_event_mode mode;ktime_t next_event;};最重要的是set_next_event(), event_handler(). 前者是设置下一个clock事件的触发条件,一般就是往clock device里重设一下定时器. 后者是event handler, 事件处理函数.该处理函数会在时钟中断ISR里被调用. 如果这个clock用来做为ticker时钟,那么handler的执行和之前kernel的时钟中断ISR基本相同, 类似timer_tick().事件处理函数可以在运行时动态替换, 这就给kernel一个改变整个时钟中断处理方式的机会,也就给highres tick及dynamic tick一个动态挂载的机会.目前kernel内部有periodic/highres/dynamic tick三种时钟中断处理方式. 后面会介绍.hrtimer & timer wheel首先说一下timer wheel. 它就是kernel一直采用的基于jiffies的timer机制,接口包括init_timer(), mod_timer(), del_timer()等, 很熟悉把.hrtimer 的出现, 并没有抛弃老的timer wheel机制(也不太可能抛弃:)).hrtimer做为kernel里的timer定时器, 而timer wheel则主要用来做timeout定时器.分工比较明确. hrtimers采用红黑树来组织timers, 而timer wheel采用链表和桶.hrtimer精度由原来的timer wheel的jiffies提高到nanosecond.主要用于向应用层提供nanosleep, posix-timers和itimer接口,当然驱动和其他子系统也会需要high resolution的timer.kernel 里原先每秒周期性地产生HZ个ticker(中断),被在下一个过期的hrtimer的时间点上产生中断代替. 也就是说时钟中断不再是周期性的,而是由timer来驱动(靠clockevent的set_next_event接口设置下一个事件中断),只要没有hrtimer加载, 就没有中断. 但是为了保证系统时间(进程时间统计,jiffies的维护)更新, 每个tick_period(NSEC_PER_SEC/HZ,再次强调hrtimer精度是nsec)都会有一个叫做tick_sched_timer的hrtimer加载.接下来对比一下, hrtimer引入之前及之后, kernel里时钟中断的处理的不同. (这里都是基于armarch的source去分析)1)no hrtimerkernel 起来, setup_arch()之后的time_init()会去初始化相应machine结构下的timer.初始化timer函数都在各个machine的体系结构代码中, 初始化完硬件时钟, 注册中断服务函数,使能时钟中断. 中断服务程序会清中断, 调用timer_tick(), 它执行:1. profile_tick(); /* kernel profile, 不是很了解*/2. do_timer(1); /* 更新jiffies */3. update_process_times(); /* 计算进程耗时, 唤起TIMER_SOFTIRQ(timer wheel),重新计算调度时间片等等*/最后中断服务程序设置定时器, 使其在下一个tick产生中断.这样的框架, 使得high-res的timer很难加入. 所有中断处理code都在体系结构代码里被写死,并且代码重用率很低, 毕竟大多的arch都会写同样的中断处理函数.2)hrtimerkernel 里有了clockevent/source的引入, 就把clocksource的中断以一种事件的方式被抽象出来.事件本身的处理交给event handler. handler可以在kernel里做替换从而改变时钟中断的行为.时钟中断ISR会看上去象这样:static irqreturn_t timer_interrupt(int irq, void *dev_id) {/* clear timer interrupt flag */...../* call clock event handler */arch_clockevent.event_handler(&arch_clockevent);....return IRQ_HANDLED;}event_handler 在注册clockevent device时, 会被默认设置成tick_handle_periodic().所以kernel刚起来的时候, 时钟处理机制仍然是periodic的, ticker中断周期性的产生.tick_handle_periodic()会做和timer_tick差不多的事情,然后调用clockevents_program_event() =>arch_clockevent.set_next_event()去设置下一个周期的定时器.tick-common.c里把原来kernel时钟的处理方式在clockevent框架下实现了, 这就是periodictick的时钟机制.hres tick机制在第一个TIMER SOFTIRQ里会替换掉periodic tick, 当然要符合一定条件,比如command line里没有把hres(highres=off)禁止掉,clocksource/event支持hres和oneshot的能力. 这里的切换做的比较ugly,作者的comments也提到了, 每次timer softirq被调度,都要调用hrtimer_run_queues()检查一遍hres是否active,如果能在timer_init()里就把clocksource/event的条件check过, 直接切换到hres就最好了,不知道是不是有什么限制条件. TIMER SOFTIRQ代码如下:static void run_timer_softirq(struct softirq_action *h) {tvec_base_t *base = __get_cpu_var(tvec_bases);hrtimer_run_queues(); /* 有机会就切换到hres或者nohz */if (time_after_eq(jiffies, base->timer_jiffies)) __run_timers(base); /* timer wheel */}切换的过程比较简单, 用hrtimer_interrupt()替换当前clockevent hander, 加载一个hrtimer:tick_sched_timer在下一个tick_period过期, retrigger下一次事件.hrtimer_interrupt ()将过期的hrtimers从红黑树上摘下来,放到相应clock_base->cpu_base->cb_pending列表里,这些过期timers会在HRTIMER_SOFTIRQ里执行.然后根据剩余的最早过期的timer来retrigger下一个event, 再调度HRTIMER_SOFTIRQ. hrtimer softirq执行那些再cb_pending上的过期定时器函数.tick_sched_timer这个hrtimer在每个tick_period都会过期, 执行过程和timer_tick()差不多,只是在最后调用hrtimer_forward将自己加载到下一个周期里去,保证每个tick_period都能正确更新kernel内部时间统计.TimekeepingTimekeeping子系统负责更新xtime, 调整误差, 及提供get/settimeofday接口. 为了便于理解,首先介绍一些概念:Times in Kernelkernel的time基本类型:1) system timeA monotonically increasing value that represents the amount of time the system has been running. 单调增长的系统运行时间, 可以通过time source,xtime及wall_to_monotonic计算出来.2) wall timeA value representing the the human time of day, as seen on a wrist-watch.Realtime时间: xtime.3) time sourceA representation of a free running counter running at a known frequency, usually in hardware, e.g GPT. 可以通过clocksource->read()得到counter值4) tickA periodic interrupt generated by a hardware-timer, typically with a fixed interval defined by HZ: jiffies这些time之间互相关联, 互相可以转换.system_time = xtime + cyc2ns(clock->read() - clock->cycle_last) +wall_to_monotonic;real_time = xtime + cyc2ns(clock->read() - clock->cycle_last)也就是说real time是从1970年开始到现在的nanosecond, 而systemtime是系统启动到现在的nanosecond.这两个是最重要的时间, 由此hrtimer可以基于这两个time来设置过期时间. 所以引入两个clock base.Clock BaseCLOCK_REALTIME: base在实际的wall timeCLOCK_MONOTONIC: base在系统运行system timehrtimer可以选择其中之一, 来设置expire time, 可以是实际的时间, 也可以是相对系统的时间.他们提供get_time()接口:CLOCK_REALTIME 调用ktime_get_real()来获得真实时间,该函数用上面提到的等式计算出realtime.CLOCK_MONOTONIC 调用ktime_get(), 用system_time的等式获得monotonic time.timekeeping提供两个接口do_gettimeofday()/do_settimeofday(), 都是针对realtime操作. 用户空间对gettimeofday的syscall也会最终跑到这里来.do_gettimeofday()会调用__get_realtime_clock_ts()获得时间, 然后转成timeval.do_settimeofday(), 将用户设置的时间更新到xtime, 重新计算xtime到monotonic的转换值,最后通知hrtimers子系统时间变更.int do_settimeofday(struct timespec *tv){unsigned long flags;time_t wtm_sec, sec = tv->tv_sec;long wtm_nsec, nsec = tv->tv_nsec;if ((unsigned long)tv->tv_nsec >= NSEC_PER_SEC) return -EINVAL;write_seqlock_irqsave(&xtime_lock, flags);nsec -= __get_nsec_offset();wtm_sec = wall_to__sec + (_sec - se c);wtm_nsec = wall_to__nsec + (_nsec - nsec);set_normalized_timespec(&xtime, sec, nsec); /* 重新计算x time:用户设置的时间减去上一个周期到现在的nsec */set_normalized_timespec(&wall_to_monotonic, wtm_sec, w tm_nsec); /*重新调整wall_to_monotonic */clock->error = 0;ntp_clear();update_vsyscall(&xtime, clock);write_sequnlock_irqrestore(&xtime_lock, flags);/* signal hrtimers about time change */clock_was_set();return 0;}Userspace Applicationhrtimer的引入, 对用户最有用的接口如下:Clock APIclock_gettime(clockid_t, struct timespec *)获取对应clock的时间clock_settime(clockid_t, const struct timespec *)设置对应clock时间clock_nanosleep(clockid_t, int, const struct timespec *, struct timespec *)进程nano sleepclock_getres(clockid_t, struct timespec *)获取时间精度, 一般是nanosecclockid_t 定义了四种clock:CLOCK_REALTIMESystem-wide realtime clock. Setting this clock requires appropriate privileges. CLOCK_MONOTONICClock that cannot be set and represents monotonic time since some unspecified starting point.CLOCK_PROCESS_CPUTIME_IDHigh-resolution per-process timer from the CPU.CLOCK_THREAD_CPUTIME_IDThread-specific CPU-time clock.前两者前面提到了, 后两个是和进程/线程统计时间有关系, 还没有仔细研究过,是utime/stime之类的时间. 应用层可以利用这四种clock, 提高灵活性及精度.Timer APITimer 可以建立进程定时器,单次或者周期性定时。
Linux设备驱动程序学习(10)-时间、延迟及延缓操作Linux设备驱动程序学习(10)-时间、延迟及延缓操作度量时间差时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据HZ 值来设定,HZ 是一个体系依赖的值,在<linux/param.h>中定义或该文件包含的某个子平台相关文件中。
作为通用的规则,即便如果知道HZ 的值,在编程时应当不依赖这个特定值,而始终使用HZ。
对于当前版本,我们应完全信任内核开发者,他们已经选择了最适合的HZ值,最好保持HZ 的默认值。
对用户空间,内核HZ几乎完全隐藏,用户HZ 始终扩展为100。
当用户空间程序包含param.h,且每个报告给用户空间的计数器都做了相应转换。
对用户来说确切的HZ 值只能通过/proc/interrupts 获得:/proc/interrup ts 的计数值除以/proc/uptime 中报告的系统运行时间。
对于ARM体系结构:在<linux/param.h>文件中的定义如下:也就是说:HZ 由__KERNEL__和CONFIG_HZ决定。
若未定义__KERNEL__,H Z为100;否则为CONFIG_H Z。
而CONFIG_HZ是在内核的根目录的.config文件中定义,并没有在make menuconfig的配置选项中出现。
Linux的\arch\arm\configs\s3c2410_defconfig文件中的定义为:所以正常情况下s3c24x0的HZ为200。
这一数值在后面的实验中可以证实。
每次发生一个时钟中断,内核内部计数器的值就加一。
这个计数器在系统启动时初始化为0,因此它代表本次系统启动以来的时钟嘀哒数。
这个计数器是一个64-位变量( 即便在32-位的体系上)并且称为“jiffies_64”。
但是驱动通常访问jiffies 变量(unsigned long)(根据体系结构的不同:可能是jiffies_64 ,可能是jiffies_64 的低32位)。
中断处理流程linux中断处理浅析最近在研究异步消息处理, 突然想起linux内核的中断处理, ⾥⾯由始⾄终都贯穿着"重要的事马上做, 不重要的事推后做"的异步处理思想. 于是整理⼀下~第⼀阶段--获取中断号每个CPU都有响应中断的能⼒, 每个CPU响应中断时都⾛相同的流程. 这个流程就是内核提供的中断服务程序.在进⼊中断服务程序时, CPU已经⾃动禁⽌了本CPU上的中断响应, 因为CPU不能假定中断服务程序是可重⼊的.中断处理程序的第⼀步要做两件事情:1. 将中断号压⼊栈中; (不同中断号的中断对应不同的中断服务程序⼊⼝)2. 将当前寄存器信息压⼊栈中; (以便中断退出时恢复)显然, 这两步都是不可重⼊的(如果在保存寄存器值时被中断了, 那么另外的操作很可能就把寄存器给改写了, 现场将⽆法恢复), 所以前⾯说到的CPU进⼊中断服务程序时要⾃动禁⽌中断.栈上的信息被作为函数参数, 调⽤do_IRQ函数.第⼆阶段--中断串⾏化进⼊do_IRQ函数, 第⼀步进⾏中断的串⾏化处理, 将多个CPU同时产⽣的某⼀中断进⾏串⾏化.其⽅法是如果当前中断处于"执⾏"状态(表明另⼀个CPU正在处理相同的中断), 则重新设置它的"触发"标记, 然后⽴即返回. 正在处理同⼀中断的那个CPU完成⼀次处理后, 会再次检查"触发"标记, 如果设置, 则再次触发处理过程.于是, 中断的处理是⼀个循环过程, 每次循环调⽤handle_IRQ_event来处理中断.第三阶段--关中断条件下的中断处理进⼊handle_IRQ_event函数, 调⽤对应的内核或内核模块通过request_irq函数注册的中断处理函数.注册的中断处理函数有个中断开关属性, ⼀般情况下, 中断处理函数总是在关中断的情况下进⾏的. ⽽调⽤request_irq注册中断处理函数时也可以设置该中断处理函数在开中断的情况下进⾏,这种情况⽐较少见, 因为这要求中断处理代码必须是可重⼊的. (另外, 这⾥如果开中断, 正在处理的这个中断⼀般也是会被阻塞的. 因为正在处理某个中断的时候, 硬件中断控制器上的这个中断并未被ack, 硬件不会发起下⼀次相同的中断.)中断处理函数的过程可能会很长, 如果整个过程都在关中断的情况下进⾏, 那么后续的中断将被阻塞很长的时间.于是, 有了soft_irq. 把不可重⼊的⼀部分在中断处理程序中(关中断)去完成, 然后调⽤raise_softirq 设置⼀个软中断, 中断处理程序结束. 后⾯的⼯作将放在soft_irq⾥⾯去做.第四阶段--开中断条件下的软中断上⼀阶段循环调⽤完当前所有被触发的中断处理函数后, do_softirq函数被调⽤, 开始处理软件中断.在软中断机制中, 为每个CPU维护了⼀个若⼲位的掩码集, 每位掩码代表⼀个中断号. 在上⼀阶段的中断处理函数中, 调⽤raise_softirq设置了对应的软中断, 到了这⾥, 软中断对应的处理函数就会被调⽤(处理函数由open_softirq函数来注册).可以看出, 软中断与中断的模型很类似, 每个CPU有⼀组中断号, 中断有其对应的优先级, 每个CPU处理属于⾃⼰的中断. 最⼤的不同是开中断与关中断.于是, ⼀个中断处理过程被分成了两部分, 第⼀部分在中断处理函数⾥⾯关中断的进⾏, 第⼆部分在软中断处理函数⾥⾯开中断的进⾏.由于这⼀步是在开中断条件下进⾏的,这⾥还可能发⽣新的中断(中断嵌套),然后新中断对应的中断处理⼜将开始⼀个新的第⼀阶段~第三阶段。
关于Linux0.00中的时钟中断代码的⼀点理解在读Linux0.00 head.s的代码时,时钟中断这段代码折腾了我半天才弄懂,先贴上代码1 /*2 * head.s contains the 32-bit startup code.3 * Two L3 task multitasking. The code of tasks are in kernel area,4 * just like the Linux. The kernel code is located at 0x10000.5 */6 KRN_BASE = 0x100007 TSS0_SEL = 0x208 LDT0_SEL = 0x289 TSS1_SEL = 0X3010 LDT1_SEL = 0x381112 .text13startup_32:14 movl $0x10,%eax15mov %ax,%ds16mov %ax,%es17mov %ax,%fs18mov %ax,%gs19lss stack_ptr,%esp2021 # setup base fields of descriptors.22 movl $KRN_BASE, %ebx23 movl $gdt, %ecx24lea tss0, %eax25 movl $TSS0_SEL, %edi26call set_base27lea ldt0, %eax28 movl $LDT0_SEL, %edi29call set_base30lea tss1, %eax31 movl $TSS1_SEL, %edi32call set_base33lea ldt1, %eax34 movl $LDT1_SEL, %edi35call set_base3637call setup_idt38call setup_gdt39 movl $0x10,%eax # reload all the segment registers40mov %ax,%ds # after changing gdt.41mov %ax,%es42mov %ax,%fs43mov %ax,%gs44lss stack_ptr,%esp4546 # setup up timer 8253 chip.47 movb $0x36, %al48 movl $0x43, %edx49 outb %al, %dx50 movl $11930, %eax # timer frequency 100 HZ51 movl $0x40, %edx52 outb %al, %dx53 movb %ah, %al54 outb %al, %dx5556 # setup timer & system call interrupt descriptors.57 movl $0x00080000, %eax58 movw $timer_interrupt, %ax59 movw $0x8E00, %dx60 movl $0x20, %ecx61lea idt(,%ecx,8), %esi62 movl %eax,(%esi)63 movl %edx,4(%esi)64 movw $system_interrupt, %ax65 movw $0xef00, %dx66 movl $0x80, %ecx67lea idt(,%ecx,8), %esi68 movl %eax,(%esi)69 movl %edx,4(%esi)7071 # unmask the timer interrupt.72 movl $0x21, %edx73 inb %dx, %al74 andb $0xfe, %al75 outb %al, %dx77 # Move to user mode (task 0)78 pushfl79 andl $0xffffbfff, (%esp)80 popfl81 movl $TSS0_SEL, %eax82ltr %ax83 movl $LDT0_SEL, %eax84lldt %ax85 movl $0, current86sti87 pushl $0x1788 pushl $stack0_ptr89 pushfl /* ⼿动设置返回环境*/90 pushl $0x0f /**/91 pushl $task0 /**/92iret9394 /****************************************/95setup_gdt:96lgdt lgdt_opcode97ret9899setup_idt:100lea ignore_int,%edx101 movl $0x00080000,%eax102 movw %dx,%ax /* selector = 0x0008 = cs */103 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 104lea idt,%edi105mov $256,%ecx106rp_sidt:107 movl %eax,(%edi)108 movl %edx,4(%edi)109 addl $8,%edi110dec %ecx111jne rp_sidt112lidt lidt_opcode113ret114115 # in: %eax - logic addr; %ebx = base addr ;116 # %ecx - table addr; %edi - descriptors offset.117set_base:118 addl %ebx, %eax119 addl %ecx, %edi120 movw %ax, 2(%edi)121 rorl $16, %eax122 movb %al, 4(%edi)123 movb %ah, 7(%edi)124 rorl $16, %eax125ret126127write_char:128push %gs129 pushl %ebx130 pushl %eax131mov $0x18, %ebx132mov %bx, %gs133 movl scr_loc, %bx134shl $1, %ebx135 movb %al, %gs:(%ebx)136shr $1, %ebx137 incl %ebx138 cmpl $2000, %ebx139jb 1f140 movl $0, %ebx1411: movl %ebx, scr_loc142 popl %eax143 popl %ebx144pop %gs145ret146147 /***********************************************/148 /* This is the default interrupt "handler" :-) */149 .align 2150ignore_int:151push %ds152 pushl %eax153 movl $0x10, %eax154mov %ax, %ds155 movl $67, %eax /* print 'C' */156call write_char157 popl %eax158pop %ds159iret161 /* Timer interrupt handler */162 .align 2163timer_interrupt:164push %ds165 pushl %edx166 pushl %ecx167 pushl %ebx168 pushl %eax169 movl $0x10, %eax170mov %ax, %ds171 movb $0x20, %al172 outb %al, $0x20173 movl $1, %eax174 cmpl %eax, current175je 1f176 movl %eax, current177 ljmp $TSS1_SEL, $0178jmp 2f1791: movl $0, current180 ljmp $TSS0_SEL, $01812: popl %eax182 popl %ebx183 popl %ecx184 popl %edx185pop %ds186iret187188 /* system call handler */189 .align 2190system_interrupt:191push %ds192 pushl %edx193 pushl %ecx194 pushl %ebx195 pushl %eax196 movl $0x10, %edx197mov %dx, %ds198call write_char199 popl %eax200 popl %ebx201 popl %ecx202 popl %edx203pop %ds204iret205206 /*********************************************/207current:.long 0208scr_loc:.long 0209210 .align 2211 .word 0212lidt_opcode:213 .word 256*8-1 # idt contains 256 entries214 .long idt + KRN_BASE # This will be rewrite by code.215 .align 2216 .word 0217lgdt_opcode:218 .word (end_gdt-gdt)-1 # so does gdt219 .long gdt + KRN_BASE # This will be rewrite by code.220221 .align 3222idt: .fill 256,8,0 # idt is uninitialized223224gdt: .quad 0x0000000000000000 /* NULL descriptor */225 .quad 0x00c09a01000007ff /* 8Mb 0x08, base = 0x10000 */ 226 .quad 0x00c09201000007ff /* 8Mb 0x10 */227 .quad 0x00c0920b80000002 /* screen 0x18 - for display */ 228229 .quad 0x0000e90100000068 # TSS0 descr 0x20230 .quad 0x0000e20100000040 # LDT0 descr 0x28231 .quad 0x0000e90100000068 # TSS1 descr 0x30232 .quad 0x0000e20100000040 # LDT1 descr 0x38233end_gdt:234 .fill 128,4,0235stack_ptr:236 .long stack_ptr237 .word 0x10238239 /*************************************/240 .align 3241ldt0: .quad 0x0000000000000000242 .quad 0x00c0fa01000003ff # 0x0f, base = 0x10000243 .quad 0x00c0f201000003ff # 0x17245 .long 0 /* back link */246 .long stack0_krn_ptr, 0x10 /* esp0, ss0 */247 .long 0, 0 /* esp1, ss1 */248 .long 0, 0 /* esp2, ss2 */249 .long 0 /* cr3 */250 .long 0 /* eip */251 .long 0 /* eflags */252 .long 0, 0, 0, 0 /* eax, ecx, edx, ebx */253 .long 0, 0, 0, 0 /* esp, ebp, esi, edi */254 .long 0,0,0,0,0,0 /* es, cs, ss, ds, fs, gs */255 .long LDT0_SEL /* ldt */256 .long 0x8000000 /* trace bitmap */257258 .fill 128,4,0259stack0_krn_ptr:260 .long 0261262 /************************************/263 .align 3264ldt1: .quad 0x0000000000000000265 .quad 0x00c0fa01000003ff # 0x0f, base = 0x10000266 .quad 0x00c0f201000003ff # 0x17267tss1:268 .long 0 /* back link */269 .long stack1_krn_ptr, 0x10 /* esp0, ss0 */270 .long 0, 0 /* esp1, ss1 */271 .long 0, 0 /* esp2, ss2 */272 .long 0 /* cr3 */273 .long task1 /* eip */274 .long 0x200 /* eflags */275 .long 0, 0, 0, 0 /* eax, ecx, edx, ebx */276 .long stack1_ptr, 0, 0, 0 /* esp, ebp, esi, edi */277 .long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */278 .long LDT1_SEL /* ldt */279 .long 0x8000000 /* trace bitmap */280281 .fill 128,4,0282stack1_krn_ptr:283 .long 0284285 /************************************/286task0:287 movl $0x17, %eax288 movw %ax, %ds289 movl $65, %al /* print 'A' */290int $0x80291 movl $0xfff, %ecx2921: loop 1b293jmp task0294295 .fill 128,4,0296stack0_ptr:297 .long 0298299task1:300 movl $0x17, %eax301 movw %ax, %ds302 movl $66, %al /* print 'B' */303int $0x80304 movl $0xfff, %ecx3051: loop 1b306jmp task1307308 .fill 128,4,0309stack1_ptr:310 .long 0311312 /*** end ***/question:在代码的第177⾏(177 ljmp $TSS1_SEL, $0)和第180⾏(180 ljmp $TSS0_SEL, $0)可以看到,中断程序转移到对应的任务去执⾏了,后边的代码怎么执⾏?iret永远也执⾏不到了么?answer:实际上,程序在开始运⾏时,从第79⾏(79 andl $0xffffbfff, (%esp))可以看到标志寄存器的中断嵌套位NT被设置成了0,也就是说中断返回了esp0,ss0,ldt,trace bitmap设置了外,其余是0.嵌套任务标志NT(Nested Task)嵌套任务标志NT⽤来控制中断返回指令IRET的执⾏。
linux定时器实现原理Linux定时器是Linux操作系统中的一种机制,用于在指定的时间间隔内执行特定的任务或程序。
它是实现自动化任务和定时执行的重要工具之一。
本文将介绍Linux定时器的实现原理和使用方法。
一、Linux定时器的实现原理Linux定时器的实现原理主要基于操作系统的时钟中断机制。
当系统启动时,操作系统会初始化一个硬件时钟,并且设置一个固定的时间间隔,通常为几毫秒。
当时钟达到设定的时间间隔时,操作系统会触发一个时钟中断,即产生一个中断信号,通知操作系统进行相应的处理。
在Linux内核中,定时器是通过一个称为“定时器列表”的数据结构来实现的。
定时器列表是一个双向链表,用于存储所有的定时器对象。
每个定时器对象包含了定时器的属性和回调函数等信息。
当一个定时器被创建时,它会被加入到定时器列表中,并根据定时器的触发时间,在列表中找到合适的位置插入。
在每次时钟中断发生时,操作系统会遍历定时器列表,检查是否有定时器已经到达触发时间。
如果有定时器到达触发时间,操作系统将调用相应的回调函数执行任务或程序。
二、Linux定时器的使用方法在Linux中,可以使用多种方式来创建和使用定时器。
以下是使用Linux定时器的常见方法:1. 使用系统调用函数:Linux提供了系统调用函数(如timer_create、timer_settime等)来创建和设置定时器。
通过这些系统调用函数,可以设置定时器的触发时间、定时器的属性以及定时器到达触发时间时要执行的任务或程序。
2. 使用命令行工具:Linux还提供了一些命令行工具(如cron、at 等),可以通过命令行来创建和管理定时器。
通过这些命令行工具,可以设置定时器的触发时间、定时器的属性以及定时器到达触发时间时要执行的任务或程序。
3. 使用编程语言:除了系统调用函数和命令行工具,还可以使用编程语言来创建和使用定时器。
在C语言中,可以使用POSIX定时器库(如timer_create、timer_settime等函数)来实现定时器的功能。
Linux内核0.11体系结构——《Linux内核完全注释》笔记打卡0 总体介绍⼀个完整的操作系统主要由4部分组成:硬件、操作系统内核、操作系统服务和⽤户应⽤程序,如图0.1所⽰。
操作系统内核程序主要⽤于对硬件资源的抽象和访问调度。
图0.1 操作系统组成部分内核的主要作⽤是为了与计算机硬件进⾏交互,实现对硬件部件的编程控制和接⼝操作,调度对硬件资源的访问,并为计算机上的⽤户程序提供⼀个⾼级的执⾏环境和对硬件的虚拟接⼝。
1 Linux内核模式操作系统内核的结构模式主要可分为整体式的单内核模式和层次是的微内核模式。
Linux 0.11采⽤了单内核模式。
如图1.2所⽰,单内核操作系统所提供的服务流程为:应⽤主程序使⽤指定的参数值执⾏系统调⽤指令(int x80),使CPU从⽤户态切换到核⼼态,然后操作系统根据具体的参数值调⽤特定的系统调⽤服务程序,这些服务程序根据需要再调⽤底层的⼀些⽀持函数以完成特定的功能。
完成服务后,系统使CPU从核⼼态回到⽤户态,执⾏后续的指令。
图1.1 单内核模式的简单模型结构2 Linux内核系统体系结构Linux内核主要由5个模块构成,分别为:进程调度模块、内存管理模块、⽂件系统模块、进程间通信模块和⽹络接⼝模块。
模块之间的依赖关系如图2.1所⽰,虚线部分表⽰0.11版本内核中未实现部分(所有的模块都与进程调度模块存在依赖关系)。
图2.1 Linux内核系统模块结构及相互依赖关系从单内核模式结构模型出发,Linux 0.11内核源代码的结构将内核主要模块分配如图2.2所⽰。
(除了硬件控制⽅框,其他粗线分别对应内核源代码的⽬录组织结构)图2.2 内核结构框图3 Linux内核对内存的管理和使⽤对于机器中的物理内存,Linux 0.11内核中,系统初始化阶段将其划分的功能区域如图3.1所⽰。
图3.1 物理内存使⽤的功能分布图虚拟地址:(virtual address)由程序产⽣的由段选择符合段内偏移地址两个部分组成的地址。
时钟中断调度的原理
时钟中断调度是操作系统中的一种调度方式,它基于中断的机制实现。
当时钟中断发生时,操作系统会暂停当前任务的执行,切换到另一个任务,以实现多任务并发执行。
时钟中断调度的原理如下:
1. 定时中断:操作系统会设置一个时钟中断定时器,它会在固定的时间间隔内产生一个时钟中断。
这个时间间隔通常被称为时间片(time slice),一般设置为几毫秒或十几毫秒。
2. 中断处理程序:当时钟中断发生时,CPU会立即跳转到操作系统的中断处理程序。
中断处理程序是操作系统内核的一部分,它会执行一系列的操作,包括保存当前执行任务的上下文,切换到另一个任务的上下文等。
3. 任务切换:在中断处理程序中,操作系统会选择一个新的任务来执行。
这个选择可以基于各种调度算法,例如轮转调度、优先级调度等。
4. 上下文切换:在中断处理程序中,操作系统会保存当前任务的上下文,包括寄存器的值、栈指针等,然后加载新任务的上下文,将控制权交给新的任务。
这样,新任务就开始执行了。
5. 恢复执行:当操作系统完成任务切换后,它会返回到中断发生前的程序继续执行。
这样,原来的任务就被暂停了,而新的任务开始运行。
通过时钟中断调度,操作系统能够以很短的时间片轮转方式,实现多任务并发执行。
每个任务都能够获得一定的执行时间,从而提高系统的吞吐量和响应速度。
第七章 Linux内核的时钟中断(By 詹荣开,NUDT)Copyright © 2003 by 詹荣开E-mail:***************Linux-2.4.0Version 1.0.0,2003-2-14摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。
本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、时钟、定时器申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。
发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。
更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。
如果还没有,写信给:The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA欢迎各位指出文档中的错误与疑问。
前言时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。
一般说来,内核主要需要两种类型的时间:1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。
2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。
这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。
linux中断源的分类Linux中断源的分类Linux是一种开源操作系统,它的设计目标是为了在各种硬件平台上运行。
在Linux中,中断是一种重要的机制,它用于处理硬件设备的异步事件。
中断源是触发中断的事件或条件,根据中断源的不同特性,我们可以将Linux中断源分为以下几类。
1. 硬件中断源:硬件中断源是指由硬件设备产生的中断。
例如,当键盘按下某个键时,键盘控制器将产生一个硬件中断,通知操作系统有键盘事件发生。
其他常见的硬件中断源包括鼠标、硬盘、网卡等设备。
硬件中断源的触发通常是由硬件设备本身引起的,操作系统需要及时响应并处理这些中断。
2. 软件中断源:软件中断源是指由软件产生的中断。
在Linux中,软件中断源主要有两种:一种是由操作系统内核产生的中断,例如时钟中断、定时器中断等;另一种是由用户程序产生的中断,例如系统调用、信号等。
软件中断源的触发是由软件主动发起的,它可以用于实现各种功能和服务,如进程调度、定时任务等。
3. 异常中断源:异常中断源是指由异常事件引起的中断。
异常是指在程序执行过程中发生的一些非正常事件,例如除零错误、缺页错误等。
当这些异常事件发生时,操作系统会产生一个异常中断,通常会导致程序终止或进行相应的错误处理。
异常中断源的触发是由程序执行过程中的错误引起的。
4. 外部中断源:外部中断源是指由外部事件引起的中断。
在Linux中,外部中断源通常是指由外部设备或外部信号引起的中断。
例如,当有新的网络数据包到达时,网卡会产生一个外部中断,通知操作系统有数据可读。
外部中断源的触发是由外部事件的发生引起的,操作系统需要及时响应并处理这些中断。
5. 虚拟中断源:虚拟中断源是指由虚拟化技术引起的中断。
在虚拟化环境中,虚拟机监视器(VMM)负责管理和分配物理资源,并模拟硬件设备给虚拟机使用。
当虚拟机运行时,VMM会将一部分中断源模拟给虚拟机,例如时钟中断、网络中断等。
虚拟中断源的触发是由虚拟化环境中的事件引起的,VMM需要及时将这些中断传递给相应的虚拟机。
Linux之时钟中断详解⽬录时钟中断的产⽣Linux实现时钟中断的全过程1.可编程定时/计数器的初始化2.与时钟中断相关的函数3.系统调⽤返回函数:总结在Linux的0号中断是⼀个定时器中断。
在固定的时间间隔都发⽣⼀次中断,也是说每秒发⽣该中断的频率都是固定的。
该频率是常量HZ,该值⼀般是在100 ~ 1000之间。
该中断的作⽤是为了定时更新系统⽇期和时间,使系统时间不断地得到跳转。
另外该中断的中断处理函数除了更新系统时间外,还需要更新本地CPU统计数。
指的是调⽤scheduler_tick递减进程的时间⽚,若进程的时间⽚递减到0,进程则被调度出去⽽放弃CPU使⽤权。
时钟中断的产⽣Linux的OS时钟的物理产⽣原因是可编程定时/计数器产⽣的输出脉冲,这个脉冲送⼊CPU,就可以引发⼀个中断请求信号,我们就把它叫做时钟中断。
“时钟中断”是特别重要的⼀个中断,因为整个操作系统的活动都受到它的激励。
系统利⽤时钟中断维持系统时间、促使环境的切换,以保证所有进程共享CPU;利⽤时钟中断进⾏记帐、监督系统⼯作以及确定未来的调度优先级等⼯作。
可以说,“时钟中断”是整个操作系统的脉搏。
时钟中断的物理产⽣如图所⽰:操作系统对可编程定时/计数器进⾏有关初始化,然后定时/计数器就对输⼊脉冲进⾏计数(分频),产⽣的三个输出脉冲Out0、Out1、Out2各有⽤途,很多接⼝书都介绍了这个问题,我们只看Out0上的输出脉冲,这个脉冲信号接到中断控制器8259A_1的0号管脚,触发⼀个周期性的中断,我们就把这个中断叫做时钟中断,时钟中断的周期,也就是脉冲信号的周期,我们叫做“滴答”或“时标”(tick)。
从本质上说,时钟中断只是⼀个周期性的信号,完全是硬件⾏为,该信号触发CPU去执⾏⼀个中断服务程序,但是为了⽅便,我们就把这个服务程序叫做时钟中断。
Linux实现时钟中断的全过程1.可编程定时/计数器的初始化IBM PC中使⽤的是8253或8254芯⽚。
Linux内核中断处理的irq_thread机制是一种将中断处理任务分配给单独线程的方法,以提高Linux内核中断处理的irq_thread机制是一种将中断处理任务分配给单独线程的方法,以提高系统的性能和响应速度。
在传统的中断处理模型中,中断处理程序(IRQ handler)是在中断发生时由内核直接调用的,这会导致CPU上下文切换,从而影响系统性能。
而使用irq_thread机制,可以将中断处理任务分配给一个专门的线程来执行,从而减少上下文切换的次数,提高系统的响应速度。
irq_thread机制的主要步骤如下:
1. 当中断发生时,内核首先将中断处理程序标记为可运行状态。
2. 然后,内核启动一个新的线程来执行这个中断处理程序。
这个线程通常被称为irq_thread。
3. irq_thread线程在执行过程中会调用do_IRQ()函数来处理实际的中断事件。
4. do_IRQ()函数会根据中断类型调用相应的硬件抽象层(HAL)中的处理函数,以完成对硬件设备的操作。
5. 最后,irq_thread线程会返回到用户空间,继续执行之前的程序。
通过使用irq_thread机制,Linux内核可以在多个处理器核心之间实现高效的中断处理,从而提高系统的整体性能。
同时,这种机制还可以简化中断处理程序的设计,使其更加模块化和易于维护。
如果说cfs是linux的一个很有创意的机制的话,那么linux中另一个创意就是nohz,我在前面已经写了好几篇关于nohz的文章了,因此本文就不再阐述代码细节了,linux的创意在于设计而不在代码,代码主要解决的问题是实用性,就像gcc一样,就是一个编译器,应用编译原理设计而出,它内部却充实着编译原理之外的巧妙。
有血有肉才活得精彩,如果说nohz之前的linux内核是骨架的话,那么从 nohz之后,linux开始了精彩,之后几乎瞬时,cfs出现了,然后是cgroup...cgroup正式开始了虚拟容器,从此linux再也不用被 unix老大们看作是小孩子了,nohz标志着linux开始成熟起来。
nohz为何这么重要呢?因为它直接关系到了性能,直接联系着系统的心跳,在之前,系统总是被动的接受时钟中断,然后运行中断处理程序最终可能导致调度的发生,如果实在没有任务可以运行,那么就执行idle,这也许也算一种创意,可是时钟中断还是会周期性的打破idle,然后查询有没有需要做的事情,如果没有继续idle,这种方式没有什么问题,可是我们总是希望系统可以主动的做些事情,比如不是被动的接受中断而是主动的设置什么时候中断,因此必须将系统时钟发生中断这件事进行向上抽象,于是相应的clocksource和 clock_event_device,这两个结构体就是时钟以及时钟行为的抽象,clocksource代表了一个时钟源,一般都会有一个计数器,其中的read回调函数就是负责读出其计数器的值,可是我们为何找不到write或者set之类的回调函数呢?这些回调函数其实不应该在closksource中,而应该在clock_event_device中,实际上,clockevent只是一个钟,你可以类比我们用的钟表,clocksource就是一个钟表,我们需要一个钟表就是需要读出它的指针的值从而知道现在几点,就是这些,因此钟表都会有显示盘用于读数,至于钟表怎么运作,那就是钟表内部的机械原理了,记住,钟表就是用来读数的,另外我们为了害怕误事而需要闹铃,需要的是闹铃在一个时间段之后把我们唤醒,这是个事情,而这个事情不一定非要有钟表,当然钟表的读数会为我们提供有用的参考值,这样的话钟表和闹铃就解耦合了,再重申一遍,不要因为有闹钟的存在就说钟表都会响铃或者说闹铃都有钟表,它们其实是两个东西,钟表为你展示某些事情,而闹铃需要你的设置,设想一个场景,你手边有一个没有闹铃的钟表,还有一个没有钟表的闹铃,这个闹铃只能设置绝对时间,然后到期振铃,你现在不知道几点,可是你要睡觉并且得到通知必须在四个小时后去参加一个聚会,那么你现在要做什么?你肯定要看看你的钟表,然后设置你的闹钟。
Linux内核的时钟中断机制opyright © 2003 by 詹荣开E-mail:zhanrk@Linux-2.4.0Version 1.0.0,2003-2-14摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。
本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、时钟、定时器申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。
发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。
更详细的情况请参阅GNU 通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。
如果还没有,写信给:The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA欢迎各位指出文档中的错误与疑问。
前言时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。
一般说来,内核主要需要两种类型的时间:(1)、在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。
(2)、维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。
这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。
1、 时钟硬件1.1 实时时钟RTC自从IBM PC AT起,所有的PC机就都包含了一个叫做实时时钟(RTC)的时钟芯片,以便在PC机断电后仍然能够继续保持时间。
显然,RTC是通过主板上的电池来供电的,而不是通过PC机电源来供电的,因此当PC机关掉电源后,RTC仍然会继续工作。
通常,CMOS RAM和RTC被集成到一块芯片上,因此RTC也称作“CMOS Timer”。
最常见的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的扩展。
本节内容主要基于MC146818这一标准的RTC芯片。
具体内容可以参考MC146818的Datasheet。
1.1.1 RTC寄存器MC146818 RTC芯片一共有64个寄存器。
它们的芯片内部地址编号为0x00~0x3F(不是I/O端口地址),这些寄存器一共可以分为三组:(1)时钟与日历寄存器组:共有10个(0x00~0x09),表示时间、日历的具体信息。
在PC 机中,这些寄存器中的值都是以BCD格式来存储的(比如23dec=0x23BCD)。
(2)状态和控制寄存器组:共有4个(0x0A~0x0D),控制RTC芯片的工作方式,并表示当前的状态。
(3)CMOS配置数据:通用的CMOS RAM,它们与时间无关,因此我们不关心它。
时钟与日历寄存器组的详细解释如下:Address Function00 Current second for RTC01 Alarm second02 Current minute03 Alarm minute04 Current hour05 Alarm hour06 Current day of week(01=Sunday)07 Current date of month08 Current month09 Current year(final two digits,eg:93)状态寄存器A(地址0x0A)的格式如下:其中:(1)bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。
(2)bit[6:4]——这三位是“除法器控制位”(divider-control bits),用来定义RTC的操作频率。
各种可能的值如下:Divider bits Time-base frequency Divider Reset Operation ModeDV2 DV1 DV00 0 0 4.194304 MHZ NO YES0 0 1 1.048576 MHZ NO YES0 1 0 32.769 KHZ NO YES1 1 0/1 任何 YES NOPC机通常将Divider bits设置成“010”。
(3)bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS bits 4.194304或1.048578 MHZ 32.768 KHZRS3 RS2 RS1 RS0 周期性中断方波周期性中断方波0 0 0 0 None None None None0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ0 0 1 0 61.035μs 16.384 KHZ0 0 1 1 122.070μs 8.192KHZ0 1 0 0 244.141μs 4.096KHZ0 1 0 1 488.281μs 2.048KHZ0 1 1 0 976.562μs 1.024KHZ0 1 1 1 1.953125ms 512HZ1 0 0 0 3.90625ms 256HZ1 0 0 1 7.8125ms 128HZ1 0 1 0 15.625ms 64HZ1 0 1 1 31.25ms 32HZ1 1 0 0 62.5ms 16HZ1 1 0 1 125ms 8HZ1 1 1 0 250ms 4HZ1 1 1 1 500ms 2HZPC机BIOS对其默认的设置值是“0110”。
状态寄存器B的格式如下所示:各位的含义如下:(1)bit[7]——SET标志。
为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。
为0表示将允许更新过程继续。
(2)bit[6]——PIE标志,周期性中断使能标志。
(3)bit[5]——AIE标志,告警中断使能标志。
(4)bit[4]——UIE标志,更新结束中断使能标志。
(5)bit[3]——SQWE标志,方波信号使能标志。
(6)bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。
BIOS总是将它设置为0。
(7)bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。
PC机BIOS总是将它设置为1。
(8)bit[0]——DSE标志。
BIOS总是将它设置为0。
状态寄存器C的格式如下:(1)bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求发生。
(2)bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
(3)bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
(4)bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。
状态寄存器D的格式如下:(1)bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC已经掉电。
(2)bit[6:0]——总是为0,未定义。
1.1.2 通过I/O端口访问RTC在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。
其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端口。
读RTC芯片寄存器的步骤是:mov al, addrout 70h, al ; Select reg_addr in RTC chipjmp $+2 ; a slight delay to settle thingin al, 71h ;写RTC寄存器的步骤如下:mov al, addrout 70h, al ; Select reg_addr in RTC chipjmp $+2 ; a slight delay to settle thingmov al, valueout 71h, al1.2 可编程间隔定时器PIT每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。
当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:(1)通道0用来负责更新系统时钟。
每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2)通道1通常用于控制DMAC对RAM的刷新。
(3)通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-cycle。
每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。
因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。
当各通道的计数器减到0时,我们就说该通道处于“Terminal count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生18.2次时钟中断。
1.2.1 PIT的I/O端口在i386平台上,8254芯片的各寄存器的I/O端口地址如下:Port Description40h Channel 0 counter(read/write)41h Channel 1 counter(read/write)42h Channel 2 counter(read/write)43h PIT control word(write only)其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控制寄存器来决定。
8254 PIT的控制寄存器的格式如下:(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。