《30天自制操作系统》到30天结束了,但是操作系统还有很多地方可以完善。本文将给“纸娃娃操作系统”增加获取时钟的API以及标准I/O接口

获取时钟

时钟无论是对于人来说还是计算机来说都非常重要,但是“纸娃娃操作系统”还不能获取当前的计算机时钟,所以需要增加获取时钟这个API。

实现原理

在之前实现操作系统计时器的时候,已经接触过了PIT,PIT会在一定时间内产生信号,操作系统通过PIT的信号来进行计时,从而实现任务调度等依赖于计时的功能。除了PIT之外,计算机中还存在着一个RTC,用来维持计算机的时钟,具体的时钟信息被保存在了一个CMOS RAM中,寄存器地址选择端口号为0x70,寄存器数据读取端口号为0x71,而RAM中具体时钟信息对应的寄存器如下。

寄存器地址 内容
0x00
0x02
0x04
0x06 星期
0x07
0x08
0x09 年(世纪中的年份)
0x32 世纪
0x0A 状态寄存器A
0x0B 状态寄存器B

所以,读取CMOS中数据的方法就是,先将寄存器地址写入到端口0x70,然后从端口0x71读取寄存器中的值。

int get_rtc_register(char address)
{
	int value = 0;
	io_cli();
	io_out8(0x70, address);
	value = io_in8(0x71);
	io_sti();
	return value;
}

CMOS中的值在不断更新中的,如果我们在CMOS更新的时候读取时钟的话,可能会得到类似“60分”、“60秒”这些中间结果,这些错误的时钟值是应用程序不希望得到的。状态寄存器A第7位(掩码为0x80)显示CMOS是否正在被更新(1——正在更新,0——不在更新)。

从CMOS直接读取的数值不一定是应用程序需要的值,可能存在不同的格式:

  • 二进制/BCD编码

  • 12小时制/24小时制

当时钟为12小时制时,小时数值的第7位(掩码为0x80)表示上下午(1——下午,0——上午)。

具体获取到了什么格式的编码,需要检查状态寄存器B中的值:

  • 状态寄存器B,第1位(掩码为0x02):1——24小时制,0——12小时制

  • 状态寄存器B,第2位(掩码为0x04):1——二进制,0——BCD编码

时钟API

了解了获取时钟的原理之后,就可以增加一个28号API了。在读取时钟之前,需要确保CMOS不处于更新状态,然后读取相应寄存器中的值,接着如果数值为BCD编码,那么将其转化为二进制编码,遇到12小时制的小时数值,需要转化为24小时制的数值。

我们在haribote/console.c中增加以下代码:

int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	...
	switch (edx) {
		...
		case 28:
			// Make sure an update isn't in progress
			while (get_rtc_register(RTC_REG_A) & 0x80);
			i = get_rtc_register(eax);
			// Convert BCD to binary values if necessary
			j = get_rtc_register(RTC_REG_B);
			if (!(j & 0x04)) 
				i = (i & 0x0F) + ((i / 16) * 10);
			// Convert 12 hour clock to 24 hour clock if necessary
			if (eax == RTC_HOURS && !(j & 0x02) && (i & 0x80))
				i = ((i & 0x7F) + 12) % 24;
			reg[7] = i;
	}
	return 0;
}

除了增加API实现代码,还需要在apilib/中创建一个API函数入口汇编代码api028.nas,并且修改相应的Makefile文件。

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api028.nas"]

		GLOBAL	_api_rtc

[SECTION .text]

_api_rtc:		; int api_rtc(int option);
		MOV		EDX,28
		MOV		EAX, [ESP+4]
		INT		0x40
		RET

以及在apilib.h添加API函数声明和CMOS寄存器地址的宏定义。

#define RTC_SECOND			0x00
#define RTC_MINUTE			0x02
#define RTC_HOURS			0x04
#define RTC_WEEKDAY			0x06
#define RTC_DAY_OF_MONTH	0x07
#define RTC_MONTH 			0x08
#define RTC_YEAR			0x09
#define RTC_CENTURY			0x32
...
int api_rtc(int option);

显示系统时钟程序

有了获取时钟信息的API,就可以编写获取时钟的应用程序了。

#include "../stdlib.h"
#include "../apilib.h"

void HariMain(void)
{
	const char *days[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
	const int second = api_rtc(RTC_SECOND);
	const int minute = api_rtc(RTC_MINUTE);
	const int hour = api_rtc(RTC_HOURS);
	const int weekday = api_rtc(RTC_WEEKDAY);
	const int day = api_rtc(RTC_DAY_OF_MONTH);
	const int month = api_rtc(RTC_MONTH);
	const int year = api_rtc(RTC_YEAR);
	const int century = api_rtc(RTC_CENTURY);
	printf("%d-%d-%d %s %d:%02d:%02d\n", 
		century*100+year, month, day, days[weekday], hour, minute, second);
	exit(0);
}

标准I/O

输入输出一直是应用程序非常重要的一般部分,但是“纸娃娃操作系统”的输入输出还是非常简陋,输入API每次只能读取一个字符,还没有实现读取字符串以及格式化输入的功能。我们可以在stdlib/stdlib.c文件中添加函数定义,在stdlib.h中添加函数声明来实现更多的接口。

读取字符并回显

使用api_getkey可以读取一个字符,但是作为一个输入过程来说,输出的字符应当回显。

int getchar()
{
	int c = api_getkey(1);
	putchar(c);
	return c;
}

字符串输入/输出

puts函数用于显示字符串并且在末尾加上换行,最后返回字符串的长度。

int puts(const char *str)
{
	api_putstr0(str);
	putchar('\n');
	return strlen(str);
}

gets函数用来读取一行字符串,并且在字符串的末尾加上’\0’,返回字符串的指针。在目前的libc中,gets函数因为不限制字符串长度容易被缓冲区溢出攻击,因而早已被抛弃,至于“纸娃娃操作系统”就不需要考虑那么多了。

char *gets(char *str)
{
	char *ptr = str;
	while (1) {
		char c = getchar();
		if (c == '\r' || c == '\n') {
			*ptr = '\0';
			return str;
		} else {
			*ptr = c;
			ptr++;
		}
	}
}

格式化输入

printf可以使用sprintf来实现,但是scanf需要根据实时的输入字符进行处理,所以只能独立实现。由于scanf函数的格式相当复杂,所以这里只实现了’%s’、’%c’、’%d’、’%x’、’%o’、’%b’这六种格式,功能有待完善。

实现方法:

首先扫描格式字符串。

  • 所有的空格都被无视,空格的定义就是isspace返回真。

  • 遇到’%’,读取下一个字符判断格式。

    • 格式为’%c’:读取一个非空字符放入目标地址中;

    • 格式为’%s’:读取一段不包含空格的连续字符串放入目标地址中;

    • 格式为’%d’、’%x’、’%o’、’%b’:读取一段不包含空格的连续字符串,然后将字符串转化为数值。

  • 遇到非’%’,那么读取一个字符。

    • 如果是空格字符,那么无视,继续读取下一个字符;

    • 如果读取的字符和格式中的字符匹配,那么处理格式中的下一个字符;

    • 如果不匹配,那么函数结束。

int scanf(const char *format, ...)
{
    int count = 0, base;
    char *temp, buffer[MAX_BUF];
    va_list ap;
    va_start(ap, format);
    while (*format) {
		while (isspace(*format))
		    format++;
		if (*format == '%') {
		    format++;
		    for (; *format; format++)
				if (strchr("dibouxcsefg%", *format))
				    break;
		    if (*format == 's') {
		    	char c = getchar();
				while (isspace(c))
					c = getchar();
				temp = va_arg(ap, char *);
				while (!isspace(c)) {
					*temp = c;
					temp++;
					c = getchar();
				}
				*temp = '\0';
		    } else if (*format == 'c') {
		    	char c = getchar();
				while (isspace(c))
					c = getchar();
				temp = va_arg(ap, char *);
				*temp = c;
		    } else if (strchr("dobxu", *format)) {
		    	char c = getchar();
				while (isspace(c))
					c = getchar();
				if (*format == 'd' || *format == 'u')
				    base = 10;
				else if (*format == 'x')
				    base = 16;
				else if (*format == 'o')
				    base = 8;
				else if (*format == 'b')
				    base = 2;
				temp = buffer;
				while (!isspace(c)) {
					*temp = c;
					temp++;
					c = getchar();
				}
				*temp = '\0';
				int *numptr = va_arg(ap, int *);
				*numptr = strtol(buffer, NULL, base);
		    }
			count++;
		    format++;
		} else if (*format) {
			char c = getchar();
		    while (isspace(c)) 
				c = getchar();
		    if (*format != c)
				break;
		    else 
		    	format++;
		}
	}
	va_end(ap);
    return (count);
}

格式化输入应用

有了格式化输入,就可以实现一个简陋的”A?B”计算器了。

#include "../stdlib.h"

void HariMain(void)
{
	int a, b, ans;
	char op;
	while (scanf("%d %c %d", &a, &op, &b) == 3) {
		switch (op) {
			case '+':
				ans = a + b;
				break;
			case '-':
				ans = a - b;
				break;
			case '*':
				ans = a * b;
				break;
			case '/':
				ans = a / b;
				break;
			case '%':
				ans = a % b;
				break;
			default:
				printf("Unkown operator\n");
		}
		printf("%d ", a);
		putchar(op);
		printf(" %d = %d\n", b, ans);
	}
	exit(0);
}

参考资料