0%

c++ 的高性能内存池

一般,使用malloc freenew delete来动态管理内存,因为牵扯到系统调用,因此效率很低。内存池的思想就是使用很少的系统调用,申请一大块内存自己管理。内存池向系统申请大块内存,然后再分为小片给程序使用,每次请求内存,起始都是在内存池里拿内存而非通过系统调用。(线程池和对象池有着类似的思想)。内存池的优点:

  • 原理上比malloc和new快
  • 分配内存时不存在开销(overhead)
  • 几乎不存在内存碎片(为什么???)
  • 无需一个个释放对象,只需要释放内存池即可

缺点:

  • 需要知道分配对象的大小
  • 需要根据应用的情况来调节
1
2
3
4
5
6
/*******
* 赋值 *
*******/
// 使用 = delete 禁用拷贝赋值,只保留移动赋值
MemoryPool& operator=(const MemoryPool& mp) = delete;
MemoryPool& operator=(const MemoryPool&& mp) noexcept;
1
2
// static_assert: 编译期断言检查
static_assert(BlockSize >= 2 * sizeof(Slot_), "BlockSize too small.")

每个slot作为一个块

1
2
3
4
union Slot_ {
value_type element;
Slot_* next;
};

一个奇怪的构造函数

1
2
3
4
5
6
7
8
9
// MemoryPool.h
MemoryPool(const MemoryPool& mp) noexcept; // 拷贝构造函数

// MemoryPool.cpp
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool(const MemoryPool& mp) noexcept: MemoryPool() {}

// 推测其意思就是调用无参数构造函数。
// 可是明明走的是拷贝构造函数

析构函数中的强制类型转换

1
2
3
4
5
6
7
8
9
10
11
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::~MemoryPool()
noexcept
{
slot_pointer_ curr = currentBlock_;
while (curr != nullptr) {
slot_pointer_ prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}
img
1
2
3
4
Slot_ *currentBlock_;
Slot_ *currentSlot_;
Slot_ *lastSlot_;
Slot_ *freeSlots_;

内存池拥有四个指针,freeSlots应该就是空闲slot

一个内存池MemoryAlloc包含若干个block块,空闲内存块通过链表的形式记录,block内以slot呈现。

首先我们使用 typedef 定义了某个东西成为了 allocator,这是毫无疑问的,而这个东西却是:

1
typename Alloc::template rebind<Node>::other

它长得很奇怪,这其实是为了解决让编译器不认识代码的问题而出现的写法(语法)。

首先我们定义了 Alloc = std::allocator<T>,而 rebind 其实是 std::allocator 的一个成员。巧就巧在,rebind 本身又是另一个模板, C++ 称其为 dependent name。完整的形式本来应该是:

1
std::allocator<T>::rebind<Node>::other

但是模板的相关解析已经在 <T> 出现过了,后面的 <Node> 中的 < 只能被解释为小于符号,这会导致编译出错。为了表示 dependent name 是一个模板,就必须使用 template 前缀。如果没有 template 前缀,< 会被编译器解释为小于符号。所以,我们必须写成下面的形式:

1
std::allocator<T>::template rebind<Node>::other

最后,编译器在其实根本没有任何办法来区分 other 究竟是一个类型,还是一个成员。但我们其实知道 other 是一个类型,所以使用 typename 来明确指出这是一个类型,最终才有了:

1
typename std::allocator<T>::template rebind<Node>::other

四个文件

memory_pool

​ |– MemoryPool.h

​ |– MemoryPool.cpp

​ |– StackAlloc.h

​ |– Test.cpp

MemoryPool.h

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//
// Created by 张庆 on 2022/7/22.
//

#ifndef TINY_RECORD_MEMORYPOOL_H
#define TINY_RECORD_MEMORYPOOL_H

#pragma once
#include <cstddef> // for size_t
#include <cstdint> // for uintptr_t
#include <utility> // for std::swap, std::forward

template <typename T, size_t BlockSize = 4096> class MemoryPool {
public:
template<typename U>
struct rebind{ // 结构体 rebind
typedef MemoryPool<U> other;
};

/***********************
* T 类型的构造和析构
* ********************/
// 内存池的构造和析构
MemoryPool() noexcept; // 构造函数
MemoryPool(const MemoryPool &mem) noexcept; // 拷贝构造
MemoryPool(MemoryPool &&mem) noexcept; // 移动构造

// 拷贝构造函数,以另一个类型为模板 MemoryPool<U>
template <typename U>
MemoryPool(const MemoryPool<U> &mem) noexcept;

// 析构函数
~MemoryPool() noexcept;


// 赋值,禁用拷贝赋值,只保留移动赋值
// 我猜测拷贝复制对于内存池而言会出现和c++ string使用cow一样的问题
MemoryPool &operator = (const MemoryPool &mem) = delete;
MemoryPool &operator = (MemoryPool &&mem) noexcept;

// 获取元素的地址
// 返回引用类型变量地址
inline T *address(T &element) const noexcept{return &element;} // 括号后的const不可修改函数内的内容
// 返回常量引用类型变量地址
inline const T *address(const T &element) const noexcept{return &element;}


/***********************
* Allocator
* ********************/
/** 拥有四个接口
1. 从内存池为对象分配内存
2. 从内存池为对象释放内存a
3. 构造和析构对象
4. new与delete
**/

/***********************
* 从内存池为对象分配内存
* ********************/
// 分配足够的存储空间来存储T的n个对象实例
// hint 是建议地址,没有特别需求设为0。
// 函数会返回一个实际map的地址
inline T *allocate(size_t n = 1, const T *hint = nullptr){
// 有freeSLot_,那么可以直接往后使用
if(freeSlot_ !=nullptr){
T *result = reinterpret_cast<T *>(freeSlot_); // 指针的一个强制转换
freeSlot_ = freeSlot_->next;
return result;
}else{ // do not have freeSlot_
// freeSlot_还未初始化或者已经用完
if(currentSlot_ >= lastSlot_){ // 耗尽
allocateBlock();
}
return reinterpret_cast<T *>(currentSlot_++);
}
}


/***********************
* 从内存池为对象释放内存
* ********************/
inline void deallocate(T *p, size_t n = 1){ // n是对象个数
// TODO:解决??? 只释放了一个SLot. Block貌似就是一个对象
if(p != nullptr){
// freeSlot 头插法建立
// 释放的链表头插法进去,如果freeSlot_目前是A->B->C, 释放了个X,那么变为X->A->B->C
reinterpret_cast<Slot_ *>(p)->next = freeSlot_;
freeSlot_ = reinterpret_cast<Slot_ *>(p);
}
}


/***********************
* 构造与析构对象
* ********************/
// 使用p指向的args参数构造一个对象
template<typename U, typename... Args>
inline void construct(U* p, Args &&... args){
new (p) U(std::forward<Args>(args)...);
// 在p指向的内存,用给定模板参数列表(转发、解包)新建一个U类型的对象
}

// 调用p指向的对象的析构函数
template<typename U>
inline void destroy(U *p){
p->~U();
}


/***********************
* new 与 delete
* ********************/
template<typename... Args> // 变参模板
inline T *newElement(Args &&... args){
T *result = allocate();
construct(result, std::forward<Args>(args)...);
// std::forward的模板参数必须是<Args>,而不能是<Args...>
// 这是由于我们不能对Args进行解包之后传递给std::forward
return result;
}

inline void deleteElement(T *p){
if(p != nullptr){ // p指向的对象不为空
p->~T(); // 对象析构
deallocate(p); // 释放指针
}
}

/***********************
* 最多能容纳多少个对象
* ********************/
inline size_t max_size() const noexcept{
// -1 表示当前能寻址的最大内存地址
size_t max_Block = -1/BlockSize;
// 每个Block能容纳T的个数
size_t max_T = (BlockSize - sizeof(char *))/sizeof(Slot_);
return max_Block*max_T;
}

private:
union Slot_{
T element;
Slot_ *next;
};

Slot_ *currentBlock_;
Slot_ *currentSlot_;
Slot_ *lastSlot_;
Slot_ *freeSlot_;

void allocateBlock();

/***********************
* 对齐align
* ********************/
// uintptr_t是可以容纳指针大小的integer type,
// 但是size_t不一定是,在一些具有分段寻址机制的平台,size_t可能比一个指针的大小还小。
// padding代表需要往后移多少个位置,才能按照align的大小进行对齐
// 比如 2 4, 那么还需要后移2个位置
inline size_t padPointer(char* p, size_t align) const noexcept{
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
size_t padding = (align - addr) % align;
// 测试一下,开源代码给的测试用例
// printf("addr = %08lx\n, align = %02lx, and padding = %02lx\n", addr, align, padding);
return padding;
}

// 编译期断言检查
static_assert(BlockSize >= 2*sizeof(Slot_),"static_assert: Block is too small!");
};








#endif //TINY_RECORD_MEMORYPOOL_H

MemoryPool.cpp

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
//
// Created by 张庆 on 2022/7/22.
//
#include "MemoryPool.h"
using namespace std;

// 默认构造函数
template<typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool() noexcept{
currentBlock_ = nullptr;
currentSlot_ = nullptr;
lastSlot_ = nullptr;
freeSlot_ = nullptr;
}

// 拷贝构造函数
template<typename T, size_t BlockSize>
MemoryPool< T, BlockSize>::MemoryPool(const MemoryPool &mem) noexcept
:MemoryPool() {}
// TODO: // 为什么调用无参数构造函数???

// 拷贝构造
template<typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool(MemoryPool &&mem) noexcept{
currentBlock_ = mem.currentBlock_;
mem.currentBlock_ = nullptr; // 释放之前的block指针
currentSlot_ = mem.currentSlot_;
lastSlot_ = mem.lastSlot_;
freeSlot_ = mem.freeSlot_;
}

// 另一个类型U
template<typename T, size_t BlockSize>
template<typename U>
MemoryPool< T, BlockSize>::MemoryPool(const MemoryPool<U> &mem) noexcept
:MemoryPool() {}

// 析构一个Block
template<typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::~MemoryPool() noexcept {
Slot_ * cur = currentBlock_;
while(cur !=nullptr){
Slot_ *nxt = cur->next;
::operator delete(reinterpret_cast<void *>(cur));
cur = nxt;
}
}

// 移动赋值
// 拷贝赋值已经被delete, 即禁用
// 重载operator=
template<typename T, size_t BlockSize>
MemoryPool<T, BlockSize> &MemoryPool<T, BlockSize>::
operator=(MemoryPool &&mem) noexcept {
if(this != &mem){ // 若不是给自己赋值
// 交换第一块Block的位置
// swap内部也是移动赋值机制,确保了不会有多个指针指向一块空间
// 这样不会出现一块空间被重复释放的问题
std::swap(currentBlock_, mem.currentBlock_);
currentSlot_ = mem.currentSlot_;
lastSlot_ = mem.lastSlot_;
freeSlot_ = mem.freeSlot_;
}
return *this; // 对象
}

// 为内存池分配内存
// allocateBlock()
template<typename T, size_t BlockSize>
void MemoryPool<T, BlockSize>::allocateBlock() {
// Block 也按照头插法建立
char *newBlock = reinterpret_cast<char *>(::operator new(BlockSize));
reinterpret_cast<Slot_ *>(newBlock)->next = currentBlock_; // 头插
currentBlock_ = reinterpret_cast<Slot_ *>(newBlock);
// 需要做一个内存对齐,因为第一个指针指向Block,而不是Slot_
// 第一个指针之后的都是Slot_
char *curPoint = newBlock + sizeof(Slot_ *);
// alignof(type)将指定类型的对齐方式返回为类型的值 size_t
size_t padding = padPointer(curPoint, alignof(Slot_));
currentSlot_ = reinterpret_cast<Slot_ *>(curPoint + padding);
lastSlot_ = reinterpret_cast<Slot_ *>(newBlock + BlockSize - sizeof(Slot_) + 1);
}

StackAlloc.h

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
//
// Created by 张庆 on 2022/7/22.
//

#ifndef TINY_RECORD_STACKALLOC_H
#define TINY_RECORD_STACKALLOC_H

#include <memory>

/**
* 实现堆栈结构的模板类
* 使用allocator_traits(specifically with MemoryPool)
* **/

template<typename T>
struct StackNode_{
T data;
StackNode_* prev;
};

/**
* T 是存储在stack中的对象,alloc 是所使用的allocator
* **/
template<typename T, typename alloc = std::allocator<T>>
class StackAlloc {
public:
typedef StackNode_<T> node;
// rebind 是 std::allocator的一个成员
// rebind本身就是另一个模板, 形式类似于 std::allocator<T>::rebind<Node>::other
// template 是为了说明other是个类型,而不是一个成员
typedef typename alloc::template rebind<node>::other allocator;

/** 构造函数 **/
StackAlloc(){head_ = 0;}

/** 析构函数 **/
~StackAlloc(){clear();}

/** 当前栈是否为空 */
bool isEmpty(){
return (head_ == 0);
}

/** 清除所有元素,stack清空 **/
void clear(){ // T is an object
node* cur = head_;
while(cur!=0){
node* temp = cur->prev;
allocator_.destroy(cur);
allocator_.deallocate(cur,1);
cur = temp;
}
head_ = 0;
}


/** put an element on the top of the stack*/
// element is an object
void push(T element){
node* newEle = allocator_.allocate(1); // 分配一个新的Slot_
allocator_.construct(newEle, node()); // inline void construct(U* p, Args&&... args){
newEle->data = element;
newEle->prev = head_;
head_ = newEle;
}

T top(){
return head_->data;
}
/** 移除顶端元素,返回对象 */
T pop(){
node* cur = head_->prev;
T cur_ele = head_->data;
allocator_.destroy(head_);
allocator_.deallocate(head_,1);
head_ = cur;
return cur_ele;
}

private:
allocator allocator_;
node* head_;
};


#endif //TINY_RECORD_STACKALLOC_H

Test.cpp

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
//
// Created by 张庆 on 2022/7/22.
//

#include <iostream>
#include <cassert>
#include <time.h>
#include <vector>

#include "MemoryPool.cpp"
#include "StackAlloc.h"


#define MAXELES 100000
#define REPS 50

using namespace std;


int main(){
/*****************
*
* use default allocator
*
* ***************/
cout<< "======Use default allocator======"<<endl;
clock_t start;

// 使用 StackAlloc, 此时allocator为默认allocator<int>
StackAlloc<int, std::allocator<int>> stackDefault;

start = clock();
for(int i = 0;i<REPS;++i){
assert(stackDefault.isEmpty()); // 为0则中断
for(int j = 0;j<MAXELES/4;++j){
stackDefault.push(j);
stackDefault.push(j);
stackDefault.push(j);
stackDefault.push(j);
}

for(int j = 0;j<MAXELES/4;++j){
stackDefault.pop();
stackDefault.pop();
stackDefault.pop();
stackDefault.pop();
}
}

cout<<"default allocator time: ";
cout<< ((double)(clock() - start)/CLOCKS_PER_SEC) <<endl;


/*****************
*
* use MemoryPool
*
* ***************/
cout<< "======Use MemoryPool======"<<endl;
StackAlloc<int, MemoryPool<int>> stackWithMem;

start = clock();
for(int i = 0;i<REPS;++i){
assert(stackWithMem.isEmpty()); // 为0则中断
for(int j = 0;j<MAXELES/4;++j){
stackWithMem.push(j);
stackWithMem.push(j);
stackWithMem.push(j);
stackWithMem.push(j);
}

for(int j = 0;j<MAXELES/4;++j){
stackWithMem.pop();
stackWithMem.pop();
stackWithMem.pop();
stackWithMem.pop();
}
}

cout<<"default allocator time: ";
cout<< ((double)(clock() - start)/CLOCKS_PER_SEC) <<endl;


/**
* 用动态数组vector代替stack
* */
cout<< "======Use vector======"<<endl;
std::vector<int> vec;
start = clock();

for(int i = 0;i<REPS;++i){
assert(vec.empty()); // 为0则中断
for(int j = 0;j<MAXELES/4;++j){
vec.push_back(j);
vec.push_back(j);
vec.push_back(j);
vec.push_back(j);
}

for(int j = 0;j<MAXELES/4;++j){
vec.pop_back();
vec.pop_back();
vec.pop_back();
vec.pop_back();
}
}

cout<<"vector time: ";
cout<< ((double)(clock() - start)/CLOCKS_PER_SEC) <<endl;
return 0;
}

image-20220723224515428

Network Driver (default final project)

Lab 6 Network Driver

网卡基于E1000芯片

除了编写驱动之外,还需要创建一个 system call 来访问我们的驱动程序。我们将实现缺失的网络服务器代码,以在网络堆栈和驱动程序之间传输数据包。我们还可以通过完成web服务器将所有内容绑定在一起。使用新的web服务器,我们能够从文件系统中提供文件。

没有框架文件,没有固定的系统调用接口,许多设计决策都留给您。

QEMU’s virtual network

QEMU是一个模拟处理器软件,默认提供一个运行在IP 10.0.2.2 上的虚拟路由,并为JOS分配IP 10.0.2.15 ,为了解决内网连接问题,host无法运行在QEMU上的web server,我们配置QEMU在主机某个端口上运行服务。该端口只需连接到JOS中的某个端口,并在真实主机和虚拟网络之间来回传送数据。

image-20220710105135537

echo server是回显服务,用户输入什么,服务器将输入信息返回给客户端。

webserver 是指驻留于因特网上某种类型计算机的程序。当Web浏览器(客户端)连到服务器上并请求文件时,服务器将处理该请求并将文件反馈到该浏览器上,附带的信息会告诉浏览器如何查看该文件(即文件类型)。服务器使用HTTP(超文本传输协议)与客户机浏览器进行信息交流,这就是人们常把它们称为HTTP服务器的原因。目前最主流的三个Web服务器是Apache Nginx IIS。

Packet Inspection

Makefile 配置了QEMU的网络栈,将进出的网络包记录到 qemu.pcap中。我们可以使用图形化的 wireshark,或是使用命令行获得网络包的hex/ASCII信息。

1
tcpdump -XXnr qemu.pcap

Debugging the E1000

The E1000 can produce a lot of debug output, so you have to enable specific logging channels. Some channels you might find useful are:

Flag Meaning
tx 日志发送操作
txerr 日志发送错误
rx Log changes to RCTL
rxfilter Log 日志入方向过滤
rxerr Log 接受错误
unknown Log 对未知寄存器进行读写操作
eeprom Log reads from the EEPROM(带电可擦可编程只读存储器)
interrupt Log 记录中断和中断寄存器的更改。

To enable “tx” and “txerr” logging, for example, use make E1000_DEBUG=tx,txerr ….

The Network Server

从零开始写一个网络栈是十分困难的,因此我们使用lwIP, 其是一个开源的轻量级TCP/IP协议套件,其中包含网络栈。

网络服务器实际上是四个环境的组合:

  • 核心网络服务器环境(包括套接字调用调度程序和lwIP)

  • 输入环境

  • 输出环境

  • 计时器的环境

我们将实现图中标记为绿色的部分。

MIT6.828 Lab6_Network Driver

The Core Network Server Environment

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

img

img

img

For each user environment IPC, the dispatcher in the network server calls the appropriate BSD socket interface function provided by lwIP on behalf of the user.

Regular user environments do not use the nsipc_* calls directly. Instead, they use the functions in lib/sockets.c, which provides a file descriptor-based sockets API.

文件服务器和网络服务器之间有一个关键的区别。像accept和recv这样的BSD套接字调用可以无限阻塞。由于这是不可接受的,网络服务器使用用户级线程来避免阻塞整个服务器环境。

除了核心网络环境之外,还有三个辅助环境。除了接收来自用户应用程序的消息外,核心网络环境的调度程序还接收来自输入环境和计时器环境的消息。

核心网络服务器环境是由所有套接字调用分发者和lwIP组成。套接字调度器和文件服务器工作类似。用户环境使用根(lib/nsipc.c
)发送IPC消息给核心网络环境。如果你查看lib/nsipc.c
,你可以看到核心网络服务器和文件服务器一样:i386_init
创建带有NS_TYPE_NS
的环境,所以我们扫描envs
,找到特殊的环境类型。对于每个用户环境IPC,网络服务器的调度器调用合适BSD套接字接口函数

常用的用户环境不使用nsipc_*
直接调用。相反,他们使用lib/sockets.c
中的函数,它提供了一个基于文件描述符的套接字API。因此,用户环境通过文件描述符指向套接字,就像指向磁盘上的文件一样。大量的操作(connect
, accept
等)是指定套接字的,但是read
, write
close
是通过普通的文件描述符lib/fd.c
完成的。和文件服务器为所有打开的文件保留唯一ID一样,lwIP也生成唯一ID为所有打开的套接字。在文件服务器和网络服务器中,我们使用保存在struct Fd
中的信息来映射每个环境文件描述符到这些唯一ID空间

尽管文件服务器和网络服务器的IPC调度器行为一样,但是还有一个关键的区别。BSD套接字调用像accept
recv
能无限阻塞。如果调度器允许lwIP执行阻塞调用中的一个,调度器也会阻塞,整个系统,同一个时间只有一个网络调用。因为这是不能接受的,所以网络服务器使用用户级别的线程来避免阻塞整个服务环境。对于每个到达的IPC消息,调度器创建一个线程,在线程中处理请求。如果线程阻塞,只有那个线程进入休眠,其他线程会继续执行

除了核心网络环境之外,也有三个辅助环境,除了从用户程序接收消息,核心网络环境调度器也从输入和时间环境接收消息

输出环境

当正在服务用户环境的套接字调用时,lwIP会为网卡生成包数据.lwIP发送每个包给输出辅助环境,使用NSREQ_OUTPUT
的IPC消息,包数据会在IPC的页参数带上。输出环境负责接收这些消息,通过你创建的系统调用接口分发给设备驱动

输入环境

通过网卡接收到的包,需要注入到lwIP中,对于每一个设备驱动接收到的包,输入环境从内核空间拉出包(使用你需要实现的内核系统调用),然后发送包数据到核心服务环境,使用NSREQ_INPUT
的IPC消息

包输入功能与核心网络环境分离,因为JOS很难同时接收IPC消息,同时轮询或等待设备驱动数据包也很困难。在JOS中,我们没有select
系统调用,这个系统调用可以监听多个输入源来区分哪个输入准备进行了

如果你阅读过net/input.c
net/output.c
,你会发现这两个都需要你实现。这是主要的,因为实现决定了系统调用接口。在你实现了驱动和系统调用接口之后,你需要为两个辅助环境编写代码

定时器环境

定时器环境周期性发送NSREQ_TIMER
类型消息给核心网络服务器,通知一个定时器已经过期了。来自线程的定时器消息被lwIP用来实现各种各样的网络超时

Part A: Initialization and transmitting packets 实现和转发包(报文)

你的内核代码没有时间概念,所以你需要添加。现在有时钟中断,这是由硬件每10ms生成的。每个时钟中断,我们可以递增变量来表明时间已经提前了10ms。这个实现在kern/time.c
,但是没有植入到你的内核中

我们需要为我们的内核添加时间概念。使用时钟中断,并用一个变量进行计数。

Add a call to time_tick for every clock interrupt in kern/trap.c(in kern/time.c). Implement sys_time_msec and add it to syscall in kern/syscall.c so that user space has access to the time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1
// LAB 4: Your code here.
case (IRQ_OFFSET + IRQ_TIMER):
// 回应8259A 接收中断。
lapic_eoi();
time_tick();
sched_yield();
break;
// 2
static int
sys_time_msec(void)
{
// LAB 6: Your code here.
return time_msec();
// panic("sys_time_msec not implemented");
}
// 3
case SYS_time_msec:
return sys_time_msec();

The Network Interface Card

Browse Intel’s Software Developer’s Manual for the E1000. 本手册涵盖了几个密切相关的以太网控制器。QEMU模拟82540EM。

接收和发送描述:

数据包接收

​ 识别到网络上存在一个数据包,

​ 执行地址过滤,

​ 将数据包存储在接收数据FIFO中,

​ 将数据传输到主机内存中的接收缓冲区,

​ buffer size RCTL.BSIZE & RCTL.BSEX

​ updating the state of a receive descriptor.

​ Status

​ Errors

​ Specia

PCI Interface

PCI是外围设备互连(Peripheral Component Interconnect)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:

在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
每个功能对应1个256字节的PCI配置空间

为了在引导期间执行PCI初始化,PCI代码遍历PCI总线以寻找设备。 当它找到设备时,它会读取其供应商ID和设备ID,并使用这两个值作为搜索pci_attach_vendor数组的键。 该数组由struct pci_driver条目组成,如下所示:

1
2
3
4
struct pci_driver {
uint32_t key1, key2;
int (*attachfn) (struct pci_func *pcif);
};

如果发现的设备的供应商ID和设备ID与数组中的条目匹配,则PCI代码会调用该条目的attachfn来执行设备初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct pci_func {
struct pci_bus *bus;

uint32_t dev;
uint32_t func;

uint32_t dev_id;
uint32_t dev_class;

uint32_t reg_base[6];
uint32_t reg_size[6];
uint8_t irq_line;
};

我们重点关注struct pci_func的最后三个成员,因为它们记录了设备的negotiated memory,I/O 和中断资源。 reg_basereg_size数组包含最多六个基址寄存器或BAR的信息。reg_base存储内存映射 I/O 区域的基地址(或 I/O 端口资源的基本 I/O 端口),reg_size包含reg_base中相应基值的大小(以字节为单位)或 I/O 端口数, 和irq_line包含分配给设备的 IRQ line 以进行中断。

pci_func_enable将会使能设备,协调资源,并将其填入struct pci_func

Exercise 3

Implement an attach function to initialize the E1000.

查看手册以及根据内核启动时的打印信息,我们可以知道 E1000 的 Vender ID = 0x8086, Device ID = 0x100E。

Add an entry to the pci_attach_vendor array in kern/pci.c to trigger your function if a matching PCI device is found,将E1000加入数组中

1
2
3
4
struct pci_driver pci_attach_vendor[] = {
{ PCI_E1000_VENDOR_ID, PCI_E1000_DEVICE_ID, &pci_e1000_attach},
{ 0, 0, 0 },
};

编写 e1000.c and e1000.h文件。.c文件写 attach函数, 在.h 文件中定义其 PCI ID等信息。

just enable the E1000 device via pci_func_enable.

Memory-mapped I/O

软件通过 Memory-mapped I/O 与 E1000 进行通信。之前我们已经两次接触过 MMIO这个概念了,分别在 CGA控制台和 lapic。 我们通过内存地址对设备进行读写。这些以内存地址为基础的读写目标并不是 DRAM,而是设备。

pci_func_enable 为 E1000分配了一个 MMIO 区域并且在 BAR0 中存储了它的 base and size。这是分配给设备的一系列物理内存地址,这意味着我们必须通过虚拟地址访问它。 由于MMIO区域被分配了非常高的物理地址(通常高于3GB),因为JOS的256MB限制,我们无法使用KADDR(内核地址)访问它。所以我们将创建一个新的映射。我们还是使用mmio_map_region分配MMIOBASE以上的区域,其保证了我们不会修改到之前创建的 LAPIC 映射。由于PCI设备初始化发生在JOS创建用户环境之前,因此您可以在kern_pgdir中创建映射,并且它总是可用的。

Exercise 4

In your attach function, create a virtual memory mapping for the E1000’s BAR 0 by calling mmio_map_region (which you wrote in lab 4 to support memory-mapping the LAPIC).

将E1000的物理地址映射到虚拟地址,同时记录映射的虚拟地址,方便之后对 E1000 设备的访问。

1
2
// 映射,并保存其虚拟地址,方便访问。
e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
1
2
3
4
5
6
7
8
cprintf("device status:[%08x]\n", *(uint32_t *)((uint8_t *)e1000 + E1000_DEVICE_STATUS));
// 或者写为 e1000[E1000_DEVICE_STATUS >> 2];
#define E1000_LOCATE(offset) (offset >> 2)
cprintf("device status:[%08x]\n", e1000[E1000_LOCATE(E1000_DEVICE_STATUS)]);

// 结果
PCI function 00:03.0 (8086:100e) enabled
device status:[80080783]

DMA

通过读写 E1000 寄存器来传输接收数据包是十分低效的做法,并且还需要E1000 缓存数据包。因此考虑到各个方面的因素,我们采用 DMA 的方式直接访问内存。驱动程序负责为发送和接收队列分配内存,设置DMA描述符,并使E1000定位到这些队列,但之后的所有内容都会变成异步的。

从抽象层来看,接收和发送队列非常相似。 两者都由一系列描述符组成。 虽然这些描述符的确切结构各不相同,但每个描述符包含一些标志和数据包数据缓冲区的物理地址(要发送的网卡的数据包数据,或者操作系统为网卡写入接收数据包而分配的缓冲区)。

Transmitting Packets

初始化要发送的网卡。传输初始化的第一步是设置传输队列。

MIT6.828 Lab6_Network Driver

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Transmit Descriptor */
struct E1000TxDesc {
uint64_t buffer_addr; /* Address of the descriptor's data buffer */

uint16_t length; /* Data buffer length */
uint8_t cso; /* Checksum offset */
uint8_t cmd; /* Descriptor control */

uint8_t status; /* Descriptor status */
uint8_t css; /* Checksum start */
uint16_t special;

}__attribute__((packed));

在驱动程序初始化期间为每个描述符保留数据包缓冲区的空间,并简单地将数据包数据复制到这些预分配的缓冲区中。

Exercise 5

执行第14.5节(但不是它的子节)中描述的初始化步骤。使用第13节作为初始化过程中引用的寄存器的参考,使用第3.3.3节和第3.4节作为传输描述符和传输描述符数组的参考。

在对各寄存器进行初始化时,要不断得在第13章找到其偏移地址和详细定义。最后按照要求写入地址。

我们必须将要传输的数据包添加到传输队列的尾部,这意味着需要将数据包数据复制到下一个数据包缓冲区,然后更新TDT(传输描述符尾部)寄存器以通知网卡在传输队列中有另一个数据包。[Note] TDT 是传输描述符数组的索引而不是偏移地址。

如果在发送描述符的命令字段中设置RS(Report Status)位,则当网卡在该描述符中发送了数据包时,网卡将在描述符的状态字段中设置DD(Descriptor Done)位。 如果设置了描述符的DD位,我们就能回收该描述符并使用它来传输另一个数据包。

如果用户调用传输系统调用,但下一个描述符的DD位未置位,表明传输队列已满,该怎么办? 你必须决定在这种情况下该做什么。你可以简单地丢弃数据包。网络协议对此具有弹性,但如果丢弃大量数据包,协议可能无法恢复。我们可以告诉用户环境它必须重试,就像我们对sys_ipc_try_send所做的那样。This has the advantage of pushing back on the environment generating the data.。

Exercise 6

编写一个函数,通过检查下一个描述符是否空闲来传输一个包,将包数据复制到下一个描述符,并更新TDT。确保您处理的传输队列已满。

Exercise 7

添加一个系统调用,允许您从用户空间传输数据包。具体的界面取决于您。不要忘记检查从用户空间传递给内核的指针

Transmitting Packets: Network Server传输报文:网络服务器

既然我们已经有了 packet send 的系统调用,也是时候发送数据包了。output helper environment 运行在一个循环中:从 core network server 接收 NSREQ_OUTPUT IPC 信息,使用 system call 将伴随 IPC 发送过来的 Packet 发送到设备驱动中。 NSREQ_OUTPUT由在net/lwip/jos/jif/jif.c文件中的low_level_output 函数发送 , which glues the lwIP stack to JOS’s network system. 每个IPC 都包含一个由 union Nsipc构成的页,packet 存在于 struct kif_pkt字段。struct jif_pkt looks like:

1
2
3
4
struct jif_pkt {
int jp_len;
char jp_data[0];
};

在结构的末尾使用像jp_data这样的零长度数组是用于表示没有预定长度的缓冲区的常见C技巧(有些人会说是恶心)。 由于C不进行数组边界检查,只要确保结构后面有足够的未使用内存,就可以使用jp_data,就好像它是一个任意大小的数组一样。

Exercise 8

Implement net/output.c.

/net/serv.c中创建了一个相当于守护进程output,在output 中我们需要接收 ipc, 解析信息然后调用 system call 发送数据包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
output(envid_t ns_envid)
{
binaryname = "ns_output";

// LAB 6: Your code here:
// - read a packet from the network server
// - send the packet to the device driver
uint32_t whom;
int perm;
int32_t req;

while (1) {
req = ipc_recv((envid_t *)&whom, &nsipcbuf, &perm);
if (req != NSREQ_OUTPUT) {
continue;
}
while (sys_pkt_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len) < 0) {
sys_yield();
}
}
}

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

Lab 5: File system, Spawn and Shell

Introduction

In this lab, you will implement spawn, a library call that loads and runs on-disk executables. 然后,您将充实内核和库操作系统,使其足以在控制台上运行shell。这些特性需要一个文件系统,本实验室介绍了一个简单的读写文件系统。

file
fs/fs.c 操纵文件系统磁盘上结构的代码。
fs/bc.c 一个建立在用户级页面错误处理设施之上的简单块缓存。
fs/ide.c 最小的基于pio(非中断驱动)的IDE驱动程序代码。
fs/serv.c 使用文件系统IPCS与客户机环境交互的文件系统服务器。
lib/fd.c 实现通用类unix文件描述符接口的代码。
lib/file.c 用于磁盘上文件类型的驱动程序,实现为文件系统IPC客户机。
lib/console.c 控制台输入/输出文件类型的驱动程序。
lib/spawn.c spawn库调用的代码框架。

File system preliminaries(文件系统初步)

我们要完成一个相对简单的文件系统,其可以实现创建、读、写以及删除在分层目录结构中组织的文件。目前我们的OS只支持单用户,因此我们的文件系统也不支持UNIX文件拥有或权限的概念。同时也不支持硬链接、符号链接、时间戳或是特别的设备文件。

On-Disk File System Structure

大多数UNIX文件系统将可用磁盘空间划分为两种主要类型的区域:inode区域和数据区域。UNIX文件系统为文件系统中的每个文件分配一个inode;一个文件的inode保存着关于该文件的关键元数据,比如它的属性和指向其数据块的指针。数据区域被划分为更大的数据块(通常为8KB或更多),文件系统在其中存储文件数据和目录元数据。目录项包含文件名和指向索引节点的指针;如果文件系统中的多个目录条目引用了该文件的inode,则该文件被称为硬链接文件。由于我们的文件系统不支持硬链接,我们不需要这种间接级别,因此可以进行方便的简化:我们的文件系统根本不使用inode,而只是将一个文件(或子目录)的所有元数据存储在描述该文件的(唯一的)目录条目中。

文件和目录在逻辑上都由一系列数据块组成,这些数据块可以分散在磁盘中,就像环境的虚拟地址空间的页面可以分散在物理内存中一样。文件系统环境隐藏了块布局的细节,提供了在文件中任意偏移位置读取和写入字节序列的接口。文件系统环境在内部处理对目录的所有修改,作为执行文件创建和删除等操作的一部分。我们的文件系统允许用户环境直接读取目录元数据(例如,使用read),这意味着用户环境可以自己执行目录扫描操作(例如,实现ls程序),而不必依赖于对文件系统的额外特殊调用。这种目录扫描方法的缺点是,它使应用程序依赖于目录元数据的格式,在不更改或至少重新编译应用程序的情况下,很难更改文件系统的内部布局,这也是大多数现代UNIX变体不鼓励使用这种方法的原因。

Sectors and Blocks

扇区是对磁盘的概念,块是对OS的概念。 块的 size 必须是扇区size 的整数倍。

Superblocks

文件系统通常在磁盘上的“易于查找”位置保留某些磁盘块(例如从最开始或最后)以保存描述文件系统属性的元数据,例如块大小 ,磁盘大小,查找根目录所需的任何元数据,上次挂载文件系统的时间,文件系统上次检查错误的时间等等。 这些特殊块称为超级块。

我们的文件系统将只有一个超级块,它始终位于磁盘上的第1块。它的布局由struct Super在inc/fs.h中定义。块0通常保留来保存引导加载程序和分区表,因此文件系统通常不使用第一个磁盘块。许多“真正的”文件系统维护多个超级块,复制到磁盘上几个间隔很宽的区域,因此,如果其中一个超级块损坏或磁盘在该区域出现了媒体错误,仍然可以找到其他超级块,并使用它们访问文件系统。

1
2
3
4
5
struct Super {
uint32_t s_magic; // Magic number: FS_MAGIC
uint32_t s_nblocks; // Total number of blocks on disk
struct File s_root; // Root directory node
};

Superblock, Inode, Dentry 和 File 都属于元数据(Metadata),根据维基百科中的解释,所谓元数据,就是描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。Linux/Unix 文件系统的元数据以多级结构保存

superblock:记录此filesystem 的整体信息,包括inode[表情]ock的总量、使用量、剩余量, 以及档案系统的格式与相关信息等;
inode:记录档案的属性,一个档案占用一个inode,同时记录此档案的资料所在的block 号码;
block:实际记录档案的内容,若档案太大时,会占用多个block 。

Superblock 是文件系统最基本的元数据,它定义了文件系统的类似、大小、状态,和其他元数据结构的信息(元数据的元数据)。

Disk layout

File Meta-data

The layout of the meta-data describing a file in our file system is described by struct File in inc/fs.h. Unlike in most “real” file systems, for simplicity we will use this one File structure to represent file meta-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct File {
char f_name[MAXNAMELEN]; // filename
off_t f_size; // file size in bytes
uint32_t f_type; // file type

// Block pointers.
// A block is allocated iff its value is != 0.
uint32_t f_direct[NDIRECT]; // direct blocks
uint32_t f_indirect; // indirect block

// Pad out to 256 bytes; must do arithmetic in case we're compiling
// fsformat on a 64-bit machine.
uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed)); // required only on some 64-bit machines

File structure

Directories versus Regular Files(目录与普通文件)

我们的文件系统中的超级块包含一个 File结构,其保存了文件系统根目录的元数据。这个目录文件的内容是一系列文件结构体,其描述了文件系统根目录下的文件和目录。任何根目录下的子目录可能包含更多表示子子目录的文件结构体,以此类推。

The File System

我们实现的文件系统的关键部分是:

  • 读数据到缓存中并能写回到磁盘;

  • 分配磁盘块;

  • 将文件偏移映射到磁盘块;

  • 并在IPC接口中实现读,写和打开。

Disk Access

操作系统中的文件系统环境需要能够访问磁盘,但是我们还没有在内核中实现任何磁盘访问功能。我们没有采用传统的“单片”操作系统策略,即在内核中添加IDE磁盘驱动程序以及允许文件系统访问它的必要系统调用,而是将IDE磁盘驱动程序作为用户级文件系统环境的一部分来实现。我们仍然需要稍微修改内核,以便让文件系统环境拥有实现磁盘访问所需的权限。

只要我们依赖于轮询、基于“编程I/O”(PIO)的磁盘访问,并且不使用磁盘中断,就很容易通过这种方式在用户空间中实现磁盘访问。也可以在用户模式下实现中断驱动的设备驱动程序(例如,L3和L4内核就可以做到这一点),但难度更大,因为内核必须字段设备中断并将它们分派到正确的用户模式环境中。

x86处理器使用EFLAGS寄存器中的IOPL位来确定是否允许保护模式代码执行特殊的设备I/O指令,如in和OUT指令。因为我们需要访问的所有IDE磁盘寄存器都位于x86的I/O空间中,而不是内存映射的,所以给文件系统环境“I/O特权”是我们唯一需要做的事情,以便允许文件系统访问这些寄存器。实际上,EFLAGS寄存器中的IOPL位为内核提供了一种简单的“全或无”方法来控制用户模式代码是否可以访问I/O空间。在我们的示例中,我们希望文件系统环境能够访问I/O空间,但是我们根本不希望任何其他环境能够访问I/O空间。

Exercise 1:

i386_init通过将类型ENV_TYPE_FS传递给环境创建函数env_create来标识文件系统环境。在env.c中修改env_create,使它赋予文件系统环境I/O权限,但不赋予任何其他环境该权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env* e; // 新建一个进程
int r = env_alloc(&e, 0);
if(r<0){
panic("env_create: env alloc error: %e", r);
}
// 分配成功
e->env_type = type;
// 添加文件权限
if(type == ENV_TYPE_FS){
e->env_tf.tf_eflages |= FL_IOPL_MASK
}
load_icode(e, binary);
}

Question

  1. Do you have to do anything else to ensure that this I/O privilege setting is saved and restored properly when you subsequently switch from one environment to another? Why?

    不需要,因为在环境切换时,会保存eflags的值,后续也会使用env_pop_tf来恢复eflags的值。

The Block Cache

在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“缓冲区缓存”(实际上只是块缓存)。块缓存的代码在fs/bc.c中。

我们的文件系统只能处理3GB或更小的磁盘。我们在文件系统环境的地址空间中保留了一个很大的、固定的3GB区域,从0x10000000 (DISKMAP)到0xD0000000 (DISKMAP+DISKMAX),作为磁盘的“内存映射”版本。例如,磁盘块0映射到虚拟地址0x10000000,磁盘块1映射到虚拟地址0x10001000,依此类推。fs/bc.c中的diskaddr函数实现了从磁盘块号到虚拟地址的转换(以及一些完整性检查)。

将整个磁盘读入内存需要很长时间,因此我们将实现一种请求页面调度,当进程在运行时需要访问某部分程序和数据时,若发现请求页面不在内存,便提出请求,由OS将其所需页面调入内存。这样,我们就可以假装整个磁盘都在内存中。

Exercise 2:

implement the bc_pgfault and flush_block functions in fs/bc.c.

bc_pgfault is a page fault handler, just like the one your wrote in the previous lab for copy-on-write fork, 只不过它的工作是从磁盘加载页面以响应页面错误.

When writing this, keep in mind that

​ (1) addr may not be aligned to a block boundary and

​ (2) ide_read operates in sectors, not blocks.

如果需要,flush_block函数应该将一个块写入磁盘。如果块甚至不在块缓存中(也就是说,页面没有映射),或者它不是脏的,则Flush_block不应该做任何事情。我们将使用VM硬件来跟踪磁盘块自上次从磁盘读取或写入磁盘以来是否被修改过。要查看一个块是否需要写入,我们只需查看uvpt条目中是否设置了PTE_D“脏”位。PTE_D位由处理器设置,以响应对该页的写操作;参见386参考手册第五章5.2.4.3。)将块写入磁盘后,flush_block应该使用sys_page_map清除PTE_D位。

采用了wb

块号与扇区号有一定的区别,在完成这两个函数时,要注意区分这两个概念。 JOS 块大小位4kB,扇区大小为512B,每次读写一个块,就需要读写4个扇区。因此,JOS使用了一个宏定义#define BLKSECTS (BLKSIZE / SECTSIZE)来描述两者的关系。

bc_pgfault()

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
static void
bc_pgfault(struct UTrapframe *utf)
{
void *addr = (void *) utf->utf_fault_va;
uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
int r;

// Check that the fault was within the block cache region
if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
panic("page fault in FS: eip %08x, va %08x, err %04x",
utf->utf_eip, addr, utf->utf_err);

// Sanity check the block number.
if (super && blockno >= super->s_nblocks)
panic("reading non-existent block %08x\n", blockno);

// Allocate a page in the disk map region, read the contents
// of the block from the disk into that page.
// Hint: first round addr to page boundary. fs/ide.c has code to read
// the disk.
//
// LAB 5: you code here:
addr = (void*) ROUNDUP(addr, PGSIZE);
// Allocate a page of memory and map it at 'addr' with permission 'PTE_P|PTE_W|PTE_U' in the address space of 'envid'.
if((r = sys_page_alloc(0,addr,PTE_P|PTE_W|PTE_U))<0){
panic("in bc_pgfault, sys_page_alloc: %e", r);
}

if ( (r = ide_read(blockno*BLKSECTS, addr, BLKSECTS)) < 0) {
panic("in bc_pgfault, ide_read: %e",r);
}

// Clear the dirty bit for the disk block page since we just read the
// block from disk
if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)
panic("in bc_pgfault, sys_page_map: %e", r);

// Check that the block we read was allocated. (exercise for
// the reader: why do we do this *after* reading the block
// in?)
if (bitmap && block_is_free(blockno))
panic("reading free block %08x\n", blockno);
}

flush_block()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 如果需要,将包含VA的块的内容清除到磁盘,然后使用sys_page_map清除PTE_D位。
// 如果该块不在块缓存中或不是脏的,则不做任何操作。
// Hint: Use va_is_mapped, va_is_dirty, and ide_write.
// Hint: Use the PTE_SYSCALL constant when calling sys_page_map.
// Hint: Don't forget to round addr down.
void
flush_block(void *addr)
{
uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;

if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE)) // 越界
panic("flush_block of bad va %08x", addr);

// LAB 5: Your code here.
//panic("flush_block not implemented");
addr = (void *)ROUNDDOWN(addr, PGSIZE);
if (va_is_mapped(addr) && va_is_dirty(addr)) { // 映射过并且为脏
ide_write(blockno*BLKSECTS, addr , BLKSECTS); // 写回到磁盘
if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0) // 清空PTE_D (脏)位
panic("in flush_block, sys_page_map: %e", r);
}
}

fs/fs.c中的fs_init()将会初始化super和bitmap全局指针变量。

至此对于文件系统进程只要访问虚拟内存[DISKMAP, DISKMAP+DISKMAX]范围中的地址addr,就会访问到磁盘((uint32_t)addr - DISKMAP) / BLKSIZE block中的数据。

如果block数据还没复制到内存物理页,bc_pgfault()缺页处理函数会将数据从磁盘拷贝到某个物理页,并且将addr映射到该物理页。这样FS进程只需要访问虚拟地址空间[DISKMAP, DISKMAP+DISKMAX]就能访问磁盘了。

JOS FS进程地址空间和磁盘映射:

image-20220708171856337

The Block Bitmap

在fs_init设置位图(btimap)指针之后,我们可以将位图视为一个位的压缩数组,磁盘上的每个块对应一个位。例如,block_is_free,它只是检查位图中给定的块是否被标记为空闲。

Exercise 3:

使用free_block作为模型来实现fs/fs.c中的alloc_block,它应该在位图中找到一个空闲的磁盘块,标记它已被使用,并返回该块的编号。当您分配一个块时,您应该立即使用flush_block将更改后的位图块刷新到磁盘,以帮助文件系统保持一致性。

我们以一个实例来分析 bitmap的工作原理, 若标记第35个块(块号为34)为使用状态, 则将bitmap[1] 的第 2 (34%32)位标记为 0。 讲道理应该位图位为0是free的呀,JOS这里反过来了。

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
// Search the bitmap for a free block and allocate it.  When you
// allocate a block, immediately flush the changed bitmap block
// to disk.
//
// Return block number allocated on success,
// -E_NO_DISK if we are out of blocks.
//
// Hint: use free_block as an example for manipulating the bitmap.
int
alloc_block(void)
{
// The bitmap consists of one or more blocks. A single bitmap block
// contains the in-use bits for BLKBITSIZE blocks. There are
// super->s_nblocks blocks in the disk altogether.

// LAB 5: Your code here.
size_t i;
for(i = 1; i<super->s_nblocks;++i){
// block_is_free(): Check to see if the block bitmap indicates that block 'blockno' is free.
// Return 1 if the block is free, 0 if not.
if(block_is_free(i)){ // 没有使用,则标记为已使用
bitmap[i/32] &= ~(1<<(i%32));
flush_block(&bitmap[i/32]); // 写回并重置脏位
return i;
}
}

//panic("alloc_block not implemented");
return -E_NO_DISK;
}

File Operations

Exercise 4:

实现file_block_walk和file_get_block。file_block_walk将文件中的块偏移量映射到struct file或间接块中该块的指针,非常像pgdir_walk对页表所做的。File_get_block更进一步,映射到实际的磁盘块,如果需要,分配一个新的磁盘块。

file_block_walk 获得文件第filebno块的地址(其本身是个指针),编写需要注意以下几点。

  • ppdiskbno 是块指针(记录块的地址)
  • f_indirect 直接记录块号,而不是记地址。
  • Don’t forget to clear any block you allocate. 对分配的块进行清零操作后,要写入 disk 中。
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
// Find the disk block number slot for the 'filebno'th block in file 'f'.
// Set '*ppdiskbno' to point to that slot.
// The slot will be one of the f->f_direct[] entries,
// or an entry in the indirect block.
// When 'alloc' is set, this function will allocate an indirect block
// if necessary.
//
// Returns:
// 0 on success (but note that *ppdiskbno might equal 0).
// -E_NOT_FOUND if the function needed to allocate an indirect block, but
// alloc was 0.
// -E_NO_DISK if there's no space on the disk for an indirect block.
// -E_INVAL if filebno is out of range (it's >= NDIRECT + NINDIRECT).
//
// Analogy: This is like pgdir_walk for files.
// Hint: Don't forget to clear any block you allocate.
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
// filenno file block number
// LAB 5: Your code here.
// panic("file_block_walk not implemented");
// ppdiskbno 块指针
if (filebno < NDIRECT) { // NDIRECT: Number of block pointers in a File descriptor
// but note that *ppdiskbno might equal 0
if(ppdiskbno)
*ppdiskbno = &(f->f_direct[filebno]);
else
return 0;
}

if (filebno >= NDIRECT + NINDIRECT) // NINDIRECT: Number of direct block pointers in an indirect block
return -E_INVAL;

// 如果文件还没分配
filebno -= NDIRECT;
if (!!f->f_indirect) { // !!把非0转换为1,而0还是0
if (alloc == 0)
return -E_NOT_FOUND;
// 分配一个 indirect block
uint32_t blockno;
if ( (blockno = alloc_block()) < 0)
return blockno;
// f_indirect 直接记录块号,而不是记地址
// f->f_indirect = (uint32_t)diskaddr(blockno);
f->f_indirect = blockno;
memset(diskaddr(blockno), 0, BLKSIZE);
flush_block(diskaddr(blockno));
}
if (ppdiskbno)
*ppdiskbno = (uint32_t *)diskaddr(f->f_indirect) + filebno;
return 0;
}
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
// Set *blk to the address in memory where the filebno'th
// block of file 'f' would be mapped.
// 将*blk设置为文件'f'的第filebno块在内存中的映射地址。
// Returns 0 on success, < 0 on error. Errors are:
// -E_NO_DISK if a block needed to be allocated but the disk is full.
// -E_INVAL if filebno is out of range.
//
// Hint: Use file_block_walk and alloc_block.
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
// LAB 5: Your code here.
uint32_t *pdiskbno; // 块的块号指针
int r;
if ( (r = file_block_walk(f, filebno, &pdiskbno, 1))< 0) //
return r; // 找到文件第filebno块的位置pdiskbno

if(*pdiskbno == 0) { // 这个位置还没分配
// 文件块还未分配
if ( (r = alloc_block()) < 0) // alloc_block 在位图中搜索一个空闲块并分配它。当您分配一个块时,立即将更改的位图块刷新到磁盘。
return r;
*pdiskbno = r;
memset(diskaddr(r), 0, BLKSIZE);
flush_block(diskaddr(r)); // wb
}

// 最终指向块
*blk = diskaddr(*pdiskbno); // // Return the virtual address of this disk block.
return 0;
// panic("file_get_block not implemented");
}

The file system interface

由于其他环境无法直接调用文件系统环境中的函数,因此我们将通过 RPC 或在JOS的IPC机制上构建的RPC抽象来公开对文件系统环境的访问。

RPC(Remote Procedure Call)。它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Regular env FS env
+---------------+ +---------------+
| read | | file_read |
| (lib/fd.c) | | (fs/fs.c) |
...|.......|.......|...|.......^.......|...............
| v | | | | RPC mechanism
| devfile_read | | serve_read |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| fsipc | | serve |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| ipc_send | | ipc_recv |
| | | | ^ |
+-------|-------+ +-------|-------+
| |
+-------------------+

本质上RPC还是借助IPC机制实现的,普通进程通过IPC向FS进程间发送具体操作和操作数据,然后FS进程执行文件操作,最后又将结果通过IPC返回给普通进程。从上图中可以看到客户端的代码在lib/fd.c和lib/file.c两个文件中。服务端的代码在fs/fs.c和fs/serv.c两个文件中。
相关数据结构之间的关系可用下图来表示:

lab5_7_open原理.png

文件系统服务端代码在fs/serv.c中,serve()中有一个无限循环,接收IPC请求,将对应的请求分配到对应的处理函数,然后将结果通过IPC发送回去。
对于客户端来说:发送一个32位的值作为请求类型,发送一个Fsipc结构作为请求参数,该数据结构通过IPC的页共享发给FS进程,在FS进程可以通过访问fsreq(0x0ffff000)来访问客户进程发来的Fsipc结构。
对于服务端来说:FS进程返回一个32位的值作为返回码,对于FSREQ_READ和FSREQ_STAT这两种请求类型,还额外通过IPC返回一些数据。

Exercise 5

Implement serve_read in fs/serv.c.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int
serve_read(envid_t envid, union Fsipc *ipc) // 从指定文件中读取出内容,返回给调用者
{
struct Fsreq_read *req = &ipc->read; // 指定文件
struct Fsret_read *ret = &ipc->readRet; // 调用者

if (debug)
cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);

// Lab 5: Your code here:
int r;
struct openFile* opf;
if((r = openfile_lookup(envid, req->req_fileid, &opf))<0){ // Look up an open file for envid.
return r;
}
if((r = file_read(opf->o_file, ret->ret_buf, req->req_n, opf->o_fd->fd_offset))<0){
return r;
}
of->o_fd->fd_offset +=r;

return r;
}

Exercise 6

Implement serve_write in fs/serv.c and devfile_write in lib/file.c.

serve_write: 在file_write中考虑了块边界的问题bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);,因此我们同样不需要对 req_n 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
serve_write(envid_t envid, struct Fsreq_write *req)
{
if (debug)
cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
int r;
struct OpenFile *of;
int reqn;
if ( (r = openfile_lookup(envid, req->req_fileid, &of)) < 0)
return r;
reqn = req->req_n > PGSIZE? PGSIZE:req->req_n;

if ( (r = file_write(of->o_file, req->req_buf, reqn, of->o_fd->fd_offset)) < 0)
return r;

of->o_fd->fd_offset += r;
return r;
}

devfile_write: devfile_write需要调用fsipc,其向文件服务器发送一个进程间请求,并等待回复。请求体保存在fsipcbuf中,回复部分也应该写回到 fsipcbuf中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ssize_t
devfile_write(struct Fd *fd, const void *buf, size_t n)
{
// LAB 5: Your code here
int r;
if ( n > sizeof (fsipcbuf.write.req_buf))
n = sizeof (fsipcbuf.write.req_buf);

fsipcbuf.write.req_fileid = fd->fd_file.id;
fsipcbuf.write.req_n = n;
memmove(fsipcbuf.write.req_buf, buf, n);

return fsipc(FSREQ_WRITE, NULL);
}

Spawning Processes

slib/spawn.c中的spawn()创建一个新的进程,从文件系统加载用户程序,然后启动该进程来运行这个程序。spawn()就像UNIX中的fork()后面马上跟着exec()。 spawn(const char *prog, const char **argv)`做如下一系列动作:

  1. 从文件系统打开prog程序文件
  2. 调用系统调用sys_exofork()创建一个新的Env结构
  3. 调用系统调用sys_env_set_trapframe(),设置新的Env结构的Trapframe字段(该字段包含寄存器信息)。
  4. 根据ELF文件中program herder,将用户程序以Segment读入内存,并映射到指定的线性地址处。
  5. 调用系统调用sys_env_set_status()设置新的Env结构状态为ENV_RUNNABLE。

我们实现了spawn而不是UNIX风格的exec,因为在没有内核特殊帮助的情况下,spawn更容易以“exokernel fashion”从用户空间实现。

Exercise 7

spawn C依赖新的系统调用sys_env_set_trapframe来初始化新创建的环境的状态。 在kern/syscall.c中实现sys_env_set_trapframe(不要忘记在sycall()中调度新的系统调用)。

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
// Set envid's trap frame to 'tf'.
// tf is modified to make sure that user environments always run at code
// protection level 3 (CPL 3), interrupts enabled, and IOPL of 0.
//
// Returns 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
// LAB 5: Your code here.
// Remember to check whether the user has supplied us with a good
// address!
// panic("sys_env_set_trapframe not implemented");
struct Env *env;
int r;
if ( (r = envid2env(envid, &env, 1)) < 0)
return r;

// 什么时候会出现没有权限访问的问题?
user_mem_assert(env, tf, sizeof(struct Trapframe), PTE_U);
// 直接整个结构体也是可以赋值的
env->env_tf = *tf;
env->env_tf.tf_cs |= 0x3;
env->env_tf.tf_eflags &= (~FL_IOPL_MASK);
env->env_tf.tf_eflags |= FL_IF;
return 0;
}

Sharing library state across fork and spawn

在fork和spawn之间共享library的状态

在JOS中,每个设备类型都具有相应的struct Dev,其中包含指向实现读/写/等的函数指针。对于该设备类型。 lib / fd.c在此基础上实现了类似UNIX的通用文件描述符接口。 每个结构体Fd表示它的设备类型,lib/fd.c中的大多数函数只是将操作分派给适当的struct Dev中的函数。

1
2
3
4
5
6
7
8
9
10
// Per-device-class file descriptor operations
struct Dev {
int dev_id;
const char *dev_name;
ssize_t (*dev_read)(struct Fd *fd, void *buf, size_t len);
ssize_t (*dev_write)(struct Fd *fd, const void *buf, size_t len);
int (*dev_close)(struct Fd *fd);
int (*dev_stat)(struct Fd *fd, struct Stat *stat);
int (*dev_trunc)(struct Fd *fd, off_t length);
};

UNIX文件描述符是一个大的概念,包含pipe,控制台I/O。在JOS中每种设备对应一个struct Dev结构,该结构包含函数指针,指向真正实现读写操作的函数。
lib/fd.c文件实现了UNIX文件描述符接口,但大部分函数都是简单对struct Dev结构指向的函数的包装。

我们希望共享文件描述符,JOS中定义PTE新的标志位PTE_SHARE,如果有个页表条目的PTE_SHARE标志位为1,那么这个PTE在fork()和spawn()中将被直接拷贝到子进程页表,从而让父进程和子进程共享相同的页映射关系,从而达到父子进程共享文件描述符的目的。

Exercise 8

修改lib/fork.c中的duppage(),使之正确处理有PTE_SHARE标志的页表条目。同时实现lib/spawn.c中的copy_shared_pages()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int
duppage(envid_t envid, unsigned pn)
{
int r;

// LAB 4: Your code here.
void *addr = (void*) (pn * PGSIZE);
if (uvpt[pn] & PTE_SHARE) {
sys_page_map(0, addr, envid, addr, PTE_SYSCALL); //对于标识为PTE_SHARE的页,拷贝映射关系,并且两个进程都有读写权限
} else if ((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)) { //对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为PTE_COW
if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
panic("sys_page_map:%e", r);
if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
panic("sys_page_map:%e", r);
} else {
sys_page_map(0, addr, envid, addr, PTE_U|PTE_P); //对于只读的页,只需要拷贝映射关系即可
}
return 0;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Copy the mappings for shared pages into the child address space.
static int
copy_shared_pages(envid_t child)
{
// LAB 5: Your code here.
uintptr_t addr;
for (addr = 0; addr < UTOP; addr += PGSIZE) {
if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&
(uvpt[PGNUM(addr)] & PTE_U) && (uvpt[PGNUM(addr)] & PTE_SHARE)) {
sys_page_map(0, (void*)addr, child, (void*)addr, (uvpt[PGNUM(addr)] & PTE_SYSCALL));
}
}
return 0;
}

The keyboard interface

目前我们只能在内核监视器中才能接收输入。kern/console.c already contains the keyboard and serial drivers that have been used by the kernel monitor since lab 1, but now you need to attach these to the rest of the system.

kern/console.c 已经包含了从实验1开始内核监视器就在使用的键盘和串行驱动程序,但是现在您需要将它们附加到系统的其他部分。

/kern/console.c/cons_getc()中的代码,实现了在 monitor 模式下(禁止中断)可以正常获取用户输入。

1
2
3
4
5
// poll for any pending input characters,
// so that this function works even when interrupts are disabled
// (e.g., when called from the kernel monitor).
serial_intr();
kbd_intr();

在 trap.c 中加入中断处理函数。

1
2
3
4
5
6
7
8
case (IRQ_OFFSET + IRQ_KBD):
lapic_eoi();
kbd_intr();
break;
case (IRQ_OFFSET + IRQ_SERIAL):
lapic_eoi();
serial_intr();
break;

The Shell

总结

Lab5 主要介绍了文件系统的基本组成,为超级块分配易查找的位置,并在超级块中记录根目录文件,此后递进存储即实现了FS的多级目录。利用虚拟地址和MMIO实现了类似统一编址方式,我们可以很方便实现文件访问,其操作过程与内存访问很类似(在文件结构体中 walk 到块号)。

JOS在用户环境实现FS,FS接口是这个Lab的重点。其通过RPC公开接口,在JOS中利用IPC机制构建RPC抽象。regular env->read->ipc_send -> ipc_recv->serve->file_read。
这实际上以微内核的方式实现的FS,FS的serv相当于一种微服务进程,其接收、解析内核转发的信息再执行相应的操作。消息通过一个页映射的Union Fsipc进行传递。

spawn函数表现得像在Unix下创建子进程带有一个立刻执行execfork函数。exec()会把当前执行进程覆盖掉来执行外部程序,spawn()则会创建一个新的进程来执行。对于spawn的设计,还是有一些困惑,因为不明白 Unix-Style的exec是如何实现的,所以不能理解为什么spawn更容易在用户空间实现。

最后的Keyboard 接口和Shell都相对简单,比较容易理解。

  1. 其他环境无法直接调用文件系统环境中的函数,要通过IPC,进程间消息传递实现。这里应该就是微内核的概念了。但为什么不能直接调用,是怎么实现不能直接调用的?是特意不让别的环境直接使用其函数吗?有没有方法可以实现不同用户程序可以直接调用其他用户程序的函数?

不能直接调用,应该是因为每个用户态的代码都存在于自身的地址空间中,其他用户程序无法访问到。但是我如果在源代码中的某个环境直接#include并且调用另一个环境文件夹下的代码(例如fs),即在编译前就调用了,这会出现什么情况?这样就相当于是宏内核的概念了吗? 或者是这样设计会增大代码的耦合性?这让我十分疑惑。毕竟这与越过系统调用不一样,系统调用有权限限制。

LAB4: Preemtive Multitasking 抢占式多任务处理

您将在多个同时活跃的用户模式环境中实现抢占式多任务处理。

在A部分中,您将向JOS添加多处理器支持,实现循环调度,并添加基本的环境管理系统调用(创建和销毁环境的调用,以及分配/映射内存的调用)。

在B部分中,您将实现一个类unix的fork(),它允许用户模式环境创建自身的副本。

最后,在C部分中,您将添加对进程间通信(IPC)的支持,允许不同的用户模式环境显式地相互通信和同步。您还将添加对硬件时钟中断和抢占的支持

Part A: Multiprocessor Support and Cooperative Multitasking多处理器支持和多任务协作

我们首先需要把 JOS 扩展到在多处理器系统中运行。然后实现一些新的 JOS 系统调用来允许用户进程创建新的进程。我们还要实现协同轮询调度,允许内核在当前进程自愿放弃CPU(或退出cpu)时从一个环境切换到另一个环境。

我们即将使 JOS 能够支持“对称多处理” (Symmetric MultiProcessing, SMP)。这种模式使所有 CPU 能对等地访问内存、I/O 总线等系统资源。虽然 CPU 在 SMP 下以同样的方式工作,在启动过程中他们可以被分为两个类型:引导处理器(BootStrap Processor, BSP) 负责初始化系统以及启动操作系统;应用处理器( Application Processors, AP ) 在操作系统拉起并运行后由 BSP 激活。哪个 CPU 作为 BSP 由硬件和 BIOS 决定。也就是说目前我们所有的 JOS 代码都运行在 BSP 上。
在 SMP 系统中,每个 CPU 都有一个附属的 LAPIC 单元。LAPIC 单元用于传递中断,并给它所属的 CPU 一个唯一的 ID。在 lab4 中,我们将会用到 LAPIC 单元的以下基本功能 ( 见kern/lapic.c中 ):

  • 读取 APIC ID 来判断我们的代码运行在哪个 CPU 之上。(see cpunum() )
  • 从 BSP 发送STARTUP 跨处理器中断 (InterProcessor Interrupt, IPI) 来启动 AP。(see lapic_startap() )
  • 在 part C 中,我们为 LAPIC 的内置计时器编程来触发时钟中断以支持抢占式多任务处理。(see apic_init() )

处理器通过映射在内存上的 I/O (Memory-Mapped I/O, MMIO) 来访问它的 LAPIC。在 MMIO 中,物理内存的一部分被硬连接到一些 I/O 设备的寄存器,因此,访问内存的 load/store 指令可以被用于访问设备的寄存器。实际上,我们在 lab1 中已经接触过这样的 IO hole,如0xA0000被用来写 VGA 显示缓冲。LAPIC 开始于物理地址0xFE000000 ( 4GB以下32MB处 )。如果用以前的映射算法(将0xF0000000 映射到 0x00000000,也就是说内核空间最高只能到物理地址0x0FFFFFFF)显然太高了。因此,JOS 在 MMIOBASE (即 虚拟地址0xEF800000) 预留了 4MB 来映射这类设备。我们需要写一个函数来分配这个空间并在其中映射设备内存。

image-20220705140300109

LAPIC 和 IOAPIC

LAPIC 和 IOAPIC

APIC全称是Advanced Programmable Interrupt Controller,高级可编程中断控制器。它是在奔腾P54C之后被引入进来的。

在现在的计算机它通常由两个部分组成,分别为LAPIC(Local APIC,本地高级可编程中断控制器)IOAPIC(I/O高级可编程中断控制器)

LAPIC在CPU中,IOAPIC通常位于南桥。

APIC是在PIC (Programmable Interrupt Controller) 的基础上发展而来的

IOAPIC: IOAPIC的主要作用是中断的分发。最初有一条专门的APIC总线用于IOAPIC和LAPIC通信,在Pentium4 和Xeon 系列CPU出现后,他们的通信被合并到系统总线中。

Exercise 1: Multiprocessor Support

Implement mmio_map_region in kern/pmap.c. To see how this is used, look at the beginning of lapic_init in kern/lapic.c. You’ll have to do the next exercise, too, before the tests for mmio_map_region will run.

*void mmio_map_region(physaddr_t pa, size_t size)

1
2
3
Reserve size bytes in the MMIO region and map [pa,pa+size) at this location.  Return the base of the reserved region.
在MMIO区域保留大小为size字节的区域,然后把[pa, pa+size]映射到此区域,返回该区域的base.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void *
mmio_map_region(physaddr_t pa, size_t size)
{
// Where to start the next region. Initially, this is the
// beginning of the MMIO region. Because this is static, its
// value will be preserved between calls to mmio_map_region
// (just like nextfree in boot_alloc).
static uintptr_t base = MMIOBASE;

// Hint: The staff solution uses boot_map_region.
//
// Your code here:
// panic("mmio_map_region not implemented");

// 取整页数
size_t roundup_sz = ROUNDUP(size, PGSIZE);
if(base+roundup_sz > MMIOLIM){
panic("mmio_map_region: out of MMIOLIM.");
}
boot_map_region(kern_pgdir, base, roundup_sz, pa, PTE_PCD|PTE_PWT|PTE_W);
uintptr_t res_region_base = base;
base += roundup_sz;
return (void*)res_region_base;
}

void lapic_init(void)

image-20220705153621702

该函数一开始就调用了mmin_map_region函数,pa为lapicaddr,size为4096 = 4KB,即将从lapicaddr开始的4KB的物理地址映射到虚拟地址(保留区域),以便我们能访问,然后返回保留区域的起始地址。

Exercise 2: Application Processor Bootstrap

在启动 APs 之前,BSP 需要先搜集多处理器系统的信息,例如 CPU 的总数,CPU 各自的 APIC ID,LAPIC 单元的 MMIO 地址。kern/mpconfig.c 中的 mp_init() 函数通过阅读 BIOS 区域内存中的 MP 配置表来获取这些信息。
boot_aps() 函数驱动了 AP 的引导。APs 从实模式开始,如同 boot/boot.S 中 bootloader 的启动过程。因此 boot_aps() 将 AP 的入口代码 (kern/mpentry.S) 拷贝到实模式可以寻址的内存区域 (0x7000, MPENTRY_PADDR)。
此后,boot_aps() 通过发送 STARTUP 这个跨处理器中断到各 LAPIC 单元的方式,逐个激活 APs。激活方式为:初始化 AP 的 CS:IP 值使其从入口代码执行。通过一些简单的设置,AP 开启分页进入保护模式,然后调用 C 语言编写的 mp_main()boot_aps() 等待 AP 发送 CPU_STARTED 信号,然后再唤醒下一个。

Read boot_aps() and mp_main() in kern/init.c, and the assembly code in kern/mpentry.S. Make sure you understand the control flow transfer during the bootstrap of APs. Then modify your implementation of page_init() in kern/pmap.cto avoid adding the page at MPENTRY_PADDR to the free list, so that we can safely copy and run AP bootstrap code at that physical address. Your code should pass the updated check_page_free_list() test (but might fail the updated check_kern_pgdir() test, which we will fix soon).

image-20220705161234358

image-20220705161244343

image-20220705162615525

real mode表示我们看到的都是直接的物理地址

Question

  1. Compare kern/mpentry.S side by side with boot/boot.S. Bearing in mind that kern/mpentry.S is compiled and linked to run above KERNBASE just like everything else in the kernel, what is the purpose of macro MPBOOTPHYS? Why is it necessary in kern/mpentry.S but not in boot/boot.S? In other words, what could go wrong if it were omitted in kern/mpentry.S?
    Hint: recall the differences between the link address and the load address that we have discussed in Lab 1.

MPBOOTPHYS的作用:

kern/mpentry.S是运行在kernbase之上的,因此在实地址模式下,无法进行寻址(即无法访问)。

kern/mpentry.S

1
2
3
4
5
6
7
# This code is similar to boot/boot.S except that
# - it does not need to enable A20
# - it uses MPBOOTPHYS to calculate absolute addresses of its
# symbols, rather than relying on the linker to fill them

#define RELOC(x) ((x) - KERNBASE)
#define MPBOOTPHYS(s) ((s) - mpentry_start + MPENTRY_PADDR)

((s) - mpentry_start + MPENTRY_PADDR) 表示把mpentry地址转换到MPENTRY_PADDR。

image-20220705163912143

kern/init.c中已经进行了内容的拷贝,因此boot.S不需要再次 进行拷贝。而mpentry.S需要宏进行拷贝。

Per-CPU State and Initialization(CPU私有状态和初始化)

当写一个多处理器操作系统时,分清 CPU 的私有状态 ( per-CPU state) 及全局状态 (global state) 非常关键。 kern/cpu.h 定义了大部分的 per-CPU 状态。
我们需要注意的 per-CPU 状态有:

  • Per-CPU 内核栈
    因为多 CPU 可能同时陷入内核态,我们需要给每个处理器一个独立的内核栈。用户态切到内核态,内核将用户态时的堆栈寄存器的值保存在内核栈中,以便于从内核栈切换回进程栈时能找到用户栈的地址。

    percpu_kstacks[NCPU][KSTKSIZE]
    在 Lab2 中,我们将 BSP 的内核栈映射到了 KSTACKTOP 下方。相似地,在 Lab4 中,我们需要把每个 CPU 的内核栈都映射到这个区域,每个栈之间留下一个空页作为缓冲区避免 overflow。CPU 0 ,即 BSP 的栈还是从 KSTACKTOP 开始,间隔 KSTACKGAP 的距离就是 CPU 1 的栈,以此类推。

  • Per-CPU TSS 以及 TSS 描述符
    为了指明每个 CPU 的内核栈位置,需要任务状态段 (Task State Segment, TSS),其功能在 Lab3 中已经详细讲过。

  • Per-CPU 当前环境指针
    因为每个 CPU 能够同时运行各自的用户进程,我们重新定义了基于cpus[cpunum()]curenv

  • Per-CPU 系统寄存器
    所有的寄存器,包括系统寄存器,都是 CPU 私有的。因此,初始化这些寄存器的指令,例如 lcr3(), ltr(), lgdt(), lidt() 等,必须在每个 CPU 都执行一次。

Exercise 3.

Modify mem_init_mp() (in kern/pmap.c) to map per-CPU stacks starting at KSTACKTOP, as shown in inc/memlayout.h. The size of each stack is KSTKSIZE bytes plus KSTKGAP bytes of unmapped guard pages. Your code should pass the new check in check_kern_pgdir().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
mem_init_mp(void)
{

// LAB 4: Your code here:
// LAB 2 中只把BSP的内核栈 map 到了KSTACKTOP下方,这次把所有的cpu都进行map
// 在 kern/cpu.h 中可以找到对 NCPU 以及全局变量 percpu_kstacks 的声明
uintptr_t start_addr = KSTACKTOP - KSTKSIZE; // 倒着来
for(size_t i = start_addr; i<NCPU;++i){ // i代表cpu编号
boot_map_region(kern_pgdir, (uintptr_t)start_addr, KSTKSIZE, PADDR(percpu_kstacks[i]), PTE_W|PTE_P);
start_addr-=(KSTKSIZE+KSTKGAP); //gap是两个cpu之间的缓冲区
}
// 即使从KSTACKTOP重新开始分配,之前的BSP会直接被覆盖掉,不会有影响
}

Exercise 4.

inc/memlayout.h 中可以看到之前分配BSP时,TSS0的定义

image-20220705172629111

这只是对单CPU而言的定义,那么对于多个cpu,就需要其他的TSS,如何寻找这个TSS?trap_init_percpu()注释中说明了

image-20220705172824320

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
// Initialize and load the per-CPU TSS and IDT
void
trap_init_percpu(void)
{

// LAB 4: Your code here:
struct Taskstate* this_ts = &thiscpu->cpu_ts; //!!!!!!!!!!!!
// 一开始写成了struct Taskstate this_ts = thiscpu->cpu_ts;
// 导致cpu的值并未改变,然后出现triple fault !!!!!!!!!!
// Setup a TSS so that we get the right stack
// when we trap to the kernel.
ts.ts_esp0 = KSTACKTOP;
ts.ts_ss0 = GD_KD;
ts.ts_iomb = sizeof(struct Taskstate);

// Initialize the TSS slot of the gdt.
gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&ts),
sizeof(struct Taskstate) - 1, 0);
gdt[GD_TSS0 >> 3].sd_s = 0;

// Load the TSS selector (like other segment selectors, the
// bottom three bits are special; we leave them 0)
ltr(GD_TSS0);

// Load the IDT
lidt(&idt_pd);
}

运行 make qemu CPUS=4 (or make qemu-nox CPUS=4)

image-20220705180117691

Exercise5: Locking

我们现在的代码在初始化 AP 后就会开始自旋。在进一步操作 AP 之前,我们要先处理几个 CPU 同时运行内核代码的竞争情况。最简单的方法是用一个大内核锁 (big kernel lock)。它是一个全局锁,在某个进程进入内核态时锁定,返回用户态时释放。这种模式下,用户进程可以并发地在 CPU 上运行,但是同一时间仅有一个进程可以在内核态,其他需要进入内核态的进程只能等待。
kern/spinlock.h 声明了一个大内核锁 kernel_lock。它提供了 lock_kernel()unlock_kernel() 方法用于获得和释放锁。在以下 4 个地方需要使用到大内核锁:

  • i386_init(),BSP 唤醒其他 CPU 之前获得内核锁
  • mp_main(),初始化 AP 之后获得内核锁,之后调用 sched_yield() 在 AP 上运行进程。
  • trap(),当从用户态陷入内核态时获得内核锁,通过检查 tf_Cs 的低 2bit 来确定该 trap 是由用户进程还是内核触发。
  • env_run(),在切换回用户模式前释放内核锁。

Apply the big kernel lock as described above, by calling lock_kernel() and unlock_kernel() at the proper locations.

kern/init.c/i386_init()

1
2
3
4
5
// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();
// Starting non-boot CPUs
boot_aps();

init.c/mp_main()

1
2
3
4
5
6
7
// Now that we have finished some basic setup, call sched_yield()
// to start running processes on this CPU. But make sure that
// only one CPU can enter the scheduler at a time!
//
// Your code here:
lock_kernel();
sched_yield();

trap.c/trap()

1
2
3
4
5
6
7
if ((tf->tf_cs & 3) == 3) {   // 陷入了内核态
// Trapped from user mode.
// Acquire the big kernel lock before doing any
// serious kernel work.
// LAB 4: Your code here.
lock_kernel();
assert(curenv);

env.c/env_run()

1
2
3
lcr3(PADDR(e->env_pgdir));
unlock_kernel();
env_pop_tf(&e->env_tf);

为什么要在这几处加大内核锁
从根本上来讲,其设计的初衷就是保证独立性。由于分页机制的存在,内核以及每个用户进程都有自己的独立空间。而多进程并发的时候,如果两个进程同时陷入内核态,就无法保证独立性了。例如内核中有某个全局变量 A,cpu1 让 A=1, 而后 cpu2 却让 A=2,显然会互相影响。

BPS 启动 AP 前,获取内核锁,所以 AP 会在 mp_main 执行调度之前阻塞,在启动完 AP 后,BPS 执行调度,运行第一个进程,env_run() 函数中会释放内核锁,这样一来,其中一个 AP 就可以开始执行调度,运行其他进程。

Question 2.
It seems that using the big kernel lock guarantees that only one CPU can run the kernel code at a time. Why do we still need separate kernel stacks for each CPU? Describe a scenario in which using a shared kernel stack will go wrong, even with the protection of the big kernel lock

在某进程即将陷入内核态的时候(尚未获得锁),其实在 trap() 函数之前已经在 trapentry.S 中对内核栈进行了操作,压入了寄存器信息。如果共用一个内核栈,那显然会导致信息错误。

Exercise6: Round-Robin Scheduling 轮询调度

下一个任务是让 JOS 内核能够以轮询方式在多个任务之间切换。其原理如下:

  • kern/sched.c 中的 sched_yield() 函数用来选择一个新的进程运行。它将从上一个运行的进程开始,按顺序循环搜索 envs[] 数组,选取第一个状态为 ENV_RUNNABLE 的进程执行。
  • sched_yield()不能同时在两个CPU上运行同一个进程。如果一个进程已经在某个 CPU 上运行,其状态会变为 ENV_RUNNING
  • 程序中已经实现了一个新的系统调用 sys_yield(),进程可以用它来唤起内核的 sched_yield() 函数,从而将 CPU 资源移交给一个其他的进程

Exercise 6.
Implement round-robin scheduling in sched_yield() as described above. Don’t forget to modify syscall() to dispatch sys_yield().
Make sure to invoke sched_yield() in mp_main.
Modify kern/init.c to create three (or more!) environments that all run the program user/yield.c.

首先要找到正在运行的进程在envs[]中的序号。

image-20220706103342874

sched.c/sched_yield()

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
// Choose a user environment to run and run it.
void
sched_yield(void)
{
struct Env *idle;

// Implement simple round-robin scheduling.
//
// Search through 'envs' for an ENV_RUNNABLE environment in
// circular fashion starting just after the env this CPU was
// last running. Switch to the first such environment found.
//
// If no envs are runnable, but the environment previously
// running on this CPU is still ENV_RUNNING, it's okay to
// choose that environment.
//
// Never choose an environment that's currently running on
// another CPU (env_status == ENV_RUNNING). If there are
// no runnable environments, simply drop through to the code
// below to halt the cpu.

// LAB 4: Your code here.
idle = curenv;
size_t running_idx = (idle == NULL)?:-1:ENVX(idle->env_id);
for(size_t i = 0;i<NENV;++i){
running_idx = (running_idx+1 == NENV)?0:running_idx+1; // 0代表从头再搜,因为起始点可能在中间
if(envs[running_idx].env_status == RUNNABLE){ // 选取第一个状态为RUNNABLE的进程执行
env_run(&envs[running_idx]);
return;
}
}
// 如果没有可运行的环境,但是之前在此CPU上运行的环境仍然是ENV_RUNNING,则可以选择该环境
if(idle && envs[idle].env_status == ENV_RUNNING){
env_run(idle);
return;
}
// sched_halt never returns
sched_halt();
}

inc/syscall.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* system call numbers */ 在syscall.h中有定义,syscall不是trap!!!

/* system call numbers */
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
SYS_page_alloc,
SYS_page_map,
SYS_page_unmap,
SYS_exofork,
SYS_env_set_status,
SYS_env_set_pgfault_upcall,
SYS_yield,
SYS_ipc_try_send,
SYS_ipc_recv,
NSYSCALLS
};

Question 3.
In your implementation of env_run() you should have called lcr3(). Before and after the call to lcr3(), your code makes references (at least it should) to the variable e, the argument to env_run. Upon loading the %cr3 register, the addressing context used by the MMU is instantly changed. But a virtual address (namely e) has meaning relative to a given address context–the address context specifies the physical address to which the virtual address maps. Why can the pointer e be dereferenced both before and after the addressing switch?

大意是问为什么通过 lcr3() 切换了页目录,还能照常对 e 解引用。回想在 lab3 中,曾经写过的函数 env_setup_vm()。它直接以内核的页目录作为模版稍做修改。因此两个页目录的 e 地址映射到同一物理地址。

Question 4.
Whenever the kernel switches from one environment to another, it must ensure the old environment’s registers are saved so they can be restored properly later. Why? Where does this happen?

在进程陷入内核时,会保存当前的运行信息,这些信息都保存在内核栈上。而当从内核态回到用户态时,会恢复之前保存的运行信息。
具体到 JOS 代码中,保存发生在 kern/trapentry.S,恢复发生在 kern/env.c。可以对比两者的代码。
保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define TRAPHANDLER_NOEC(name, num)
.globl name;
.type name, @function;
.align 2;
name:
pushl $0;
pushl $(num);
jmp _alltraps
...

_alltraps:
pushl %ds // 保存当前段寄存器
pushl %es
pushal // 保存其他寄存器

movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es
pushl %esp // 保存当前栈顶指针
call trap

恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
env_pop_tf(struct Trapframe *tf)
{
// Record the CPU we are running on for user-space debugging
curenv->env_cpunum = cpunum();

asm volatile(
"\tmovl %0,%%esp\n" // 恢复栈顶指针
"\tpopal\n" // 恢复其他寄存器
"\tpopl %%es\n" // 恢复段寄存器
"\tpopl %%ds\n"
"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
"\tiret\n"
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
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
➜  lab git:(lab4) ✗ make qemu-nox CPUS=2
***
*** Use Ctrl-a x to exit qemu
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log -smp 2
6828 decimal is 15254 octal!
Physical memory: 131072K available, base = 640K, extended = 130432K
check_page_free_list() succeeded!
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
SMP: CPU 0 found 2 CPU(s)
enabled interrupts: 1 2
SMP: CPU 1 starting
[00000000] new env 00001000
[00000000] new env 00001001
[00000000] new env 00001002
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Hello, I am environment 00001000.
Incoming TRAP frame at 0xeffeffbc
Hello, I am environment 00001001.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP fIncoming TRAP frame at 0xefffffbc
rame at 0xeffeffbc
Incoming TRAP frame at 0Incoming TRAP frame at 0xefffffbc
Hello, I am environment 00001002.
xeffeffbc
Back in environment 00001000, iteration 0.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001001, iteration 0.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001002, iteration 0.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001000, iteration 1.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001001, iteration 1.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001002, iteration 1.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001000, iteration 2.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001001, iteration 2.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001002, iteration 2.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001000, iteration 3.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001001, iteration 3.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
Incoming TRAP frame at 0xefffffbc
Back in environment 00001002, iteration 3.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001000, iteration 4.
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xeffeffbc
All done in environment 00001000.
Incoming TRAP frame at 0xefffffbc
Back in environment 00001001, iteration 4.
Incoming TRAP frame at 0xeffeffbc
[00001000] exiting gracefully
[00001000] free env 00001000
Incoming TRAP frame at 0xefffffbc
All done in environment 00001001.
Incoming TRAP frame at 0xeffeffbc
Back in environment 00001002, iteration 4.
Incoming TRAP frame at 0xefffffbc
[00001001] exiting gracefully
[00001001] free env 00001001
Incoming TRAP frame at 0xeffeffbc
All done in environment 00001002.
Incoming TRAP frame at 0xeffeffbc
[00001002] exiting gracefully
[00001002] free env 00001002
No runnable environments in the system!
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.

cpu=2时,三个进程通过sys_yield切换了5次。

Exercise 7: System Calls for Environment Creation(系统调用: 创建进程)

现在我们的内核已经可以运行多个进程,并在其中切换了。不过,现在它仍然只能运行内核最初设定好的程序 (kern/init.c) 。现在我们即将实现一个新的系统调用,它允许进程创建并开始新的进程。
Unix 提供了 fork() 这个原始的系统调用来创建进程。fork()将会拷贝父进程的整个地址空间来创建子进程。在用户空间里,父子进程之间的唯一区别就是它们的进程 ID。fork()在父进程中返回其子进程的进程 ID,而在子进程中返回 0。父子进程之间是完全独立的,任意一方修改内存,另一方都不会受到影响。
我们将为 JOS 实现一个更原始的系统调用来创建新的进程。涉及到的系统调用如下:

  • sys_exofork:
    这个系统调用将会创建一个空白进程:在其用户空间中没有映射任何物理内存,并且它是不可运行的。刚开始时,它拥有和父进程相同的寄存器状态。sys_exofork 将会在父进程返回其子进程的envid_t,子进程返回 0(当然,由于子进程还无法运行,也无法返回值,直到运行:)
  • sys_env_set_status:
    设置指定进程的状态。这个系统调用通常用于在新进程的地址空间和寄存器初始化完成后,将其标记为可运行。
  • sys_page_alloc:
    分配一个物理页并将其映射到指定进程的指定虚拟地址上。
  • sys_page_map:
    从一个进程中拷贝一个页面映射(而非物理页的内容)到另一个。即共享内存。
  • sys_page_unmap:
    删除到指定进程的指定虚拟地址的映射。

Exercise 7.
Implement the system calls described above in kern/syscall.c. You will need to use various functions in kern/pmap.c and kern/env.c, particularly envid2env(). For now, whenever you call envid2env(), pass 1 in the checkperm parameter. Be sure you check for any invalid system call arguments, returning -E_INVAL in that case. Test your JOS kernel with user/dumbfork and make sure it works before proceeding.

一个比较冗长的练习。重点应该放在阅读 user/dumbfork.c 上,以便理解各个系统调用的作用。
user/dumbfork.c 中,核心是 duppage() 函数。它利用 sys_page_alloc() 为子进程分配空闲物理页,再使用sys_page_map() 将该新物理页映射到内核 (内核的 env_id = 0) 的交换区 UTEMP,方便在内核态进行 memmove 拷贝操作。在拷贝结束后,利用 sys_page_unmap() 将交换区的映射删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
duppage(envid_t dstenv, void *addr)
{
int r;

// This is NOT what you should do in your fork.
if ((r = sys_page_alloc(dstenv, addr, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_alloc: %e", r);
if ((r = sys_page_map(dstenv, addr, 0, UTEMP, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_map: %e", r);
memmove(UTEMP, addr, PGSIZE);
if ((r = sys_page_unmap(0, UTEMP)) < 0)
panic("sys_page_unmap: %e", r);
}

sys_exofork() 函数

该函数主要是分配了一个新的进程,但是没有做内存复制等处理。唯一值得注意的就是如何使子进程返回0。
sys_exofork()是一个非常特殊的系统调用,它的定义与实现在 inc/lib.h 中,而不是 lib/syscall.c 中。并且,它必须是 inline 的。

1
2
3
4
5
6
7
8
9
10
// This must be inlined.  Exercise for reader: why?
static inline envid_t __attribute__((always_inline))
sys_exofork(void)
{
envid_t ret;
asm volatile("int %2"
: "=a" (ret)
: "a" (SYS_exofork), "i" (T_SYSCALL));
return ret;
}

可以看出,它的返回值是 %eax 寄存器的值。那么,它到底是什么时候返回?这就涉及到对整个 进程->内核->进程 的过程的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
static envid_t
sys_exofork(void)
{
// LAB 4: Your code here.
// panic("sys_exofork not implemented");
struct Env *e;
int r = env_alloc(&e, curenv->env_id);
if (r < 0) return r;
e->env_status = ENV_NOT_RUNNABLE;
e->env_tf = curenv->env_tf;
e->env_tf.tf_regs.reg_eax = 0;
return e->env_id;
}

在该函数中,子进程复制了父进程的 trapframe,此后把 trapframe 中的 eax 的值设为了0。最后,返回了子进程的 id。注意,根据 kern/trap.c 中的 trap_dispatch() 函数,这个返回值仅仅是存放在了父进程的 trapframe 中,还没有返回。而是在返回用户态的时候,即在 env_run() 中调用 env_pop_tf() 时,才把 trapframe 中的值赋值给各个寄存器。这时候 lib/syscall.c 中的函数 syscall() 才获得真正的返回值。因此,在这里对子进程 trapframe 的修改,可以使得子进程返回0。

sys_page_alloc() 函数
在进程 envid 的目标地址 va 分配一个权限为 perm 的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int
sys_page_alloc(envid_t envid, void *va, int perm)
{
// LAB 4: Your code here.
// panic("sys_page_alloc not implemented");
if ((~perm & (PTE_U|PTE_P)) != 0) return -E_INVAL;
if ((perm & (~(PTE_U|PTE_P|PTE_AVAIL|PTE_W))) != 0) return -E_INVAL;
if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) return -E_INVAL;

struct PageInfo *pginfo = page_alloc(ALLOC_ZERO);
if (!pginfo) return -E_NO_MEM;
struct Env *e;
int r = envid2env(envid, &e, 1);
if (r < 0) return -E_BAD_ENV;
r = page_insert(e->env_pgdir, pginfo, va, perm);
if (r < 0) {
page_free(pginfo);
return -E_NO_MEM;
}
return 0;
}

sys_page_map() 函数
简单来说,就是建立跨进程的映射。

在srcenvid地址空间的’srcva’映射到dstenvid地址空间的’dstva’,并赋予’perm ‘权限。Perm具有与sys_page_alloc相同的限制,但它也不能授予对只读页面的写访问权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int
sys_page_map(envid_t srcenvid, void *srcva,
envid_t dstenvid, void *dstva, int perm)
{
// LAB 4: Your code here.
// panic("sys_page_map not implemented");

if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) != 0) return -E_INVAL;
if ((uintptr_t)dstva >= UTOP || PGOFF(dstva) != 0) return -E_INVAL;
if ((perm & PTE_U) == 0 || (perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) != 0) return -E_INVAL;
struct Env *src_e, *dst_e;
if (envid2env(srcenvid, &src_e, 1)<0 || envid2env(dstenvid, &dst_e, 1)<0) return -E_BAD_ENV;
pte_t *src_ptab;
// page_look_up() 返回映射到虚拟地址 va 的页面
// page_insert() 建立一个虚拟地址与物理页的映射
struct PageInfo *pp = page_lookup(src_e->env_pgdir, srcva, &src_ptab); // 查询srcva的page
if ((*src_ptab & PTE_W) == 0 && (perm & PTE_W) == 1) return -E_INVAL;
if (page_insert(dst_e->env_pgdir, pp, dstva, perm) < 0) return -E_NO_MEM; // srcpage 映射到dstva
return 0;
}

sys_page_unmap() 函数
取消映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
static int
sys_page_unmap(envid_t envid, void *va) //取消envid的地址空间中地址va的页映射
{
// Hint: This function is a wrapper around page_remove().

// LAB 4: Your code here.
// panic("sys_page_unmap not implemented");
if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) return -E_INVAL;
struct Env *e;
if (envid2env(envid, &e, 1) < 0) return -E_BAD_ENV; // 获取envid对应的进程
page_remove(e->env_pgdir, va);
return 0;
}

sys_env_set_status() 函数
设置状态,在子进程内存 map 结束后再使用。

1
2
3
4
5
6
7
8
9
10
11
12
static int
sys_env_set_status(envid_t envid, int status)
{
// LAB 4: Your code here.
// panic("sys_env_set_status not implemented");

if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) return -E_INVAL;
struct Env *e;
if (envid2env(envid, &e, 1) < 0) return -E_BAD_ENV;
e->env_status = status;
return 0;
}

最后,不要忘记在 kern/syscall.c 中添加新的系统调用类型,注意参数的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
case SYS_exofork:
retVal = (int32_t)sys_exofork();
break;
case SYS_env_set_status:
retVal = sys_env_set_status(a1, a2);
break;
case SYS_page_alloc:
retVal = sys_page_alloc(a1,(void *)a2, (int)a3);
break;
case SYS_page_map:
retVal = sys_page_map(a1, (void *)a2, a3, (void*)a4, (int)a5);
break;
case SYS_page_unmap:
retVal = sys_page_unmap(a1, (void *)a2);
break;
...

make grade 成功。至此,part A 结束。

链接:https://www.jianshu.com/p/10f822b3deda

Part B: Copy-on-Write Fork 写时拷贝的Fork

在Part A中,通过把父进程的所有内存数据拷贝到子进程实现了fork(), 这也是 Unix 系统早期的实现。这个拷贝到过程是 fork() 时最昂贵的操作。
然而,调用了 fork() 之后往往立即就会在子进程中调用 exec() ,将子进程的内存更换为新的程序,例如 shell 经常干的(HW:Shell)。这样,复制父进程的内存这个操作就完全浪费了。

因此,后来的 Unix 系统让父、子进程共享同一片物理内存,直到某个进程修改了内存。这被称作 copy-on-write。为了实现它,fork()时内核只拷贝页面的映射关系,而不拷贝其内容,同时将共享的页面标记为只读 (read-only)。当父子进程中任一方向内存中写入数据时,就会触发 page fault。此时,Unix 就知道应该分配一个私有的可写内存给这个进程。这个优化使得 fork() + exec() 连续操作变得非常廉价。在执行 exec() 之前,只需要拷贝一个页面,即当前的栈。

在 Part B 中,我们将实现上述更佳实现方式的 fork()

User-level page fault handling 用户级别的页错误

内核必须要记录进程不同区域出现页面错误时的处理方法。例如,一个栈区域的 page fault 会分配并映射一个新的页。一个 BSS 区域(用于存放程序中未初始化的全局变量、静态变量)的页错误会分配一个新的页面,初始化为0,再映射。
用户级别的页错误处理流程为:

  1. 页错误异常,陷入内核
  2. 内核修改 %esp 切换到进程的异常栈,修改 %eip 让进程运行 _pgfault_upcall
  3. _pgfault_upcall 将运行 page fault handler,此后不通过内核切换回正常栈

EIP为:返回本次调用后,下一条指令的地址。

ESP:存放当前线程的栈顶指针。

EBP:存放当前线程的栈底指针。

在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbolsegment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

Exercise 8: 设置页错误处理函数

为处理自己的页错误,进程需要在 JOS 注册一个 page fault handler entrypoint。进程通过 sys_env_set_pgfault_upcall 注册自己的 entrypoint,并在 Env 结构体中新增 env_pgfault_upcall 来记录该信息。

Exercise 8.
Implement the sys_env_set_pgfault_upcall system call. Be sure to enable permission checking when looking up the environment ID of the target environment, since this is a “dangerous” system call.

进程的正常栈和异常栈

正常运行时,JOS 的进程会运行在正常栈上,ESPUSTACKTOP开始往下生长,栈上的数据存放在 [USTACKTOP-PGSIZE, USTACKTOP-1] 上。当出现页错误时,内核会把进程在一个新的栈(异常栈)上面重启,运行指定的用户级别页错误处理函数。也就是说完成了一次进程内的栈切换。这个过程与 trap 的过程很相似。
JOS 的异常栈也只有一个物理页大小,并且它的栈顶定义在虚拟内存 UXSTACKTOP 处。当运行在这个栈上时,用户级别页错误处理函数可以使用 JOS 的系统调用来映射新的页,以修复页错误。
每个需要支持用户级页错误处理的函数都需要分配自己的异常栈。可以使用 sys_page_alloc() 这个系统调用来实现。

用户页错误处理函数

现在我们需要修改 kern/trap.c 以支持用户级别的页错误处理。
如果没有注册 page fault handler,JOS内核就直接销毁进程。否则,内核就会初始化一个 trap frame 记录寄存器状态,在异常栈上处理页错误,恢复进程的执行。UTrapframe 在异常栈栈上如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi end of struct PushRegs
tf_err (error code)
fault_va <-- %esp when handler is run

相比 trap 时使用的 Trapframe,多了记录错误位置的 fault_va,少了段选择器%cs, %ds, %ss。这反映了两者最大的不同:是否发生了进程的切换。
如果异常发生时,进程已经在异常栈上运行了,这就说明 page fault handler 本身出现了问题。这时,我们就应该在 tf->tf_esp 处分配新的栈,而不是在 UXSTACKTOP。首先需要 push 一个空的 32bit word 作为占位符,然后是一个 UTrapframe 结构体。
为检查 tf->tf_esp 是否已经在异常栈上了,只要检查它是否在区间 [UXSTACKTOP-PGSIZE, UXSTACKTOP-1] 上即可。

以下9,10,11三个练习,建议按照调用顺序来看,即 11(设置handler)->9(切换到异常栈)->10(运行handler,切换回正常栈)。

Exercise 9.

Implement the code in page_fault_handler in kern/trap.c required to dispatch page faults to the user-mode handler. Be sure to take appropriate precautions when writing into the exception stack. (What happens if the user environment runs out of space on the exception stack?)

可参考 Exercise 10 的 lib/pfentry.S 中的注释
较有难度的一个练习。首先需要理解用户级别的页错误处理的步骤是:
进程A(正常栈) -> 内核 -> 进程A(异常栈) -> 进程A(正常栈)
那么内核的工作就是修改进程 A 的某些寄存器,并初始化异常栈,确保能顺利切换到异常栈运行。需要注意的是,由于修改了eip, env_run() 是不会返回的,因此不会继续运行后面销毁进程的代码。
值得注意的是,如果是嵌套的页错误,为了能实现递归处理,栈留出 32bit 的空位,直接向下生长。

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
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;

// Read processor's CR2 register to find the faulting address
fault_va = rcr2();

// Handle kernel-mode page faults.

// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0) panic("Page fault in kernel-mode");

// LAB 4: Your code here.
if(curenv->env_pgfault_upcall){ // Page fault upcall entry point
// 建立异常栈
struct UTrapframe* utf; // 初始化
if(tf->tf_esp < UXSTACKTOP && tf->tf->esp >=UXSTACKTOP-PGSIZE){
utf = (struct UTrapframe *)(tf->tf_esp - 4 - sizeof(struct UTrapframe));
} else {
utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}
user_mem_assert(curenv, (void *)utf, sizeof(struct UTrapframe), PTE_U | PTE_W | PTE_P);
// 检查用户是否有权限读取,无权限则摧毁,之后不再进行
// 转存寄存器信息
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_trapno;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;
// 修改 esp 完成栈切换,修改 eip 运行 handler
tf->tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
// tf->esp = (uintptr_t)utf - 1; 不需要减1
tf->tf_esp = (uintptr_t)utf;
env_run(curenv);
}

// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}

Question
What happens if the user environment runs out of space on the exception stack?

inc/memlayout.h 中可以找到:

1
2
#define UXSTACKTOP  UTOP
// Next page left invalid to guard against exception stack overflow;

image-20220707135436275

下面一页是空页,内核和用户访问都会报错。

用户模式页错误入口

在处理完页错误之后,现在我们需要编写汇编语句实现从异常栈到正常栈的切换。

Exercise 10.

Implement the _pgfault_upcall routine in lib/pfentry.S. The interesting part is returning to the original point in the user code that caused the page fault. You’ll return directly there, without going back through the kernel. The hard part is simultaneously switching stacks and re-loading the EIP.

汇编苦手,写的很艰难,最终还是参考了别人的答案

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
.text
.globl _pgfault_upcall
_pgfault_upcall:
// 调用用户定义的页错误处理函数
// Call the C page fault handler.
pushl %esp // function argument: pointer to UTF
movl _pgfault_handler, %eax
call *%eax
addl $4, %esp // pop function argument

// LAB 4: Your code here.
movl 48(%esp), %ebp
subl $4, %ebp
movl %ebp, 48(%esp)
movl 40(%esp), %eax
movl %eax, (%ebp)

// Restore the trap-time registers. After you do this, you
// can no longer modify any general-purpose registers.
// LAB 4: Your code here.
// 跳过 utf_err 以及 utf_fault_va
addl $8, %esp
// popal 同时 esp 会增加,执行结束后 %esp 指向 utf_eip
popal

// Restore eflags from the stack. After you do this, you can
// no longer use arithmetic operations or anything else that
// modifies eflags.
// LAB 4: Your code here.
// 跳过 utf_eip
addl $4, %esp
// 恢复 eflags
popfl

// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
// 恢复 trap-time 的栈顶
popl %esp
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
// ret 指令相当于 popl %eip
ret

首先必须要理解异常栈的结构,下图所示的是嵌套异常时的情况。其中左边表示内容,右边表示地址。需要注意的是,上一次异常的栈顶之下间隔 4byte,就是一个新的异常。

img

uxstack.png

最难理解的是这一部分:

1
2
3
4
5
movl 48(%esp), %ebp  // 使 %ebp 指向 utf_esp
subl $4, %ebp
movl %ebp, 48(%esp) // 更新 utf_esp 值为 utf_esp-4
movl 40(%esp), %eax
movl %eax, (%ebp) // 将 utf_esp-4 地址的内容改为 utf_eip

经过这一部分的修改,异常栈更新为(红字标出):

img

uxstack_new.png

此后就是恢复各寄存器,最后的 ret 指令相当于 popl %eip,指令寄存器的值修改为 utf_eip,达到了返回的效果。

Exercise 11.

Finish set_pgfault_handler() in lib/pgfault.c.

该练习是用户用来指定缺页异常处理方式的函数。代码比较简单,但是需要区分清楚 handler_pgfault_handler_pgfault_upcall 三个变量。

  1. handler 是传入的用户自定义页错误处理函数指针。
  2. _pgfault_upcall 是一个全局变量,在 lib/pfentry.S 中完成的初始化。它是页错误处理的总入口,页错误除了运行 page fault handler,还需要切换回正常栈。
  3. _pgfault_handler 被赋值为handler,会在 _pgfault_upcall 中被调用,是页错误处理的一部分。具体代码是:
1
2
3
4
5
6
7
8
.text
.globl _pgfault_upcall
_pgfault_upcall:
// Call the C page fault handler.
pushl %esp // function argument: pointer to UTF
movl _pgfault_handler, %eax
call *%eax
addl $4, %esp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
int r;

if (_pgfault_handler == 0) {
// First time through!
// LAB 4: Your code here.
// panic("set_pgfault_handler not implemented");
envid_t e_id = sys_getenvid();
r = sys_page_alloc(e_id, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_W | PTE_P);
if (r < 0) {
panic("pgfault_handler: %e", r);
}
// r = sys_env_set_pgfault_upcall(e_id, handler);
r = sys_env_set_pgfault_upcall(e_id, _pgfault_upcall);
if (r < 0) {
panic("pgfault_handler: %e", r);
}
}

// Save handler pointer for assembly to call.
_pgfault_handler = handler;
}

若是第一次调用,需要首先分配一个页面作为异常栈,并且将该进程的 upcall 设置为 Exercise 10 中的程序。此后如果需要改变handler,不需要再重复这个工作。
最后直接通过 make grade 测试,满足要求。

Question
Why user/faultalloc and user/faultallocbad behave differently?

两者的 page fault handler 一样,但是一个使用 cprintf() 输出,另一个使用 sys_cput() 输出。
sys_cput()直接通过 lib/syscall.c 发起系统调用,其实现在 kern/syscall.c 中:

1
2
3
4
5
6
7
8
9
10
11
static void
sys_cputs(const char *s, size_t len)
{
// Check that the user has permission to read memory [s, s+len).
// Destroy the environment if not.

// LAB 3: Your code here.
user_mem_assert(curenv, s, len, PTE_U);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}

它检查了内存,因此在这里 panic 了。中途没有触发过页错误。

cprintf() 的实现可以在 lib/printf.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
int
vcprintf(const char *fmt, va_list ap)
{
struct printbuf b;

b.idx = 0;
b.cnt = 0;
vprintfmt((void*)putch, &b, fmt, ap);
sys_cputs(b.buf, b.idx);

return b.cnt;
}

int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;

va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);

return cnt;
}

它在调用 sys_cputs() 之前,首先在用户态执行了 vprintfmt() 将要输出的字符串存入结构体 b 中。在此过程中试图访问 0xdeadbeef 地址,触发并处理了页错误(其处理方式是在错误位置处分配一个字符串,内容是 "this string was faulted in at ..."),因此在继续调用 sys_cputs() 时不会出现 panic。

Implementing Copy-on-Write Fork

如同 dumbfork() 一样,fork() 也要创建一个新进程,并且在新进程中建立与父进程同样的内存映射。关键的不同点是,**dumbfork() 拷贝了物理页的内容,而 fork() 仅拷贝了映射关系,仅在某个进程需要改写某一页的内容时,才拷贝这一页的内容。其基本流程如下:**

  1. 父进程使用 set_pgfault_handlerpgfault() 设为 page fault handler
  2. 父进程使用 sys_exofork() 建立一个子进程
  3. 对每个在 UTOP 之下可写页面以及 COW 页面(用 PTE_COW 标识),父进程调用 duppage 将其“映射”到子进程,同时将其权限改为只读,并用 PTE_COW 位来与一般只读页面区别
    异常栈的分配方式与此不同,需要在子进程中分配一个新页面。因为 page fault handler 会实实在在地向异常栈写入内容,并在异常栈上运行。如果异常栈页面都用 COW 机制,那就没有能够执行拷贝这个过程的载体了
  4. 父进程会为子进程设置 user page fault entrypoint
  5. 子进程已经就绪,父进程将其设为 runnable

进程第一次往一个 COW page 写入内容时,会发生 page fault,其流程为:

  1. 内核将 page fault 传递至 _pgfault_upcall,它会调用 pgfault() handler
  2. pgfault() 检查错误类型,以及页面是否标记为PTE_COW
  3. pgfault() 分配一个新的页面并将 fault page 的内容拷贝进去,然后将旧的映射覆盖,使其映射到该新页面。
Exercise 12.

Implement fork, duppage and pgfault in lib/fork.c.
Test your code with the forktree program.

非常难的一个练习。

  • fork() 函数

    首先从主函数 fork() 入手,其大体结构可以仿造 user/dumbfork.c 写,但是有关键几处不同:

    • 设置 page fault handler,即 page fault upcall 调用的函数
    • duppage 的范围不同,fork() 不需要复制内核区域的映射
    • 为子进程设置 page fault upcall,之所以这么做,是因为 sys_exofork() 并不会复制父进程的 e->env_pgfault_upcall 给子进程。
    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
    envid_t
    fork(void)
    {
    // LAB 4: Your code here.
    // panic("fork not implemented");

    set_pgfault_handler(pgfault);
    envid_t e_id = sys_exofork();
    if (e_id < 0) panic("fork: %e", e_id);
    if (e_id == 0) {
    // child
    thisenv = &envs[ENVX(sys_getenvid())];
    return 0;
    }

    // parent
    // extern unsigned char end[];
    // for ((uint8_t *) addr = UTEXT; addr < end; addr += PGSIZE)
    for (uintptr_t addr = UTEXT; addr < USTACKTOP; addr += PGSIZE) {
    if ( (uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) ) {
    // dup page to child
    duppage(e_id, PGNUM(addr));
    }
    }
    // alloc page for exception stack
    int r = sys_page_alloc(e_id, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_W | PTE_P);
    if (r < 0) panic("fork: %e",r);

    // DO NOT FORGET
    extern void _pgfault_upcall();
    r = sys_env_set_pgfault_upcall(e_id, _pgfault_upcall);
    if (r < 0) panic("fork: set upcall for child fail, %e", r);

    // mark the child environment runnable
    if ((r = sys_env_set_status(e_id, ENV_RUNNABLE)) < 0)
    panic("sys_env_set_status: %e", r);

    return e_id;
    }

    duppage() 函数

    该函数的作用是复制父、子进程的页面映射。尤其注意一个权限问题。由于 sys_page_map() 页面的权限有硬性要求,因此必须要修正一下权限。之前没有修正导致一直报错,后来发现页面权限为 0x865,不符合 sys_page_map() 要求。

    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 int
    duppage(envid_t envid, unsigned pn)
    {
    int r;

    // LAB 4: Your code here.
    // panic("duppage not implemented");

    envid_t this_env_id = sys_getenvid();
    void * va = (void *)(pn * PGSIZE);

    int perm = uvpt[pn] & 0xFFF;
    if ( (perm & PTE_W) || (perm & PTE_COW) ) {
    // marked as COW and read-only
    perm |= PTE_COW;
    perm &= ~PTE_W;
    }
    // IMPORTANT: adjust permission to the syscall
    perm &= PTE_SYSCALL;
    // cprintf("fromenvid = %x, toenvid = %x, dup page %d, addr = %08p, perm = %03x\n",this_env_id, envid, pn, va, perm);
    if((r = sys_page_map(this_env_id, va, envid, va, perm)) < 0)
    panic("duppage: %e",r);
    if((r = sys_page_map(this_env_id, va, this_env_id, va, perm)) < 0)
    panic("duppage: %e",r);
    return 0;
    }

    pgfault() 函数

    这是 _pgfault_upcall 中调用的页错误处理函数。在调用之前,父子进程的页错误地址都引用同一页物理内存,该函数作用是分配一个物理页面使得两者独立。
    首先,它分配一个页面,映射到了交换区 PFTEMP 这个虚拟地址,然后通过 memmove() 函数将 addr 所在页面拷贝至 PFTEMP,此时有两个物理页保存了同样的内容。再将 addr 也映射到 PFTEMP 对应的物理页,最后解除了 PFTEMP 的映射,此时就只有 addr 指向新分配的物理页了,如此就完成了错误处理。

    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
    static void
    pgfault(struct UTrapframe *utf)
    {
    void *addr = (void *) utf->utf_fault_va;
    uint32_t err = utf->utf_err;
    int r;

    // Check that the faulting access was (1) a write, and (2) to a
    // copy-on-write page. If not, panic.
    // Hint:
    // Use the read-only page table mappings at uvpt
    // (see <inc/memlayout.h>).

    // LAB 4: Your code here.
    if ((err & FEC_WR)==0 || (uvpt[PGNUM(addr)] & PTE_COW)==0) {
    panic("pgfault: invalid user trap frame");
    }
    // Allocate a new page, map it at a temporary location (PFTEMP),
    // copy the data from the old page to the new page, then move the new
    // page to the old page's address.
    // Hint:
    // You should make three system calls.

    // LAB 4: Your code here.
    // panic("pgfault not implemented");
    envid_t envid = sys_getenvid();
    if ((r = sys_page_alloc(envid, (void *)PFTEMP, PTE_P | PTE_W | PTE_U)) < 0)
    panic("pgfault: page allocation failed %e", r);

    addr = ROUNDDOWN(addr, PGSIZE);
    memmove(PFTEMP, addr, PGSIZE);
    if ((r = sys_page_unmap(envid, addr)) < 0)
    panic("pgfault: page unmap failed (%e)", r);
    if ((r = sys_page_map(envid, PFTEMP, envid, addr, PTE_P | PTE_W |PTE_U)) < 0)
    panic("pgfault: page map failed (%e)", r);
    if ((r = sys_page_unmap(envid, PFTEMP)) < 0)
    panic("pgfault: page unmap failed (%e)", r);
    }

    可以通过 make run-forktree 验证结果

ref:https://www.jianshu.com/p/d9b6dcce1c48

Part C: 抢占式多进程处理 & 进程间通信

作为 lab4 的最后一步,我们要修改内核使之能抢占一些不配合的进程占用的资源,以及允许进程之间的通信。

Part I: 时钟中断以及抢占

尝试运行一下 user/spin 测试,该测试建立一个子进程,该子进程获得 CPU 资源后就进入死循环,这样内核以及父进程都无法再次获得 CPU。这显然是操作系统需要避免的。为了允许内核从一个正在运行的进程抢夺 CPU 资源,我们需要支持来自硬件时钟的外部硬件中断。

Interrupt discipline

外部中断用 IRQ(Interrupt Request) 表示。一共有 16 种 IRQ,在 picirq.c中将其增加了 IRQ_OFFSET 的偏移映射到了 IDT。
inc/trap.h 中, IRQ_OFFSET 被定义为 32。因此,IDT[32] 包含了时钟中断的处理入口地址。
联想 Lab3 中的内容:

x86 的所有异常可以用中断向量 031 表示,对应 IDT 的第 031 项。例如,页错误产生一个中断向量为 14 的异常。大于 32 的中断向量表示的都是中断

相对 xv6,在 JOS 中我们中了一个关键的简化:在内核态时禁用外部设备中断。外部中断使用 %eflag 寄存器的 FL_IF 位控制。当该位置 1 时,开启中断。由于我们的简化,我们只在进入以及离开内核时需要修改这个位。

我们需要确保在用户态时 FL_IF 置 1,使得当有中断发生时,可以被处理。我们在 bootloader 的第一条指令 cli就关闭了中断,然后再也没有开启过。

Exercise 13.

Modify kern/trapentry.S and kern/trap.c to initialize the appropriate entries in the IDT and provide handlers for IRQs 0 through 15. Then modify the code in env_alloc() in kern/env.c to ensure that user environments are always run with interrupts enabled.

比较简单,跟 Lab3 中的 Exercise 4 大同小异。相关的常数定义在 inc/trap.h 中可以找到。
kern/trapentry.S 中加入:

1
2
3
4
5
6
7
// IRQs
TRAPHANDLER(handler32, IRQ_OFFSET + IRQ_TIMER)
TRAPHANDLER(handler33, IRQ_OFFSET + IRQ_KBD)
TRAPHANDLER(handler36, IRQ_OFFSET + IRQ_SERIAL)
TRAPHANDLER(handler39, IRQ_OFFSET + IRQ_SPURIOUS)
TRAPHANDLER(handler46, IRQ_OFFSET + IRQ_IDE)
TRAPHANDLER(handler51, IRQ_OFFSET + IRQ_ERROR)

kern/trap.ctrap_init() 中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    // IRQs
void handler32();
void handler33();
void handler36();
void handler39();
void handler46();
void handler51();
...
// IRQs
SETGATE(idt[IRQ_OFFSET + IRQ_TIMER], 0, GD_KT, handler32, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_KBD], 0, GD_KT, handler33, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL], 0, GD_KT, handler36, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, handler39, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_IDE], 0, GD_KT, handler46, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_ERROR], 0, GD_KT, handler51, 0);

kern/env.cenv_alloc() 中加入:

1
2
3
// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;

Handling Clock Interrupts

user/spin 程序中,子进程开启后就陷入死循环,此后 kernel 无法再获得控制权。我们需要让硬件周期性地产生时钟中断,强制将控制权交给 kernel,使得我们能够切换到其他进程。

Exercise 14.

Modify the kernel’s trap_dispatch() function so that it calls sched_yield() to find and run a different environment whenever a clock interrupt takes place.

直接在 trap_dispatch() 中添加时钟中断的分支即可。

1
2
3
4
5
6
7
8
// Handle clock interrupts. Don't forget to acknowledge the
// interrupt using lapic_eoi() before calling the scheduler!
// LAB 4: Your code here.
if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
lapic_eoi();
sched_yield();
return;
}

Part II: 进程间通信(Inter-Process communication)IPC

IPC in JOS

我们将实现两个系统调用:sys_ipc_recv 以及 sys_ipc_try_send ,再将他们封装为两个库函数,ipc_recvipc_send 以支持通信。
实际上,进程之间发送的信息是由两个部分组成,一个 int32_t,一个页面映射(可选)。

发送和接收消息

进程使用 sys_ipc_recv 来接收消息。该系统调用会将程序挂起,让出 CPU 资源,直到收到消息。在这个时期,任一进程都能给他发送信息,不限于父子进程。
为了发送信息,进程会调用 sys_ipc_try_send,以接收者的进程 id 以及要发送的值为参数。如果接收者已经调用了 sys_ipc_recv ,则成功发送消息并返回0。否则返回 E_IPC_NOT_RECV 表明目标进程并没有接收消息。
ipc_send 库函数将会反复执行 sys_ipc_try_send 直到成功。

传递页面

当进程调用 sys_ipc_recv 并提供一个虚拟地址 dstva (必须位于用户空间) 时,进程表示它希望能接收一个页面映射。如果发送者发送一个页面,该页面就会被映射到接收者的 dstva。同时,之前位于 dstva 的页面映射会被覆盖。

当进程调用 sys_ipc_try_send 并提供一个虚拟地址 srcva (必须位于用户空间),表明发送者希望发送位于 srcva的页面给接收者,权限设置为 perm

在一个成功的 IPC 之后,发送者和接受者将共享一个物理页。

Exercise 15.

Implement sys_ipc_recv and sys_ipc_try_send in kern/syscall.c. Read the comments on both before implementing them, since they have to work together. When you call envid2env in these routines, you should set the checkperm flag to 0, meaning that any environment is allowed to send IPC messages to any other environment, and the kernel does no special permission checking other than verifying that the target envid is valid.
Then implement the ipc_recv and ipc_send functions in lib/ipc.c.

首先需要仔细阅读 inc/env.h 了解用于传递消息的数据结构。

1
2
3
4
5
6
// Lab 4 IPC
bool env_ipc_recving; // 当前进程的状态,表明当前进程是否处于接受状态。
void *env_ipc_dstva; // VA at which to map received page
uint32_t env_ipc_value; // 当前进程接收到的数据(如果用页来传递数据)。
envid_t env_ipc_from; // envid of the sender
int env_ipc_perm; // Perm of page mapping received

然后需要注意的是通信流程。

  1. 调用 ipc_recv,设置好 Env 结构体中的相关 field
  2. 调用 ipc_send,它会通过 envid 找到接收进程,并读取 Env 中刚才设置好的 field,进行通信。
  3. 最后返回实际上是在 ipc_send 中设置好 reg_eax,在调用结束,退出内核态时返回。

lib/ipc.c

ipc_recv()

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
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
// panic("ipc_recv not implemented");
int r;
if(pg!=NULL){
r = sys_ipc_recv(pg); // pg是个虚拟地址(必须位于用户空间),表示希望能接受一个页面映射到此虚拟地址
}else{
r = sys_ipc_recv(UTOP); // 如果不需要共享界面,则把虚拟地址设置为UTOP
}

if(r<0){
// fail
if (from_env_store != NULL) *from_env_store = 0;
if (perm_store != NULL) *perm_store = 0;
return r;
}else{
if (from_env_store != NULL)
*from_env_store = thisenv->env_ipc_from; // 存取ipc sender's page
if (perm_store != NULL)
*perm_store = thisenv->env_ipc_perm;
return thisenv->env_ipc_value;
}
}

ipc_send

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
// panic("ipc_send not implemented");
// sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
// 发送位于srcva出得界面给接收者
int r;
if(pg == NULL){
pg = (void*) UTOP;
}
do{
r = sys_ipc_try_send(to_env, val, pg, perm);
if(r<0 && r!=-E_IPC_NOT_RECV){
panic("ipc send failed.")
}
sys_yield();
}while(r!=0); // 一直尝试发送
}

kern/syscall.c

sys_page_map() 非常相似,但是其中最大的区别在于,ipc 通信并不限于父子进程之间,而 sys_page_map() 最初设计的作用就是用于 fork()

sys_ipc_try_send

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
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
// panic("sys_ipc_try_send not implemented");
envid_t src_envid = sys_getenvid();
struct Env* dst_e;
if(envid2env(env_id, &dst_e, 0)<0){ // Converts an envid to an env pointer.
return -E_BAD_ENV;
}
if(dst_e->env_ipc_recving == false){
return -E_BAD_ENV;
}
// pass the value
dst_e->env_ipc_value = value;
dst_e->env_ipc_perm = 0;

// pass the page
if ((uintptr_t)srcva < UTOP) {
// customerize 0x200 as PTE_NO_CHECK
unsigned tmp_perm = perm | 0x200;
// sys_page_map(envid_t srcenvid, void *srcva, envid_t dstenvid, void *dstva, int perm)
// Map the page of memory at 'srcva' in srcenvid's address space
// at 'dstva' in dstenvid's address space with permission 'perm'.
int r = sys_page_map(src_envid, srcva, envid, (void *)dst_e->env_ipc_dstva, tmp_perm); //
if (r < 0) return r;
dst_e->env_ipc_perm = perm;
}
dst_e->env_ipc_from = src_envid;
dst_e->env_status = ENV_RUNNABLE;
dst_e->env_tf.tf_regs.reg_eax = 0;
dst_e->env_ipc_recving = false;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int
sys_page_map(envid_t srcenvid, void *srcva,
envid_t dstenvid, void *dstva, int perm)
{
// LAB 4: Your code here.
// panic("sys_page_map not implemented");

if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) != 0) return -E_INVAL;
if ((uintptr_t)dstva >= UTOP || PGOFF(dstva) != 0) return -E_INVAL;
if ((perm & PTE_U) == 0 || (perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) != 0) return -E_INVAL;
struct Env *src_e, *dst_e;
// add for lab4 exercise 15 for ipc.
// customerize 0x200 as PTE_NO_CHECK
// and we assume 0x200 is not used elsewhere, so we restore perm here.
bool check_perm = (perm & 0x200);
perm &= (~0x200);
if (envid2env(srcenvid, &src_e, !check_perm)<0 || envid2env(dstenvid, &dst_e, !check_perm)<0) return -E_BAD_ENV;
pte_t *src_ptab;
struct PageInfo *pp = page_lookup(src_e->env_pgdir, srcva, &src_ptab);
if ((*src_ptab & PTE_W) == 0 && (perm & PTE_W) == 1) return -E_INVAL;
if (page_insert(dst_e->env_pgdir, pp, dstva, perm) < 0) return -E_NO_MEM;
return 0;
}

LAB3: User Environments

实验三中,you will implement the basic kernel facilities required to get a protected user-mode environment (i.e., “process”) running. 将实现运行受保护的用户模式环境(即进程)所需的基本内核功能。将增强JOS内核以设置数据结构来跟踪 用户环境、创建单个用户环境、将程序映像(program image)加载到其中并开始运行。还需要使得JOS内核能够处理用户环境进行的任何调用和解决它所造成的一切异常情况。

killall XXX杀掉所有的进程 kill xxx(PID)杀死进程号xxx的进程,查看所有运行进程的命令:ps -aux

Part A: User Environment and Expection Handling

image-20220629161158698

inc/env.h 查看最大进程个数NENV为1024

kern/env.c 全局变量

1
2
3
4
5
6
7
// env指针指向一个由Env结构体组成的数组,同时,不活动的Env记录在env_free_list 中, 和之前的page_free_list很像
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env curenv记录着现在正在运行的进程,在第一个进程运行之前为空
static struct Env *env_free_list; // Free environment list
// (linked by Env->env_link)

#define ENVGENSHIFT 12 // >= LOGNENV

inc/enc.h 中查看Env结构体的具体信息

1
2
3
4
5
6
7
8
9
10
11
12
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run

// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};

env_id 进程的身份标识。

env_status 进程的运行状态。

env_link 类似于PageInfo中的pp_link,用于创建空闲进程链表,若进程槽已分配,则置为NULL。

env_tf 是一个Trapframe型的结构体(Trapframe是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构),用于登记进程运行时的寄存器信息。

env_pgdir 指向进程运行所用的页目录,即描述了进程的用户地址空间。

在JOS中,进程的运行实体是线程,而env_tf描述的实际上是所说的之行线程。文档中说明:当进程不在运行状态时,env_tf保存了进程的寄存器信息,内核在硬件控制权由用户转到内核时保存这些信息,使得进程可以在之后恢复到它交出控制权时的状态。

Exercise 1: Allocating the Environments Array

修改 kern/pmap.c 中的 mem_init() 来分配和映射 envs 数组。这个数组完全由 ENEV(1024) 个结构体Env组成,与分配 pages 数组的方式非常相似。同时像 pages 数组一样, 在UENVS(定义在inc/memlayout.h)上的用户,内存也该被映射为只读,这样用户进程就可以从这个数组中读取数据。

You should run your code and make sure check_kern_pgdir() succeeds.

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
void
mem_init(void)
{
uint32_t cr0;
size_t n;

// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();

// Remove this line when you're ready to test this function.
//panic("mem_init: This function is not finished\n");

//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);

//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)

// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:


//////////////////////////////////////////////////////////////////////
// Make 'envs' point to an array of size 'NENV' of 'struct Env'.
// LAB 3: Your code here.

//////////////////////////////////////////////////////////////////////
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert
page_init();

check_page_free_list(1);
check_page_alloc();
check_page();

//////////////////////////////////////////////////////////////////////
// Now we set up virtual memory

//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:

//////////////////////////////////////////////////////////////////////
// Map the 'envs' array read-only by the user at linear address UENVS
// (ie. perm = PTE_U | PTE_P).
// Permissions:
// - the new image at UENVS -- kernel R, user R
// - envs itself -- kernel RW, user NONE
// LAB 3: Your code here.

//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:

//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:

// Check that the initial page directory has been set up correctly.
check_kern_pgdir();

// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created. Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));

check_page_free_list(0);

// entry.S set the really important flags in cr0 (including enabling
// paging). Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE|CR0_PG|CR0_AM|CR0_WP|CR0_NE|CR0_MP;
cr0 &= ~(CR0_TS|CR0_EM);
lcr0(cr0);

// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();

// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();

// Remove this line when you're ready to test this function.
// panic("mem_init: This function is not finished\n");

//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);

//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)

// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:
// 创建一个struct PageInfo 的数组
// kernel 使用这个数组来耿总每个物理页
// 对于每一个物理页,都会有一个对应的 struct PageInfo 在数组中
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo));
// npages 是内存中物理页的数量
memset(pages, 0, npages * sizeof(struct PageInfo));

//////////////////////////////////////////////////////////////////////
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert
page_init();

check_page_free_list(1);
check_page_alloc();
check_page();

//////////////////////////////////////////////////////////////////////
// Now we set up virtual memory

//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:
// UPAGES是JOS记录物理页面使用情况的数据结构,只有kernel能够访问
// 为了使用户空间能访问这块数据结构,会将PAGES映射到UPAGES的位置
// 但是现在需要让用户空间能够读取这段线性地址,因此需要建立映射,将用户空间的一块内存映射到存储该数据结构的物理地址上
// boot_map_region() 建立映射关系
boot_map_region(kern_pgdir, (uintptr_t)UPAGES, npages*sizeof(struct PageInfo), PADDR(pages), PTE_U | PTE_P);
// 目前建立了一个页目录,kernel_pgdir
// pgdir为页目录指针, UPAGES为虚拟地址,npages*sizeof(struct* PageInfo)为映射的内存块大小
// PADDR(pages) 为物理地址, PTE_U | PTE为权限 (PTE_U 表示用户可读)


//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:
// kernel 内核栈
// kernel stack 从虚拟地址 KSTACKTOP 开始,向低地址增长,所以KSTACKTOP实际上是栈顶
// KSTACKTOP = 0xf0000000,
// KSTKSIZE = (8*PGSIZE) = 8*4096(bytes) = 32KB
// 只需要映射 [KSTACKTOP, KSTACKTOP - KSTKSIZE) 范围的虚拟地址
boot_map_region(kern_pgdir, (uintptr_t)(KSTACKTOP - KSTKSIZE), KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);
// PTE_W 开启了写权限,但是并未打开 PTE_U, 因此用户没有权限,只有内核有权限


//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
// 内核部分
// KERNBASE = 0xF0000000, VA大小为 2^32 - KERNBASE
// ROUNDUP(a,n) 将a四舍五入到最接近n的倍数
boot_map_region(kern_pgdir, (uintptr_t)KERNBASE, ROUNDUP(0xffffffff - KERNBASE + 1, PGSIZE), 0, PTE_W | PTE_P);

// Check that the initial page directory has been set up correctly.
check_kern_pgdir();

// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created. Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));

check_page_free_list(0);

// entry.S set the really important flags in cr0 (including enabling
// paging). Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_MP;
cr0 &= ~(CR0_TS | CR0_EM);
lcr0(cr0);

// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();


}

Exercise 2: Creating and Running Environments

You will now write the code in kern/env.c necessary to run a user environment. Because we do not yet have a filesystem, we will set up the kernel to load a static binary image that is embedded within the kernel itself. JOS embeds this binary in the kernel as a ELF executable image.

In i386_init() in kern/init.c you’ll see code to run one of these binary images in an environment. However, the critical functions to set up user environments are not complete; you will need to fill them in.

在kern/env.c中,补充完成以下函数

env_init()

​ 初始化 envs 数组中所有的结构体 Env,并把它们添加到env_free_list。

​ 调用 env_init_percpu,它为 privilege level 0(kernel) 和 privilege level 3(user) 配置特定的段。

env_steup_vm()

​ 为新环境分配一个页目录,并且初始化新环境地址空间的内核部分。

region_alloc()

​ 为环境分配和映射物理内存。

load_icode()

​ 您需要解析ELF二进制映像,就像引导加载程序已经做的那样,并将其内容加载到新环境的用户地址空间中。

env_create()

​ 使用env_alloc分配一个环境,并调用load_icode将ELF二进制文件加载到其中。

env_run()

​ 启动以用户模式运行的给定环境。

As you write these functions, you might find the new cprintf verb %e useful – it prints a description corresponding to an error code. For example,

1
2
r = -E_NO_MEM;
panic("env_alloc: %e", r);

will panic with the message “env_alloc: out of memory”.

Below is a call graph of the code up to the point where the user code is invoked. Make sure you understand the purpose of each step.

  • start (kern/entry.S)
  • i386_init (kern/init.c)
    • cons_init
    • mem_initenv_init
    • trap_init (still incomplete at this point)
    • env_create
    • env_run
      • env_pop_tf

boot.S中通过ljmp跳转指令使cs的索引值指向代码段描述符,并将选择子ds、es、fs、gs和ss的索引值均设置为指向数据段描述符,至此这些段选择子均不曾被更改过。现在我们要创建进程,而原先的GDT中并没有特权级为3的数据段和代码段描述符,所以我们必须加载一个新的GDT。env_init_percpu重新加载了GDT,并设置了各个段选择子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
truct Segdesc gdt[] =
{
// 0x0 - unused (always faults -- for trapping NULL far pointers)
SEG_NULL,

// 0x8 - kernel code segment
[GD_KT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 0),

// 0x10 - kernel data segment
[GD_KD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 0),

// 0x18 - user code segment
[GD_UT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 3),

// 0x20 - user data segment
[GD_UD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 3),

// 0x28 - tss, initialized in trap_init_percpu()
[GD_TSS0 >> 3] = SEG_NULL
};

struct Pseudodesc gdt_pd = {
sizeof(gdt) - 1, (unsigned long) gdt
};

image-20220701172502082

env_init()

1
2
3
4
5
6
7
8
9
10
11
12
// Set up envs array
// LAB 3: Your code here.
// 初始化 envs 数组中所有的结构体 Env,添加到env_free_list
int i = NENV;
while(i>0){
i--;
envs[i].env_id = 0;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
// Per-CPU part of the initialization
env_init_percpu(); // 重新加载

env_steup_vm()

为新环境分配一个页目录,并且初始化新环境地址空间的内核部分。

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
static int
env_setup_vm(struct Env *e)
{
int i;
struct PageInfo *p = NULL;

// Allocate a page for the page directory
if (!(p = page_alloc(ALLOC_ZERO))) // 给p分配个 页目录
return -E_NO_MEM;

// Now, set e->env_pgdir and initialize the page directory.
//
// Hint:
// - The VA space of all envs is identical above UTOP
// (except at UVPT, which we've set below).
// See inc/memlayout.h for permissions and layout.
// Can you use kern_pgdir as a template? Hint: Yes.
// (Make sure you got the permissions right in Lab 2.)
// - The initial VA below UTOP is empty.
// - You do not need to make any more calls to page_alloc.
// - Note: In general, pp_ref is not maintained for
// physical pages mapped only above UTOP, but env_pgdir
// is an exception -- you need to increment env_pgdir's
// pp_ref for env_free to work correctly.
// - The functions in kern/pmap.h are handy.
// LAB 3: Your code here.
// env_setup_vm负责创建进程自己的页目录,并初始化内核地址空间。
// 它不需要为内核地址空间另外创建页表,只要先将内核页目录kern_pgdir的所有目录项复制过来即可,以后再设置用户地址空间。
// // pde_t *env_pgdir; // Kernel virtual address of page dir
e->env_pgdir = page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
p->pp_ref++; // p此时指向了页目录

// UVPT maps the env's own page table read-only.
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
// PDX() page directory index NPDENTRIES 1024
return 0;
}

region_alloc()

为环境env分配 len 字节的物理内存,并将其映射到环境地址空间中的虚拟地址va。不以任何方式将映射页归零或初始化。页面应该是用户和内核可写的。Panic if any allocation attempt fails.

region_alloc为一个进程分配指定长度的内存空间,并按指定的起始线性地址映射到分配的物理内存上。该函数只在加载用户程序到内存中时(目前通过load_icode)才被用到,分配用户栈的工作将交给load_icode完成。我们会用到lab2中实现的page_alloc和page_insert分别完成物理页的分配与映射。我们不需要对被分配的物理页进行初始化,物理页的权限将被设置为内核和用户都可读写,va和va+len需要设置为页对齐(corner-case应该是当分配的地址超过UTOP时,这里直接panic)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void
region_alloc(struct Env *e, void *va, size_t len)
{
// LAB 3: Your code here.
// (But only if you need it for load_icode.)
//
// Hint: It is easier to use region_alloc if the caller can pass
// 'va' and 'len' values that are not page-aligned.
// You should round va down, and round (va + len) up.
// (Watch out for corner-cases!)
uintptr_t va_start = ROUNDDOWN((uintptr_t)va, PGSIZE);
uintptr_t va_end = ROUNDUP((uintptr_t)va + len, PGSIZE);
struct PageInfo *pginfo = NULL;
for(int cur_va = va_start;cur_va<va_end;cur_va+=PGSIZE){
pginfo = page_alloc(0);
if(!pginfo){
panic("region_alloc: pageinfo failed.");
}
cprintf("insert page at %08x\n.", cur_va);
page_insert(e->env_pgdir, pginfo, (void*)cur_va, PTE_U | PTE_W | PTE_P);
}
}

load_icode()

由于我们还没有实现文件系统,甚至连磁盘都没有,所以当然不可能从磁盘上加载一个用户程序到内存中。因此,我们暂时将ELF可执行文件嵌入内核,并从内存中加载这样的ELF文件,以此模拟从磁盘加载用户程序的过程。lab3 GNUmakefile负责设置将这样的ELF可执行文件直接嵌入内核

Hint:

在ELF段头指定的地址处把每个程序段加载到虚拟内存中。

你应该只加载 ph->p_type == ELF_PROG_LOAD 的段。

每个段的虚拟地址可以使用 ph->p_va 得到,同时它在内存中的大小可通过ph->memsz得到。

The ph->p_filesz bytes from the ELF binary, starting at binary + ph->p_offset, should be copied to virtual address ph->p_va.

ELF头应该有 ph->p_filesz <= ph->p_memsz.

所有的页面是用户可读/写的。

ELF段不一定是页面对齐的,但是你可以假设这个函数中没有两个段会接触同一个虚拟页面。

您还必须对程序的入口点做一些操作,以确保环境从那里开始执行。

怎么切换页目录?
lcr3([页目录物理地址]) 将地址加载到 cr3 寄存器。

怎么更改函数入口?
env->env_tf.tf_eip 设置为 elf->e_entry,等待之后的 env_pop_tf() 调用。

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
static void
load_icode(struct Env *e, uint8_t *binary)
{
// Hints:
// Load each program segment into virtual memory
// at the address specified in the ELF segment header.
// You should only load segments with ph->p_type == ELF_PROG_LOAD.
// Each segment's virtual address can be found in ph->p_va
// and its size in memory can be found in ph->p_memsz.
// The ph->p_filesz bytes from the ELF binary, starting at
// 'binary + ph->p_offset', should be copied to virtual address
// ph->p_va. Any remaining memory bytes should be cleared to zero.
// (The ELF header should have ph->p_filesz <= ph->p_memsz.)
// Use functions from the previous lab to allocate and map pages.
//
// All page protection bits should be user read/write for now.
// ELF segments are not necessarily page-aligned, but you can
// assume for this function that no two segments will touch
// the same virtual page.
//
// You may find a function like region_alloc useful.
//
// Loading the segments is much simpler if you can move data
// directly into the virtual addresses stored in the ELF binary.
// So which page directory should be in force during
// this function?
//
// You must also do something with the program's entry point,
// to make sure that the environment starts executing there.
// What? (See env_run() and env_pop_tf() below.)

// LAB 3: Your code here.
struct Elf *elf_hdr = (struct Elf *)binary; // ELF头// 准备对二进制ELF文件进行操作
struct Proghdr *ph, *eph; // 程序头

if(elf_hdr->e_magic != ELF_MAGIC){
panic("load_icode: invalid ELF binary.");
}
ph = (struct Proghdr*)(binary + elf_hdr->e_phoff); // 加上偏移量
eph = ph+elf_hdr->e_phnum;

// 切换页目录
lcr3(PADDR(e->env_pgdir)); // lcr3() to switch to its address space.

for(;ph<eph;ph++){
if(ph->p_type == ELF_PROG_LOAD){ // 满足ELF的条件
if (ph->p_filesz > ph->p_memsz) {
panic("load_icode: file size is greater than memory size");
}
region_alloc(e, (void*)ph->p_va, ph->p_memsz);
memcpy((void*)ph->p_va, binary+ph->p_offset, ph->p_filesz); // 拷贝文件内容
memset((void*)ph->p_va + ph->p_filesz, 0, ph->p_memsz - ph->p_filesz);
}
}
e->env_tf.tf_eip = elf_hdr->e_entry; // 切换入口
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
// 现在映射一个页面为程序的初始堆栈在虚拟地址USTACKTOP - PGSIZE.
// LAB 3: Your code here.
region_alloc(e,(void*)USTACKTOP - PGSIZE, PGSIZE);
lcr3(PADDR(kern_pgdir));
}

env_create()
作用是新建一个进程。调用已经写好的 env_alloc() 函数即可,之后更改类型并且利用 load_icode() 读取 ELF。

这里的进程即环境.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 作用是新建一个进程。
// 调用已经写好的 env_alloc() 函数即可,之后更改类型并且利用 load_icode() 读取 ELF。
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env* e; // 新建一个进程
int r = env_alloc(&e, 0);
if(r<0){
panic("env_create: env alloc error.\n");
}
// 分配成功
e->env_type = ENV_TYPE_USER;
load_icode(e, binary);
}

env_run()

​ 启动以用户模式运行的给定环境。

//步骤1:如果这是一个上下文切换(一个新的环境正在运行):

/ / 1。设置当前环境(如果有的话)回ENV_RUNNABLE如果它是ENV_RUNNING(想想它可以处于什么其他状态),

/ / 2。将’curenv’设置为新环境,

/ / 3。设置它的状态为ENV_RUNNING,更新env_status

/ / 4。更新它的’env_runs’计数器,

/ / 5。使用lcr3()切换到它的地址空间。

//步骤2:使用env_pop_tf()恢复环境的寄存器,并在环境中进入用户模式。

//Hint: 这个函数从e->env_tf加载新环境的状态。返回前面编写的代码,确保将e->env_tf的相关部分设置为合理的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
env_run(struct Env *e)
{
// Hint: This function loads the new environment's state from
// e->env_tf. Go back through the code you wrote above
// and make sure you have set the relevant parts of
// e->env_tf to sensible values.

// LAB 3: Your code here.
// struct Env *curenv 记录着当前正在运行的进程
if(curenv && curenv->env_status == ENV_RUNNING){
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
e->env_status = ENV_RUNNING;
e->env_runs++;
lcr3(PADDR(e->env_pgdir));
env_pop_tf(&e->env_tf);

// panic("env_run not yet implemented");
}

Exercise 3: Handling Interrupts and Exceptions

用户空间中的第一个int $0x30系统调用指令是一个死胡同:一旦处理器进入用户模式,就没有办法返回。现在需要实现基本的异常和系统调用处理,以便内核能够从用户模式代码中恢复对处理器的控制。您应该做的第一件事是彻底熟悉x86中断和异常机制。

Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer’s Manual (or Chapter 5 of the IA-32 Developer’s Manual), if you haven’t already.

根据xv6讲义,一共有三种必须将控制由用户程序转移到内核的情形:系统调用、异常和中断。

系统调用发生在用户程序请求一项操作系统的服务时。

异常发生在用户程序想要执行某种非法的操作时,如除以零或者访问不存在的页表项。

中断发生在某个外部设备需要操作系统的留意时,比如时钟芯片会定时产生一个中断提醒操作系统可以将硬件资源切换给下一个进程使用一会儿了。

在大多数处理器上,这三种情形都是由同一种硬件机制来处理的。对于x86,系统调用和异常本质上也是生成一个中断,因此操作系统只需要提供一套针对中断的处理策略就可以了。

操作系统处理中断时要用到中断描述符表IDT程序状态段TSS

  1. 中断描述符表IDT (interrupt descriptor table)

​ x86最多支持256个不同中断和异常的条目,每个包含一个中断向量,是一个0~255之间的数字,代表中断来源:不同的设备及类型错误。

​ IDT使得系统调用、异常和中断都只能经由被内核定义的入口进入正确的中断处理程序。每一个中断处理程序都对应一个中断向量或中断号,处理器接收中断号后,会以它作为索引值从IDT中找到对应的中断描述符。接着,处理器从描述符中取出定位中断处理程序要用到的 eip(指令指针寄存器)cs(代码段寄存器) 的值。

EIP中的值指向内核中处理这类异常的代码。 Extend Instruction Pointer

CS中的最低两位表示优先级,因此寻址空间少两位 .

在JOS中,所有异常都在内核模式处理,优先级为0(用户模式为3)

  1. 任务状态段 (Task State Segment, TSS)

​ 处理器需要保存中断和异常出现时的自身状态,例如EIP和CS,以便处理完后能返回原函数继续执行。但是存储区域必须禁止用户访问,避免恶意代码或bug的破坏。

​ 因此,当x86处理器处理从用户态到内核态的模式转换时,也会切换到内核栈。而TSS指明段选择器和栈地址,处理器将SS, ESP, EFLAGS, CS, EIP压入新栈,然后从IDT读取EIP和CS,根据新栈设置ESP和SS。

SS是堆栈段寄存器。它指向将用于堆栈的内存的一般区域。

ESP是堆栈指针寄存器。它指向在存储器的“堆栈段”区域内的堆栈“顶部”的任何给定点处的精确位置。

EFLAGS标志寄存器。

JOS仅利用TSS来定义需要切换的内核栈。由于内核模式在JOS优先级是0,因此处理器用TSS的ESP0和SS0来定义内核栈,无需TSS结构体中的其他内容。其中,SS0中存储的是GD_KD

#define GD_KD 0x10 // kernel data

ESP0中存储的是KSTACKTOP

1
2
#define KSTACKTOP   KERNBASE
#define KERNBASE 0xF0000000

在x86体系中,中断向量范围为0-255(vector number),最多表示256个异常或者中断,用一个8位的无符号整数表示,前32个vector为处理器保留用作异常处理。

32-255被指定为用户定义的中断,并且不由处理器保留。这些vector通常分配给外部I/O设备,以便这些设备能够向处理器发送中断。

一个例子

通过一个例子来理解上面的知识。假设处理器正在执行用户环境的代码,遇到了”除0”异常。

  1. 处理器切换到内核栈,利用了上文 TSS 中的 ESP0 和 SS0。

  2. 处理器将异常参数 push 到了内核栈。一般情况下,按顺序 push SS, ESP, EFLAGS, CS, EIP
    +——————–+ KSTACKTOP
    | 0x00000 | old SS | “ - 4
    | old ESP | “ - 8
    | old EFLAGS | “ - 12
    | 0x00000 | old CS | “ - 16
    | old EIP | “ - 20 <—- ESP
    +——————–+
    存储这些寄存器状态的意义是:SS(堆栈选择器) 的低 16 位与 ESP 共同确定当前栈状态;EFLAGS(标志寄存器)存储当前FLAG;CS(代码段寄存器) 和 EIP(指令指针寄存器) 确定了当前即将执行的代码地址,E 代表”扩展”至32位。根据这些信息,就能保证处理中断结束后能够恢复到中断前的状态。

  3. 因为我们将处理一个”除0”异常,其对应中断向量是0,因此,处理器读取 IDT 的条目0,设置 CS:EIP 指向该条目对应的处理函数。

  4. 处理函数获得程序控制权并且处理该异常。例如,终止进程的运行。

image-20220703163747621

ref:https://www.jianshu.com/p/f67034d0c3f2

嵌套的异常和中断

内核和用户进程都会引起异常和中断。然而,仅在从用户环境进入内核时才会切换栈。如果中断发生时已经在内核态了(此时, CS 寄存器的低 2bit 为 00) ,那么 CPU 就直接将状态压入内核栈,不再需要切换栈。这样,内核就能处理内核自身引起的”嵌套异常”,这是实现保护的重要工具。
如果处理器已经处于内核态,然后发生了嵌套异常,由于它并不进行栈切换,所以无须存储 SSESP 寄存器状态。对于不包含 error code 的异常,在进入处理函数前内核栈状态如下所示:

image-20220703163920093

对于包含了 error code 的异常,则将 error code 继续 push 到 EIP之后。
警告:如果 CPU 处理嵌套异常的时候,无法将状态 push 到内核栈(由于栈空间不足等原因),则 CPU 无法恢复当前状态,只能重启。当然,这是内核设计中必须避免的。

头文件 inc/trap.hkern/trap.h 包含了与中断和异常相关的定义,需要仔细阅读。其中 kern/trap.h 包含内核私有定义,而 inc/trap.h 包含对内核以及用户进程和库都有用的定义。
每个异常和中断都应该在 trapentry.Strap_init() 有自己的处理函数,并在 IDT 中将这些处理函数的地址初始化。每个处理函数都需要在栈上新建一个 struct Trapframe(见 inc/trap.h),以其地址为参数调用 trap() 函数,然后进行异常处理。

image-20220703165253213

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
=> 0xf0103734 <env_pop_tf+15>:  iret   
0xf0103734 486 asm volatile(
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xf0224030 0xf0224030
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0xf0103734 0xf0103734 <env_pop_tf+15>
eflags 0x96 [ PF AF SF ]
cs 0x8 8
ss 0x10 16
ds 0x23 35
---Type <return> to continue, or q <return> to quit---
es 0x23 35
fs 0x23 35
gs 0x23 35
(gdb) si
=> 0x800020: cmp $0xeebfe000,%esp
0x00800020 in ?? ()
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xeebfe000 0xeebfe000
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x800020 0x800020
eflags 0x2 [ ]
cs 0x1b 27
ss 0x23 35
ds 0x23 35
---Type <return> to continue, or q <return> to quit---
es 0x23 35
fs 0x23 35
gs 0x23 35

可以看到=> 0x800020: cmp $0xeebfe000,%esp时,进入了用户态,因为cs的值变为了GD_UT | 0X3 = 0x1b,esp的值为USTACKTOP的值0xeebfe000

image-20220704103409165

Exercise 4.

Exercise 4. Edit trapentry.S and trap.c and implement the features described above. The macros TRAPHANDLER and TRAPHANDLER_NOEC in trapentry.S should help you, as well as the T_* defines in inc/trap.h. You will need to add an entry point in trapentry.S (using those macros) for each trap defined in inc/trap.h, and you’ll have to provide _alltraps which the TRAPHANDLER macros refer to. You will also need to modify trap_init() to initialize the idt to point to each of these entry points defined in trapentry.S; the SETGATE macro will be helpful here.

Your _alltraps should:

  1. push values to make the stack look like a struct Trapframe
  2. load GD_KD into %ds and %es
  3. pushl %esp to pass a pointer to the Trapframe as an argument to trap()
  4. call trap (can trap ever return?)

Consider using the pushal instruction; it fits nicely with the layout of the struct Trapframe.

Test your trap handling code using some of the test programs in the user directory that cause exceptions before making any system calls, such as user/divzero. You should be able to get make grade to succeed on the divzero, softint, and badsegment tests at this point.

查看trapentry.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* TRAPHANDLER defines a globally-visible function for handling a trap.
* It pushes a trap number onto the stack, then jumps to _alltraps.
* Use TRAPHANDLER for traps where the CPU automatically pushes an error code.
*
* You shouldn't call a TRAPHANDLER function from C, but you may
* need to _declare_ one in C (for instance, to get a function pointer
* during IDT setup). You can declare the function with
* void NAME();
* where NAME is the argument passed to TRAPHANDLER.
*/
#define TRAPHANDLER(name, num) \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
pushl $(num); \
jmp _alltraps

.global 定义了全局符号。

​ 汇编函数如果需要在其他文件内调用,需要把函数声明为全局,此时就会使用.global这个伪操作。

.type 用来制定一个符号类型是函数类型或者是对象类型,对象类型一般是数据

.type symbol, @object

.type symbol, @function

.align 用来指定内存对齐方式

.align size

​ 表示按size字节对齐内存

这一步做了什么?光看这里很难理解,提示说是构造一个 Trapframe 结构体来保存现场,但是这里怎么直接就 push 中断向量了?实际上,在上文已经指出, cpu 自身会先 push 一部分寄存器(见例子所述),而其他则由用户和操作系统决定。由于中断向量是操作系统定义的,所以从这部分开始就已经不属于 cpu 的工作范畴了。

trapentry.S 中:

根据inc/trap.h 绑定

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
// TRAPHANDLER defines a globally-visible function for handling a trap.
// It pushes a trap number onto the stack, then jumps to _alltraps.

// Use TRAPHANDLER for traps where the CPU automatically pushes an error code.
// Use TRAPHANDLER_NOEC for traps where the CPU doesn't push an error code.

/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
// 绑定异常到自定义处理器
TRAPHANDLER_NOEC(handler0, T_DIVIDE)
TRAPHANDLER_NOEC(handler1, T_DEBUG)
TRAPHANDLER_NOEC(handler2, T_NMI)
TRAPHANDLER_NOEC(handler3, T_BRKPT)
TRAPHANDLER_NOEC(handler4, T_OFLOW)
TRAPHANDLER_NOEC(handler5, T_BOUND)
TRAPHANDLER_NOEC(handler6, T_ILLOP)
TRAPHANDLER_NOEC(handler7, T_DEVICE)
TRAPHANDLER(handler8, T_DBLFLT)
// 9 deprecated since 386
TRAPHANDLER(handler10, T_TSS)
TRAPHANDLER(handler11, T_SEGNP)
TRAPHANDLER(handler12, T_STACK)
TRAPHANDLER(handler13, T_GPFLT)
TRAPHANDLER(handler14, T_PGFLT)
// 15 reserved by intel
TRAPHANDLER_NOEC(handler16, T_FPERR)
TRAPHANDLER(handler17, T_ALIGN)
TRAPHANDLER_NOEC(handler18, T_MCHK)
TRAPHANDLER_NOEC(handler19, T_SIMDERR)
// system call (interrupt)
TRAPHANDLER_NOEC(handler48, T_SYSCALL)

// 该函数是全局的,但是在 C 文件中使用的时候需要使用 void name(); 再声明一下。

/*
* 你不应该从C调用TRAPHANDLER函数,但你可能需要在C中_declare_(例如,在IDT设置期间获得一个函数指针)。您可以使用声明函数
void NAME();
其中NAME是传递给TRAPHANDLER的参数。
*/

/*
* Lab 3: Your code here for _alltraps
*/
_alltraps:
pushl %es
pushl %ds
pushal

movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es

pushl %esp
call trap


// 不能用立即数直接给段寄存器赋值。因此不能直接写movw $GD_KD, %ds。

SETGATE MACRO

Set up a normal interrupt/trap gate descriptor.

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
// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
// see section 9.6.1.3 of the i386 reference: "The difference between
// an interrupt gate and a trap gate is in the effect on IF (the
// interrupt-enable flag). An interrupt that vectors through an
// interrupt gate resets IF, thereby preventing other interrupts from
// interfering with the current interrupt handler. A subsequent IRET
// instruction restores IF to the value in the EFLAGS image on the
// stack. An interrupt through a trap gate does not change IF."
// - sel: 代码段选择器 for interrupt/trap handler
// - off: 代码段的偏移量 for interrupt/trap handler
// - dpl: Descriptor Privilege Level -
// the privilege level required for software to invoke
// this interrupt/trap gate explicitly using an int instruction.
/*
gate
这是一个 struct Gatedesc。
istrap
该中断是 trap(exception) 则为1,是 interrupt 则为0。
sel
代码段选择器。进入内核的话是 GD_KT。
off
相对于段的偏移,简单来说就是函数地址。
dpl(Descriptor Privileged Level)
权限描述符。
*/
#define SETGATE(gate, istrap, sel, off, dpl) \
{ \
(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff; \
(gate).gd_sel = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t) (off) >> 16; \
}

Challenge! You probably have a lot of very similar code right now, between the lists of TRAPHANDLER in trapentry.S and their installations in trap.c. Clean this up. Change the macros in trapentry.S to automatically generate a table for trap.c to use. Note that you can switch between laying down code and data in the assembler by using the directives .text and .data.

Questions

Answer the following questions in your answers-lab3.txt:

  1. What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

    为每个异常/中断提供单独的处理函数的目的是什么?(也就是说,如果所有异常/中断都被交付给同一个处理程序,当前实现中存在的哪些特性不能提供?)

    答:因为每个异常和中断的处理方式不同,例如除0异常不会继续返回程序执行,而I/O操作中断后会继续返回程序处理。一个handler难以处理多种情况。

  2. Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint‘s code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint‘s int $14instruction to invoke the kernel’s page fault handler (which is interrupt vector 14)?

Part B: Page Faults, Breakpoints Exceptions, and System Calls

Exercise5: Handling Page Faults

缺页错误异常,中断向量 14 (T_PGFLT),是一个非常重要的异常类型,lab3 以及 lab4 都强烈依赖于这个异常处理。当程序遇到缺页异常时,它将引起异常的虚拟地址存入 CR2 控制寄存器( control register)。在 trap.c 中,我们已经提供了page_fault_handler() 函数用来处理缺页异常。

修改trap_dispatch(),将页面故障异常分派到page_fault_handler()。现在,您应该能够在faultread、faultreadkernel、faultwrite和faultwritekernel测试中获得成功。如果其中任何一个不工作,找出原因并解决它们。记住,您可以使用make run-x或make run-x-nox将JOS引导到特定的用户程序中。例如,make run-hello-nox运行hello用户程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void
trap_dispatch(struct Trapframe *tf)
{
// Handle processor exceptions.
// LAB 3: Your code here.
switch(tf->tf_trapno){
case T_PGFLT:
page_fault_handler();
break;
default:
// Unexpected trap: The user process or the kernel has a bug.
print_trapframe(tf);
if (tf->tf_cs == GD_KT)
panic("unhandled trap in kernel");
else {
env_destroy(curenv); // 销毁
return;
}
}
}

Exercise 6: The Breakpoint Exception

断点异常,中断向量 3 (T_BRKPT) 允许调试器给程序加上断点。原理是暂时把程序中的某个指令替换为一个 1 字节大小的 int3软件中断指令。在 JOS 中,我们将它实现为一个伪系统调用。这样,任何程序(不限于调试器)都能使用断点功能。

Modify trap_dispatch() to make breakpoint exceptions invoke the kernel monitor. You should now be able to get make grade to succeed on the breakpoint test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void
trap_dispatch(struct Trapframe *tf) // dispatch 调度
{
// Handle processor exceptions.
// LAB 3: Your code here.
switch(tf->tf_trapno){
case T_PGFLT:
page_fault_handler(tf);
break;
case T_BRKPT:
monitor(tf);
break;
default:
// Unexpected trap: The user process or the kernel has a bug.
print_trapframe(tf);
if (tf->tf_cs == GD_KT)
panic("unhandled trap in kernel");
else {
env_destroy(curenv); // 销毁
return;
}
}
}

Challenge! Modify the JOS kernel monitor so that you can ‘continue’ execution from the current location (e.g., after the int3, if the kernel monitor was invoked via the breakpoint exception), and so that you can single-step one instruction at a time. You will need to understand certain bits of the EFLAGS register in order to implement single-stepping.

Optional: If you’re feeling really adventurous, find some x86 disassembler source code - e.g., by ripping it out of QEMU, or out of GNU binutils, or just write it yourself - and extend the JOS kernel monitor to be able to disassemble and display instructions as you are stepping through them. Combined with the symbol table loading from lab 1, this is the stuff of which real kernel debuggers are made.

Questions

  1. The break point test case will either generate a break point exception or a general protection fault depending on how you initialized the break point entry in the IDT (i.e., your call to SETGATE from trap_init). Why? How do you need to set it up in order to get the breakpoint exception to work as specified above and what incorrect setup would cause it to trigger a general protection fault?

    断点测试用例将生成断点异常或一般保护故障,这取决于您在IDT中初始化断点条目的方式(即,从trap_init调用SETGATE)。为什么?您需要如何设置它才能使断点异常像上面指定的那样工作?哪些不正确的设置会导致它触发一般的保护故障?

    答:权限问题。特权级别是个很重要的点。每个IDT的entries内的中断描述符都为中断处理程序设定了一个DPL(Descriptor Privilege Level)。用户程序的特权级别是3,内核的特权级别是0(可知0级别更高)。如果用户产生的中断/异常需要级别0,那么用户就无权请内核调用这个处理程序,就会产生一个general protection fault,如果是内核发生中断/异常的话,特权级别总是够的

    SETGATE(idt[T_BRKPT], 1, GD_KT, brkpt_handler, 3);中如果最后一个参数dpl设为3就会产生一个breakpoint exception,如果设为0就会产生一个general protection fault。这也是由于特权级别影响的。breakpoint test程序的特权级别是3,如果断点异常处理程序特权设为3那就可以是断点异常,如果设为0就产生保护错误。

  2. What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

    里面是这条代码asm volatile("int $14");本来想中断调用页面错误处理,结果因为特权级别不够而产生一个保护异常,所以重点应该是要分清特权级别吧。要区分$14$0x30

    优先级别低的代码无法访问优先级高的代码,优先级高低由gd_dpl判断。数字越小越高。

Exercise 7: System calls

用户进程通过调用系统调用请求内核为它们做一些事情。当用户进程调用一个系统调用时,处理器进入内核模式,处理器和内核合作保存用户进程的状态,内核执行相应的代码来执行系统调用,然后恢复用户进程。用户进程如何获得内核的注意,以及用户进程如何指定要执行的调用,这些具体细节在不同的系统中有所不同。

在JOS内核中,我们将使用int指令,它会导致处理器中断。特别地,我们将使用int $0x30作为系统调用中断。我们已经为您定义了常量T_SYSCALL到48 (0x30)。您必须设置中断描述符,以允许用户进程引起中断。注意,中断0x30不能由硬件生成,所以不会因为允许用户代码生成它而产生歧义。

应用程序将在寄存器中传递系统调用号和系统调用参数。这样,内核就不需要在用户环境的堆栈或指令流中到处寻找了。系统调用号将进入%eax,参数(最多5个)将分别进入%edx、%ecx、%ebx、%edi和%esi。内核将返回值返回到%eax中。调用系统调用的汇编代码已经为您编写,在lib/syscall.c中的syscall()中。你应该通读一遍,确保你明白发生了什么.

exercise: 在内核中为中断向量T_SYSCALL添加一个处理程序。你必须编辑kern/trapentry。S和kern/trap.c的trap_init()。您还需要更改trap_dispatch()来处理系统调用中断,方法是使用适当的参数调用sycall(在kern/syscall.c中定义),然后将返回值传递回%eax中的用户进程。最后,你需要在kern/syscall.c中实现syscall()。确保如果系统调用号无效,syscall()返回-E_INVAL。您应该阅读并理解lib/syscall.c(特别是内联汇编例程),以确认您对系统调用接口的理解。通过为每个调用调用相应的内核函数来处理inc/syscall.h中列出的所有系统调用。

Run the user/hello program under your kernel (make run-hello). It should print “hello, world” on the console and then cause a page fault in user mode. If this does not happen, it probably means your system call handler isn’t quite right. You should also now be able to get make grade to succeed on the testbss test.

inc/syscall.h

定义了系统调用号

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef JOS_INC_SYSCALL_H
#define JOS_INC_SYSCALL_H

/* system call numbers */
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
NSYSCALLS
};

#endif /* !JOS_INC_SYSCALL_H */

lib/syscall.c

这是系统调用的通用模板,不同的系统调用都会以不同参数调用syscall函数。

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
// System call stubs.

#include <inc/syscall.h>
#include <inc/lib.h>

static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
int32_t ret;

// Generic system call: 在AX中传递系统呼叫号,在DX、CX、BX、DI、SI中最多传递5个参数。
// 使用t_sycall中断内核。
// “volatile”告诉汇编器不要因为没有使用返回值而对该指令进行优化。
// 最后一个子句告诉汇编器,这可能会改变条件代码和任意内存位置。
// 可以看到,该段汇编的output为 ret
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");

if(check && ret > 0)
panic("syscall %d returned %d (> 0)", num, ret);
// 函数的返回为ret
return ret;
}

void
sys_cputs(const char *s, size_t len)
{
syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}

int
sys_cgetc(void)
{
return syscall(SYS_cgetc, 0, 0, 0, 0, 0, 0);
}

int
sys_env_destroy(envid_t envid)
{
return syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0);
}

envid_t
sys_getenvid(void)
{
return syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0);
}

补充知识:GCC内联汇编
其语法固定为:
asm volatile (“asm code”:output:input:changed);

1
2
3
4
5
6
7
8
9
10
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");
限定符 意义
“m”、”v”、”o” 内存单元
“r” 任何寄存器
“q” 寄存器eax、ebx、ecx、edx之一
“i”、”h” 直接操作数
“E”、”F” 浮点数
“g” 任意
“a”、”b”、”c”、”d” 分别表示寄存器eax、ebx、ecx和edx
“S”、”D” 寄存器esi、edi
“I” 常数 (0至31)

除了这些约束之外, 输出值还包含一个约束修饰符:

输出修饰符 描述
+ 可以读取和写入操作数
= 只能写入操作数
% 如果有必要操作数可以和下一个操作数切换
& 在内联函数完成之前, 可以删除和重新使用操作数

根据表格内容,可以看出该内联汇编作用就是引发一个int中断,中断向量为立即数 T_SYSCALL,同时,对寄存器进行操作。看懂这,就清楚了,这一部分应该不需要我们改动,因为我们处理的是中断已经产生后的部分。当然,还有另一种更简单的思路,inc/ 目录下的,其实都是操作系统留给用户的接口,所以才会在里面看到 stdio.hassert.h 等文件。那么,要进行系统调用肯定也是先调用 inc/ 中的那个,具体处理应该是在 kern/ 中实现。

ref:https://www.jianshu.com/p/f67034d0c3f2

kern/trap.c

trap_init()添加

image-20220704144047847

权限更改为3,以便用户进程可以触发该中断

修改trap_dispatch()

1
2
3
4
5
6
7
8
case T_SYSCALL:
tf-tf_regs.reg_eax = syscall(tf->tf_regs.reg_eax,
tf->tf_regs.reg_edx,
tf->tf_regs.reg_ecx,
tf->tf_regs.reg_ebx,
tf->tf_regs.reg_edi,
tf->tf_regs.reg_esi);
break;

kern/syscall.c

我们在 kern/trap.c 中调用的实际上就是这里的 syscall 函数,而不是 lib/syscall.c 中的那个。想明白这一点,设置参数也就很简单了,注意返回值的处理。

image-20220704154830887
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
// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
// Call the function corresponding to the 'syscallno' parameter.
// Return any appropriate return value.
// LAB 3: Your code here.

//panic("syscall not implemented");
int32_t retVal = 0;
switch (syscallno) {
case SYS_cputs:
sys_cputs((const char *)a1, a2);
break;
case SYS_cgetc:
retVal = sys_cgetc();
break;
case SYS_getenvid:
retVal = sys_getenvid()>=0; // // Returns the current environment's envid.
break;
case SYS_env_destroy:
retVal = sys_env_destroy(a1); // env_id
break;
default:
return -E_INVAL;
}
return retVal;
}

运行 make grade 可以通过 testbss,运行 make run-hello 可以打印出 hello world,紧接着提示了页错误。

image-20220704163653390 image-20220704163812230 image-20220704163804732

通过 exercise 7,可以看出 JOS系 统调用的步骤为:

  1. 用户进程使用 inc/ 目录下暴露的接口
  2. lib/syscall.c 中的函数将系统调用号及必要参数传给寄存器,并引起一次 int $0x30 中断
  3. kern/trap.c 捕捉到这个中断,并将 TrapFrame 记录的寄存器状态作为参数,调用处理中断的函数
  4. kern/syscall.c 处理中断

Exercise 8: User-mode startup

一个用户程序开始运行在lib/entry.S的顶部。在一些设置之后,这段代码调用lib/libmain.c中libmain()。你应该修改libmain()来初始化全局指针thisenv,使其指向envs[]数组中的环境结构体Env。(Note that lib/entry.S has already defined envs to point at the UENVS mapping you set up in Part A.)

Hint: look in inc/env.h and use sys_getenvid.

libmain() then calls umain, which, in the case of the hello program, is in user/hello.c. Note that after printing “hello, world”, it tries to access thisenv->env_id. This is why it faulted earlier.

Now that you’ve initialized thisenv properly, it should not fault. If it still faults, you probably haven’t mapped the UENVS area user-readable (back in Part A in pmap.c; this is the first time we’ve actually used the UENVS area).

Add the required code to the user library, then boot your kernel. You should see user/hello print “hello, world“ and then print “i am environment 00001000“. user/hello then attempts to “exit” by calling sys_env_destroy() (see lib/libmain.c and lib/exit.c). Since the kernel currently only supports one user environment, it should report that it has destroyed the only environment and then drop into the kernel monitor. You should be able to get make grade to succeed on the hello test.

umain.c

user/hello.c

1
2
3
4
5
6
void
umain(int argc, char **argv)
{
cprintf("hello, world\n");
cprintf("i am environment %08x\n", thisenv->env_id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
libmain(int argc, char **argv)
{
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
thisenv = &envs[ENVX(sys_getenvid())]; // &envs[ENVX(envid)]

// save the name of the program so that panic() can use it
if (argc > 0)
binaryname = argv[0];

// call user main routine
umain(argc, argv);

// exit gracefully
exit();
}

image-20220704172003993

hello 完成

Exercise 9: Page faults and memory protection

内存保护是操作系统的关键功能,它确保了一个程序中的错误不会导致其他程序或是操作系统自身的崩溃。
操作系统通常依赖硬件的支持来实现内存保护。操作系统会告诉硬件哪些虚拟地址可用哪些不可用。当某个程序想访问不可用的内存地址或不具备权限时,处理器将在出错指令处停止程序,然后陷入内核。如果错误可以处理,内核就处理并恢复程序运行,否则无法恢复。
作为可以修复的错误,设想某个自动生长的栈。在许多系统中内核首先分配一个页面给栈,如果某个程序访问了页面外的空间,内核会自动分配更多页面以让程序继续。这样,内核只用分配程序需要的栈内存给它,然而程序感觉仿佛可以拥有任意大的栈内存。
系统调用也为内存保护带来了有趣的问题。许多系统调用接口允许用户传递指针给内核,这些指针指向待读写的用户缓冲区。内核处理系统调用的时候会对这些指针解引用。这样就带来了两个问题:

  1. 内核的页错误通常比用户进程的页错误严重得多,如果内核在操作自己的数据结构时发生页错误,这就是一个内核bug,会引起系统崩溃。因此,内核需要记住这个错误是来自用户进程。
  2. 内核比用户进程拥有更高的内存权限,用户进程给内核传递的指针可能指向一个只有内核能够读写的区域,内核必须谨慎避免解引用这类指针,因为这样可能导致内核的私有信息泄露或破坏内核完整性。

我们将对用户进程传给内核的指针做一个检查来解决这两个问题。内核将检查指针指向的是内存中用户空间部分,页表也允许内存操作。

Change kern/trap.c to panic if a page fault happens in kernel mode.

Hint: to determine whether a fault happened in user mode or in kernel mode, check the low bits of the tf_cs. (tf_cs是0x18还是0x1b, 0x18|0x03 = 0x1b此时是用户模式,0x18是内核模式,检查&3后的低2位)

Read user_mem_assert in kern/pmap.c and implement user_mem_check in that same file.

Change kern/syscall.c to sanity check arguments to system calls.

Boot your kernel, running user/buggyhello. The environment should be destroyed, and the kernel should not panic. You should see:

1
2
3
4
[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!

Finally, change debuginfo_eip in kern/kdebug.c to call user_mem_check on usd, stabs, and stabstr. If you now run user/breakpoint, you should be able to run backtrace from the kernel monitor and see the backtrace traverse into lib/libmain.c before the kernel panics with a page fault. What causes this page fault? You don’t need to fix it, but you should understand why it happens.

kern/trap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;

// Read processor's CR2 register to find the faulting address
fault_va = rcr2();

// Handle kernel-mode page faults.
// LAB 3: Your code here.
if(!(tf->tf_cs & 3)){
panic("page fault in kernel mode.");
}
// We've already handled kernel-mode exceptions, so if we get here,
// the page fault happened in user mode.

// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}

kern/pmap.c

user_mem_check

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
// 检查一个环境是否允许访问权限为'perm | PTE_P'的内存范围[va, va+len]。
// 通常'perm'至少包含PTE_U,但这不是必需的。'va'和'len'不需要按页面对齐;必须测试包含任何该范围的每个页面。
// 你可以测试'len/PGSIZE', 'len/PGSIZE + 1',或者'len/PGSIZE + 2'页面。
// 用户程序可以访问一个虚拟地址,如果
// (1)该地址在ULIM之下
// (2)页表给了它权限。这些正是您应该在这里实现的测试。
// 这些正是您应该在这里实现的测试。
// 如果有错误,设置'user_mem_check_addr'变量为第一个错误的虚拟地址。
// 如果用户程序可以访问这个地址范围,返回0,否则返回-E_FAULT。
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
// 内存范围[va, va+len]
uintptr_t start_va = ROUNDDOWN((uintptr_t)va, PGSIZE);
uintptr_t end_va = ROUNDUP((uintptr_t)va + len, PGSIZE);
// 访问内存,通过页表,那么直接看page table entry
// 遍历查看是否有权限
// pgdir_walk returns a pointer to the page table entry (PTE) for linear address 'va'.
// pte_t *pgdir_walk(pde_t *pgdir, const void *va, int create)
for((uintptr_t)cur_va = start_va;cur_va<end_va;cur_va+=PGSIZE){
// env->env_pgdir 取进程对应的页表
pte_t* cur_pte = pgdir_walk(env->env_pgdir, (void*)cur_va, 0); // 只查询不建立
if(cur_pte == NULL || (*cur_pte & (perm | PTE_P))!=(perm | PTE_P) || cur_va>=ULIM){ // 不满足要求
if(cur_va == start_va){ // 因为va做了近似,所以第一个页面地址要做处理
user_mem_check_addr = va;
}else{
user_mem_check_addr = cur_va;
}
// 程序不能访问这个地址,返回-E_FAULT
return -E_FAULT;
}
}
// 可以访问
return 0;
}
image-20220704223729337

不能写为cur_va<=end_va。会内存越界

user_mem_assert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Checks that environment 'env' is allowed to access the range
// of memory [va, va+len) with permissions 'perm | PTE_U | PTE_P'.
// If it can, then the function simply returns.
// If it cannot, 'env' is destroyed and, if env is the current
// environment, this function will not return.
//
void
user_mem_assert(struct Env *env, const void *va, size_t len, int perm)
{
if (user_mem_check(env, va, len, perm | PTE_U) < 0) {
cprintf("[%08x] user_mem_check assertion failure for "
"va %08x\n", env->env_id, user_mem_check_addr);
env_destroy(env); // may not return
}
}

调用了user_mem_check(),不满足则摧毁页面

运行 make run-buggyhello-nox

image-20220704213103991

[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!

最后,change debuginfo_eip in kern/kdebug.c to call user_mem_check on usd, stabs, and stabstr.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LAB 3: Your code here.
// 就是查看能否访问 UserStabData
// int user_mem_check(struct Env *env, const void *va, size_t len, int perm)
if(user_mem_check(curenv, (void*) usd, sizeof(struct UserStabData), PTE_U) < 0){
return -1;
}

stabs = usd->stabs;
stab_end = usd->stab_end;
stabstr = usd->stabstr;
stabstr_end = usd->stabstr_end;

// Make sure the STABS and string table memory is valid.
// LAB 3: Your code here.
if(user_mem_check(curenv, (void*) stabs, stab_end - stabs, PTE_U) < 0){
return -1;
}
if(user_mem_check(curenv, (void*) stabstr, stabstr_end - stabstr, PTE_U) < 0){
return -1;
}

make run-breakpoint-nox 然后 使用 backtrace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
K> backtrace
Stack backtrace:
ebp efffff10 eip f0100a39 args 00000001 efffff28 f0226000 00000000 f01e4a40
kern/monitor.c:222: monitor+260
ebp efffff80 eip f0103f80 args f0226000 efffffbc 00000000 00000000 00000000
kern/trap.c:200: trap+187
ebp efffffb0 eip f010409d args efffffbc 00000000 00000000 eebfdfd0 efffffdc
kern/trapentry.S:93: <unknown>+0
ebp eebfdfd0 eip 0080007b args 00000000 00000000 00000000 00000000 00000000
lib/libmain.c:26: libmain+63
Incoming TRAP frame at 0xeffffe8c
kernel panic at kern/trap.c:272: page fault in kernel mode.
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.

可以看到遍历到了:lib/libmain.c:26: libmain+63

ebp(基址指针寄存器)寄存器的内容,efffff10,efffff80,efffffb0都位于内核栈,eebfdfd0位于用户栈。

输入make run-breakpoint-nox-gdb make gdb

把断点打在 monitor()函数处(kern/monitor.c)

image-20220704220321417

查看ebp的内容

1
2
调用者ebp   返回地址eip  参数1  参数2
参数3 参数4 参数5 ...

在查看0xeebfdfd0到用户栈栈顶0xeebfdff0的内容

image-20220704221209942

试图越界访问,可得到

image-20220704221230450

可以看到,最后就只剩了12个字符

这一次,backtrace的打印内容为

ebp eebfdfd0 eip 0080007b args 00000000 00000000 00000000 00000000 00000000

那么下一次为

ebp eebfdff0 eip 00800031 args 00000000 00000000 之后的三个参数全部超过内存访问界限了

现在修改backtrace的函数,输出两个args,backtrace位于kern/monitor.c

image-20220704221854670

image-20220704223813288

Lab2: Memory Management

Intro:

在本实验中,您将为您的操作系统编写内存管理代码。内存管理有两个组件。

第一个组件是内核的物理内存分配器,以便内核可以分配内存并在以后释放它。您的分配器将以 4096 字节为单位运行,称为 pages。您的任务是维护数据结构,记录哪些物理页面是空闲的,哪些是已分配的,以及有多少进程共享每个分配的页面。您还将编写例程来分配和释放内存页面。

内存管理的第二个组成部分是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。x86 硬件的内存管理单元 (MMU) 在指令使用内存时执行映射,并参考一组页表。您将根据我们提供的规范修改 JOS 以设置 MMU 的页表。

boot_map_region映射的物理页不改变对应的pp_ref,一个物理页被这个函数映射与它是否被使用没有任何关系;而通过page_insert映射的物理页,同时就表示了该物理页被使用了一次,要给pp_ref加1。

切换到lab2

image-20220603213654097

image-20220603213707953

memlayout.h描述了您必须通过修改pmap.c来实现的虚拟地址空间的布局。

memlayout.hpmap.h定义了PageInfo 用于跟踪哪些物理内存页面空闲的结构。

image-20220623205319533

kclock.c and kclock.h manipulate the PC’s battery-backed clock and CMOS RAM hardware, in which the BIOS records the amount of physical memory the PC contains, among other things.

pmap.c中的代码需要读取这个设备硬件,以便计算出有多少物理内存,但是这部分代码是为您完成的:您不需要了解 CMOS 硬件如何工作的细节。

请特别注意memlayout.hpmap.h,因为本实验要求您使用并理解它们包含的许多定义。您可能还想查看inc/mmu.h,因为它还包含许多对本实验有用的定义。

在开始实验之前,不要忘记add -f 6.828获取 6.828 版本的 QEMU。

Part 1: Physical Page Management

您现在将编写物理页分配器。它通过对象的链接列表跟踪哪些页面是空闲的struct PageInfo(与 xv6 不同,这些对象嵌入空闲页面本身),每个对象对应一个物理页面。您需要先编写物理页面分配器,然后才能编写其余的虚拟内存实现,因为您的页表管理代码将需要分配物理内存来存储页表。

Exercise 1

在文件kern/pmap.c中,您必须实现以下函数的代码(可能按照给定的顺序)。

1
2
3
4
5
boot_alloc()
mem_init()(仅限于调用check_page_free_list(1))
page_init()
page_alloc()
page_free()

check_page_free_list() 和 check_page_alloc()测试您的物理页面分配器。

您应该启动 JOS 并查看是否check_page_alloc() 报告成功。

Fix您的代码以使其通过。You may find it helpful to add your own assert()s to verify that your assumptions are correct.

boot_alloc() 是一个内存分配器

函数的核心是维护一个静态变量nextfree,代表下一个可以使用的空闲内存空间的虚拟地址

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
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;

// Initialize nextfree if this is the first time.
// 'end' is a magic symbol automatically generated by the linker,
// which points to the end of the kernel's bss segment:
// the first virtual address that the linker did *not* assign
// to any kernel code or global variables.
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}

// Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
//
// LAB 2: Your code here.
if (n == 0) { // if n == 0, returns the address of the next free page without allocating anything.
return nextfree;
}
// n > 0 分配足够的连续物理内存页以容纳〞n”个字节。returns a kernel virtual address.
result = nextfree;
nextfree += ROUNDUP(n, PGSIZE); // Round up to the nearest multiple of PGSIZE
return result;
}

mem_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
void
mem_init(void)
{
uint32_t cr0;
size_t n;

// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();

// Remove this line when you're ready to test this function.
// panic("mem_init: This function is not finished\n");

//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);

//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)

// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:
// 创建一个struct PageInfo 的数组
// kernel 使用这个数组来耿总每个物理页
// 对于每一个物理页,都会有一个对应的 struct PageInfo 在数组中
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo));
// npages 是内存中物理页的数量
memset(pages, 0, npages * sizeof(struct PageInfo));

//////////////////////////////////////////////////////////////////////
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert
page_init();

check_page_free_list(1);
check_page_alloc();
check_page();

//////////////////////////////////////////////////////////////////////
// Now we set up virtual memory

//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:

//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:

//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:

// Check that the initial page directory has been set up correctly.
check_kern_pgdir();

// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created. Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));

check_page_free_list(0);

// entry.S set the really important flags in cr0 (including enabling
// paging). Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_MP;
cr0 &= ~(CR0_TS | CR0_EM);
lcr0(cr0);

// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();
}

page_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
void
page_init(void)
{
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
size_t i;
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 将页 0 标记为使用状态
pages[0].pp_ref = 1;
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 剩下的标为空闲状态
for(i = 1;i<npages_basemem;++i){
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// io端口, 不能被分配
for(i = IOPHYSMEM/PGSIZE;i<EXTPHYSMEM/PGSIZE;++i){
pages[i].pp_ref = 1;
}

// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
// 找到第一个能分配的页面
// boot_alloc有个 nextfree指针,但是是虚拟地址,我们要将其转换为物理地址 physical address
// PADDR 可以实现地址的转换
size_t first_free_address = PADDR(boot_alloc(0));
// 看看extend physical memory 是不是free
for(i = EXTPHYSMEM/PGSIZE;i<first_free_address/PGSIZE;++i){
pages[i].pp_ref = 1;
}
// 把页面设为空闲,插入链表头部
for (i = first_free_address/PGSIZE; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}

page_alloc() 完成页面的分配。

分配是基于PageInfo的,只是把页面标记为使用,并未真正的分配页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// page2kva: page to kernel virtual address
struct PageInfo *
page_alloc(int alloc_flags) {
// Fill this function in
// 超过空闲内存的范围,则返回NULL
// 所分配界面的 pp_link 设置为空, 以便page_free可以检查double-free bug。
// 分配物理页面。如果(alloc_flags & ALLOC_ZERO),用'\0'字节填充整个返回的物理页面。
if(page_free_list == NULL){
return NULL;
}
struct PageInfo* allocated_page = page_free_list;
page_free_list = page_free_list->pp_link;
allocated_page->pp_link = NULL;
if(alloc_flags & ALLOC_ZERO){
memset(page2kva(allocated_page),'\0',PGSIZE);
}
return allocated_page;
}

page_free()

释放一个页面,到page_free_list中

(This function should only be called when pp->pp_ref reaches 0.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Return a page to the free list.
// (This function should only be called when pp->pp_ref reaches 0.)
//
void
page_free(struct PageInfo *pp) {
// Fill this function in
// Hint: You may want to panic if pp->pp_ref is nonzero or
// pp->pp_link is not NULL.
if(pp->pp_ref>0 || pp->pp_link!=NULL){
panic("Double check failed when dealloc page");
return;
}
pp->pp_link = page_free_list; // 头插
page_free_list = pp;
}

使用 make qemu-nox 运行,发现报了个panic, 需要把panic注释掉。看漏了

image-20220626094749150

image-20220626094809597

image-20220626104146086

Part 2: Virtual Memory

虚拟内存
当 cpu 拿到一个地址并根据地址访问内存时,在 x86架构下药经过至少两级的地址变换:段式变换和页式变换。分段机制的主要目的是将代码段、数据段以及堆栈段分开,保证互不干扰。分页机制则是为了实现虚拟内存。
虚拟内存主要的好处是:

让每个程序都以为自己独占计算机内存空间,概念清晰,方便程序的编译和装载。
通过将部分内存暂存在磁盘上,可以让程序使用比物理内存大得多的虚拟内存,突破物理内存的限制。
通过对不同进程设置不同页表,可以防止进程访问其他进程的地址空间。通过在不同进程之间映射相同的物理页,又可以提供进程间的共享。

虚拟、线性和物理地址

虚拟地址
最原始的地址,也是 C/C++ 指针使用的地址。由前 16bit 段 (segment) 选择器和后 32bit 段内的偏移 (offset) 组成,显然一个段大小为 4GB。通过虚拟地址可以获得线性地址。
线性地址
前 10bit 为页目录项(page directory entry, PDE),即该地址在页目录中的索引。中间 10bit 为页表项(page table entry, PTE),代表在页表中的索引,最后 12bit 为偏移,也就是每页 4kB。通过线性地址可以获得物理地址。

页目录偏移DIR |页表偏移Table|页内偏移Offset

物理地址
经过段转换以及页面转换,最终在 RAM 的硬件总线上的地址。

JOS只有一个段,因此虚拟地址在数值上等于线性地址。

Exercise 4

image-20220626152236756

pgdir 是指向页目录的指针。

pgdir_walk() returns a pointer to page table entry(PTE) for linear address(va)。查找一个虚拟地址对应的页表项地址。

img

在页目录项、页表项中存储的是页表项的物理地址前 20bit 外加 12bit 的 flag。

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
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns
// a pointer to the page table entry (PTE) for linear address 'va'.
// This requires walking the two-level page table structure.
//
// The relevant page table page might not exist yet.
// If this is true, and create == false, then pgdir_walk returns NULL.
// Otherwise, pgdir_walk allocates a new page table page with page_alloc.
// - If the allocation fails, pgdir_walk returns NULL.
// - Otherwise, the new page's reference count is incremented,
// the page is cleared,
// and pgdir_walk returns a pointer into the new page table page.
//
// Hint 1: you can turn a PageInfo * into the physical address of the
// page it refers to with page2pa() from kern/pmap.h.
//
// Hint 2: the x86 MMU checks permission bits in both the page directory
// and the page table, so it's safe to leave permissions in the page
// directory more permissive than strictly necessary.
//
// Hint 3: look at inc/mmu.h for useful macros that manipulate page
// table and page directory entries.
//
// typedef uint32_t pte_t;
// pgdir_walk returns a pointer to the page table entry (PTE) for linear address 'va'.
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
// pgdir 页目录项地址
// va 虚拟地址,jos只有一个段,因此虚拟地址等于线性地址
// create 若页目录项不存在是否创建
// return 页表项指针
uint32_t page_dir_index = PDX(va);
uint32_t page_table_index = PTX(va);

pte_t* pgtab;
if(pgdir[page_dir_index] && PTE_P){ // 存在且可写
pgtab = KADDR(PTE_ADDR(pgdir[page_dir_index])); // KADDR->virtual address
}else{ // 不存在
if(create){
// 创建新的页表项
// For page_alloc, zero the returned physical page.
// ALLOC_ZERO = 1<<0,
struct PageInfo* new_pageInfo = page_alloc(ALLOC_ZERO);
if(new_pageInfo){
new_pageInfo->pp_ref+=1;
// 存入数组
// 依次获取 table_index 和 dir_index
// page2kva() page to kernel virtual address
pgtab = (pte_t*)page2kva(new_pageInfo);
pgdir[page_dir_index] = PADDR(pgtab) | PTE_P | PTE_W | PTE_U; // PADDR 虚拟到物理
}else{
return NULL;
}
}else{
return NULL;
};
}
return &pgtab[page_table_index];
}

page_lookup()

// 返回映射到虚拟地址 va 的页面
// pgdir_walk 只查询,不创建,create为0
// pa2page 由物理地址 返回对应的页面描述

1
2
Map [va, va+size) of virtual address space to physical [pa, pa+size) in the page table rooted at pgdir.  Size is a multiple of PGSIZE, and va and pa are both page-aligned.
This function is only intended to set up the ``static'' mappings above UTOP. As such, it should *not* change the pp_ref field on the mapped pages.
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
// Return the page mapped at virtual address 'va'.
// If pte_store is not zero, then we store in it the address
// of the pte for this page. This is used by page_remove and
// can be used to verify page permissions for syscall arguments,
// but should not be used by most callers.
//
// Return NULL if there is no page mapped at va.
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
// 返回映射到虚拟地址 va 的页面
// pgdir_walk 只查询,不创建,create为0
// pa2page 由物理地址 返回对应的页面描述
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
// pdgir 页目录地址
// va 虚拟地址
// pte_store 指向页表指针的指针 the address of the pte for this page
// If pte_store is not zero, then we store in it the address of the pte for this page.
pde_t* find_pgtab = pgdir_walk(pgdir, va, 0); // 根据va,返回一个指向page table entry的指针
if(!find_pgtab){ // 没找到
return NULL;
}
// 找到了
// 再找page table的虚拟地址
if(pte_store){
*pte_store = find_pgtab; // 保存下
}
// 返回页面描述 struct PageInfo *
return pa2page(PTE_ADDR(*find_pgtab)); // PTE_ADDR 将页表指针指向的内容转为物理地址
}

page_remove()

移除一个虚拟地址与对应物理地址的映射关系

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
//
// Unmaps the physical page at virtual address 'va'.
// If there is no physical page at that address, silently does nothing.
//
// Details:
// - The ref count on the physical page should decrement.
// - The physical page should be freed if the refcount reaches 0.
// - The pg table entry corresponding to 'va' should be set to 0.
// (if such a PTE exists)
// - The TLB must be invalidated if you remove an entry from
// the page table.
//
// Hint: The TA solution is implemented using page_lookup,
// tlb_invalidate, and page_decref.
//
// 移除一个虚拟地址与对应物理地址的映射关系
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
// pgdir 页目录地址
// va 虚拟地址
// 首先要找到 va对应的物理地址, 使用 page_lookup
// page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
pte_t* pgtab;
pte_t** pte_store = &pgtab;
struct PageInfo* pInfo = page_lookup(pgdir, va, pte_store);
if(!pInfo){ // 空的
return;
}
page_decref(pInfo); // 减少页上的引用计数,如果没有引用则释放该计数。
*pgtab = 0;
// tlb_invalidate(pde_t *pgdir, void *va)
tlb_invalidate(pgdir, va); // 使TLB条目无效,但前提是正在编辑的页表是当前处理器正在使用的页表。
}

page_insert()

建立一个虚拟地址与物理页的映射,与page_remove() 对应。

The permissions (the low 12 bits) of the page table entry should be set to ‘perm|PTE_P’.

requirement:

// 如果已经有一个页面映射到’va’,it should be page_remove()d.

// 如果有必要,应按需分配一个页表,并插入到’pgdir’。

// 如果插入成功,pp->pp_ref应该加1。

// TLB必须无效,如果 ‘va’ 对应的一个页面已经存在。

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
// 建立一个虚拟地址与物理页的映射,与page_remove() 对应
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
// pgdir 页目录指针
// pp 页描述结构体 指针
// va 虚拟地址
// perm 权限
pte_t* pgtab = pgdir_walk(pgdir, va, 1); // 查询该虚拟地址对应的页表项(struct PageInfo),不存在则建立
if(!pgtab){ // 建立失败
return -E_NO_MEM;
}
// 建立成功
if(*pgtab && PTE_P){ // 可以写入
// 若该虚拟地址va已经映射到了其他物理页
if(page2pa(pp) == PTE_ADDR(*pgtab)){ // PTE_ADDR Address in page table or page directory entry
// 更改权限,不增加引用
*pgtab = page2pa(pp) | perm | PTE_P;
return 0;
}else{
// 更新映射的物理页,则需要删除之前的映射关系
page_remove(pgdir, va);
}
}
// 删除后建立新的物理页
*pgtab = page2pa(pp) | perm | PTE_P;
pp->pp_ref++;
return 0;
}

boot_map_region()

映射一片虚拟页到制定物理页,大小为size, size是PGSIZE的倍数

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
//
// Map [va, va+size) of virtual address space to physical [pa, pa+size)
// in the page table rooted at pgdir. Size is a multiple of PGSIZE, and
// va and pa are both page-aligned.
// Use permission bits perm|PTE_P for the entries.
//
// This function is only intended to set up the ``static'' mappings
// above UTOP. As such, it should *not* change the pp_ref field on the
// mapped pages.
//
// Hint: the TA solution uses pgdir_walk
// 映射一片虚拟页到指定物理页,大小为size, size是PGSIZE的倍数
// va -> pa
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
// *pgdir 页目录指针
// va 虚拟地址
// size size是PGSIZE的倍数,
// pa 物理地址
// perm 权限
// 直接使用页数来分配,避免溢出
pte_t* pgtab;
size_t pg_count = PGNUM(size); // size能分成多少页
// pte_t* pgdir_walk(pde_t *pgdir, const void *va, int create)
for(size_t i = 0;i<pg_count;++i){
pgtab = pgdir_walk(pgdir, (void*)va, 1);
// pgdir_walk returns a pointer to the page table entry (PTE) for linear address 'va'.
*pgtab = pa | perm | PTE_P; // 权限
va+=PGSIZE;
pa+=PGSIZE;
}

}

image-20220627153213219

Part 3: Kernel Address Space

image-20220627204004274

JOS将处理器的32位线性地址划分为 用户地址() 和 内核地址(),二者以ULIM划分。

计算可得出一个物理页大小是4MB

ULIM = (MMIOLIM - PTSIZE) = (KSTACKTOP - PTSIZE - PTSIZE) = 0xF0000000 - 0x00400000 - 0x00400000 = 0xef800000

查看memlayout.h 可以看到,的确为0xef800000

image-20220627204422272

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
//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:
// UPAGES是JOS记录物理页面使用情况的数据结构,只有kernel能够访问
// 但是现在需要让用户空间能够读取这段线性地址,因此需要建立映射,将用户空间的一块内存映射到存储该数据结构的物理地址上
// boot_map_region() 建立映射关系
boot_map_region(kern_pgdir, (uintptr_t)UPAGES, npages*sizeof(struct PageInfo), PADDR(pages), PTE_U | PTE_P);
// 目前建立了一个页目录,kernel_pgdir
// pgdir为页目录指针, UPAGES为虚拟地址,npages*sizeof(struct* PageInfo)为映射的内存块大小
// PADDR(pages) 为物理地址, PTE_U | PTE为权限 (PTE_U 表示用户可读)


//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:
// kernel 内核栈
// kernel stack 从虚拟地址 KSTACKTOP 开始,向低地址增长,所以KSTACKTOP实际上是栈顶
// KSTACKTOP = 0xf0000000,
// KSTKSIZE = (8*PGSIZE) = 8*4096(bytes) = 32KB
// 只需要映射 [KSTACKTOP, KSTACKTOP - KSTKSIZE) 范围的虚拟地址
boot_map_region(kern_pgdir, (uintptr_t)(KSTACKTOP - KSTKSIZE), KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);
// PTE_W 开启了写权限,但是并未打开 PTE_U, 因此用户没有权限,只有内核有权限


//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
// 内核部分
// KERNBASE = 0xF0000000, VA大小为 2^32 - KERNBASE
// ROUNDUP(a,n) 将a四舍五入到最接近n的倍数
boot_map_region(kern_pgdir, (uintptr_t)KERNBASE, ROUNDUP(0xffffffff - KERNBASE + 1, PGSIZE), 0, PTE_W | PTE_P);

image-20220628111506229

问题

  1. 此时,页面目录中填写了哪些条目(行)?他们映射什么地址,指向哪里?换句话说,尽可能多地填写这张表格:

    入口 基本虚拟地址 指向(逻辑上):
    1023 0xffc00000 page table for [252,256)MB of physical address
    1022 0xff900000 page table for [248,252)MB of physical address
    960 0xf0000000 page table for [0,4)MB of physical address
    959 0xefc00000
    958 0xef800000 ULIM
    957 0xef400000 State register (UVPT)
    956 0xef000000 UPAGES, array of PageInfo
    955 0xeec00000 UPAGES, array of PageInfo
    NULL
    1 0x00400000 NULL
    0 0x00000000 same as 960
    1
    2
    3
    4
    // User read-only virtual page table (see 'uvpt' below)
    #define UVPT (ULIM - PTSIZE)
    // Read-only copies of the Page structures
    #define UPAGES (UVPT - PTSIZE)
  2. 我们已将内核和用户环境放置在同一地址空间中。为什么用户程序无法读取或写入内核的内存?哪些特定机制可以保护内核内存?

    页表内的标记位可以设置权限,PTE_U设置为1,用户才有权利读写。

  3. 这个操作系统可以支持的最大物理内存量是多少?为什么?

    UPAGES 大小是4096bytes,即4MB,每个结构体 PageInfo 占8bytes。指针占4字节,uint16_t占两字节,对齐后8字节。

    那么共有 4MB / 8B = 2^19 页,

    每页的大小PGSIZE = 4096 bytes

    那么最多使用 2^19 * 4096 = 2^31 = 2GB 的物理内存

    image-20220628152236712

  4. 如果我们真的有最大数量的物理内存,有多少空间来管理内存?这个开销是怎么分解的?

    为2GB的最大内存时,UPAGES的大小为4MB,page table directory 的大小为4MB, 一共8MB。

  5. 重新访问kern/entry.Skern/entrypgdir.c中的页面表设置。在我们打开分页后,EIP仍然是一个低数字(略高于1MB)。我们什么时候过渡到KERNBASE上方的EIP运行?when we enable paging and when we begin running at an EIP above KERNBASE,是什么使我们能够继续以低EIP执行?为什么需要这种过渡?

    EIP寄存器存储着CPU读取的下一条指令的地址,相当于PC计数器。在8086中,EIP=PC。

    image-20220628163341015

在jmp处打上断点,向后执行一步,产生了映射,分页机制启动

image-20220628163655193

把虚拟地址的[0,4M) [KERNBASE, KERNBASE+4M)两个区间都映射到同一个物理地址区间[0,4M)的原因在于不要让指令的寻址受到地址空间变化的影响。

  • Display in a useful and easy-to-read format all of the physical page mappings (or lack thereof) that apply to a particular range of virtual/linear addresses in the currently active address space. For example, you might enter 'showmappings 0x3000 0x5000' to display the physical page mappings and corresponding permission bits that apply to the pages at virtual addresses 0x3000, 0x4000, and 0x5000.
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
// 添加lab2 中的映射函数,以显示pa和va的对应关系
int
mon_showmappings(int argc, char **argv, struct Trapframe *tf){
// 检查参数个数
if(argc != 3){
cprintf("invalid arguments num. \n");
return -1;
}
// 参数个数符合,那么依次提取pa和va
char *errChar;
uintptr_t start_addr = strtol(argv[1], &errChar, 16);
// typedef uint32_t uintptr_t; uintptr_t represents the numerical value of virtual address.
if(*errChar){
cprintf("invalid virtual address. \n");
}
uintptr_t end_addr = strtol(argv[2], &errChar, 16);
if(*errChar){
cprintf("invalid virtual address. \n");
}

if(start_addr > end_addr){
cprintf("address 1 should be lower than address 2\n");
return -1;
}

// 按页对齐
start_addr = ROUNDDOWN(start_addr, PGSIZE);
end_addr = ROUNDUP(end_addr, PGSIZE);

// 依次访问
uintptr_t cur_addr = start_addr;
while(cur_addr <= end_addr){
// 查询当前地址,没有则创建
pte_t *cur_pte = pgdir_walk(kern_pgdir, (void*)cur_addr, 0);
// pgdir_walk() returns a pointer to the page table entry (PTE) for linear address 'va'.
if(!cur_pte || !(cur_pte && PTE_P)){
cprintf("virtual address [%08x] - not mapped\n", cur_addr);
}else{
cprintf("virtual address [%08x] - physical address [%08x], permission", cur_addr, PTE_ADDR(*cur_pte));
// 查询三种权限
char perm_PS = (*cur_pte & PTE_PS)?'S':'-';
char perm_W = (*cur_pte & PTE_W)?'W':'-';
char perm_U = (*cur_pte & PTE_U)?'U':'-';
cprintf("=%c===%c%cP\n", perm_PS, perm_W, perm_U);
}
cur_addr += PGSIZE;
}
return 0;
}

image-20220628213911940

参考ref:https://www.jianshu.com/u/6913c26d8b2c

相似题目

ref : https://leetcode.cn/problems/unique-substrings-in-wraparound-string/solution/xi-fa-dai-ni-xue-suan-fa-yi-ci-gao-ding-qian-zhui-/

模板

例题1 (presum)

有 N 个的正整数放到数组 A 里,现在要求一个新的数组 B,新数组的第 i 个数 B[i]是原数组 A 第 0 到第 i 个数的和。

求解:正常前缀和

1
2
3
4
5
6
vector<int> sum;
int n = sum.size();
vector<int> psum(n+1);
vector<int> psum1(n);
psum[i] = psum[i-1]+sum[i]; // [l,r] -> psum[r] - psum[l-1]
psum1[i] = psum[i-1]+sum[i-1]; // [l, r] -> psum[r+1] - psum[l];

例题2 (countArray)

求一个数组的连续子数组总个数。比如 [1,3,4],其连续子数组有:[1], [3], [4], [1,3], [3,4] , [1,3,4],需要返回 6。

总的连续子数组个数等于:以索引为 0 结尾的子数组个数 + 以索引为 1 结尾的子数组个数 + … + 以索引为 n - 1 结尾的子数组个数

利用 例题1 的前缀和思路, 边遍历边求和。

1
2
3
4
5
6
7
8
int countArray(vector<int> nums){
int ans = 0;
int temp = 1;
for(auto& a: nums){
temp+=1;
ans+=k;
}
}
  • 时间复杂度:O(N),其中 N 为数组长度。
  • 空间复杂度:O(1)

例题3 (countArrayGapK)

求一个数组相邻差为 1 连续子数组的总个数,就是索引差 1 的同时,值也差 1。

1
2
3
4
5
6
7
8
9
10
11
12
int countArrayGap1(vector<int> nums){
int n = nums.size();
int ans = 0;
int temp = 1;
for(int i = 1;i<n;++i){
if(nums[i] = nums[i-1] + 1){
temp+=1;
}else{
temp = 0;
}
}
}

例题4 (countArrayLessEqualK)

求出不大于 k 的子数组的个数。不大于 k 指的是子数组的全部元素都不大于 k。 比如 [1,3,4] 子数组有 [1], [3], [4], [1,3], [3,4] , [1,3,4],不大于 3 的子数组有 [1], [3], [1,3] ,那么 [1,3,4] 不大于 3 的子数组个数就是 3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int countArrayLessK(int k, vector<int> nums){
int n = nums.size();
int ans = 0;
int temp = 0;
for(int i = 0;i<n;++i){
if(nums[i] <= k){
temp++;
}else{
temp = 0;
}
ans+=temp;
}
return ans;
}

例题5(countArraMostEqualK)

求出子数组最大值刚好是 k 的子数组的个数。 比如 [1,3,4] 子数组有 [1], [3], [4], [1,3], [3,4] , [1,3,4],子数组最大值刚好是 3 的子数组有 [3], [1,3] ,那么 [1,3,4] 子数组最大值刚好是 3 的子数组个数就是 2。

1
2
3
int countArraMostEqualK(int k1, int k2, vector<int> nums){

}

子数组最大值刚好是k的子数组的个数 countArraMostEqualK可以使用求不大于k的子数组的方法countArrayLessEqualK 来求解,即

countArraMostEqualK(k) = countArrayLessK (k) - countArrayLessK (k-1)

例题6 (countArraBetween)

求出子数组最大值刚好是 介于 k1 和 k2 的子数组的个数。

countArraBetween(k1, k2, nums) 等价于

countArrayLessK(k2, nums) - countArrayLessK(k1 - 1, nums), k2>k1

1
2
3
int countArraBetween(int k1, int k2, vector<int> nums){

}

小于等于 k2 的区域 减去 小于 k1 的区域 就是 大于等于 k1 且小于等于 k2 的区域

467. 环绕字符串中唯一的子字符串

把字符串 s 看作是 “abcdefghijklmnopqrstuvwxyz” 的无限环绕字符串,所以 s 看起来是这样的:

“…zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd….” .
现在给定另一个字符串 p 。返回 s 中 唯一 的 p 的 非空子串 的数量 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例 1:

输入: p = "a"
输出: 1
解释: 字符串 s 中只有一个"a"子字符。
示例 2:

输入: p = "cac"
输出: 2
解释: 字符串 s 中的字符串“cac”只有两个子串“a”、“c”。.
示例 3:

输入: p = "zab"
输出: 6
解释: 在字符串 s 中有六个子串“z”、“a”、“b”、“za”、“ab”、“zab”。


提示:
1 <= p.length <= 10^5
p 由小写英文字母构成

套模板3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void case467(string p){
int ans = 0;
int n = p.size();
int temp = 1;
for(int i = 1;i<n;++i){
if((p[i] == 'a' && p[i-1] =='z') || (p[i]-'a') == (p[i-1] - 'a' + 26 + 1)%26){
// 连续
temp++;
}else{ // 不连续
temp = 1;
}
ans+=temp;
}
return ans;
}

直接套模板是错误的,因为对于cac这种情况,返回值是3, 但实际上是2,因为c被计算了两次,这就意味着需要去重,如果对于输入abcd使用set去重可以看到

1
2
3
4
5
6
7
8
a
b
c
ab (a, b)
cd (c, d)
abc (a,b,c,ab,bc,abc)
bcd (b,c,d,bc,cd,bcd)
abcd (....)

发现set中的元素是连续的,那么只用记录以某个字母为结束点的最大长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void case467(string p){
vector<int> dp(26,0);
int n = p.size();
int temp = 1;
for(int i = 1;i<n;++i){
if((p[i] == 'a' && p[i-1] =='z') || (p[i]-'a') == (p[i-1] - 'a' + 26 + 1)%26){
// 连续
temp++;
}else{ // 不连续
temp = 1;
}
dp[p[i]-'a'] = max(dp[p[i]-'a'], temp);
}
return accumulate(dp.begin(),dp.end(),0);
}

795. 区间子数组个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
给你一个整数数组 nums 和两个整数:left 及 right 。找出 nums 中连续、非空且其中最大元素在范围 [left, right] 内的子数组,并返回满足条件的子数组的个数。

生成的测试用例保证结果符合 32-bit 整数范围。

示例 1:

输入:nums = [2,1,4,3], left = 2, right = 3
输出:3
解释:满足条件的三个子数组:[2], [2, 1], [3]
示例 2:

输入:nums = [2,9,2,5,6], left = 2, right = 8
输出:7

提示:

1 <= nums.length <= 10^5
0 <= nums[i] <= 10^9
0 <= left <= right <= 10^9

可以看到满足例题6, countArrayBetween, 可以使用 mostK(right) - mostK(left - 1)来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int numSubarrayBoundedMax(vector<int>& nums, int left, int right) {
std::function<int(vector<int>&, int)> atMostK = [&](vector<int>& nums, int k)->int{
int ans = 0;
int temp = 0;
int n = nums.size();
for(int i = 0;i<n;++i){
if(nums[i] <= k){
temp++;
}else{
temp = 0;
}
ans+=temp;
}
return ans;
};
return atMostK(nums, right) - atMostK(nums, left - 1);
}

904. 水果成篮

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
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

示例 1:
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。


提示:
1 <= fruits.length <= 10^5
0 <= fruits[i] < fruits.length

就是给你一个数组, 让你选定一个子数组, 这个子数组最多只有两种数字,这个选定的子数组最大可以是多少。

和例题四一个样,只不过lessEqual判断为辨别可使用篮子的个数。采摘要连续

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
unordered_map<int, int> mp;
int j = 0; // 记录需要删除水果的起始下标
int k = 2;
int ans = 0;
for (int i = 0; i < n; ++i) {
if (mp[fruits[i]] == 0) { // 没使用过
--k;
}
mp[fruits[i]]++;
while (k < 0) { // 有新水果进来,篮子已超出了两个
// 直至删除空了一种水果,画图好理解
mp[fruits[j]]--;
if (mp[fruits[j]] == 0) {
++k;
}
j++;
}
ans = max(ans, i - j + 1);
}
return ans;
}

992. K 个不同整数的子数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
给定一个正整数数组 nums和一个整数 k ,返回 num 中 「好子数组」 的数目。

如果 nums 的某个子数组中不同整数的个数恰好为 k,则称 nums 的这个连续、不一定不同的子数组为 「好子数组 」。

例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。
子数组 是数组的 连续 部分。

示例 1:
输入:nums = [1,2,1,2,3], k = 2
输出:7
解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].

示例 2:
输入:nums = [1,2,1,3,4], k = 3
输出:3
解释:恰好由 3 个不同整数组成的子数组:[1,2,1,3], [2,1,3], [1,3,4].


提示:
1 <= nums.length <= 2 * 10^4
1 <= nums[i], k <= nums.length

条件为不同数字的个数。用个set,不断剔除掉最早出现的数字。

由例题 6,知:equalK = MostEqualK(k) - MostEqualK(k - 1), 因此答案便呼之欲出了。其他部分和上面的题目 904. 水果成篮 一样。

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
int subarraysWithKDistinct(vector<int>& nums, int k) {
auto mostEqualK = [&](vector<int>& nums, int k)->int{
unordered_map<int, int> mp;
int j = 0; // 保存最早出现的数字
int n = nums.size();
int ans = 0;
for(int i = 0;i<n;++i){
if(mp[nums[i]] == 0){ // 没出现过,加进去
--k; // 剩余的不同数目减一
}
mp[nums[i]]++;
// 没出现过的数字加进去后发现超出了不同数字的要求值
while(k<0){
mp[nums[j]]--;
if(mp[nums[j]] == 0){ // 直至清空前面任何一个数字
// 因为删除空的数字之前的那部分也是不能使用的,因为不连续
++k; // k从-1归0, 此时正好用完
}
++j;
}
ans += (i - j + 1); // 按照之前求countArray的方法
}
return ans;
};
return mostEqualK(nums, k) - mostEqualK(nums, k-1);
}

1109. 航班预订统计

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
这里有 n 个航班,它们分别从 1 到 n 进行编号。

有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。
请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。

示例 1:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号 1 2 3 4 5
预订记录 1 : 10 10
预订记录 2 : 20 20
预订记录 3 : 25 25 25 25
总座位数: 10 55 45 25 25
因此,answer = [10,55,45,25,25]

示例 2:
输入:bookings = [[1,2,10],[2,2,15]], n = 2
输出:[10,25]
解释:
航班编号 1 2
预订记录 1 : 10 10
预订记录 2 : 15
总座位数: 10 25
因此,answer = [10,25]

提示:
1 <= n <= 2 * 10^4
1 <= bookings.length <= 2 * 10^4
bookings[i].length == 3
1 <= firsti <= lasti <= n
1 <= seatsi <= 10^4
1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
// 差分数组
vector<int> nums(n+1, 0);
for(int i = 0;i<bookings.size();++i){
nums[bookings[i][0]-1] += bookings[i][2];
nums[bookings[i][1]] -= bookings[i][2];
}
for(int i = 1;i<n;++i){
nums[i]+=nums[i-1];
}
nums.pop_back();
return nums;
}

PC Bootstrap

The PC’s Physical Address Space

image-20220517122025755
  • 早期PC,16位,只有可用物理地址只有1M(0x00000000~0x000FFFFF)
  • 最重要的部分是**Basic Input/Output System (BIOS)**,它占据从0x000F0000到0x000FFFFF的64KB区域。在早期的pc机中,BIOS保存在read-only memory (ROM)中,现在pc机将BIOS存储在updateable flash memory(可更新的闪存)中。
  • BIOS负责执行基本的系统初始化,如激活显卡和检查已安装的内存数量。在执行此初始化之后,BIOS从某些适当的位置(如 floppy disk(软盘), hard disk, CD-ROM, or the network)加载操作系统,并将机器的控制权传递给操作系统。
  • 虽然现在可以处理器支持4GB的物理地址空间了,但还是保留那1MB的物理地址的设定来向后兼容

Exercise 3

学习boot.s,main.c,boot.asm,了解在GDB中执行the boot loader时发生了什么?

在地址 0x7c00 处设置断点,它是加载后的引导扇区的位置。继续运行,直到那个断点。在 boot/boot.S 中跟踪代码,使用源代码和反汇编文件 obj/boot/boot.asm 去保持跟踪。

boot/main.c 文件中跟踪进入 bootmain() ,然后进入 readsect()。识别 readsect() 中相关的每一个语句的准确汇编指令。跟踪 readsect() 中剩余的指令,然后返回到 bootmain() 中,识别 for 循环的开始和结束位置,这个循环从磁盘上读取内核的剩余扇区。找出循环结束后运行了什么代码,在这里设置一个断点,然后继续。接下来再走完引导加载器的剩余工作。

完成之后,就能够回答下列的问题了:

  • 处理器开始运行 32 代码时指向到什么地方?从 16 位模式切换到 32 位模式的真实原因是什么?
1
2
3
4
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
7c2d: ea 32 7c 08 00 66 b8 ljmp $0xb866,$0x87c32

在boot.asm 这里切换到32位代码。应该是经过64与60端口的控制,加载完GDT表后,CRO的bit0位为1,此时机器已处于保护模式,故处理器从16位模式转为32位模式。

全局描述表(GDT Global Descriptor Table):在保护模式下一个重要的数据结构。用来存储内存的分段信息。

GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。

  • 引导加载器执行的最后一个指令是什么,内核加载之后的第一个指令是什么?

引导加载程序的最后一条指令是boot/main.c中bootmain函数最后的((void (*)(void)) (ELFHDR->e_entry))(); 这个第一条指令位于/kern/entry.S文件中,第一句 movw $0x1234, 0x472

image-20220517140436330 image-20220517140447848
  • 内核的第一个指令在哪里?

位于/kern/entry.S文件中

  • 为从硬盘上获取完整的内核,引导加载器如何决定有多少扇区必须被读入?在哪里能找到这些信息?

通过ELF program headers决定,他在操作系统内核映像文件的ELF头部信息里找到。

Examine the full list of the names, sizes, and link addresses of all the sections in the kernel executable by typing:

image-20220517155248619

  • .text:程序的可运行指令。
  • .rodata:只读数据,比如,由 C 编译器生成的 ASCII 字符串常量。(然而我们并不需要操心设置硬件去禁止写入它)
  • .data:保持在程序的初始化数据中的数据节,比如,初始化声明所需要的全局变量,比如,像 int x = 5;

look at the .text section of the boot loader:

image-20220517155417627

Exercise4

下载 pointers.c 的源代码,运行它,然后确保你理解了输出值的来源的所有内容。尤其是,确保你理解了第 1 行和第 6 行的指针地址的来源、第 2 行到第 4 行的值是如何得到的、以及为什么第 5 行指向的值表面上看像是错误的。

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
#include <stdio.h>
#include <stdlib.h>

void
f(void)
{
int a[4];
int *b = malloc(16); //一个int四个字节,共16个字节
int *c;
int i;

printf("1: a = %p, b = %p, c = %p\n", a, b, c);

c = a;
for (i = 0; i < 4; i++)
a[i] = 100 + i;
c[0] = 200; // c指向a
printf("2: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
a[0], a[1], a[2], a[3]);

c[1] = 300;
*(c + 2) = 301;
3[c] = 302;
printf("3: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
a[0], a[1], a[2], a[3]);

c = c + 1; // c指向a[1]。由于a中存int,一个int占四个字节,所以c的地址会+4,而不是+1
*c = 400;
printf("4: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
a[0], a[1], a[2], a[3]);

c = (int *) ((char *) c + 1);
//如果先将c转化为字符串,再直接对字符串+1,这样导致c实际地址就是+1
*c = 500;
printf("5: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
a[0], a[1], a[2], a[3]);

b = (int *) a + 1;
c = (int *) ((char *) a + 1);
printf("6: a = %p, b = %p, c = %p\n", a, b, c);
}

int
main(int ac, char **av)
{
f();
return 0;
}
img

如果先将c转化为字符串,再直接对字符串+1,这样导致c实际地址就是+1,从0x7fffd33e72c40x7fffd33e72c5

c = (int *) ((char *) c + 1) 与c=c+1的区别

image-20220517161940295

image-20220517162003852

转换为char后,c只向前移动了1位,即八个bit。

*c=500,将灰色框内的值变为500.

image-20220517162103314

其他
如果int *p=(int*)100,则(int)p+1和(int)(p+1)不同,前者是101,但后者是104。当指针加整数时,向第二个例子,整数隐式乘以指针所指向的对象的大小。
p[i]定义为和*(p+i)相同,代表p指向的内存的第i个对象,当对象大于1字节时,这条规则起作用
&p[i]和(p+i)相同,代表p指向的内存的第i个对象的地址
在对内存地址进行加法时,要确定他是integer addition or pointer addition

ref: https://blog.csdn.net/han_hhh/article/details/121701438

Exercise 5

内核的文件头:

image-20220518152817746 image-20220518152831699

得到了Program Header文件头,上面省略了余下的一些输出内容。每个LOAD都是一个ELF对象,里面包含了相对本文件的索引off、虚拟内址vaddr、物理地址paddr、对齐align、对象在文件和内存中的大小filesz, memsz

这个headerbootmain函数中通过readseg函数加载到了内存中,位置在0x10000,并通过一个宏ELFHDR索引。header中存放的数据采用的是默认的对齐方式,所以可以直接通过一个struct Elf指针访问各个属性。

image-20220518153305171

在文件头struct Elf中,我们拿到了关于结构体struct Proghdr数组的信息e_phoff。从这个数组的成员中,我们拿到了关于每个segment的信息,也就可以把它们正式从硬盘中拷贝到内存中的指定位置。

image-20220518153337802

第一个segment对象的地址存放在ELFHDR->e_phoff中,是从硬盘中读入的原始数据。转化为指针如下:

1
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);

也就是加上了一个偏置。同样利用elf文件的数据,查出这个“对象数组”的长度,也就可以遍历这个Proghdr数组了:

1
2
for (; ph < eph; ph++)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

ph是个struct Proghdr类型的指针,直接++让地址的值前进相应地址长度,非常方便。

至此,内核完全加载完毕。

image-20220518154931248

image-20220518170934792 image-20220518170900480

获取kernel ELF文件的相关信息:

image-20220518173511422

可看到程序入口的虚拟地址是0x10000c,程序头表偏移为52B,程序头表条目是2.

如果你得到一个错误的引导加载器链接地址,通过再次跟踪引导加载器的前几个指令,你将会发现第一个指令会 “中断” 或者出错。然后在 boot/Makefrag 修改链接地址来修复错误,运行 make clean,使用 make 重新编译,然后再次跟踪引导加载器去查看会发生什么事情。不要忘了改回正确的链接地址,然后再次 make clean

image-20220518181251389

修改0x7C00为0x7d00,使得bootloader无法正确加载。

image-20220518181451276

可能就是GDT表加载错误才导致后面加载失败,因为加载信息都错掉了。

Exercise 6

在BIOS进入 the boot loader时检查内存0x00100000处的8个字,然后在 the boot loader进入内核时再检查一次。

image-20220518185538199

image-20220518185555496

0x00100000往后8个字前后不一样应该是由于bootmain将内核的某个section存入了该地址处。由于要在进入内核时再看一次,故第二个断点应该设置在call entry处,
即 *b 0x7d69

image-20220518185643002

内核的入口地址是0x0010000c,也在此范围中,可能是.text段的内容,因为内核最先加载的就是.text

➜ lab git:(lab1) objdump -x obj/kern/kernel

obj/kern/kernel: file format elf32-i386
obj/kern/kernel
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c

Program Header:
LOAD off 0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 212
filesz 0x0000ee68 memsz 0x0000ee68 flags r-x
LOAD off 0x00010000 vaddr 0xf010f000 paddr 0x0010f000 align 212
filesz 0x0000a948 memsz 0x0000a948 flags rw-

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000178e f0100000 00100000 00001000 22
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000704 f01017a0 001017a0 000027a0 25
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000044d1 f0101ea4 00101ea4 00002ea4 22
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 00008af3 f0106375 00106375 00007375 20
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f010f000 0010f000 00010000 212
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000648 f0119300 00119300 0001a300 25
CONTENTS, ALLOC, LOAD, DATA
6 .comment 00000011 00000000 00000000 0001a948 20
CONTENTS, READONLY

The kernel

Exercise 7

在0x0010000c处打上断点,si单步执行,直到 movl %eax, %cr0 前一步

image-20220522172810387

查看0x00100000 和 0xf0100000 的内容

image-20220522172955485

执行movl %eax, %cr0 , 再次查看内容

image-20220522173104250

发现VMA和LMA有相同的内容,这是因为分页后,0x00100000 被映射到了 0xf0100000 处,完成了分页操作。

make clean 后,注释掉movl %eax, %cr0 (kern/entry.S)

重新运行

image-20220522194929977

qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log -S
qemu: fatal: Trying to execute code outside RAM or ROM at 0xf010002c

EAX=f010002c EBX=00010074 ECX=00000000 EDX=000000d5
ESI=00010074 EDI=00000000 EBP=00007bf8 ESP=00007bec
EIP=f010002c EFL=00000086 [–S–P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
DS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
FS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
GS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT= 00007c4c 00000017
IDT= 00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00117000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
CCS=00000084 CCD=80010011 CCO=EFLAGS
EFER=0000000000000000
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=00000000000000000000000000000000 XMM01=00000000000000000000000000000000
XMM02=00000000000000000000000000000000 XMM03=00000000000000000000000000000000
XMM04=00000000000000000000000000000000 XMM05=00000000000000000000000000000000
XMM06=00000000000000000000000000000000 XMM07=00000000000000000000000000000000
make: *** [qemu-nox-gdb] Aborted (core dumped)

由于未开启分页机制,虚拟地址还未映射到物理地址。

Exercise 8

补全”%o”

image-20220522213815891

problem 1

解释printf.cconsole.c之间的接口。具体来说, console.c导出什么函数 ?printf.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
// printf.c
// Simple implementation of cprintf console output for the kernel,
// based on printfmt() and the kernel console's cputchar().
//为内核简单实现cprintf控制台输出,
//基于printfmt()和内核控制台的cputchar()。

#include <inc/types.h>
#include <inc/stdio.h>
#include <inc/stdarg.h>


static void
putch(int ch, int *cnt)
{
cputchar(ch);
*cnt++;
}

int
vcprintf(const char *fmt, va_list ap)
{
int cnt = 0;

vprintfmt((void*)putch, &cnt, fmt, ap);
return cnt;
}

int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;

va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);

return cnt;
}
image-20220522214112303 image-20220522214134785
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
/*
* 03FD r line status register
bit 7 = 0 reserved
bit 6 = 1 transmitter shift and holding registers empty
bit 5 = 1 transmitter holding register empty. Controller is
ready to accept a new character to send.
bit 4 = 1 break interrupt. the received data input is held in
in the zero bit state longer than the time of start
bit + data bits + parity bit + stop bits.
bit 3 = 1 framing error. the stop bit that follows the last
parity or data bit is a zero bit.
bit 2 = 1 parity error. Character has wrong parity
bit 1 = 1 overrun error. a character was sent to the receiver
buffer before the previous character in the buffer
could be read. This destroys the previous
character.
bit 0 = 1 data ready. a complete incoming character has been
received and sent to the receiver buffer register.
* */
static void
serial_putc(int c)
{
int i;
// COM1 = 0x3F8, COM_LSR(Line Status Register) = 5, COM_LSR_TXRDY(传输缓冲区) = 0x20,
// 0x03F8 + 5 => 0x03FD 0x03FD & 0x20(0010 0000) => 取 bit 5
// bti 5 = 1 :
// transmitter holding register empty. Controller is ready to accept a new character to send.
// 如果bit 5 = 1,那么传输方寄存器已空,controller可以接受一个新的字符了
for (i = 0;
!(inb(COM1 + COM_LSR) & COM_LSR_TXRDY) && i < 12800; // bit 5 != 1 && 没超时
i++)
delay();
//COM_TX = 0 Out: Transmit buffer
//serial_putc()函数的功能首先就是在bit 5 =1 的时候,跳出循环,否则只要 i <12800就会一直循环等待。
outb(COM1 + COM_TX, c); // 将c写入I/O端口
}
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
/***** Parallel port output code *****/
// For information on PC parallel port programming, see the class References
// page.
// 并行端口输入
/* 0x378~0x37A parallel printer port

0378 w data port
0379 r/w status port
bit 7 = 0 busy * 0x80
bit 6 = 0 acknowledge
bit 5 = 1 out of paper
bit 4 = 1 printer is selected
bit 3 = 0 error
bit 2 = 0 IRQ has occurred
bit 1-0 reserved

037A r/w control port
bit 7-5 reserved
bit 4 = 1 enable IRQ
bit 3 = 1 select printer * 0x08
bit 2 = 0 initialize printer *0x04
bit 1 = 1 automatic line feed
bit 0 = 1 strobe * 0x01
*/
static void
lpt_putc(int c) // 并行端口输入
{
int i;
// 0x379 & 0x80 读取0x379的内容,和0x80相与,取bits 7,判断是否繁忙
for (i = 0; !(inb(0x378+1) & 0x80) && i < 12800; i++) // io端口不繁忙且未超时,一直等待,直到使用了端口或等待时间到
delay();
outb(0x378+0, c); // write char c
outb(0x378+2, 0x08|0x04|0x01); //初始化 printer
outb(0x378+2, 0x08); // 选择 printer
}
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
static void
cga_putc(int c)
{
// if no attribute given, then use black on white
// 如果没有给定属性,则使用黑白相间的颜色
if (!(c & ~0xFF)) // c 低16位为字符值,高16位为显示属性
c |= 0x0700;
// crt_pos:当前输出位置指针,指向内存区中对应输出映射地址。
switch (c & 0xff) { // 取低16位
case '\b': // backspace
if (crt_pos > 0) {
crt_pos--;
crt_buf[crt_pos] = (c & ~0xff) | ' '; // 删除处使用' '填充
}
break;
case '\n': // //new line:换行, 自动添加回车
crt_pos += CRT_COLS; // CRT_COLS默认输出格式下整个屏幕的列数,为80。
// CRT_ROWS:默认输出格式下整个屏幕的行数,为25。
/* fallthru */
case '\r':
crt_pos -= (crt_pos % CRT_COLS);
break;
case '\t': // tab 转换为五个 ' '
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
cons_putc(' ');
break;
default:
crt_buf[crt_pos++] = c; /* write the character */
break;
}

// What is the purpose of this?
// CRT_SIZE:是CRT_COLS和CRT_ROWS的乘积,即2000=80*25,是不翻页时一页屏幕最大能容纳的字数。
// 当前屏幕写满了,
if (crt_pos >= CRT_SIZE) {
int i;
/*
* 函数:memmove(): memmove(void *dst, const void *src, size_t n).
* 意为将从src指向位置起的n字节数据送到dst指向位置,可以在两个区域重叠时复制。
* */
// 所有数据向前挪动一行,最上面一行数据丢失
memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
// 清空最后一行,用空格填充
for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
crt_pos -= CRT_COLS;
}
// 写光标位置
/* move that little blinky thing */
outb(addr_6845, 14);
outb(addr_6845 + 1, crt_pos >> 8);
outb(addr_6845, 15);
outb(addr_6845 + 1, crt_pos);
}

problem 2

  1. 从console.c

    解释以下内容:

    1
    2
    3
    4
    5
    6
    7
    1      if (crt_pos >= CRT_SIZE) {
    2 int i;
    3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    5 crt_buf[i] = 0x0700 | ' ';
    6 crt_pos -= CRT_COLS;
    7 }

见上

problem 3

  1. For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC’s calling convention on the x86.

    Trace the execution of the following code step-by-step:

    1
    2
    int x = 1, y = 3, z = 4;
    cprintf("x %d, y %x, z %d\n", x, y, z);
    • In the call to cprintf(), to what does fmt point? To what does ap point?
    • List (in order of execution) each call to cons_putc, va_arg, and vcprintf. For cons_putc, list its argument as well. For va_arg, list what ap points to before and after the call. For vcprintf list the values of its two arguments.

对于以下问题,您可能希望查阅第 2 讲的注释。这些注释涵盖了 GCC 在 x86 上的调用约定。

逐步跟踪以下代码的执行:

1
2
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
1
  • 在调用 tocprintf()时,fmt指向什么?ap指向什么?
  • 列出(按执行顺序)对cons_putcva_argvcprintf的每个调用。对于cons_putc,也列出它的argument。对于 va_arg,列出ap调用前后所指向的内容。列出它的两个参数的vcprintf值。

将代码复制到该位置

image-20220522214913927 image-20220522215011773

在gdb给该行打断点

image-20220522215850429

disassemble 反汇编当前代码段的部分指令

image-20220522215943610

在kernel.asm中查找该地址

(vim命令模式下,/+查找字符是向下查找,?是向上查找。 如果你要继续查找此关键字,敲字符n就可以继续查找了)

image-20220522220412446

call 0xf0100906 <cprintf>

image-20220522221005338

va_start,函数名称,读取可变参数的过程其实就是在栈区中,使用指针,遍历栈区中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程·

可以看到 fmt 是 cprintf 函数的第一个参数,即指向字符串"x %d, y %x, z %d\n"的指针。

ap 指向第二个参数的地址。注意 ap 中存放的是第二个参数的地址,而非第二个参数。

image-20220522221407297 image-20220522221535010
2

调用关系为 cprintf -> vcprintf -> vprintfmt -> putch -> cputchar -> cons_putc

接上图,可以看到vpprintf后是vprintfmt

image-20220522222019680

image-20220522222138140

之后,vprintfmt依次调用putch和cputchar

image-20220522222344930

cputchar调用 cons_putc

image-20220522222504049

image-20220522222602885 image-20220522222620191 image-20220522222706554

0xf010022b之后, 准备输出

image-20220522223041430 image-20220522223632755 image-20220522223329251 image-20220522223357850 image-20220522223512459

120,32, 49, 44, 32, 121, 32, 51, 44, 32, 122, 32, 52, 10

ascii 码转换为char

image-20220522223726095

image-20220522223755233

problem 4

Run the following code.

1
2
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);

What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise.

The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

image-20220523130430575

image-20220523131152245 image-20220523131208149 image-20220523131709311

71, 101, 49, 49, 48, 32, 87, 111, 114, 108, 100

He110 World

image-20220523131908104

%x 指无符号十六进制数

image-20220523133643397

57616转换为16进制,正好是e110

%s指字符串,0x00646c72在小端模式下对应的ASCII码为 0x72, 0x6c, 0x64, 0x00, 可得’rld’

如果是在大端 (big endian) 模式下要得到同样的输出,应该改为:

1
2
unsigned int i = 0x726c6400;
cprintf("H%x Wo%s", 57616, &i);

problem 5

In the following code, what is going to be printed after ‘ y= ‘ ? (note: the answer is not a specific value.) Why does this happen?

1
cprintf("x=%d y=%d", 3);
image-20220523141401716

image-20220523144320399

y的值没有给定,所以输出一个不确定的值

problem 6

Let’s say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

The Stack

Exercise 9

确定内核初始化堆栈的位置,以及堆栈在内存中的确切位置。内核如何为其堆栈保留空间?堆栈指针初始化为指向该保留区域的哪一端?

image-20220523172823907

bootstacktop是栈顶,地址为0xf0117000

mmu.h中

image-20220523174049954

memlayout.h

image-20220523174104956 image-20220523174140644

栈大小是32KB,栈顶指针存在esp,为 0xf0117000

image-20220523174417402

Exercise 10

要熟悉x86上的C调用约定,请在obj/kern/kernel.asm中找到test_backtrace函数的地址,在那里设置一个断点,并检查内核启动后每次调用它时会发生什么。test_backtrace的每个递归嵌套级别在堆栈上推送多少32位字,这些字是什么?

image-20220528164332775

给back_trace的test_backtrace(x-1); mon_backtrace(0, 0, 0);和 打上断点, 查看esp的初始值。

image-20220528165206593

初始x传入值为5,向后执行直至x=0

image-20220528171319674

中途查看esp内容,发现第二列保存的是x的值。每次递归会保存两行。每一列的意思是

image-20220528210745786

image-20220528211308210

x86堆栈要倒着长,如果以为push以后esp会增加可就大错特错了。。ebp虽然叫栈底,但是永远大于等于栈顶

0xf0100069应该是test_backtrace的下一条的返回地址。

image-20220528180523233

test_backtrace(5)的栈帧范围是:esp: 0xf0116fc0 ebp: 0xf0116fc8

test_backtrace(4): esp: 0xf0116fa0 ebp: 0xf0116fa8

test_backtrace(3): esp: 0xf0116f80 ebp: 0xf0116f88

test_backtrace(2): esp: 0xf0116f60 ebp: 0xf0116f68

test_backtrace(1): esp: 0xf0116f40 ebp: 0xf0116f48

next x | this x | don’t know | don’t know |

don’t know | last x | last ebp | return addr|

Exercise 11

implement a stack backtrace function

The backtrace function should display a listing of function call frames in the following format:

1
2
3
4
Stack backtrace:
ebp f0109e58 eip f0100a62 args 00000001 f0109e80 f0109e98 f0100ed2 00000031
ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
...

Each line contains an ebp, eip, and args.

运行 make grade 这个评级脚本去查看它的输出是否是我们的脚本所期望的结果/。

1
C函数调用时,首先将参数push入栈,然后push返回地址,接着将原来的EBP push入栈,然后将ESP的值赋给EBP,令ESP指向新的栈顶。而函数返回时,会将EBP的值赋予ESP,然后pop出原来的EBP的值赋予EBP指针。

image-20220528223530430

image-20220528223513219

Exercise 12

修改堆栈回溯函数以显示每个eip对应的函数名、源文件名和行

debuginfo_eip中,__STAB_*来自哪里?这个问题有一个很长的答案;为了帮助您找到答案,以下是您可能想做的一些事情:

  • look in the file kern/kernel.ld for __STAB_*
  • run objdump -h obj/kern/kernel
  • run objdump -G obj/kern/kernel
  • run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
  • see if the bootloader loads the symbol table in memory as part of loading the kernel binary

向内核监视器添加一个backtrace 命令,并扩展您的实现mon_backtrace以调用debuginfo_eip并打印表单的每个堆栈帧的一行:

1
2
3
4
5
6
7
8
9
K> backtrace
Stack backtrace:
ebp f010ff78 eip f01008ae args 00000001 f010ff8c 00000000 f0110580 00000000
kern/monitor.c:143: monitor+106
ebp f010ffd8 eip f0100193 args 00000000 00001aac 00000660 00000000 00000000
kern/init.c:49: i386_init+59
ebp f010fff8 eip f010003d args 00000000 00000000 0000ffff 10cf9a00 0000ffff
kern/entry.S:70: <unknown>+0
K>

Each line gives the file name and line within that file of the stack frame’s eip, followed by the name of the function and the offset of the eip from the first instruction of the function (e.g., monitor+106 means the return eip is 106 bytes past the beginning of monitor).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}

.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}

image-20220528230025894

可以得到stab和 stabstr的起始地址和结束地址

1
2
3
4
__STAB_BEGIN__ = 0xf0101f2c
__STAB_END__ = 0xf0101f2c + 00004549 - 1
__STABSTR_BEGIN__ = 0xf0106475
__STABSTR_END__ = 0xf0106475 + 00008b11 - 1

objdump -G obj/kern/kernel 列出所有stab的信息

image-20220528230955470

进入内核后打个断点,查看stabstr内的内容

image-20220528232511341

使用objdump -G obj/kern/kernel > output.md将内核的符号表信息输出到output.md文件

image-20220603142339089

观察kernel.asm

image-20220603142401904

观察entry.S

image-20220603142437259

可以看到,output.md 中的各个字段,

1
2
3
4
5
6
7
Symnum   下标,整个符号表看作一个数组,Symnum是当前符号在数组中的下标
n_type 符号类型,FUN指函数名,SLINE指在text段中的行号
n_othr 不清楚
n_desc 在文件中的行号
n_value 表示地址
n_strx
String 保存信息(函数、语句啥的)

在查看kdebug.c时候发现函数的参数为int类型,但是传入是N_FUN,于是翻了一下stab.h

image-20220603145602882

修改monitor.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t ebp;
uint32_t* eip;
struct Eipdebuginfo info;
ebp = read_ebp();
cprintf("Stack backtrace:\n");
// ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
// eip = *(uint32_t *(ebp + 4))
while(ebp != 0){
eip = (uint32_t *)(ebp+4);

// 08x 八位宽无符号16进制
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",
ebp, eip[0], eip[1], eip[2], eip[3], eip[4], eip[5]);
// 打印行号

if(debuginfo_eip(eip[0], &info)==0){
cprintf(" %s:%d: %s+%d\n", info.eip_file, info.eip_line, info.eip_fn_name, eip[0] - info.eip_fn_addr);
}

// 取 ebp的 内容 返回
ebp = *((uint32_t *) ebp);

image-20220603161956295

可以看到后续多了:F(0,15)

那么需要输出指定长度,以便把后面多余的字符删除。test_backtrace 和 i386_init 为函数名

image-20220603162303721

使用eip_fn_namelen进行指定长度的输出

修改为

1
cprintf("    %s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, eip[0] - info.eip_fn_addr);

image-20220603162648588

正常输出

make grade

image-20220603162723295

summary

  1. 启动顺序 bios->bootloader->kernel

bios:

0x000FFFFF~0x0010 0000

bootloader:

sectorsize(扇区大小)是512

image-20220603165727917

0x7c00~0x7dff

BIOS找到一个可引导的软盘或硬盘,它将512字节的引导扇区加载到物理地址0x7c00到0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给the boot loader

kern 入口LMA为 00100000 被映射到虚拟地址(LMA)0xf0100000
kernel最先加载的就是 .text
所作操作:开启内存分页机制,启用虚拟内存,I/O的实现,栈的初始化。

环境配置

环境配置是个大坑

直接按照课程教程

https://pdos.csail.mit.edu/6.828/2017/tools.html

安装ubuntu(32位),我之前安装64,后来各种报错。ubuntu安装时,插上蓝牙键盘或鼠标设备有时也报错,显示cpu不兼容。

参考:https://www.cnblogs.com/kinvy/p/15074844.html <———-亲测有效

复制过来仅作记录

1.测试编译工具

shell

1
$ objdump -i

image-20210727123132004

1
$ gcc -m32 -print-libgcc-file-name 		#测试gcc

上面这条命令是测试gcc的,一般系统是没有gcc的,需要安装

安装gcc , gdb, git, vim

1
$sudo apt-get install -y build-essential gdb git vim

安装32位的支持库

1
$sudo apt-get install gcc-multilib

2. 编译安装工具链

2.1下载以下工具包

第一个包可能下载不了,可以自己搜索,或是使用下面的链接

https://mirrors.sjtug.sjtu.edu.cn/gnu/gmp/gmp-5.0.2.tar.bz2

2.2 编译安装

为了方便,将以上6个压缩包放在一个文件夹下 ,~/download/mit6.828

文件夹结构

image-20210727131718755

以下的操作都是在 ~/download/mit6.828 目录下

  1. 安装gmp-5.0.2

    1
    2
    3
    4
    5
    6
    $tar xjf gmp-5.0.2.tar.bz2
    $cd gmp-5.0.2
    $./configure --prefix=/usr/local # 可能的错误:No usable m4 in $PATH or /usr/5bin (see config.log for reasons).
    $make
    $sudo make install
    $cd ..

    逐条执行命令,每执行一条后,输出无 error 就可往下执行,后面几个安装包也是一样的

    可能的错误是第3个命令,如果报错,执行以下命令,然后再次执行第3行命令

    1
    $sudo apt install m4
  2. 安装mpfr-3.1.2

    1
    2
    3
    4
    5
    6
    $tar xjf mpfr-3.1.2.tar.bz2
    $cd mpfr-3.1.2
    $./configure --prefix=/usr/local
    $make
    $sudo make install
    $cd ..
  3. 安装mpc-0.9

    1
    2
    3
    4
    5
    6
    $tar xzf mpc-0.9.tar.gz
    $cd mpc-0.9
    $./configure --prefix=/usr/local
    $make
    $sudo make install
    $cd ..
  4. 安装binutils-2.21.1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $tar xjf binutils-2.21.1.tar.bz2
    $cd binutils-2.21.1
    $./configure --prefix=/usr/local --target=i386-jos-elf --disable-werror
    $make
    $sudo make install # This step may require privilege (sudo make install)
    $cd ..

    #测试
    $i386-jos-elf-objdump -i
    # 成功安装会输出类似下面的信息
    # BFD header file version (GNU Binutils) 2.21.1
    # elf32-i386
    # (header little endian, data little endian)
    # i386...
  5. 安装gcc-core-4.6.4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $tar xjf gcc-core-4.6.4.tar.bz2
    $cd gcc-4.6.4
    $mkdir build
    $cd build
    $../configure --prefix=/usr/local \
    --target=i386-jos-elf --disable-werror \
    --disable-libssp --disable-libmudflap --with-newlib \
    --without-headers --enable-languages=c MAKEINFO=missing
    $make all-gcc
    $sudo make install-gcc
    $make all-target-libgcc #可能会报错 [configure-target-libgcc] Error 1
    $sudo make install-target-libgcc
    $cd ../..

    #测试
    $i386-jos-elf-gcc -v
    # 成功安装会输出类似下面的信息
    # Using built-in specs.
    # COLLECT_GCC=i386-jos-elf-gcc
    # COLLECT_LTO_WRAPPER=/usr/local/libexec/gcc/i386-jos-elf/4.6.4/lto-wrapper
    # Target: i386-jos-elf

    执行11行命令可能会报错,如果报错,执行以下命令,然后再次执行第11行命令

    1
    $export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib 
  6. 安装gdb-7.3.1

    1
    2
    3
    4
    5
    6
    7
    $tar xjf gdb-7.3.1.tar.bz2
    $cd gdb-7.3.1
    $./configure --prefix=/usr/local --target=i386-jos-elf --program-prefix=i386-jos-elf- \
    --disable-werror
    $make all #可能的错误 no termcap library found
    $sudo make install
    $cd ..

    可能报错的命令第5个,如果出现错误,执行以下命令,然后再执行该命令

    1
    2
    3
    4
    5
    6
    $wget http://ftp.gnu.org/gnu/termcap/termcap-1.3.1.tar.gz
    $tar -zxv -f termcap-1.3.1.tar.gz
    $cd termcap-1.3.1
    $ ./configure
    $make
    $sudo make install

安装 QEMU

1. 安装工具包

1
2
$sudo apt install libsdl1.2-dev libtool-bin libglib2.0-dev  libz-dev  libpixman-1-dev
$sudo apt install python2

2. 下载qemu

qemu需要用6.828定制的

1
$git clone https://github.com/mit-pdos/6.828-qemu.git qemu

3. 编译安装

1
2
3
$./configure --disable-kvm --disable-werror --prefix=/usr/local  --target-list="i386-softmmu x86_64-softmmu" --python=python2
$make
$sudo make install

可能的错误:

  1. 缺少一个头文件,错误如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    qga/commands-posix.c: In function ‘dev_major_minor’:
    qga/commands-posix.c:633:13: error: In the GNU C Library, "major" is defined
    by <sys/sysmacros.h>. For historical compatibility, it is
    currently defined by <sys/types.h> as well, but we plan to
    remove this soon. To use "major", include <sys/sysmacros.h>
    directly. If you did not intend to use a system-defined macro
    "major", you should undefine it after including <sys/types.h>. [-Werror]
    *devmajor = major(st.st_rdev);
    ^~~~~~~~~~~~~~~~~~~~~~~~~~

    解决:在 qga/commands-posix.c文件中的 #include <sys/types.h> 下面增加#include <sys/sysmacros.h>即可

4.测试

1
2
3
4
5
#下载实验源码
$git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
$cd lab
$make
$make qemu-nox

测试成功

image-20210727171159202

vscode连接ubuntu

连接时一直显示失败,但是termius可以连接上

应该是vscode不支持此版本的ubuntu

换到clion后,可以使用