目录
一、前景回顾
二、任务切换相关
三、实现TSS
四、运行测试
在上一回我们已经实现了键盘的驱动编写和环形缓冲区的实现,现在让我们来想这么一个问题:
一直以来我们的程序都在最高特权级0下工作,这意味着任何程序都和操作系统平起平坐,可以改动任何资源。如果不改变这种现状的话,某个不听话的程序甚至可以给操作系统致命一击,取而代之,那么后果将不堪设想。所以从本回开始,我们便要开始着手实现用户进程,让我们的操作系统看起来更安全一点。
下面的是我自己的一些见解。
如果让我来设计任务切换,比较简单的一种思路便是:
首先我们常说的任务,就是一个程序而已,程序在内存中被分为代码段和数据段。所以我们表征多个任务,那么便是多个代码段和数据段而已。至于任务的切换,可能需要费点心思在软件层面上实现多任务调度机制。
然后现在问题出现了:
我们知道代码段和数据段需要在全局描述符表GDT中存储,一个任务需要两个描述符来存储,而我们知道全局描述符表GDT最多也就只有2^13=8192个段描述符,那么理论上也就只能容纳4096个任务,除此之外在软件层面上实现的多任务调度机制有点类似今天的用户态多线程,效率不高且安全性有诸多问题。
所以我们来看看硬件厂商和CPU厂商是如何解决任务切换的问题的,其中最主要的就是LDT和TSS。
首先是LDT。
LDT是局部描述符表,用来存储每个任务自己的私有实体资源,也就是代码和数据。LDT的地址被保存在一个段描述符中,那么理论上我们现在可以支持8192个任务了。对于当前运行的任务,其LDT的地址被存储在LDTR寄存器中,这样CPU就能根据这个地址从中拿到任务所需要的资源。每切换一个任务时,需要用lldt指令重新加载新任务的LDT地址到LDTR寄存器中。
随后便是TSS。
单核CPU要想实现多任务,唯一的方法便是多任务共享一个CPU,也就是让多个任务轮流使用CPU。前面说道,LDT是每个任务的私有资源,所以不用担心多任务时,程序的运行资源会混乱。但这还不够。
CPU执行任务时,需要把任务所需要的数据加载到寄存器、栈和内存中,因为CPU只能直接处理这些资源中的数据,这是CPU在设计之初时工程师们决定的。于是,问题来了,任务的数据和指令是CPU的处理对象,他们被存放在内存这个低速的容器中,对于CPU来讲,内存的速度太慢了,它最喜欢寄存器。因此内存中的数据往往被加载到高速的寄存器中后再处理,等处理完毕后再将结果写入到内存中,所以,任何时候,寄存器中的内容才是任务的最新状态。当任务被换下CPU后,任务的最新状态应该被保存在某个地方,以便下次重新将此任务调度到CPU时可以恢复此任务的最新状态,这样任务才能继续执行。
于是TSS就出现了,TSS是程序员为任务单独定义的一个结构体变量,当加载新任务时,CPU自动把当前任务(旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中。
TSS和其他段也是一样的,本质上是一片存储数据的内存区域,也需要某个描述符结构来描述它,这就是TSS描述符。
和LDT一样,CPU对TSS的处理也采用了类似的方法,提供一个名为TR的寄存器来存放当前任务的TSS位置。
总结一下,如图所示:
CPU原生支持的任务切换方式是针对每一个任务都有一个LDT和一个TSS结构,这种任务切换方式,在任务切换时效率比较低,所以现代操作系统并未采纳。现代操作系统放弃了LDT,只采用了TSS,但是也没有完全采纳。我们是效仿Linux的任务切换方式的,所以拿Linux为例。
Linux为每一个CPU创建一个TSS,在各个CPU上的所有任务共享一个TSS,各CPU的TR寄存器保存各CPU上的TSS,也就是说在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会切换了。在进程切换时只需要把TSS中的SS0和esp0更新为新任务的内核栈的段地址和栈指针。
那么任务的状态信息保存在哪里呢?
对于Linux来讲,Linux只在TSS中初始化esp0和SS0以及IO位图。当CPU从低特权级进入高特权级时,也就是3特权级的用户态到0特权级的内核态时(Linux只有两个特权级)CPU会自动从TSS中获取到0特权级的栈指针,然后Linux手动执行一系列的push指令将任务的状态保存在0特权级的栈中。这个地方先留一下悬念,等后面实现的时候会再次提到。
虽然我们不完全采纳TSS,但是因为TSS是硬件所要求的,所以我们必须构造一个TSS来应付硬件。在project/userprog目录下新建tss.c和tss.h文件,除此之外还需要在global.h文件中新加部分代码。
1 #include "global.h" 2 #include "thread.h" 3 #include "print.h" 4 #include "string.h" 5 #include "tss.h" 6 7 struct tss { 8 uint32_t backlink; 9 uint32_t *esp0; 10 uint32_t ss0; 11 uint32_t *esp1; 12 uint32_t ss1; 13 uint32_t *esp2; 14 uint32_t ss2; 15 uint32_t cr3; 16 uint32_t (*eip) (void); 17 uint32_t eflags; 18 uint32_t eax; 19 uint32_t ecx; 20 uint32_t edx; 21 uint32_t ebx; 22 uint32_t esp; 23 uint32_t ebp; 24 uint32_t esi; 25 uint32_t edi; 26 uint32_t es; 27 uint32_t cs; 28 uint32_t ss; 29 uint32_t ds; 30 uint32_t fs; 31 uint32_t gs; 32 uint32_t ldt; 33 uint32_t trace; 34 uint32_t io_base; 35 }; 36 37 static struct tss tss; 38 39 /*更新tss中的esp0字段的值为pthread的0级栈*/ 40 void update_tss_esp(struct task_struct *pthread) 41 { 42 tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE); 43 } 44 45 /*创建gdt描述符*/ 46 static struct gdt_desc make_gdt_desc(uint32_t *desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) 47 { 48 uint32_t desc_base = (uint32_t)desc_addr; 49 struct gdt_desc desc; 50 desc.limit_low_word = limit & 0x0000ffff; 51 desc.base_low_word = desc_base & 0x0000ffff; 52 desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16); 53 desc.base_high_byte = (desc_base >> 24); 54 desc.attr_low_byte = (uint8_t)attr_low; 55 desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)attr_high); 56 return desc; 57 } 58 59 /*在gdt中创建tss并重新加载gdt*/ 60 void tss_init(void) 61 { 62 put_str("tss_init start n"); 63 uint32_t tss_size = sizeof(tss); 64 memset(&tss, 0, tss_size); 65 tss.ss0 = SELECTOR_K_STACK; 66 tss.io_base = tss_size; 67 /*gdt的基地址为0x900,把tss放到第4个地址,也就是0x900+0x20的位置*/ 68 *((struct gdt_desc *)0xc0000920) = make_gdt_desc((uint32_t *)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH); 69 /*为用户进程提前作准备*/ 70 /*在gdt中添加dpl为3的数据段和代码段描述符*/ 71 *((struct gdt_desc *)0xc0000928) = make_gdt_desc((uint32_t *)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH); 72 *((struct gdt_desc *)0xc0000930) = make_gdt_desc((uint32_t *)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH); 73 //while(1); 74 /*gdt 16位的limit 32位的段基址*/ 75 uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); 76 asm volatile ("lgdt %0" : : "m" (gdt_operand)); 77 asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); 78 79 put_str("tss_init and ltr donen"); 80 }
tss.c
1 #ifndef __USERPROG_TSS_H 2 #define __USERPROG_TSS_H 3 #include "stdint.h" 4 5 void tss_init(void); 6 static struct gdt_desc make_gdt_desc(uint32_t *desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high); 7 void update_tss_esp(struct task_struct *pthread); 8 #endif
tss.h
1 ... 2 3 /******************** TSS描述符属性**********************/ 4 #define TSS_DESC_D 0 5 #define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0) 6 #define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS) 7 8 #define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2) + RPL0) 9 10 ...
global.h
注释写的比较清楚,我们挑重点来讲。注意这个函数:
1 /*更新tss中的esp0字段的值为pthread的0级栈*/ 2 void update_tss_esp(struct task_struct *pthread) 3 { 4 tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE); 5 }
这个函数的作用就是用来更新TSS中的esp0。我们前面在实现线程的时候,线程的PCB上有一块名为中断栈的区域一直没有被使用,现在就被用上了。忘记的话点这里。其实它就是这里所说的0级栈,用户进程从3特权级进入0特权级时,CPU会自动从TSS中获取到0特权级的栈指针,也就是0级栈。
最后还需要修改一下mbr.S和loader.S文件,为什么呢?原来在loader.S文件中,我们在开头通过jmp loader_start跳转到后面执行loader部分, 在这行代码后面实现了GDT表的建立,而jmp loader_start这行代码是需要占据3个字节的内容,这样就导致GDT表位于内存0x903地址处,不利于后面的对齐,所以我们为了让GDT表位于0x900处,需要移除jmp loader_start这行代码,但是我们知道这行代码是mbr跳转执行到的,为了让mbr直接跳转到loader部分,我们需要修改mbr.S中的最后跳转语句,修改为jmp LOADER_BASE_ADDR + 0x206 这个0x206怎么来的呢,GDT表总共有64个描述符,再加上gdt指针占用6个字节,总共便是64*8+6=518个字节,也就是0x206。这里就不多啰嗦了,直接将修改好的mbr.S和loader.S附上。
1 %include "boot.inc" 2 section MBR vstart=0x7c00 3 mov ax, cs 4 mov ds, ax 5 mov es, ax 6 mov ss, ax 7 mov fs, ax 8 mov sp, 0x7c00 9 mov ax, 0xb800 10 mov gs, ax 11 12 ;利用int 0x10 的0x06号功能实现清屏 13 mov ax, 0x600 14 mov bx, 0x700 15 mov cx, 0 16 mov dx, 0x184f 17 18 int 0x10 19 20 mov ah, 3 21 mov bh, 0 22 23 int 0x10 24 ;输出字符串“HELLO MBR” A表示绿色背景闪烁,4表示前景色为红色 25 mov byte [gs:0x00],'H' 26 mov byte [gs:0x01],0xA4 27 28 mov byte [gs:0x02],'E' 29 mov byte [gs:0x03],0xA4 30 31 mov byte [gs:0x04],'L' 32 mov byte [gs:0x05],0xA4 33 34 mov byte [gs:0x06],'L' 35 mov byte [gs:0x07],0xA4 36 37 mov byte [gs:0x08],'O' 38 mov byte [gs:0x09],0xA4 39 40 mov byte [gs:0x0A],' ' 41 mov byte [gs:0x0B],0xA4 42 43 mov byte [gs:0x0C],'M' 44 mov byte [gs:0x0D],0xA4 45 46 mov byte [gs:0x0E],'B' 47 mov byte [gs:0x0F],0xA4 48 49 mov byte [gs:0x10],'R' 50 mov byte [gs:0x11],0xA4 51 52 mov eax, LOADER_START_SECTOR ;起始扇区lba的地址 53 mov bx, LOADER_BASE_ADDR ;loader将要被写入的内存地址 54 mov cx, 4 ;待读入的扇区数 55 call rd_disk_m_16 ;调用函数,将loader写入到内存中 56 57 jmp LOADER_BASE_ADDR + 0x206 58 59 ;--------------------------------------- 60 ;功能:读取硬盘n个扇区 61 rd_disk_m_16: 62 mov esi, eax ;备份eax,eax中存放了扇区号,这里为0x2 63 mov di, cx ;备份cx,cx中存放待读入的扇区数 64 65 ;读写硬盘: 66 ;第一步:设置要读取的扇区数 67 mov dx, 0x1f2 68 mov al, cl 69 out dx, al 70 71 mov eax, esi 72 73 ;第二步:将lba地址存入到0x1f3 ~ 0x1f6 74 ;lba地址7-0位写入端口0x1f3 75 mov dx, 0x1f3 76 out dx, al 77 78 ;lba地址15-8位写入端口0x1f4 79 mov cl, 8 80 shr eax, cl 81 mov dx, 0x1f4 82 out dx, al 83 84 ;lba地址23-16位写入端口0x1f5 85 shr eax, cl 86 mov dx, 0x1f5 87 out dx, al 88 89 shr eax, cl 90 and al, 0x0f 91 or al, 0xe0 92 mov dx, 0x1f6 93 out dx, al 94 95 ;第三步:向0x1f7端口写入读命令,0x20 96 mov dx, 0x1f7 97 mov al, 0x20 98 out dx, al 99 100 ;第四步:检测硬盘状态 101 .not_ready: 102 nop 103 in al, dx 104 and al, 0x88 105 cmp al, 0x08 106 jnz .not_ready 107 108 ;第五步:从0x1f0端口读数据 109 mov ax, di 110 mov dx, 256 111 mul dx 112 mov cx, ax 113 ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要 114 ;di*512/2次,所以di*256 115 mov dx, 0x1f0 116 .go_on_read: 117 in ax, dx 118 mov [bx], ax 119 add bx,2 120 loop .go_on_read 121 ret 122 ;--------------------------------------- 123 124 times 510-($-$$) db 0 125 db 0x55, 0xaa
mbr.S
1 %include "boot.inc" 2 section loader vstart=LOADER_BASE_ADDR 3 LOADER_STACK_TOP equ LOADER_BASE_ADDR 4 ;构建gdt及其内部描述符 5 GDT_BASE: dd 0x00000000 6 dd 0x00000000 7 CODE_DESC: dd 0x0000FFFF 8 dd DESC_CODE_HIGH4 9 DATA_STACK_DESC: dd 0x0000FFFF 10 dd DESC_DATA_HIGH4 11 VIDEO_DESC: dd 0x80000007 12 dd DESC_VIDEO_HIGH4 13 14 GDT_SIZE equ $-GDT_BASE 15 GDT_LIMIT equ GDT_SIZE-1 16 times 60 dq 0 ;此处预留60个描述符的空位 17 18 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 19 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 20 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 21 22 ;以下是gdt指针,前2个字节是gdt界限,后4个字节是gdt的起始地址 23 gdt_ptr dw GDT_LIMIT 24 dd GDT_BASE 25 26 ;---------------------进入保护模式------------ 27 loader_start: 28 ;一、打开A20地址线 29 in al, 0x92 30 or al, 0000_0010B 31 out 0x92, al 32 33 ;二、加载GDT 34 lgdt [gdt_ptr] 35 36 ;三、cr0第0位(pe)置1 37 mov eax, cr0 38 or eax, 0x00000001 39 mov cr0, eax 40 41 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线 42 43 [bits 32] 44 p_mode_start: 45 mov ax, SELECTOR_DATA 46 mov ds, ax 47 mov es, ax 48 mov ss, ax 49 mov esp, LOADER_STACK_TOP 50 mov ax, SELECTOR_VIDEO 51 mov gs, ax 52 53 mov byte [gs:160], 'p' 54 ;--------------------------------------- 55 56 ;------------------开启分页机制----------------- 57 ;一、创建页目录表并初始化页内存位图 58 call setup_page 59 60 ;将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载 61 sgdt [gdt_ptr] 62 ;将gdt描述符中视频段描述符中的段基址+0xc0000000 63 mov ebx, [gdt_ptr + 2] 64 or dword [ebx + 0x18 + 4], 0xc0000000 65 66 ;将gdt的基址加上0xc0000000使其成为内核所在的高地址 67 add dword [gdt_ptr + 2], 0xc0000000 68 69 add esp, 0xc0000000 ;将栈指针同样映射到内核地址 70 71 ;二、将页目录表地址赋值给cr3 72 mov eax, PAGE_DIR_TABLE_POS 73 mov cr3, eax 74 75 ;三、打开cr0的pg位 76 mov eax, cr0 77 or eax, 0x80000000 78 mov cr0, eax 79 80 ;在开启分页后,用gdt新的地址重新加载 81 lgdt [gdt_ptr] 82 mov byte [gs:160], 'H' 83 mov byte [gs:162], 'E' 84 mov byte [gs:164], 'L' 85 mov byte [gs:166], 'L' 86 mov byte [gs:168], 'O' 87 mov byte [gs:170], ' ' 88 mov byte [gs:172], 'P' 89 mov byte [gs:174], 'A' 90 mov byte [gs:176], 'G' 91 mov byte [gs:178], 'E' 92 93 ;--------------------------------------------- 94 95 ;--------------------拷贝内核文件并进入kernel-------------------------- 96 mov eax, KERNEL_START_SECTOR ;kernel.bin所在的扇区号 0x09 97 mov ebx, KERNEL_BIN_BASE_ADDR ;从磁盘读出后,写入到ebx指定的地址0x70000 98 mov ecx, 200 ;读入的扇区数 99 100 call rd_disk_m_32 101 102 ;由于一直处在32位下,原则上不需要强制刷新,但是以防万一还是加上 103 ;跳转到kernel处 104 jmp SELECTOR_CODE:enter_kernel 105 106 enter_kernel: 107 call kernel_init 108 mov esp, 0xc009f000 ;更新栈底指针 109 jmp KERNEL_ENTRY_POINT ;内核地址0xc0001500 110 ;jmp $ 111 ;---------------------将kernel.bin中的segment拷贝到指定的地址 112 kernel_init: 113 xor eax, eax 114 xor ebx, ebx ;ebx记录程序头表地址 115 xor ecx, ecx ;cx记录程序头表中的program header数量 116 xor edx, edx ;dx记录program header 尺寸,即e_phentsize 117 118 ;偏移文件42字节处的属性是e_phentsize, 表示program header大小 119 mov dx, [KERNEL_BIN_BASE_ADDR + 42] 120 121 ;偏移文件28字节处的属性是e_phoff 122 mov ebx, [KERNEL_BIN_BASE_ADDR + 28] 123 124 add ebx, KERNEL_BIN_BASE_ADDR 125 mov cx, [KERNEL_BIN_BASE_ADDR + 44] 126 127 .each_segment: 128 cmp byte [ebx + 0], PT_NULL 129 je .PTNULL 130 131 ;为函数memcpy压入参数,参数是从右往左压入 132 push dword [ebx + 16] 133 mov eax, [ebx + 4] 134 add eax, KERNEL_BIN_BASE_ADDR 135 push eax 136 push dword [ebx + 8] 137 call mem_cpy 138 add esp, 12 139 140 .PTNULL: 141 add ebx, edx 142 loop .each_segment 143 ret 144 145 ;-----------逐字节拷贝mem_cpy(dst, src, size) 146 mem_cpy: 147 cld 148 push ebp 149 mov ebp, esp 150 push ecx 151 mov edi, [ebp + 8] 152 mov esi, [ebp + 12] 153 mov ecx, [ebp + 16] 154 rep movsb 155 156 pop ecx 157 pop ebp 158 ret 159 ;--------------------------------------------------- 160 161 162 163 164 ;--------------函数声明------------------------ 165 ;setup_page:(功能)设置分页------------ 166 setup_page: 167 ;先把页目录占用的空间逐字节清0 168 mov ecx, 4096 169 mov esi, 0 170 .clear_page_dir: 171 mov byte [PAGE_DIR_TABLE_POS + esi], 0 172 inc esi 173 loop .clear_page_dir 174 175 ;开始创建页目录项 176 .create_pde: 177 mov eax, PAGE_DIR_TABLE_POS 178 add eax, 0x1000 ;此时eax为第一个页表的位置 179 mov ebx, eax 180 181 ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存 182 ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问 183 or eax, PG_US_U | PG_RW_W | PG_P 184 185 ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性 186 mov [PAGE_DIR_TABLE_POS + 0x0], eax 187 188 mov [PAGE_DIR_TABLE_POS + 0xc00], eax 189 190 ;使最后一个目录项指向页目录表自己的地址 191 sub eax, 0x1000 192 mov [PAGE_DIR_TABLE_POS + 4092], eax 193 194 ;下面创建页表项(PTE) 195 mov ecx, 256 ;1M低端内存/每页大小4K=256 196 mov esi, 0 197 mov edx, PG_US_U | PG_RW_W | PG_P 198 .create_pte: ;创建page table entry 199 mov [ebx + esi*4], edx 200 add edx, 4096 201 inc esi 202 loop .create_pte 203 204 ;创建内核其他页表的PDE 205 mov eax, PAGE_DIR_TABLE_POS 206 add eax, 0x2000 ;此时eax为第二个页表的位置 207 or eax, PG_US_U | PG_RW_W | PG_P 208 mov ebx, PAGE_DIR_TABLE_POS 209 mov ecx, 254 ;范围为第769~1022的所有目录项数量 210 mov esi, 769 211 .create_kernel_pde: 212 mov [ebx + esi*4], eax 213 inc esi 214 add eax, 0x1000 215 loop .create_kernel_pde 216 ret 217 218 219 ;rd_disk_m_32:(功能)读取硬盘n个扇区------------ 220 rd_disk_m_32: 221 mov esi,eax ;备份eax,eax中存放了扇区号 222 mov di,cx ;备份cx,cx中存放待读入的扇区数 223 224 ;读写硬盘: 225 ;第一步:设置要读取的扇区数 226 mov dx,0x1f2 227 mov al,cl 228 out dx,al 229 230 mov eax,esi 231 232 ;第二步:将lba地址存入到0x1f3 ~ 0x1f6 233 ;lba地址7-0位写入端口0x1f3 234 mov dx,0x1f3 235 out dx,al 236 237 ;lba地址15-8位写入端口0x1f4 238 mov cl,8 239 shr eax,cl 240 mov dx,0x1f4 241 out dx,al 242 243 ;lba地址23-16位写入端口0x1f5 244 shr eax,cl 245 mov dx,0x1f5 246 out dx,al 247 248 shr eax,cl 249 and al,0x0f 250 or al,0xe0 251 mov dx,0x1f6 252 out dx,al 253 254 ;第三步:向0x1f7端口写入读命令,0x20 255 mov dx,0x1f7 256 mov al,0x20 257 out dx,al 258 259 ;第四步:检测硬盘状态 260 .not_ready: 261 nop 262 in al,dx 263 and al,0x88 264 cmp al,0x08 265 jnz .not_ready 266 267 ;第五步:从0x1f0端口读数据 268 mov ax,di 269 mov dx,256 270 mul dx 271 mov cx,ax 272 ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要 273 ;di*512/2次,所以di*256 274 mov dx,0x1f0 275 .go_on_read: 276 in ax,dx 277 mov [ebx],ax 278 add ebx,2 279 loop .go_on_read 280 ret 281 ;----------------------------------------------
loader.S
运行测试后,tss成功初始化。
在bochs控制台输入info gdt可以看到GDT表的内容,可以看到现在有7个描述符,在GDT中第4个描述符是刚安装好的TSS段描述符,其显示为32-Bit TSS(Busy),说明TSS的B位被CPU置1了,TSS已经生效了。
本回到此结束,预知后事如何,请看下回分解。