logo

11.tty是什么?聊聊linux0.12中tty与time的初始化

作者:小牛呼噜噜 ,首发于公众号「小牛呼噜噜

哈喽,大家好呀,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init,继续我们的内核探索之路

本文我们继续回到init/main.c文件中,接着看tty_inittime_init2个初始化函数上

// init/main.c
void main(void) {
  ...
	tty_init(); // tty 初始化
	time_init(); //设置开机启动时间
  ...
}

tty_init

我们来tty_init()源码:

// /kernel/chr_drv/tty_io.c

void tty_init(void)
{
	int i;

	for (i=0 ; i < QUEUES ; i++)
		tty_queues[i] = (struct tty_queue) {0,0,0,0,""};
	rs_queues[0] = (struct tty_queue) {0x3f8,0,0,0,""};
	rs_queues[1] = (struct tty_queue) {0x3f8,0,0,0,""};
	rs_queues[3] = (struct tty_queue) {0x2f8,0,0,0,""};
	rs_queues[4] = (struct tty_queue) {0x2f8,0,0,0,""};
	for (i=0 ; i<256 ; i++) {
		tty_table[i] =  (struct tty_struct) {
		 	{0, 0, 0, 0, 0, INIT_C_CC},
			0, 0, 0, NULL, NULL, NULL, NULL
		};
	}
	con_init();
	for (i = 0 ; i<NR_CONSOLES ; i++) {
		con_table[i] = (struct tty_struct) {
		 	{ICRNL,		/* change incoming CR to NL */
			OPOST|ONLCR,	/* change outgoing NL to CRNL */
			0,
			IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,
			0,		/* console termio */
			INIT_C_CC},
			0,			/* initial pgrp */
			0,			/* initial session */
			0,			/* initial stopped */
			con_write,
			con_queues+0+i*3,con_queues+1+i*3,con_queues+2+i*3
		};
	}
	for (i = 0 ; i<NR_SERIALS ; i++) {
		rs_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B2400 | CS8,
			0,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			rs_write,
			rs_queues+0+i*3,rs_queues+1+i*3,rs_queues+2+i*3
		};
	}
	for (i = 0 ; i<NR_PTYS ; i++) {
		mpty_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B9600 | CS8,
			0,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			mpty_write,
			mpty_queues+0+i*3,mpty_queues+1+i*3,mpty_queues+2+i*3
		};
		spty_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B9600 | CS8,
			IXON | ISIG | ICANON,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			spty_write,
			spty_queues+0+i*3,spty_queues+1+i*3,spty_queues+2+i*3
		};
	}
	rs_init();
	printk("%d virtual consoles\n\r",NR_CONSOLES);
	printk("%d pty's\n\r",NR_PTYS);
}

咋眼一看是不是很懵,都是啥呀?

tty_init顾名思义tty初始化函数,那我们得先了解一下什么是tty?

TTY发展与演变历史

tty一词源于TeletypesTeletypewriters,是一种机电设备:电传打字机(Teleprinter,teletypewriter, teletype or TTY),可用于通过各种通信通道以点对点点对多点配置发送和接收打字消息

早期电报技术通过摩尔斯电码来互相通信,这个需要2位电报操作员才能有效地实现相互通信,将电传打字机用于电报,
使大部分工作实现了自动化,并最终在很大程度上取代了精通摩尔斯电码的电报操作员

上图来源于:https://www.wise-geek.com/what-is-tty.htm

那么电传打字机是如何和计算机有联系的呢?

我们知道,早期计算机利用穿孔纸带来当输入的方法

或者打孔卡Punch card

后来随着计算机的发展,电传打字机经过改造,可以将输入的数据发送到计算机并打印响应;电传打字机tty取代纸带和打孔卡,成为计算机的一种终端设备terminal独立于计算机而存在

另外当时计算机还有控制台Console和计算机连在一起,是一个带有大量开关和指示灯的面板,可以控制计算机的启动,停止,运算和结果反馈等操作;

通常一个计算机只能有一个控制台,然后大型计算机又需要满足多人同时操作,比如UNIX多用户操作系统;所以这才引入终端设备可以有多个终端,每个用户通过终端设备与主机连接,登录指定的账号,获得计算机使用权

上图来源于:https://itsfoss.com/what-is-tty-in-linux/

后来计算机终端-电传打字机,早被电子视频终端video terminal取代,因为电传打字机与计算机的所有的交互都打印在纸上,输入的字符无法删除;而电子视频终端允许用户在传给主机之前修改输入信息,还带有缓存功能,1978年Digital公司生产的VT100至今仍然是终端的标准

上图来源于https://vistapointe.net/vt100.html

后来终端带有串口,通过串口传送数据到主机,串口又叫串行端口Serial Port,相较于并行,穿行它数据和控制信息是一位接一位地传送出去的。这样速度较慢,但传送距离较更长,像终端这种长距离与主机通信,适合串口。计算机把连接到串口的外设称为字符设备,外设称为"串口终端"

当然如今计算机上的串口已经基本上消失了,被USB取代,因为USB的速度比串口快多了,还支持热插拨

随着科技的进步,时代的发展,如今计算机不再是以前的"大块头",变成了个人消费级产品,除了键盘和显示器,其他外设都不存在了,比如"控制台"近乎等同于"终端",终端不再是物理设备,彻底脱离主机的硬件,而是变成软件仿真出来的终端,真正实现了多路复用

这种叫伪终端pseudo tty,在linux中输入tty命令,第一个终端**pts/0**,由于是模拟出来的,近乎无限,每打开一个终端,**pts/数字+1**

需要注意的是,终端自身并不会执行用户输入的命令,它仅仅是负责把输入的内容传送到主机系统,并把主机系统返回的结果呈现给用户。至于负责解释执行用户输入的命令并返回结果的,是shell

shell是UNIX/Linux系统中最为重要的应用程序之一,负责解释执行用户指令,打印结果,和用户交互;它是沟通用户和系统内核的中间桥梁,我们将在后续文章中聊聊linux0.12如何启动shell

Linux中支持tty

再让我们回到linux0.12的源码中,看看早期linux是如何支持tty的?

我们先来了解为操作系统为支持tty设备,所定义终端相关的数据结构:

//   /include/linux/tty.h

...

#define MAX_CONSOLES 8 // 最大虚拟控制台数量。
#define NR_SERIALS 2 // 串行终端数量。
#define NR_PTYS 4 // 伪终端数量。

extern int NR_CONSOLES; // 虚拟控制台数量。

#define TTY_BUF_SIZE 1024 // tty 缓冲队列的大小

//tty 字符缓冲队列数据结构
struct tty_queue {
	unsigned long data;  // 队列缓冲区中含有字符行数值(不是当前字符数)
                         // 对于串口终端,则存放串行端口地址
	unsigned long head;  //头指针
	unsigned long tail;  //尾指针
	struct task_struct * proc_list;  // 等待本队列的进程列表
	char buf[TTY_BUF_SIZE]; //队列的缓冲区
};


...

struct tty_struct {
	struct termios termios;  // 终端io属性和控制字符数据结构
    int pgrp; // 所属进程组
    int session; // 会话号
    int stopped; // 停止标志
    void (*write)(struct tty_struct * tty); // tty 写函数指针
    struct tty_queue *read_q; // tty 读队列
    struct tty_queue *write_q; // tty 写队列
    struct tty_queue *secondary; // tty 辅助队列(存放规范模式字符序列),
	};

extern struct tty_struct tty_table[]; //tty数组
extern int fg_console;  // 前台控制台号

...

上面主要定义了tty_struct结构体,其中包含了三个缓冲队列,read_q读队列,write_q写队列和seconddary辅助队列,这3个队列非常的重要;还有一些常量MAX_CONSOLES、NR_CONSOLES

我们接着回到tty_init这个方法的源码处:

// /kernel/chr_drv/tty_io.c


...
    
// 下面定义 tty 终端使用的缓冲队列结构数组 tty_queues 和 tty 终端表结构数组 tty_table。
 // QUEUES 是 tty 终端使用的缓冲队列最大数量。
#define QUEUES (3*(MAX_CONSOLES+NR_SERIALS+2*NR_PTYS)) // 共 54 项。
static struct tty_queue tty_queues[QUEUES]; // tty 缓冲队列数组。
struct tty_struct tty_table[256]; // tty 表结构数组。

// 下面设定各种类型的 tty 终端所使用缓冲队列结构在 tty_queues[]数组中的起始项位置。
 // 8 个虚拟控制台终端占用 tty_queues[]数组开头 24 项(3 X MAX_CONSOLES)(0 -- 23)
 // 两个串行终端占用随后的 6 项(3 X NR_SERIALS)(24 -- 29)
 // 4 个主伪终端占用随后的 12 项(3 X NR_PTYS)(30 -- 41)
 // 4 个从伪终端占用随后的 12 项(3 X NR_PTYS)(42 -- 53)
#define con_queues tty_queues
#define rs_queues ((3*MAX_CONSOLES) + tty_queues)
#define mpty_queues ((3*(MAX_CONSOLES+NR_SERIALS)) + tty_queues)
#define spty_queues ((3*(MAX_CONSOLES+NR_SERIALS+NR_PTYS)) + tty_queues)

// 下面设定各种类型 tty 终端所使用的 tty 结构在 tty_table[]数组中的起始项位置。
 // 8 个虚拟控制台终端可用 tty_table[]数组开头 64 项(0 -- 63);
 // 两个串行终端使用随后的 2 项(64 -- 65)。
 // 4 个主伪终端使用从 128 开始的项,最多 64 项(128 -- 191)。
 // 4 个从伪终端使用从 192 开始的项,最多 64 项(192 -- 255)。
#define con_table tty_table        // 定义控制台终端 tty 表符号常数。
#define rs_table (64+tty_table)    // 串行终端 tty 表
#define mpty_table (128+tty_table)  // 主master伪终端 tty 表
#define spty_table (192+tty_table)  // 从slave伪终端 tty 表

int fg_console = 0; // 当前前台控制台号(范围 0--7)

...


void tty_init(void)
{
    int i;
    //首先初始化所有终端的缓冲队列结构,设置初值。
    for (i=0 ; i < QUEUES ; i++)
        tty_queues[i] = (struct tty_queue) {0,0,0,0,""};
    //对于串行终端的读/写缓冲队列,将它们的data 字段设置为串行端口基地址值。
    //串口 1 是 0x3f8,串口 2 是 0x2f8
    rs_queues[0] = (struct tty_queue) {0x3f8,0,0,0,""};
    rs_queues[1] = (struct tty_queue) {0x3f8,0,0,0,""};
    rs_queues[3] = (struct tty_queue) {0x2f8,0,0,0,""};
    rs_queues[4] = (struct tty_queue) {0x2f8,0,0,0,""};
    for (i=0 ; i<256 ; i++) {
        //初步设置所有终端的 tty 结构
        tty_table[i] =  (struct tty_struct) {
            {0, 0, 0, 0, 0, INIT_C_CC}, //#define INIT_C_CC
            0, 0, 0, NULL, NULL, NULL, NULL
            };
    }
    con_init();//初始化控制台终端

    ...

其中特殊字符数组INIT_C_CC设置的初值定义在 include/linux/tty.h文件中

// 这里给出了终端 termios 结构中可更改的特殊字符数组 c_cc[]的初始值

/* 中断 intr=^C 退出 quit=^| 删除 erase=del 终止 kill=^U
 * 文件结束 eof=^D vtime=\0 vmin=\1 sxtc=\0
 * 开始 start=^Q 停止 stop=^S 挂起 susp=^Z 行结束 eol=\0
 * 重显 reprint=^R 丢弃 discard=^U werase=^W lnext=^V
 * 行结束 eol2=\0
 */
#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"

我们接着看一下con_init源码:

// /kernel/chr_drv/console.c

/*
 * These are set up by the setup-routine at boot-time:
 */

#define ORIG_X			(*(unsigned char *)0x90000)
#define ORIG_Y			(*(unsigned char *)0x90001)
#define ORIG_VIDEO_PAGE		(*(unsigned short *)0x90004)
#define ORIG_VIDEO_MODE		((*(unsigned short *)0x90006) & 0xff)
#define ORIG_VIDEO_COLS 	(((*(unsigned short *)0x90006) & 0xff00) >> 8)
#define ORIG_VIDEO_LINES	((*(unsigned short *)0x9000e) & 0xff)
#define ORIG_VIDEO_EGA_AX	(*(unsigned short *)0x90008)
#define ORIG_VIDEO_EGA_BX	(*(unsigned short *)0x9000a)
#define ORIG_VIDEO_EGA_CX	(*(unsigned short *)0x9000c)

#define VIDEO_TYPE_MDA		0x10	/* Monochrome Text Display	*/
#define VIDEO_TYPE_CGA		0x11	/* CGA Display 			*/
#define VIDEO_TYPE_EGAM		0x20	/* EGA/VGA in Monochrome Mode	*/
#define VIDEO_TYPE_EGAC		0x21	/* EGA/VGA in Color Mode	*/

#define NPAR 16

int NR_CONSOLES = 0;

extern void keyboard_interrupt(void);


/*
 * 这个子程序初始化控制台中断,其他什么都不做。如果你想让屏幕干净的话,就使用
 * 适当的转义字符序列调用 tty_write()函数。
 *
 * 读取 setup.s 程序保存的信息,用以确定当前显示器类型,并且设置所有相关参数
 */
void con_init(void)
{
	register unsigned char a;
	char *display_desc = "????";//屏幕描述
	char *display_ptr;
	int currcons = 0; // 当前虚拟控制台号
	long base, term;
	long video_memory;

    / 首先根据setup.s程序取得的系统硬件参数相关参数
	video_num_columns = ORIG_VIDEO_COLS; // 显示器显示字符列数
	video_size_row = video_num_columns * 2; // 每行字符需使用的字节数
	video_num_lines = ORIG_VIDEO_LINES;   // 显示器显示字符行数
	video_page = ORIG_VIDEO_PAGE;         // 当前显示页面
	video_erase_char = 0x0720;            // 擦除字符(0x20 是字符,0x07 属性)
	blankcount = blankinterval;           // 默认的黑屏间隔时间(嘀嗒数)

    //如果获得的 BIOS 显示方式等于 7,则表示是单色显示卡
	if (ORIG_VIDEO_MODE == 7)	/* Is this a monochrome display? */
	{
		video_mem_base = 0xb0000;// 设置单显映像内存起始地址
		video_port_reg = 0x3b4;  // 设置显示索引寄存器端口
		video_port_val = 0x3b5;
        //这里使用了 BX 寄存器在调用中断 int 0x10 前后是否被改变的方法来判断卡的类型。若
         // BL 在中断调用后值被改变,表示显示卡支持 Ah=12h 功能调用,是 EGA 或后推出来的 VGA 等类
         // 型的显示卡。若中断调用返回值未变,表示显示卡不支持该功能,则说明是一般单色显示卡
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
		{
			video_type = VIDEO_TYPE_EGAM;
			video_mem_term = 0xb8000;
			display_desc = "EGAm";
		}
		else
		{
			video_type = VIDEO_TYPE_MDA;
			video_mem_term = 0xb2000;
			display_desc = "*MDA";
		}
	}
    //如果显示方式不为 7,说明是彩色显示卡。
	else				/* If not, it is color. */
	{
		can_do_colour = 1;
		video_mem_base = 0xb8000; //此时文本方式下所用显示内存起始地址为 0xb8000
		video_port_reg	= 0x3d4;  // 设置显示索引寄存器端口
		video_port_val	= 0x3d5;
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
		{
			video_type = VIDEO_TYPE_EGAC;
			video_mem_term = 0xc0000;
			display_desc = "EGAc";
		}
		else
		{
			video_type = VIDEO_TYPE_CGA;
			video_mem_term = 0xba000;
			display_desc = "*CGA";
		}
	}
    //来计算当前显示卡内存上可以开设的虚拟控制台数量
	video_memory = video_mem_term - video_mem_base;
	NR_CONSOLES = video_memory / (video_num_lines * video_size_row);//计算的NR_CONSOLES的值
	if (NR_CONSOLES > MAX_CONSOLES)
		NR_CONSOLES = MAX_CONSOLES;
	if (!NR_CONSOLES)
		NR_CONSOLES = 1;
	video_memory /= NR_CONSOLES;

	/* Let the user known what kind of display driver we are using */
	//让用户知道我们正使用哪类显示驱动程序
	display_ptr = ((char *)video_mem_base) + video_size_row - 8;
	while (*display_desc)
	{
		*display_ptr++ = *display_desc++;
		display_ptr++;
	}
	
	/* Initialize the variables used for scrolling (mostly EGA/VGA)	*/
	//获取滚屏操作时的相关信息(主要用于 EGA/VGA)
	base = origin = video_mem_start = video_mem_base; // 默认滚屏开始内存位置
	term = video_mem_end = base + video_memory;       // 0 号屏幕内存末端位置,此时当前虚拟控制台号 currcons 已被初始化为 0!
	scr_end	= video_mem_start + video_num_lines * video_size_row; // 滚屏末端位置
	top	= 0;
	bottom	= video_num_lines;
  	attr = 0x07;                       // 初始设置显示字符属性(黑底白字)
  	def_attr = 0x07;
        restate = state = ESnormal;
        checkin = 0;
	ques = 0;
	iscolor = 0;
	translate = NORM_TRANS;
        vc_cons[0].vc_bold_attr = -1;

    //在设置了 0 号控制台当前光标所在位置和光标对应的内存位置 pos 后,我们循环设置其余的几
 // 个虚拟控制台结构的参数值。除了各自占用的显示内存开始和结束位置不同,它们的初始值基
 // 本上都与 0 号控制台相同
	gotoxy(currcons,ORIG_X,ORIG_Y);
  	for (currcons = 1; currcons<NR_CONSOLES; currcons++) {
		vc_cons[currcons] = vc_cons[0];
		origin = video_mem_start = (base += video_memory);
		scr_end = origin + video_num_lines * video_size_row;
		video_mem_end = (term += video_memory);
		gotoxy(currcons,0,0);
	}

 //最后设置当前前台控制台的屏幕原点(左上角)位置和显示控制器中光标显示位置,并设置键
 // 盘中断 0x21 陷阱门描述符(&keyboard_interrupt 是键盘中断处理过程地址)。然后取消中断
 // 控制芯片 8259A 中对键盘中断的屏蔽,允许响应键盘发出的 IRQ1 请求信号。最后复位键盘控
 // 制器以允许键盘开始正常工作
    
	update_screen(); //更新前台原点和设置光标位置
	set_trap_gate(0x21,&keyboard_interrupt); //设置中断描述符,将中断服务程序与idt相关联!!!
	outb_p(inb_p(0x21)&0xfd,0x21);  //取消对键盘中断的屏蔽,允许 IRQ1
	a=inb_p(0x61);   //读取键盘端口 0x61
	outb_p(a|0x80,0x61); //设置禁止键盘工作
	outb_p(a,0x61);   //再允许键盘工作,用以复位键盘
}

console.c这个文件模块是实现控制台输入输出功能,常见的操作比如回车换行、删除、插入、清屏等,都有具体代码的实现,con_init()函数主要初始化控制台终端,最终是为了终端屏幕写函数 con_write()以及实现终端屏幕显示的控制操作做准备

con_init()函数比较复杂,看的头疼,但我们可以将其初始化控制台终端,分为以下几个步骤来:

  1. 首先根据之前在setup.s程序取得的系统硬件参数,主要是和显示模式相关的参数,来设置变量参数。具体什么参数,可以回顾一下笔者之前的文章Linux0.12内核源码解读(3)-Setup.S
  2. 根据获得的BIOS显示方式是否等于7,则判断是单色显示卡还是彩色显示卡;进而来设置显存映射的内存区域
  3. 计算当前显示卡内存上可以开设的虚拟控制台数量NR_CONSOLES
  4. 获取并设置滚屏操作时的相关信息,比如默认滚屏开始内存位置和默认滚屏末行内存位置 ,以及其他属性和标志值
  5. 定位光标位置,设置键盘中断描述符,并取消8259A中对键盘中断的屏蔽

还需要注意的是,在x86实模式下,显存映射的物理内存区域如下:

起始地址 结束地址 大小 用途
0xB8000 0xBFFFF 32KB 用于文本模式显示适配器
0xB0000 0xB7FFF 32KB 用于黑白显示适配器
0xA0000 0xAFFFF 64KB 用于彩色显示适配器

这块内存区域从A0000到BFFFF,总计128K,其支持文本模式、黑白模式以及彩色模式3种模式

linux0.12是文本模式下的终端界面,我们更关注0xB8000这个地址即可。当我们往0xb8000这个地址写入数据,比如hello world, 其中每个字符在内存中占2字节,16位,其低字节为字符对应的ASCII码,高字节为字符的属性(字体颜色,亮度,背景色,闪烁),如下图所示:

接着会被映射到显存中,进而显卡会将这些数据进行运算,最后将运算的结果转化为图形输出到显示器上,这个时候我们就能在屏幕上看到这串字符

ASCII码相关知识,感兴趣可以去笔者以前的文章计算机中数值和字符串怎么用二进制表示?

我们接着回到tty_init这个方法的源码处:

// /kernel/chr_drv/tty_io.c

...

void tty_init(void) {

...
    
con_init();//初始化控制台终端,里面会获取NR_CONSOLES的值
    
for (i = 0 ; i<NR_CONSOLES ; i++) {
        //初始化控制台终端的tty表各字段
        con_table[i] = (struct tty_struct) {
            {ICRNL,		/* change incoming CR to NL */
                OPOST|ONLCR,	/* change outgoing NL to CRNL */
                0,              // 控制模式标志集
             IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,
             0,		/* console termio */
                 INIT_C_CC},
            0,			/* initial pgrp */
                0,			/* initial session */
                0,			/* initial stopped */
                con_write,    // 控制台写函数
            con_queues+0+i*3,con_queues+1+i*3,con_queues+2+i*3
            };
    }
    //然后初始化串行终端的 tty 结构各字段
    for (i = 0 ; i<NR_SERIALS ; i++) {
        rs_table[i] = (struct tty_struct) {
            {0, /* no translation */
                0,  /* no translation */
                B2400 | CS8,
             0,
             0,
             INIT_C_CC},
            0,
            0,
            0,
            rs_write,         // 串口终端写函数
            rs_queues+0+i*3,rs_queues+1+i*3,rs_queues+2+i*3
            };
    }
    // 然后再初始化伪终端使用的 tty 结构。伪终端是配对使用的,即一个主(master)伪终端配
 // 有一个从(slave)伪终端。因此对它们都要进行初始化设置。在下面循环中,我们首先初始化
 // 每个主伪终端的 tty 结构,然后再初始化其对应的从伪终端的 tty 结构。
    for (i = 0 ; i<NR_PTYS ; i++) {
        mpty_table[i] = (struct tty_struct) {
            {0, /* no translation */
                0,  /* no translation */
                B9600 | CS8,
             0,
             0,
             INIT_C_CC},
            0,
            0,
            0,
            mpty_write,          //主伪终端写函数
            mpty_queues+0+i*3,mpty_queues+1+i*3,mpty_queues+2+i*3
            };
        spty_table[i] = (struct tty_struct) {
            {0, /* no translation */
                0,  /* no translation */
                B9600 | CS8,
             IXON | ISIG | ICANON,
             0,
             INIT_C_CC},
            0,
            0,
            0,
            spty_write,          //从伪终端写函数
            spty_queues+0+i*3,spty_queues+1+i*3,spty_queues+2+i*3
            };
    }
    //最后初始化串行中断处理程序和串行接口 1 和 2,并显示系统含有的虚拟
 // 控制台数 NR_CONSOLES 和伪终端数 NR_PTYS。
    rs_init();//初始化串口终端
    printk("%d virtual consoles\n\r",NR_CONSOLES);
    printk("%d pty's\n\r",NR_PTYS);
}



上面涉及到的硬件相关参数比较多,了解一下即可,我们不仔细讲了,继续来看看rs_init()这个函数方法,其源码:

// /kernel/chr_drv/serial.c

...

// 初始化串行端口
static void init(int port)
{
    outb_p(0x80,port+3);	/* 设置线路控制寄存器的DLAB */
	outb_p(0x30,port);		/* 除数的倒数(48->2400 bps) */
	outb_p(0x00,port+1);	/* 除数的/*MS */
	outb_p(0x03,port+3);	/* 重置DLAB */
	outb_p(0x0b,port+4);	/* 设置DTR、RTS和OUT_ 2 */
	outb_p(0x0d,port+1);	/* 启用除写入之外的所有Intr */
	(void)inb(port);	/* read data port to reset things (?) */
}

void rs_init(void)
{
	set_intr_gate(0x24,rs1_interrupt); //设置两个串口的中断门描述符
	set_intr_gate(0x23,rs2_interrupt);
	init(tty_table[64].read_q->data);//初始化串行口1 
	init(tty_table[65].read_q->data);//初始化串行口2
	outb(inb_p(0x21)&0xE7,0x21); // 允许主 8259A 响应 IRQ3、IRQ4 中断请求
}

...

上面set_intr_gate这个函数我们太熟悉了,设置了0x24号和0x23号中断,对应的中断处理函数是rs1_interruptrs2_interrupt

rs_init主要是开启串口中断,为使用串行终端设备作好准备。然而如今计算机上的串口基本被USB给取代了,我们这里就不再细究了

time_init

继续回到init/main.c,我们来补充讲一下,tty_init的下一个函数time_init

/init/main.c



// 获取CMOS实时钟信息,并设置开机时间
static void time_init(void)
{
	struct tm time;

	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); //年份,只有后2位数
	} 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);//获取CMOS实时钟信息,并保存到全局变量 startup_time中
}

上面这段主要功能是,获取CMOS实时钟信息,并设置开机时间,最后将其保存到全局变量startup_time

主要依赖下面这2个宏

//这段宏读取CMOS 实时时钟信息
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \    //0x70 是地址端口,0x80|addr 是要读取的CMOS 内存地址
inb_p(0x71); \               //0x71 是数据端口
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)  //将BCD 码转换成二进制数值

因为CMOS的地址空间在基本地址空间之外,CMOS_READ这个宏主要通过端口去读取CMOS芯片(RAM), 0x70 是地址端口,0x71 是数据端口

CMOS是系统时钟芯片RTC的一部分,其主要功能:记录主板上面的重要参数,包括系统时间、CPU电压与频率、各项设备的I/O地址与IRQ等,由于这些数据的记录需要耗费电力,所以主板上CMOS旁边常常有块电池

CMOS这个器件,存放数据格式是BCD码,BCD码Binary-Coded Decimal‎,用4位二进制数来表示1位十进制中的0~9这10个数码,用二进制编码形式来表示的十进制代码,所以操作系统需要进行转换,通过BCD_TO_BIN来将BCD码转换成**二进制数值 **

我们来看下CMOS器件具体有哪些信息:

当从CMOS中获取获取秒、分钟、小时,天等时间信息,再通过kernel_mktime(&time)函数,将算出来的值赋值给startup_time中,当作开机时间

来看下kernel_mktime的源码:

//  /kernel/mktime.c

//这不是库函数,它仅供内核使用,因为内核库函数不能调用。所以必须重新实现一次
//因此我们不关心小于 1970 年的年份等,但假定一切均很正常
#define MINUTE 60
#define HOUR (60*MINUTE)
#define DAY (24*HOUR)
#define YEAR (365*DAY)

//考虑进了闰年
static int month[12] = {
	0,
	DAY*(31),
	DAY*(31+29),
	DAY*(31+29+31),
	DAY*(31+29+31+30),
	DAY*(31+29+31+30+31),
	DAY*(31+29+31+30+31+30),
	DAY*(31+29+31+30+31+30+31),
	DAY*(31+29+31+30+31+30+31+31),
	DAY*(31+29+31+30+31+30+31+31+30),
	DAY*(31+29+31+30+31+30+31+31+30+31),
	DAY*(31+29+31+30+31+30+31+31+30+31+30)
};


//用于计算从 1970 年 1 月 1 日 0 时起到开机当日经过的秒数(日历时间),作为开机时间
long kernel_mktime(struct tm * tm)
{
	long res;
	int year;

	year = tm->tm_year - 70;//从70年开始算现在经过的年数,所以这里会有2000跨年的问题
/* magic offsets (y+1) needed to get leapyears right.*/
	res = YEAR*year + DAY*((year+1)/4);//通过魔数+1,来获取闰年数
	res += month[tm->tm_mon];

	if (tm->tm_mon>1 && ((year+2)%4))//若当年不是闰年并且当前月份大于 2 月份的话,我们需要减去这天
		res -= DAY;
	res += DAY*(tm->tm_mday-1); //过去的天数的秒数时间
	res += HOUR*tm->tm_hour;    //小时数的秒数时间
	res += MINUTE*tm->tm_min;   //分钟数的秒数时间
	res += tm->tm_sec;          //1 分钟内已过的秒数
	return res;  //等于从 1970 年以来经过的秒数时间
}

本文就先到这里吧,我们下期正式进入操作系统的核心模块之任务调度~


参考资料:

https://en.wikipedia.org/wiki/Teleprinter
https://www.linusakesson.net/programming/tty/index.php
https://itsfoss.com/what-is-tty-in-linux
https://elixir.bootlin.com/linux/0.12/source/kernel/chr_drv/tty_io.c
《操作系统概念》
《Linux内核完全注释5.0》


本文首发于公众号「小牛呼噜噜」,扫码关注,即可品读更多精彩文章