您的位置:首页 > 运维架构 > Linux

Linux内核源码分析--系统时间初始化(kernel_mktime()函数)

2015-01-19 23:14 645 查看
从boot文件中的几个汇编程序执行后跳转到init文件中的main.c程序开始继续执行,该main.c函数式为系统运行的环境进行初始化的。首先来看系统时间的初始化(因为系统时间的初始化开始程序就在init文件中),其中主要还是由kernel中的mktime.c程序中的kernel_mktime函数计算时间的。

先看下time_init(void)函数及相关结构是怎么实现的:

static void time_init(void)
{
	struct tm time;// 时间结构体在Linux/include/Time.h中有定义

	do {
		time.tm_sec = CMOS_READ(0); // 这句和下面几句是通过宏函数分别获取COMS中的秒、分、时、日、月、年
		time.tm_min = CMOS_READ(2); // CMOS_READ()宏函数见下面定义
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);
// 下面是:又获取一次秒数,检查和开始获取的是否一致,保证获取时间从秒到年之间最多相差1秒;
// 如果多于1秒,则会循环再次获取所有时间日期,直到获取时间差在1秒范围内;
} while (time.tm_sec != CMOS_READ(0));
          BCD_TO_BIN(time.tm_sec);// 由于CMOS中的系统时间是由BCD码记录的,所以获取数据后要把BCD码转换成二进制数
          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);
// 记录的年份是从1~12(没人听说过有0月份或者大于12月份的),但是在函数中计算月份的时候是按照0~11来分别表示12个月的
// 比如 3月3日,其实只过去了2个月(1月和2月),计算时就用2表示。所以time.tm_mon--只是为了后面函数中好计算
   time.tm_mon--;
// startup_time表示从1970-01-01到开机时间为止过去了多少秒
	startup_time = kernel_mktime(&time);
}


从CMOS读取系统时间,其实对于CMOS操作可以看我的另外一篇blog:/article/1328203.html,但具体来对CMOS操作还是比较简单的。因为某些芯片中有多个字节来表示对CMOS的各种,所以通过向某个特定端口写入要访问的字节偏移位置,然后通过另外一个特定端口进行对该字节位置进行数据操作;而CMOS就是使用了0x70和0x71两对端口来控制时间日期的操作,首先向0x70端口写入希望操作的数据所在的字节偏移地址,然后读取其中的数据或者,向其中写入数据,端口的操作是in和out两个命令来完成的。in表示从端口往寄存器中写入(读取数据时),out表示从寄存器向端口写入数据(确定需要操作的数据所在的偏移地址和写入数据时)。

下面的CMOS_READ()宏函数仅仅是调用了确定操作字节位置宏函数和读取数据操作宏函数:

#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); //可以不用把偏移地址和0x80进行或操作\
inb_p(0x71); \
})

下面是对CMOS的两组具体操作,如果了解C代码内嵌汇编的应该很好理解下面的几个函数,只简单分析前两个;第一个:value代表想操作的数据所在的偏移地址(实在不清楚的可以查看下CMOS 的64字节信息简表),port代表端口号0x70, 汇编操作表示把value赋值给al, port赋值给dx,然后把al的值写入port中,以确定接下来要访问的字节位置;第二个:只有port,也就是端口号0x71,里面定义了个char变量,并且把这个变量传递给al,同理让dx(代表了port端口号)把其中的数据写入到变量中;

这里要提一个很重要的问题:读取的数据用al来存储,说明数据宽度为1字节(8位),其实也正是这样的,可以参考CMOS 64字节信息简表。那么问题就来了,年份也是用8位来表示的,而且这8位还不是二进制数,而是BCD码(前面已经提过CMOS中的数据时用BCD码记录的),再根据BCD码和十进制数的关系(1个十进制数用4个BCD码来表示,详情请看:),所以这8位BCD码只能表示两位十进制数,也就是年份也是用两位数来表示,即是:1988年
表示为88,1996年表示为96。所以就出现了之前2000年的Y2K,千年虫之说,2000年表示00(还有的是1999年表示为99,而99是很多系统规定的特殊意义的字符串,比如:自动删除标识字符串),很多系统是识别不了的,导致很多电力系统、金融系统、政府部门等瘫痪。这个在后面kernel_kmtime函数中也会用到。

最后一组宏函数其实是很前一组的功能一样的,都是先确定操作字节的偏移位置,然后对该位置上的数据进行操作,唯一不同的是,后面一组宏函数使用了两个空跳转,其主要是让CPU延时(大概是14~20时钟周期)。

#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))

#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
_v; \
})

#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
		"\tjmp 1f\n" \
		"1:\tjmp 1f\n" \
		"1:"::"a" (value),"d" (port))

#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
	"\tjmp 1f\n" \
	"1:\tjmp 1f\n" \
	"1:":"=a" (_v):"d" (port)); \
_v; \
})
最后来看下kernel中MKtime.c程序中的kernel_mktime函数,这个函数的主要功能是计算从1970年1月1日开始到现在所经过的秒数。为什么要选择1970年1月1日,因为unix和C语言的生日是1970年1月1日,所以Linux继承了unix的时间计算起点(很多其他系统也是以1970年1月1日作为系统时间起点的,比如一些数据库软件)。其实这个函数个人感觉是有点问题的(也有可能是自己理解错了)

long kernel_mktime(struct tm * tm)
{
	long res;
	int year;
// 1970年1月1日unix和c语言的生日
// COMS年份取最后两位,比如1990年,tm_year为90
	year = tm->tm_year - 70;
/* magic offsets (y+1) needed to get leapyears right.*/
// 计算时间的方法是先计算年份再计算零头日期
// 比如1990年9月9日,则先计算1990到1970年时间秒,然后加上9月9日时间秒
// 后面的DAY*((year+1)/4)表示的是从70年到当前年有多少个闰年,每个闰年加一天
// 润年的计算方法:不以00(百年,如:2000、2100)结尾的年份能被4整除但不能被100整除,
// 若以00结尾的百年号,则能整除400的才是润年
	res = YEAR*year + DAY*((year+1)/4);// 这里应该是每四年一次润年,为什么加1,后面会具体分析下
	res += month[tm->tm_mon];// 加上当年的月份,这里定义的二月都为润月 29天
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
	if (tm->tm_mon>1 && ((year+2)%4)) // 对非润年的天数进行调整
		res -= DAY;
	res += DAY*(tm->tm_mday-1);// 天数,-1 表示不包括当天,当天的零头另外算
	res += HOUR*tm->tm_hour;// 小时 
	res += MINUTE*tm->tm_min;// 分钟
	res += tm->tm_sec;// 秒数
	return res;
}

该函数难理解的以及重点的就是那三条语句:

1、res = YEAR*year + DAY*((year+1)/4); 前面的YEAR*year表示的从70年到现在多少年,然后化成秒数;后面的DAY*((year+1)/4),我理解的是表示四年一润,每四年中就有一年是润年。为什么要加1呢,到最后分析下;

2、res += month[tm->tm_mon]; 这个好理解就是现在是几月份,然后就加上今年的1月份到该月份的天数,化作时间秒。这个不是用零头算法,而是在前面已经把月份减一了(time.tm_mon--),所以可以直接计算了。但这语句重要一点的是二月份为29天,不管当年是不是润年,二月份都为29天。

3、if (tm->tm_mon>1 && ((year+2)%4)) res -= DAY;这个是调整非润年在语句2中是否多加了1天。这里有2种情况,第一:润年时,这条语句不执行,所以当前月份就是润年月份,也就是语句2中润年月份。第二种:非润年时,如果月份已经超过2月份了,那么就要减去在语句2中多算的一天。如果没有超过2月份则不执行该语句;

下面来分析语句1中为什么要+1,这是由润年规律和1970年这两个数值决定的。

还是举例来说明下:1970年、1971、1972、1973、1974、1975、1976,我们首先得知道其中1972年和1976年是润年,那么接下来开始分析每个年份时间;

1970年和1971年分析:由year = tm->tm_year - 70;可以得出年数不大于4年,那么语句1中不会增加1天,而语句2和语句3会相互调整,因为是非润年语句3中条件:(year+2)%4得到满足,那么若月份超过2月,则在语句2中会多加上1天(因为当年是非润年,但二月份的计算的是润年月 29天);但是别担心,语句3中的第二个条件:tm->tm_mon>1也会得到满足,那么语句3中就会减去1天。

1972年分析:同理,由year = tm->tm_year - 70;可得到年数不大于4年,那么语句1中也不会增加1天,但是语句2中的月份依旧是润年月份29天,但是语句3已经被屏蔽了(第二个条件为假)。所以由语句2来调整润年月份。

那么到这里就可以总结下语句2和语句3的作用,语句2不管什么时候都是加上润年月的。而语句3就会根据当年是否为润年来处理,若是润年,则不调整,若不是润年则会把语句2中多加的1天而减去;

1973年分析:这里会分析到为什么语句1中要加上1(现在我们假设语句1中年数先不加1处理,即是:res = YEAR*year + DAY*(year/4); )。 同理,由year = tm->tm_year - 70;年数不大于4年,那么语句1中不会增加1天,那么问题来了,1973年之前的1972年是润年,那么计算时间的时候应该多加上一天的,语句1没有加上,语句2加上的会和语句3抵消掉,那么总的时间就差了一天了。但是1974年就不一样的,因为1974年和1970年比有4年年数,语句1中会增加1天,所以问题就卡在1973年上。

为了解决这个问题,那么就把年数增加1,把1973年矫正下。其实1997年也有这个问题,因为1996是润年,而1997年中7不大于8,所以只增加一天,但本质上应该增加2天的(1972和1976)。所以语句1中增加1可以解决这个问题;



刚开始我说过感觉这个函数有点问题,我不知道是不是理解有点错误还是这个确实存在问题。那就是2100年问题,如果用上面的方法的话2100年也要当作润年来表示,而实际上2100年不是润年。可能是理解错误(因为一个这么成功的系统不太可能会出现这种问题),或者是其他地方有修正的,亦或者后期版本已经改正过。

转载请注明作者和原文出处,原文地址:http://blog.csdn.net/yuzhihui_no1/article/details/42888237

若有不正确之处,望大家指正,共同学习!谢谢!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: