CPU内部有一个RTC,会在上电的时候调用mktime函数算出从1970年1月1日0时开始到当前开机点所过的秒数
long kernel_mktime (struct tm *tm)
{
long res;
int year;
year = tm->tm_year - 70; // 从70 年到现在经过的年数(2 位表示方式),
// 因此会有2000 年问题。
/* magic offsets (y+1) needed to get leapyears right. */
/* 为了获得正确的闰年数,这里需要这样一个魔幻偏值(y+1) */
res = YEAR * year + DAY * ((year + 1) / 4); // 这些年经过的秒数时间 + 每个闰年时多1 天
res += month[tm->tm_mon]; // 的秒数时间,在加上当年到当月时的秒数。
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
/* 以及(y+2)。如果(y+2)不是闰年,那么我们就必须进行调整(减去一天的秒数时间)。 */
if (tm->tm_mon > 1 && ((year + 2) % 4))
res -= DAY;
res += DAY * (tm->tm_mday - 1); // 再加上本月过去的天数的秒数时间。
res += HOUR * tm->tm_hour; // 再加上当天过去的小时数的秒数时间。
res += MINUTE * tm->tm_min; // 再加上1 小时内过去的分钟数的秒数时间。
res += tm->tm_sec; // 再加上1 分钟内已过的秒数。
return res; // 即等于从1970 年以来经过的秒数时间。
}
而给mktime函数传来的时间结构体的赋值是由初始化时从RTC(CMOS
)中读出的
// 该子程序取CMOS 时钟,并设置开机时间 startup_time(为从1970-1-1-0 时起到开机时的秒数)。
static void time_init(void)
{
struct tm time;
do {// 参见后面CMOS 内存列表。
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--;
startup_time = kernel_mktime(&time);
}
可以看一下里面还有一个do while循环,是怕没有读到当前的值。
从main.c文件里进行的初始化。可以看到通过CMOS进行赋值的。这个就和硬件有关系了,咱也不懂,就当它能知道当前的时间吧。
所以这时就可以把时间存入全局变量中,会为JIFFIES所用
JIFFIES是一个系统的时钟滴答,一个系统滴答是10ms,也是一个定时器。
10ms一个系统滴答---->每隔10ms会引发一个定时器中断
而这个中断服务函数(timer_interrupt
)中,首先进行了jiffies的自加
_timer_interrupt:
push ds ;// save ds,es and put kernel data space
push es ;// into them. %fs is used by _system_call
push fs
push edx ;// we save %eax,%ecx,%edx as gcc doesn't
push ecx ;// save those across function calls. %ebx
push ebx ;// is saved as we use that in ret_sys_call
push eax
mov eax,10h ;// ds,es 置为指向内核数据段。
mov ds,ax
mov es,ax
mov eax,17h ;// fs 置为指向局部数据段(出错程序的数据段)。
mov fs,ax
inc dword ptr _jiffies //jiffies自加
;// 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
mov al,20h ;// EOI to interrupt controller ;//1
out 20h,al ;// 操作命令字OCW2 送0x20 端口。
;// 下面3 句从选择符中取出当前特权级别(0 或3)并压入堆栈,作为do_timer 的参数。
mov eax,dword ptr [R_CS+esp]
and eax,3 ;// %eax is CPL (0 or 3, 0=supervisor)
push eax
;// do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现。
call _do_timer ;// 'do_timer(long CPL)' does everything from
add esp,4 ;// task switching to accounting ...
jmp ret_from_sys_call
就是中断程序的正常流程,自加之后会调用一个do_timer函数
// 参数cpl 是当前特权级0 或3,0 表示内核代码在执行。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
void do_timer (long cpl)
{
extern int beepcount; // 扬声器发声时间滴答数(kernel/chr_drv/console.c,697)
extern void sysbeepstop (void); // 关闭扬声器(kernel/chr_drv/console.c,691)
// 如果发声计数次数到,则关闭发声。(向0x61 口发送命令,复位位0 和1。位0 控制8253
// 计数器2 的工作,位1 控制扬声器)。
if (beepcount)
if (!--beepcount)
sysbeepstop ();
// 如果当前特权级(cpl)为0(最高,表示是内核程序在工作),则将超级用户运行时间stime 递增;
// 如果cpl > 0,则表示是一般用户程序在工作,增加utime。
if (cpl)
current->utime++;
else
current->stime++;
// 如果有用户的定时器存在,则将链表第1 个定时器的值减1。如果已等于0,则调用相应的处理
// 程序,并将该处理程序指针置为空。然后去掉该项定时器。
if (next_timer)
{ // next_timer 是定时器链表的头指针(见270 行)。
next_timer->jiffies--;
while (next_timer && next_timer->jiffies <= 0)
{
void (*fn) (); // 这里插入了一个函数指针定义!!!??
fn = next_timer->fn;
next_timer->fn = NULL;
next_timer = next_timer->next;
(fn) (); // 调用处理函数。
}
}
// 如果当前软盘控制器FDC 的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序(245 行)。
if (current_DOR & 0xf0)
do_floppy_timer ();
if ((--current->counter) > 0)
return; // 如果进程运行时间还没完,则退出。
current->counter = 0;
if (!cpl)
return; // 对于超级用户程序,不依赖counter 值进行调度。
schedule ();
}
CPL变量就是内核中用来指示被中断程序的特权,也就是0表示内核进程,3表示用户进程。
current就是指向当前进程数据结构的指针,指向task_struct
所以utime就是用户程序的运行时间,stime是内核程序的运行时间
而这个next_timer就是一个时间链表的指针,也就是个定时器链表的指针
// 定时器链表结构和定时器数组。
static struct timer_list
{
long jiffies; // 定时滴答数。
void (*fn) (); // 定时处理程序。
struct timer_list *next; // 下一个定时器。
}
timer_list[TIME_REQUESTS], *next_timer = NULL;
通过这个数据结构再看上面的程序代码,就可以知道,其实上面那段,就是在进行定时器中断的时候,会调用这个do_timer函数,而这个函数里面就会遍历判断这个定时器链表有没有当前定时器为0的,如果为0则证明到定的时间了,就会执行这个next_timer的fn,这个定时处理的函数。
而下面的current->counter
current上面提到了是执行进程的指针,而这个counter就是我们常听到的时间片。
如果时间片--后大于0,就直接返回,就是此进程还有时间片
CPU调度的,后面学了再写吧。
了解了这个系统滴答,和这个中断函数do_timer之后,就可以搞懂很多东西了,比如说异步的操作,可能是我太笨了,一开始对异步这种东西,完全没一点头绪,知道它能干什么,但是不知道它是怎么来了,后来再我看了点redis源码后,发现redis里面有一个时间事件time event这个东西,才明白异步就是通过定时器来完成的
而学习了这个linux内核的系统滴答后,才真正明白了这个操作是怎么完成的。
本文详细解析了Linux内核中的时间管理和系统滴答原理,包括mktime函数如何计算从1970年以来的秒数,RTC如何初始化以及Jiffies如何用于系统定时。此外,还深入探讨了定时器中断处理函数do_timer的工作流程。
443

被折叠的 条评论
为什么被折叠?



