前情提要
上一节我们实现了硬盘的驱动,本来这一节打算实现文件系统的,但是文件系统中有个时间属性,所以这里我们先实现操作系统的时间管理。
一、除法
我们的系统是一个32位的系统,在编译一些除法的时候编译的时候没问题,链接的时候就会出现提示
undefined symbol ‘udivdi3’
undefined symbol 'divid3'
这是由于在除法 a/b
中,如果 a
是六十四位的,就需要用到专门的除法函数,在32位系统下,CPU无法处理这样的信息。GCC编译器找不到对应的除法函数。
为了解决这个问题,在linux中,<linux/math.h>
中提供了这样的一个函数,帮助在32位机下做64位的除法运算
1.1、仿真实现
由于上述的实现设计了大量的汇编原理,所以这里我们用C实现一个可以做64位除法的函数
uint64_t divide_u64_u32(uint64_t dividend, uint32_t divisor, uint64_t* remainder) {
// 检查除数是否为0
if (divisor == 0) {
return 0; // 除数为0,返回0
}
// 用于存储商的变量
uint64_t quotient = 0;
// 从最高位开始逐步计算商
for (int i = 63; i >= 0; i--) {
// 将余数左移1位
*remainder <<= 1;
// 获取当前位的值
uint64_t bit = (dividend >> i) & 1;
// 将当前位的值加到余数的最低位
*remainder |= bit;
// 如果余数大于等于除数,则减去除数,并将对应位的商置1
if (*remainder >= divisor) {
*remainder -= divisor;
quotient |= (1ULL << i);
}
}
return quotient; // 返回商
}
uint64_t divide_u64_u32(uint64_t dividend, uint32_t divisor, uint64_t* remainder) {
: 这是函数的声明,它接受一个 64 位无符号整数dividend
作为被除数,一个 32 位无符号整数divisor
作为除数,以及一个指向 64 位无符号整数的指针remainder
,用于存储余数。if (divisor == 0) { return 0; }
: 检查除数是否为零,如果除数为零,直接返回 0,因为除数为零时无法进行除法运算。uint64_t quotient = 0;
: 创建一个 64 位无符号整数变量quotient
,用于存储商的值。for (int i = 63; i >= 0; i--) {
: 使用循环从被除数的最高位开始逐位计算商。*remainder <<= 1;
: 将余数左移 1 位,为下一次计算商做准备。uint64_t bit = (dividend >> i) & 1;
: 获取被除数的当前位的值(0 或 1)。*remainder |= bit;
: 将被除数的当前位的值加到余数的最低位,相当于将被除数向左移动 1 位。if (*remainder >= divisor) { *remainder -= divisor; quotient |= (1ULL << i); }
: 如果余数大于等于除数,则减去除数,并将对应位的商置 1。return quotient;
: 返回计算得到的商。
这段代码实现了一种基于位操作的除法算法,称为 “恒定除法” 或 “restoring division”。
恒定除法(Restoring Division)是一种用于实现除法运算的基本算法之一。其主要原理如下:
- 初始化:首先,将除数和被除数进行比较,确定它们的相对大小,并对商和余数进行初始化。将被除数存储在一个寄存器中,将商存储在另一个寄存器中,余数初始为零。
- 逐位相除:从被除数的最高有效位(MSB)开始,逐位地与除数进行比较。在每一步中,将被除数左移一位,并将下一位的值移入余数的最低位。这相当于模拟手动长除法中的将下一位数附加到当前余数上的步骤。
- 检查余数与除数的关系:在每个步骤中,与除数相比较余数。如果余数大于或等于除数,则表示可以从余数中减去一个除数。如果余数小于除数,则不能从余数中减去一个除数。
- 减去除数并更新商:如果余数大于或等于除数,则从余数中减去除数,并将商的当前位设置为 1。否则,将商的当前位设置为 0。
- 重复步骤:重复上述步骤,直到对被除数的所有位进行了处理。
- 得到商和余数:当处理完所有位时,商的值即为除法的结果,而最后剩下的余数则是除法的余数。
1.2、汇编实现
上面虽然实现了64位的除法,但是效率不高,下面我们使用Linux内置的一种方法实现
// mod操作
#define do_mod(n, base) \
({ \
unsigned long __upper, __low, __high, __mod, __base; \
__base = (base); \
asm("" : "=a" (__low), "=d" (__high) : "A" (n));\
__upper = __high; \
if (__high) { \
__upper = __high % (__base); \
__high = __high / (__base); \
} \
asm("divl %2" : "=a" (__low), "=d" (__mod) \
: "rm" (__base), "0" (__low), "1" (__upper)); \
asm("" : "=A" (n) : "a" (__low), "d" (__high)); \
__mod; \
})
// div操作
#define do_div(n, base) ({ \
unsigned long __upper, __low, __high, __quotient, __base = (base);\
asm("" : "=a" (__low), "=d" (__high) : "A" (n));\
__upper = __high; \
if (__high) { \
__upper = __high % (__base); \
__high = __high / (__base); \
} \
asm("divl %2" : "=a" (__quotient), "=d" (__low) \
: "rm" (__base), "0" (__low), "1" (__upper)); \
asm("" : "=A" (n) : "a" (__low), "d" (__high)); \
__quotient; \
})
另一种也是C语言实现
/* 实现六十四位整形的除法 */
uint64_t div_u64_rem(uint64_t dividend, uint32_t divisor, uint32_t* remainder) {
// 定义一个联合体以将64位被除数视为两个32位部分
union {
uint64_t v64;
uint32_t v32[2];
} d = { dividend };
uint32_t upper;
// 存储被除数的高32位
upper = d.v32[1];
d.v32[1] = 0;
// 检查被除数的高部分是否大于或等于除数
if (upper >= divisor) {
// 计算高部分的商和余数
d.v32[1] = upper / divisor;
upper %= divisor;
}
// 使用除数对低32位进行除法运算
asm("divl %2" : "=a" (d.v32[0]), "=d" (*remainder) :
"rm" (divisor), "0" (d.v32[0]), "1" (upper));
// 返回组合的64位结果
return d.v64;
}
以上的三种方法都写到了 math.c
中,这三种方法都实现了 64 位整型除以 32 位整型无法连接的问题。
二、COMS
系统的时间是保存在COMS中的,读取COMS即可读取到开机时的时间,但是时间是一直在流逝的,每次读取时间都是读取COMS的话,慢,浪费时间。所以后面的时间更新就是开机时间加上我们在定时器中的滴答数算出来的。我们的滴答数是一秒100次,所以正好是一次10ms。
2.1、CMOS
CMOS(互补金属氧化物半导体)是一种集成电路技术,常用于制造微处理器、存储器和其他数字逻辑电路。在计算机中,CMOS被广泛用于实现非易失性存储器,以保存系统的基本设置和配置信息,包括日期、时间、启动顺序等。
CMOS存储器通常位于主板上,由一块称为CMOS RAM(随机存取存储器)的小型电池供电的芯片中。这个电池通常是一块纽扣电池,它持续为CMOS芯片提供电力,即使计算机关闭也能保持存储器中的数据。
在计算机开机时,系统会读取CMOS中的数据,并将其加载到内存中。这些数据包括当前的日期和时间,以及其他系统设置。用户可以通过BIOS(基本输入/输出系统)设置界面访问和修改这些信息。
由于CMOS存储器是非易失性的,即使在断电的情况下也会保持数据,因此计算机可以在重新启动后保持正确的日期和时间,而不需要用户手动重新设置。然而,如果CMOS电池耗尽或者被取下,就会导致CMOS芯片失去电力供应,从而丢失存储的信息,需要重新设置。
2.2、UNIX时间戳
UNIX时间戳是一种用于表示时间的系统,它定义了从UNIX纪元(1970年1月1日00:00:00 UTC)开始经过的秒数,不考虑闰秒。UNIX时间戳通常以整数形式表示,表示从UNIX纪元开始的秒数,可以是正数、零或负数。它被广泛用于计算机系统中,特别是在UNIX和类UNIX操作系统中。
UNIX时间戳的优点是它是一个简单的整数值,易于计算和处理。它在许多系统和编程语言中都被广泛使用,用于记录事件发生的时间、计算时间间隔等。然而,需要注意的是UNIX时间戳并不考虑闰秒,因此在涉及精确时间要求的应用中可能需要进行额外的处理。
2.3、实现时间管理
看一下时间的结构体
struct tm {
int tm_sec; /* 秒 [0, 59] */
int tm_min; /* 分 [0, 59] */
int tm_hour; /* 时 [0, 23] */
int tm_mday; /* 一个月中的天 [1, 31] */
int tm_mon; /* 月份 [0, 11] */
int tm_year; /* 年份 - 1900 */
}__attribute__((packed));
struct timespec {
uint64_t tv_sec; // 秒数
uint64_t tv_msec; // 毫秒数
};
上面的是年月日时分秒,下面的是UNIX时间戳。时间的初始化函数如下
#define BCD_TO_BIN(val) ((val) = ((val) & 15) + ((val) >> 4) * 10)
static uint8_t cmos_read(uint8_t addr) {
outb(0x70, (0x80 | (addr)));
return inb(0x71);
};
void time_init(void) {
put_str("time_init begin!\n");
do {
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_year += 100; // 读取到的年份是24年,unix时间是从1900年开始算起,所以应该在加100
time.tm_mon -= 1; // 月份从0开始算起
times.tv_sec = datetime_to_timestamp(&time);
times.tv_msec = times.tv_sec;
put_str("time_init end!\n");
}
BCD_TO_BIN
宏的作用是将数据从格雷码变为十进制。cmos_read
的作用是读取CMOS的地址,从而读出年月日。
我们返回unix时间戳的代码就很简单了。
/* 获取当前的 unix 时间戳 */
uint64_t get_time(void) {
return mod_u64_u32(ticks, 100) + times.tv_sec;
}
这个时间戳也就是现在的时间。在本节还提供两个函数,一个是从UNIX时间戳变为时间的函数,一个是从时间变为UNIX时间戳的代码。
/* 判断是否为闰年 */
static bool is_leap_year(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
/* 获取指定年份月份的天数 */
static int days_in_month(int month, int year) {
if (month == 2) {
return is_leap_year(year) ? 29 : 28;
}
else if (month == 4 || month == 6 || month == 9 || month == 11) {
return 30;
}
else {
return 31;
}
}
/* 将年月日时分秒转换为 UNIX 时间戳 */
uint64_t datetime_to_timestamp(struct tm* tm) {
if (tm == NULL) {
return 0; // 空指针检查
}
uint64_t total_seconds = 0;
// 计算年份之前的秒数
for (int y = 1970; y < tm->tm_year + 1900; y++) { // tm_year是从1900年开始计算的偏移量
total_seconds += is_leap_year(y) ? 31622400 : 31536000; // 闰年秒数 366 * 24 * 60 * 60,平年秒数 365 * 24 * 60 * 60
}
// 计算月份之前的秒数
for (int m = 0; m < tm->tm_mon; m++) { // 月份从0开始
total_seconds += days_in_month(m + 1, tm->tm_year + 1900) * 24 * 60 * 60;
}
// 计算天数之前的秒数
total_seconds += (tm->tm_mday - 1) * 24 * 60 * 60;
// 计算小时、分钟和秒数
total_seconds += tm->tm_hour * 60 * 60 + tm->tm_min * 60 + tm->tm_sec;
return total_seconds;
}
/* 将 UNIX 时间戳转换为年月日时分秒 */
void timestamp_to_datetime(uint64_t timestamp, struct tm* datetime) {
if (datetime == NULL) {
return;
}
// 总天数
uint64_t days = divide_u64_u32_no_mod(timestamp, (24 * 3600));
int year = 1970;
// 计算年份
while (days >= 365 + (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) {
days -= 365 + (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
year++;
}
datetime->tm_year = year;
// 计算月份和日期
int month = 1;
int days_in_mon;
while (days >= (days_in_mon = days_in_month(month, year))) {
days -= days_in_mon;
month++;
}
datetime->tm_mon = month - 1;
datetime->tm_mday = days + 1;
// 计算小时、分钟和秒
datetime->tm_hour = divide_u64_u32_no_mod(mod_u64_u32(timestamp , (24 * 3600)) , 3600);
datetime->tm_min = divide_u64_u32_no_mod(mod_u64_u32(timestamp , (3600)) , 60);
datetime->tm_sec = mod_u64_u32(timestamp , 60);
}
这里的代码就只是做计算了,怎么将年月日时分秒映射到时间戳。或者反向映射。
三、持续改进printf
现在的printf无法打印 %ld
,我们把这部分加进去
case 'l':
index_char = *(++index_ptr); // 得到%l后面的字符
arg_long = va_arg(ap, uint64_t);
if (index_char == 'd') {
if (arg_long < 0) {
arg_long = 0 - arg_long;
*buf_ptr++ = '-';
}
itoa64(arg_long, &buf_ptr, 10);
}
else if (index_char == 'u') {
itoa64(arg_long, &buf_ptr, 10);
}
arg_int = va_arg(ap, int); // 指针接着向后4位
index_char = *(++index_ptr); // 跳过格式字符并更新index_char
break;
}
只添加了对 %l
的处理,其中精髓在于
arg_int = va_arg(ap, int); // 指针接着向后4位
由于 Uint64_t
是一个64位的结构,所以需要指针 +8
才能指向下一位,但是我们这里在设计一个加8的函数就很复杂,所以直接在处理解除后让指针向后再加4,在下一次遇到 %
处理时也会加4,正好是 +8
。
为了处理64位无符号数,写了一个新的处理数字的函数
/* 将长整型转换成字符(integer to ascii) */
static void itoa64(uint64_t value, char** buf_ptr_addr, uint8_t base) {
uint64_t m=0, i=0;
i = divide_u64_u32(value, base, &m);
if (i) { // 倍数不为0则递归调用
itoa64(i, buf_ptr_addr, base);
}
*((*buf_ptr_addr)++) = cache[m];
}
借助了前面的除法。
仿真
结束语
本节我们实现了64位的除法,下节我们将实现文件系统,文件系统较复杂,做好准备。
老规矩,本节的代码地址:https://github.com/lyajpunov/os