PA4-2 note
离完成 PA 主体部分还差最后一步:实现与外部设备的I/O
I/O: Input/Output,”交互“是 I/O 的目的与上层表现
Intro
如何进行 I/O?
一种I/O操作方式是端口映射I/O(Port-mapped I/O, PMIO)
在 PMIO 中,内存和 I/O 设备有各自的地址空间,PMIO 通常使用特殊的 CPU 指令(这里指 in 和 out 等)专门执行 I/O 操作,用于从 I/O 设备读取字节 / 写字节到 I/O 设备。因为内存和 I/O 设备地址空间存在隔离,我们需要一条 I/O 总线进行连接
计算机中有一系列的设备控制器,这些控制器一头通过 I/O 总线与主机相连,另一头则连接着被控制的外设。设备控制器中包含一系列的寄存器:
--> 控制寄存器,用于存放主机送来的控制信号;
--> 状态寄存器,用于存放设备状态如就绪和错误信息;
--> 数据缓冲寄存器,用于临时存放主机和设备间需要交换的数据信息。通常,把以上三类寄存器统称为 I/O 端口。
实际的 I/O 过程像早期的拨号通话系统,CPU 指定一个端口号(x86 架构下有 65536 个),然后通过专门的指令和这个端口进行数据交换。
不妨看看 NEMU 的端口路由表。不同的 handler 对应不同种类的外设
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 | // From nemu/src/device/port_io.c
static struct pio_handler_map
{
uint16_t port;
pio_handler handler;
} pio_handler_table[] = {
#ifdef HAS_DEVICE_IDE
//{BMR_PORT_BASE + 0, handler_bmr},
//{BMR_PORT_BASE + 1, handler_bmr},
//{BMR_PORT_BASE + 2, handler_bmr},
//{BMR_PORT_BASE + 3, handler_bmr},
//{BMR_PORT_BASE + 4, handler_bmr},
//{BMR_PORT_BASE + 5, handler_bmr},
//{BMR_PORT_BASE + 6, handler_bmr},
//{BMR_PORT_BASE + 7, handler_bmr},
{IDE_PORT_BASE + 0, handler_ide},
{IDE_PORT_BASE + 1, handler_ide},
{IDE_PORT_BASE + 2, handler_ide},
{IDE_PORT_BASE + 3, handler_ide},
{IDE_PORT_BASE + 4, handler_ide},
{IDE_PORT_BASE + 5, handler_ide},
{IDE_PORT_BASE + 6, handler_ide},
{IDE_PORT_BASE + 7, handler_ide},
#endif
#ifdef HAS_DEVICE_KEYBOARD
{KEYBOARD_DATA_PORT, handler_keyboard_data},
#endif
#ifdef HAS_DEVICE_SERIAL
{SERIAL_PORT + 0, handler_serial},
{SERIAL_PORT + 1, handler_serial},
{SERIAL_PORT + 2, handler_serial},
{SERIAL_PORT + 3, handler_serial},
{SERIAL_PORT + 4, handler_serial},
{SERIAL_PORT + 5, handler_serial},
{SERIAL_PORT + 6, handler_serial},
{SERIAL_PORT + 7, handler_serial},
#endif
#ifdef HAS_DEVICE_TIMER
{TIMER_PORT + 0, handler_timer},
{TIMER_PORT + 1, handler_timer},
{TIMER_PORT + 2, handler_timer},
{TIMER_PORT + 3, handler_timer},
#endif
#ifdef HAS_DEVICE_VGA
{VGA_DAC_READ_INDEX, vga_dac_io_handler},
{VGA_DAC_WRITE_INDEX, vga_dac_io_handler},
{VGA_DAC_DATA, vga_dac_io_handler},
{VGA_CRTC_INDEX, vga_crtc_io_handler},
{VGA_CRTC_DATA, vga_crtc_io_handler},
#endif
#ifdef HAS_DEVICE_AUDIO
{AUDIO_DATA, audio_io_handler},
{AUDIO_CTL, audio_io_handler}
#endif
};
// From nemu/include/device/port_io.h
// macro for making a port io handler
#define make_pio_handler(name) void name(uint16_t port, size_t len, bool is_write)
|
PMIO 的数据传递方式较为单一(从只能使用 in out 指令就能看得出来),具体的解码操作由软件层实现。PMIO 的好处是实现了 I/O 操作与内存操作的独立性,控制逻辑独立
另一种I/O寻址方式是内存映射I/O(memory-mapped I/O, MMIO)
在 MMIO 中,内存和 I/O 设备共享同一个地址空间,使用相同的地址总线来处理内存和 I/O 设备
这种寻址方式通过将一部分物理内存映射到 I/O 设备空间中,使得 CPU 可以通过普通的访存指令来访问设备。这种物理内存的映射对 CPU 是透明的,CPU 觉得自己是在访问内存,但实际上可能是访问了相应的 I/O 空间,这对访问设备的灵活性有了较大的提升
在 NEMU 中需要模拟四种外设:串口(Serial)、硬盘(IDE)、显示器(VGA)和键盘(Keyboard)。其中对于 VGA 的核心实现采用更灵活的 MMIO,对于其他的实现采用更独立的 PMIO:
端口映射I/O模拟
实现端口映射 I/O,一个关键是实现 in 和 out 指令。NEMU 框架提供了 I/O 接口函数:
| // called by the out instruction
void pio_write(uint16_t port, size_t len, uint32_t data)
// called by the in instruction
uint32_t pio_read(uint16_t port, size_t len)
|
因此实现起来就非常容易了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | // I/O 地址空间只有 16 位
// 因此 edx 的值应当取掩码
make_instr_func(in_b) {
// I/O 指令不影响 eax 未被覆盖的位数
// 但是我懒了,就覆盖了(
cpu.eax = pio_read((cpu.edx & 0xFFFF), 1);
return 1;
}
make_instr_func(in_v) {
cpu.eax = pio_read((cpu.edx & 0xFFFF), data_size / 8);
return 1;
}
make_instr_func(out_b) {
pio_write((cpu.edx & 0xFFFF), 1, cpu.eax);
return 1;
}
make_instr_func(out_v) {
pio_write((cpu.edx & 0xFFFF), data_size / 8, cpu.eax);
return 1;
}
|
利用端口通信的最基础的物理接口是串口,此处对串口的端口进行写操作对应控制台的输出:
串口的端口定义为从 0x3F8 开始的连续 8 个端口
实现 serial_printc() 函数的功能,将传入的参数 ch 通过serial输出到控制台上:
kernel/include/x86/io.h 中的 in out 封装实现
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 | static inline uint8_t
in_byte(uint16_t port)
{
uint8_t data;
asm volatile("in %1, %0"
: "=a"(data)
: "d"(port));
return data;
}
static inline uint32_t
in_long(uint16_t port)
{
uint32_t data;
asm volatile("in %1, %0"
: "=a"(data)
: "d"(port));
return data;
}
static inline void
out_byte(uint16_t port, uint8_t data)
{
asm volatile("out %%al, %%dx"
:
: "a"(data), "d"(port));
}
static inline void
out_long(uint16_t port, uint32_t data)
{
asm volatile("out %%eax, %%dx"
:
: "a"(data), "d"(port));
}
static inline uint16_t
in_word(uint16_t port)
{
uint16_t data;
asm volatile("in %1, %0"
: "=a"(data)
: "d"(port));
return data;
}
static inline void
out_word(uint16_t port, uint16_t data)
{
asm volatile("out %%al, %%dx"
:
: "a"(data), "d"(port));
}
|
| void serial_printc(char ch)
{
while (!serial_idle())
; // wait until serial is idle (not busy)
// print 'ch' via out instruction here
out_byte(SERIAL_PORT, (uint8_t)ch);
}
|
接下来是对硬盘的模拟,依旧是 PMIO
硬盘的端口定义为从 0x1F0 开始的连续 8 个端口,其中 0x1F0 为数据端口,当对磁盘进行读写时,从 0x1F0 开始的4个字节都可以作为数据端口使用;0x1F7 为控制和状态端口,用于标识硬盘处于读(0x20)、写(0x30)或空闲(0x40)状态
在进行读写之前,需要进行磁盘准备工作,向 0x1F3 ~ 0x1F6 这四个端口写入要读或写的扇区号(小端方式),每个扇区大小为512字节。在使用磁盘进行读写时,首先向磁盘 0x1F3 ~ 0x1F6 控制寄存器写入要读写的扇区号;然后向 0x1F7 控制寄存器写入读或写控制命令;最后可以通过读写数据端口实现对磁盘对应扇区512字节的读写操作
ide.c 源文件(手册笔误)中提供的 ide_read() 和 ide_write() 函数是磁盘驱动程序对外提供的接口,供其它模块调用实现对磁盘的读写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | void ide_read(uint8_t *buf, uint32_t offset, uint32_t len)
{
uint32_t i;
for (i = 0; i < len; i++)
{
buf[i] = read_byte(offset + i);
}
}
void ide_write(uint8_t *buf, uint32_t offset, uint32_t len)
{
uint32_t i;
for (i = 0; i < len; i++)
{
write_byte(offset + i, buf[i]);
}
}
|
为了实现从硬盘加载程序,我们需要修改 loader() 函数,实现从硬盘加载程序(已经提供一部分实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | #ifdef HAS_DEVICE_IDE
// buf 是从硬盘读取数据到内存的临时存储区域
uint8_t buf[4096];
ide_read(buf, ELF_OFFSET_IN_DISK, 4096);
elf = (void *)buf;
Log("ELF loading from hard disk.");
#else
elf = (void *)0x0;
Log("ELF loading from ram disk.");
#endif
// ...
#ifdef HAS_DEVICE_IDE
// hard disk 下的方案 from PA 4-2
ide_read(dst, ELF_OFFSET_IN_DISK + ph->p_offset, ph->p_filesz);
#else
// ram disk 下的方案 from PA 2-2
uint8_t *src = (uint8_t *)elf + ph->p_offset;
memcpy(dst, src, file_sz);
#endif
|
对键盘的模拟需要监听键盘按下和抬起这两类事件,具体内容参考手册
键盘的端口定义为从 0x60 ,完整的实现已经由框架完成
内存映射I/O模拟
采用内存映射I/O的方式来向显示器发送待显示的数据。
约定内存从物理地址 0xa0000 开始,长度为 320 * 200 字节的区间为显存区间。
为了使得物理地址被映射到 I/O 空间,我们需要借助 is_mmio() 函数判断并获取映射号,然后通过 mmio_read() / mmio_write() 进行内存映射 I/O 的访问
相关的 mmio 函数
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 | int is_mmio(uint32_t addr)
{
int i;
for (i = 0; i < nr_map; i++)
{
if (addr >= maps[i].low && addr <= maps[i].high)
{
return i;
}
}
return -1;
}
uint32_t mmio_read(uint32_t addr, size_t len, int map_NO)
{
assert(len == 1 || len == 2 || len == 4);
MMIO_t *map = &maps[map_NO];
uint32_t data = *(uint32_t *)(map->mmio_space + (addr - map->low)) & (~0u >> ((4 - len) << 3));
map->callback(addr, len, false);
return data;
}
void mmio_write(uint32_t addr, size_t len, uint32_t data, int map_NO)
{
assert(len == 1 || len == 2 || len == 4);
MMIO_t *map = &maps[map_NO];
uint32_t mask = (~0u >> ((4 - len) << 3));
memcpy_with_mask(map->mmio_space + (addr - map->low), &data, len, (void *)&mask);
maps[map_NO].callback(addr, len, true);
}
|
原来的 paddr 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | uint32_t paddr_read(paddr_t paddr, size_t len)
{
uint32_t ret = 0;
#ifdef CACHE_ENABLED
ret = cache_read(paddr, len);
#else
ret = hw_mem_read(paddr, len);
#endif
return ret;
}
void paddr_write(paddr_t paddr, size_t len, uint32_t data)
{
#ifdef CACHE_ENABLED
cache_write(paddr, len, data);
#else
hw_mem_write(paddr, len, data);
#endif
}
|
现在的 paddr 函数:
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 | uint32_t paddr_read(paddr_t paddr, size_t len)
{
uint32_t ret = 0;
#ifdef HAS_DEVICE_VGA
int map_NO = is_mmio(paddr);
if(map_NO != -1){
ret = mmio_read(paddr, len, map_NO);
return ret;
}
#endif
#ifdef CACHE_ENABLED
ret = cache_read(paddr, len);
#else
ret = hw_mem_read(paddr, len);
#endif
return ret;
}
void paddr_write(paddr_t paddr, size_t len, uint32_t data)
{
#ifdef HAS_DEVICE_VGA
int map_NO = is_mmio(paddr);
if(map_NO != -1){
mmio_write(paddr, len, data, map_NO);
}
#endif
#ifdef CACHE_ENABLED
cache_write(paddr, len, data);
#else
hw_mem_write(paddr, len, data);
#endif
}
|
接下来需要为用户进程创建 video memory 的虚拟地址空间,调用 create_video_mapping() 函数为用户进程创建 video memory 的恒等映射
为了理解 create_video_mapping() 的内容,不妨先参照 kvm.c 的内容,两者的实现大致相同,都是将某一段物理内存映射到虚拟内存。init_page() 为 kernel 建立完整的分页机制,而 create_video_mapping 专注于 VGA
kvm.c
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 | #include "common.h"
#include "x86.h"
#include "memory.h"
#include <string.h>
PDE kpdir[NR_PDE] align_to_page; // kernel page directory
PTE kptable[PHY_MEM / PAGE_SIZE] align_to_page; // kernel page tables
// 返回内核页目录的虚拟地址
PDE *get_kpdir() { return kpdir; }
/* set up page tables for kernel */
void init_page(void)
{
CR0 cr0;
CR3 cr3;
PDE *pdir = (PDE *)va_to_pa(kpdir); // vaddr -> paddr
PTE *ptable = (PTE *)va_to_pa(kptable);
uint32_t pdir_idx, ptable_idx, pframe_idx;
/* make all PDE invalid */
memset(pdir, 0, NR_PDE * sizeof(PDE)); // 初始状态无任何有效映射
/* fill PDEs and PTEs */
pframe_idx = 0;
for (pdir_idx = 0; pdir_idx < PHY_MEM / PT_SIZE; pdir_idx++)
{
pdir[pdir_idx].val = make_pde(ptable);
pdir[pdir_idx + KOFFSET / PT_SIZE].val = make_pde(ptable);
for (ptable_idx = 0; ptable_idx < NR_PTE; ptable_idx++)
{
ptable->val = make_pte(pframe_idx << 12);
pframe_idx++;
ptable++;
}
}
/* make CR3 to be the entry of page directory */
cr3.val = 0;
cr3.page_directory_base = ((uint32_t)pdir) >> 12;
write_cr3(cr3.val);
/* set PG bit in CR0 to enable paging */
cr0.val = read_cr0();
cr0.paging = 1; // 启用分页机制
write_cr0(cr0.val);
}
/* GDT in the kernel's memory, whose virtual memory is greater than 0xC0000000. */
SegDesc gdt[NR_SEGMENTS];
static void
set_segment(SegDesc *ptr, uint32_t pl, uint32_t type)
{
ptr->limit_15_0 = 0xFFFF;
ptr->base_15_0 = 0x0;
ptr->base_23_16 = 0x0;
ptr->type = type;
ptr->segment_type = 1;
ptr->privilege_level = pl;
ptr->present = 1;
ptr->limit_19_16 = 0xF;
ptr->soft_use = 0;
ptr->operation_size = 0;
ptr->pad0 = 1;
ptr->granularity = 1;
ptr->base_31_24 = 0x0;
}
/* This is similar with the one in start.S. However the previous
* one cannot be accessed in user process, because its virtual address
* below 0xC0000000, and is not in the user process' address space. */
void init_segment(void)
{
memset(gdt, 0, sizeof(gdt));
set_segment(&gdt[SEG_KERNEL_CODE], DPL_KERNEL, SEG_EXECUTABLE | SEG_READABLE);
set_segment(&gdt[SEG_KERNEL_DATA], DPL_KERNEL, SEG_WRITABLE);
write_gdtr(gdt, sizeof(gdt));
}
|
实现对 [0xA0000, 0xA0000+SCR_SIZE) 的恒等映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | #define VMEM_ADDR 0xa0000
#define SCR_SIZE (320 * 200)
#define NR_PT ((SCR_SIZE + PT_SIZE - 1) / PT_SIZE) // number of page tables to cover the vmem
PDE *get_updir();
void create_video_mapping() {
PDE *updir = get_updir();
assert(updir != NULL);
static PTE vmem_ptable[NR_PTE] align_to_page;
uint32_t pdir_idx = VMEM_ADDR >> 22;
uint32_t pte_idx = (VMEM_ADDR >> 12) & 0x3FF;
updir[pdir_idx].val = make_pde(va_to_pa(vmem_ptable));
uint32_t paddr = VMEM_ADDR;
for (int i = 0; i < VM_PAGE; i++) {
vmem_ptable[i+pte_idx].val = make_pte(paddr);
paddr += PAGE_SIZE;
}
}
|
My problems during the debugging
看到上面的 paddr 读写实现函数了吗
我先在 Typora 中写下了代码,然后忘记在 pa_nju 上 Ctrl C+V 了
于是就有了测试全部通过,但是 VGA 不显示任何内容的情况
找问题找了 2h