作者:小牛呼噜噜 ,首发于公众号「小牛呼噜噜 」
哈喽,大家好呀,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init ,继续我们的内核探索之路
本文我们继续回到init/main.c
文件中,接着看tty_init
和time_init
2个初始化函数上
1 2 3 4 5 6 7 // init/main.c void main(void) { ... tty_init(); // tty 初始化 time_init(); //设置开机启动时间 ... }
tty_init 我们来tty_init()
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 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, OPOST|ONLCR, 0 , IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE, 0 , INIT_C_CC}, 0 , 0 , 0 , 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 , 0 , 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 , 0 , 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 , 0 , 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
一词源于Teletypes
或Teletypewriters
,是一种机电 设备:电传打字机 (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设备,所定义终端相关的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ... #define MAX_CONSOLES 8 #define NR_SERIALS 2 #define NR_PTYS 4 extern int NR_CONSOLES; #define TTY_BUF_SIZE 1024 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 ; int pgrp; int session; int stopped; void (*write)(struct tty_struct * tty); struct tty_queue *read_q ; struct tty_queue *write_q ; struct tty_queue *secondary ; }; extern struct tty_struct tty_table []; extern int fg_console; ...
上面主要定义了tty_struct
结构体,其中包含了三个缓冲队列,read_q读队列,write_q写队列和seconddary辅助队列
,这3个队列非常的重要;还有一些常量MAX_CONSOLES、NR_CONSOLES
等
我们接着回到tty_init
这个方法的源码处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 ... #define QUEUES (3*(MAX_CONSOLES+NR_SERIALS+2*NR_PTYS)) static struct tty_queue tty_queues [QUEUES ]; struct tty_struct tty_table [256]; #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) #define con_table tty_table #define rs_table (64+tty_table) #define mpty_table (128+tty_table) #define spty_table (192+tty_table) int fg_console = 0 ; ... 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(); ...
其中特殊字符数组INIT_C_CC
设置的初值定义在 include/linux/tty.h
文件中
1 2 3 4 5 6 7 8 9 #define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"
我们接着看一下con_init
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 #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 #define VIDEO_TYPE_CGA 0x11 #define VIDEO_TYPE_EGAM 0x20 #define VIDEO_TYPE_EGAC 0x21 #define NPAR 16 int NR_CONSOLES = 0 ;extern void keyboard_interrupt (void ) ;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 ; blankcount = blankinterval; if (ORIG_VIDEO_MODE == 7 ) { video_mem_base = 0xb0000 ; video_port_reg = 0x3b4 ; video_port_val = 0x3b5 ; 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" ; } } else { can_do_colour = 1 ; video_mem_base = 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); if (NR_CONSOLES > MAX_CONSOLES) NR_CONSOLES = MAX_CONSOLES; if (!NR_CONSOLES) NR_CONSOLES = 1 ; video_memory /= NR_CONSOLES; display_ptr = ((char *)video_mem_base) + video_size_row - 8 ; while (*display_desc) { *display_ptr++ = *display_desc++; display_ptr++; } base = origin = video_mem_start = video_mem_base; term = video_mem_end = base + video_memory; 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 ; 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 ); } update_screen(); set_trap_gate(0x21 ,&keyboard_interrupt); outb_p(inb_p(0x21 )&0xfd ,0x21 ); a=inb_p(0x61 ); outb_p(a|0x80 ,0x61 ); outb_p(a,0x61 ); }
console.c
这个文件模块是实现控制台输入输出功能,常见的操作比如回车换行、删除、插入、清屏等,都有具体代码的实现,con_init()
函数主要初始化控制台终端 ,最终是为了终端屏幕写函数 con_write()
以及实现终端屏幕显示的控制操作做准备
con_init()
函数比较复杂,看的头疼,但我们可以将其初始化控制台终端,分为以下几个步骤来:
首先根据之前在setup.s程序取得的系统硬件参数,主要是和显示模式相关的参数,来设置变量参数。具体什么参数,可以回顾一下笔者之前的文章Linux0.12内核源码解读(3)-Setup.S
根据获得的BIOS显示方式是否等于7,则判断是单色显示卡还是彩色显示卡;进而来设置显存映射的内存区域
计算当前显示卡内存上可以开设的虚拟控制台数量NR_CONSOLES
获取并设置滚屏操作时的相关信息,比如默认滚屏开始内存位置和默认滚屏末行内存位置 ,以及其他属性和标志值
定位光标位置,设置键盘中断描述符,并取消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
这个方法的源码处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 ... void tty_init (void ) {... con_init(); for (i = 0 ; i<NR_CONSOLES ; i++) { con_table[i] = (struct tty_struct) { {ICRNL, OPOST|ONLCR, 0 , IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE, 0 , INIT_C_CC}, 0 , 0 , 0 , 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 , 0 , 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 , 0 , 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 , 0 , 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); }
上面涉及到的硬件相关参数比较多,了解一下即可,我们不仔细讲了,继续来看看rs_init()
这个函数方法,其源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ... static void init (int port) { outb_p(0x80 ,port+3 ); outb_p(0x30 ,port); outb_p(0x00 ,port+1 ); outb_p(0x03 ,port+3 ); outb_p(0x0b ,port+4 ); outb_p(0x0d ,port+1 ); (void )inb(port); } void rs_init (void ) { set_intr_gate(0x24 ,rs1_interrupt); set_intr_gate(0x23 ,rs2_interrupt); init(tty_table[64 ].read_q->data); init(tty_table[65 ].read_q->data); outb(inb_p(0x21 )&0xE7 ,0x21 ); } ...
上面set_intr_gate
这个函数我们太熟悉了,设置了0x24号和0x23号中断
,对应的中断处理函数是rs1_interrupt
和rs2_interrupt
rs_init
主要是开启串口中断,为使用串行终端设备作好准备。然而如今计算机上的串口基本被USB给取代了,我们这里就不再细究了
time_init 继续回到init/main.c
,我们来补充讲一下,tty_init
的下一个函数time_init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /init/main.c 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 ); } 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
中
主要依赖下面这2个宏
1 2 3 4 5 6 7 #define CMOS_READ(addr) ({ \ outb_p(0x80|addr,0x70); \ inb_p(0x71 ); \ }) #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
因为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
的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #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 ) }; long kernel_mktime (struct tm * tm) { long res; int year; year = tm->tm_year - 70 ; res = YEAR*year + DAY*((year+1 )/4 ); res += month[tm->tm_mon]; 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; res += tm->tm_sec; return res; }
本文就先到这里吧,我们下期正式进入操作系统的核心模块之任务调度~
参考资料:
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》
作者:小牛呼噜噜 ,首发于公众号「 小牛呼噜噜 」
感谢阅读,原创不易 ,如果有收获的话,就点个免费的[赞 ]or[转发 ],你的支持会激励我输出更高质量的文章,感谢!