经过半个月左右的流程,我在 2024/05/09 拿到了人生的第一份实习,是腾讯音乐娱乐集团(TME)的客户端开发岗位(iOS / android),同时,在这个过程中,也对找实习的流程有了一些初步的概念,所以写点东西记录下。

1. 前期 && 简历的失误

2 月左右的时候,我并没有准备认真投实习,我随便做了下简历就投了出去,现在回想,简历里犯了一些很基础但严重的错误:

1. 简历内容太长,整体非常臃肿,我甚至想当然地在里面加入了一些诸如“求职目标”这样的主观评价

2. 内容的技术要点不清晰,一眼看下去无论是项目还是研究经历都莫名其妙

3. 排版凌乱,由于短句的大量出现,整体格局呈现左满右空的画面

也是由于这些原因,初期投递的岗位基本没有回应。

2. 焦虑期 && 做决定

等到 4 月的时候,我的一位好朋友拿到了美团的测试开发实习,同时,我了解到另一位在实验室实习的同学已经开始参与到文章的后期工作中,可能产出系统方向的 n 作了。我这时开始有些焦虑,因为我虽然准备出国,但后面的计划并不清晰…

所以经过了几天的思考之后,我决定认真找实习,随后我仔细地修改了简历,并开始有计划地投递目标岗位,主要是客户端、C/C++、嵌入式软件。

3. 准备期 && 方法

一般来说,笔试并不是很困难的事情,至少不是筛选的主要手段,面试的准备相对重要。

面试主要分为:

1. 手撕代码(leetcode easy 和 medium)

2. 概念性问题(常说的“八股”)

3. 项目经历(我除了课设,基本什么都没有)

一般企业的手撕代码并不难准备,我就把 leetcode 100(https://leetcode.cn/studyplan/top-100-liked/) 做了一半,难的题随缘,简单的基本问题不大;

对于“八股文”,因为我的操作系统、网络、C 这些基础课学的还不错,所以大部分内容不需要硬背,回顾和理解足以应付常见的问题(线程进程?http / https?);

项目经历这部分,我没什么可说的,大学过的比较失败的我只能在面试官面前支支吾吾地讲我的课设…

4. 美团 && 秒挂

美团的面试让我了解到:面试的确是随机性非常大的事情。朋友成功入职美团,并跟我说美团面试非常友好,各方面都比较基础,因此我也投递了美团测试开发,通过了笔试,但在一面就经历了拷打:

首先是写算法题:

一上来就给了一道 easy 的 lc 题(https://leetcode.cn/problems/longest-common-prefix/description/)。

我直接开始写的暴力,时间复杂度为O(m * n),但写的时候一直怀疑一定有更好的方法,这样写肯定显得很愚蠢。

写完后,我说让我想想怎么优化,面试官直接叫停了,说“这应该是道经典题,你自己下去看看吧”。结束后我想了想,原来这个题根本没有更快的方法...

接着是八股文:

被拷打了大量闻所未闻的东西,现在只记得有个问题是“如果有一个搜索框功能要开放给用户使用,你会如何测试它”;

我实在答不上来,一边瞎编、一边暗想应该已经寄了。

最后是项目:

哈哈,这我没什么可说的... 随便讲了讲最近在做的大创,他不懂、也没兴趣(其实我也还没完全搞懂...)。

5. TME && 三次面试

5.1 技术面(4.25 日 晚上)

忘记了有约面试,到点了还在 Ubuntu 里调损坏的摄像头…

可能是由于我的遗忘,上来就直接写了三道算法题(无 OJ,共享屏幕,10 mins 一道,然后说思路),我第一道就想错了思路,好在后面两道比较简单。

八股文的问题比较经典:

1. 进程 / 线程?

2. http / https?

3. 讲下 TLS 协议的四次握手(这个我刚好前一天看了眼,不然真不知道)

4...

最后还出了道脑筋急转弯,说 1 - 1000 里 7 出现了多少次?

我很没脑子的分类讨论硬算,在错了 3 次之后,说出正确的答案:300;

同学说:

1 - 1000 里的 7 出现的次数等于 000 - 999 里的 7 出现的次数

所以 1 2 3 4 5 6 7 8 9 0 都等价

7 出现的次数相当于所有数字出现次数的十分之一

也就是 3 * 1000 / 10 = 300

原来如此,我好笨。

5.2 技术面(4.29 日 下午)

应该是主管之类的人拷打项目,我被拷打的无地自容,只好连连叫道“这只是个课设,是个 toy project,我真没想那么多”。

5.2 HR面(5.8 日 下午)

走流程,公式化问答。

5.3 offer(5.9 日 下午)

谢谢腾讯音乐,有地方实习了,不枉我绿钻会员身份。

6 总结

保持好心态,一切都没问题,但要注意做事要有条理,最终总会有地方去的。

1. FEMU NVMe 协议

1.1 数据结构

提交队列 SQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct NvmeCmd {
uint8_t opcode; // 操作码
uint8_t fuse; // 组合操作FUSE
uint16_t cid; // [15:14]传输数据的方式(PSDT,PRP还是SGL) [13:10]保留 [9:8]组合操作FUSE
uint32_t nsid; // 命令空间标识NSID
uint64_t res1; // 保留
uint64_t mptr; // 元数据指针mptr,只有在命令有元数据,并且元数据没有与数据的逻辑块交织时有效,并且元数据的地址标识方式由cdw0.psdt决定
uint64_t prp1;
uint64_t prp2;
// 数据指针dptr
uint32_t cdw10;
uint32_t cdw11;
uint32_t cdw12;
uint32_t cdw13;
uint32_t cdw14;
uint32_t cdw15;
// 上面的cdw在具体命令中被指定含义
} NvmeCmd;
  1. FUSE:推测可能跟文件系统有关

  2. PRP,SGL:帮助Host告知Controller数据在Host内存中的具体地址

    PRP:由PRP1和PRP2表示传输的物理页地址,当传输的物理页无法有两个PRP表示,则PRP可以表示PRP Lists,并且每个指向的物理页的最后一个prp项指向下一个PRP Lists页。

    SGL:由于PRP只能表示一个个物理页,SGL能表示起始地址+长度,

完成队列 CQ

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct NvmeCqe {
union {
struct {
uint32_t result;
uint32_t rsvd;
} n;
uint64_t res64;
};
uint16_t sq_head; // 完成队列头指针
uint16_t sq_id; // 完成队列表示
uint16_t cid;
uint16_t status;
} NvmeCqe;

1.2 功能函数

创建I/O CQ

1
2
3
4
5
6
7
8
9
10
11
uint16_t nvme_create_cq(FemuCtrl *n, NvmeCmd *cmd)
{
...
}

uint16_t nvme_init_cq(NvmeCQueue *cq, FemuCtrl *n, uint64_t dma_addr,
uint16_t cqid, uint16_t vector, uint16_t size, uint16_t irq_enabled,
int contig)
{
...
}

创建I/O SQ

1
2
3
4
5
6
7
8
9
10
11
uint16_t nvme_create_sq(FemuCtrl *n, NvmeCmd *cmd)
{
...
}

uint16_t nvme_init_sq(NvmeSQueue *sq, FemuCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size, enum NvmeQueueFlags prio,
int contig)
{
...
}

read / write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static uint16_t nvme_rw(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
NvmeRequest *req)
{
// 解析出nvme提交队列项到NvmeReq数据结构里
// 检查cmd,slba,nlb等参数合理性
NvmeRwCmd *rw = (NvmeRwCmd *)cmd;
...
err = femu_nvme_rw_check_req(n, ns, cmd, req, slba, elba, nlb, ctrl, data_size, meta_size);

// 根据req中的参数,通过prp1和prp2获取DMA传输的数据块地址
if (nvme_map_prp(&req->qsg, &req->iov, prp1, prp2, data_size, n)) {
nvme_set_error_page(n, req->sq->sqid, cmd->cid, NVME_INVALID_FIELD,
offsetof(NvmeRwCmd, prp1), 0, ns->id);
return NVME_INVALID_FIELD | NVME_DNR;
}

// 在后端内存存/取数据
ret = femu_rw_mem_backend_bb(&n->mbe, &req->qsg, data_offset, req->is_write);
}

命令提交

1
2
3
4
void nvme_process_sq_io(void *opaque, int index_poller){
// 更新sq_head_db,生成新的nvme_io_cmd
...
}

命令完成

1
2
3
4
static void nvme_post_cqe(NvmeCQueue *cq, NvmeRequest *req){\
// 更新完成队列尾部命令寄存器(cq_tail_db)和执行中断服务例程(isr)
...
}

1.3 NVMe处理流程

  1. 主机将一个至多个执行命令放置在内存中下一个空闲提交队列槽位;
  2. 主机用提交队列尾指针作为新值更新提交队列尾门铃寄存器,告知控制器有一个新的命令被提交,需要处理;
  3. 控制器将提交队列槽位的命令传输至控制器,这里会用到仲裁机制,决定传输哪一个提交队列的命令;
  4. 控制器(可能乱序)执行命令;
  5. 命令执行完成后,控制器将完成队列项放置在与之关联的完成队列的空闲槽位,控制器会移动完成队列项中的提交队列头指针,告知主机最近一次被消耗的提交队列命令, Phase Tag 会被反转,表明完成队列项是新的(这里的理解是,由于完成队列是环形队列,当环形队列由尾部工作至头部时,表明完成一个周期,而 Phase Tag 则表示完成队列项是否完成一个周期);
  6. 控制器可能会产生中断告知主机有一个新的完成队列项要处理;
  7. 主机处理完成队列中的完成队列项(过程 A),这里可能会做相应的错误处理,过程 A 持续直至遇到反转的 Phase Tag 终止(主机处理完成队列项会将同一周期内的处理完);
  8. 主机写完成队列头部门铃寄存器,表明完成队列项已经被处理。

2. FEMU 的数据与逻辑地址分离处理

仿真时,真实数据写入内存,逻辑地址单独处理

nvme_process_sq_io:
1. 生成一个io命令 nvme_io_cmd
2. nvme_rw -> femu_rw_mem_backend_bb写入内存
3. femu_ring_enqueue(n->to_ftl[index_poller], (void *)&req, 1); 取出slba和len传给to_ftl仿真

3. FTL poller的时延仿真

1. 地址映射

1.1 页级映射

1.2 块级映射

注意需要保证页在块内的偏移位置不变

1.3 混合映射

这种情况下,对一小部分数据进行页级映射,更新的算法为:

读取:首先在日志块中查找目标页,否则读取数据块

写入:直接写入到数据块中

更新:将更新写入到日志块,将数据块中的部分置为无效

实质上,混合映射就是用顺序写吸收了更新请求。数据块需要块映射,每次更新请求都要保证块内索引一致,这样维护开销太大,所以分出来一批使用页映射,最后再把许多日志块内页映射的部分合并一起放回数据块。

合并也分为三种情况:

切换合并

条件:顺序更新 且 整块

开销:与直接块级映射相同,只需做一次擦除

部分合并

条件:顺序更新 且 非整块

开销:一次擦除 + 一次额外复制

完全合并

条件:随机写入

开销:具体计算

2. 垃圾回收(GC, Garbage Collection)

基本过程

选 -> 移 -> 擦

目标块选择

  • 贪心:无效页最多的块优先

  • 磨损均衡:考虑擦除次数

  • 数据冷热:考虑最近更新时间

3. 磨损均衡

磨损均衡的分类

  • 动态均衡:分配空闲块时写入擦除次数最少的块

  • 静态均衡:当块的最大和最小擦除次数差别超过某个阈值时,将擦除次数最小的块中的有效数据迁移到擦除次数最大的空闲块中,使得分配空闲块时写入擦除次数相对较少的块

磨损均衡的优化

  • 冷热数据分离

  • 损耗程度评估

4. NVMe 协议

NVMe 协议很复杂,涉及到很多数据结构,我们在这里只关心其中三个:

  • 提交队列(SQ, Submission Queue)

  • 完成队列(CQ, Completion Queue)

  • 门铃寄存器(DB, Doorbell Register)

SQ 和 CQ 位于主机端内存,DB 位于 SSD控制器 中,下面的图展示了一次基本的工作流程:

即:

1. 主机写SQ,表示请求I/O

2. 主机触发DB,告知SSD自己做了第 1 步

3. SSD取SQ,执行,写CQ

4. SSD触发MSI-X中断,告知主机自己做了第 3 步

5. 主机取CQ,检查,触发DB告知SSD完成

更一般地,可以简化为这样的模型:

引入

学习操作系统的时候,我们知道 MMU 硬件和页表共同完成了进程虚拟空间到物理内存空间的转换。

但是仔细想想,如果一个程序占用过大,数据不能一次性全部放在内存中,操作系统如何进行管理呢?

查资料

我们从 PTE 入手,下面是一条 PTE 对页的描述记录:

第 0 位为有效位,在有效位为 0 时,根据 1 - 31 位的数据,我们分为两种情况讨论:

1 页不在进程的地址空间

这种情况是正常的文件加载入内存的过程,需要请求从文件系统调页,操作系统直接通过 VFS -> 实际FS 来得到目标页在磁盘中的位置。

这种情况下,1 - 31 位为0。

2 页被换出到磁盘

这种换出再换入的情况被称为 Swap ,磁盘为 Swap 的情况专门组织了若干交换区,每个交换区中有若干页槽(page slot, 用于存放换出的页),交换区与文件系统区独立,被不同地格式化。

所以,在这种 Swap 的情况下,PTE的关于该页的条目被以如下方式解读:

1-7 被解读成一个交换区的区号,而 8-31 位被解读成页槽索引,而页槽[0]是此交换区的管理块,所以有效的索引从 1 开始, 1 - 31 位非0。

总结

所以,内存与磁盘的 Swap 过程可以粗略理解为:

  1. 磁盘在系统的物理内存不够用的时候,把一部分空间释放出来,以供当前运行的程序使用,这部分被称为交换(Swap)区。

  2. 目标页数据不在内存中,且是由于换出策略保存在交换区时,可以通过 PTE 表项找到交换区中保存该数据的位置。

  3. 被换出的页可能来自一些很长时间没有什么操作的程序,这些被释放的页被临时保存到 Swap 分区中,等到程序要运行时,再从 Swap 分区中恢复保存的数据到内存中。

  4. 当然不是所有从物理内存中交换出来的数据都会被放到 Swap 中,有相当一部分数据被直接交换到文件系统。例如,有的程序会打开一些文件,对文件进行读写,当需要将这些程序的内存空间交换出去时,就没有必要将文件部分的数据放到 Swap 空间中了,而可以直接对实际FS进行修改。

1. 做了什么

上学期完成了一些基于 RISC-V 指令集的 PKE(Proxy Kernel for Education) 的基础实验,这学期的课程设计要求完成后面的挑战部分。实验的代码和文档如下:

1. 代码:https://gitee.com/hustos/riscv-pke

2. 文档:https://gitee.com/hustos/pke-doc

3. 我的完成源码:https://github.com/peacewang017/OS_labs

这学期在完成的代码基础上实现了一些额外的功能,最终为其添加了一个简易的 shell ,由于原版的 PKE 实在实现的非常简陋且奇怪,自己对于其中一部分逻辑进行了改写,在这里记录下自己的实现逻辑。

2. 笔记

进程调度

对于PKE进程的调度,我们这样进行操作:

  1. 用户态中断进入内核态时,将 current 置为 ready ,并插入 ready 队列尾
  2. schedule 时,首先扫描所有 blocked 队列里的进程,如果等待子进程已是 zombie 状态,则将其设为 free,并重置 waiting_pid 字段,将等待中的进程从 blocked 队列删除并加入 ready 队列;接着从 ready 队列首取出一个进程设为 current ,并将其从 ready 队列删除,调用 switch_to 从内核态返回用户态
  3. wait 时,current 进入内核态,先执行[1],接着执行 wait 后被移除并放入 blocked 队列,并设置 waiting_pid 字段,最后执行[3]
  4. fork 时,current 进入内核态,先执行[1],接着申请一个进程,放入 ready 队列末尾
  5. free 时,current 进入内核态,先执行[1],接着被从 ready 队列移除
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
// process.c
int do_wait(int child_pid){
// handle with waiting child proc
current->waiting_pid = child_pid;
from_ready_to_blocked(current);
schedule();
return child_pid;
}

int do_fork( process* parent){
alloc_proc(child);
init_proc(child);

child->status = READY;
child->parent = parent;
from_blocked_to_ready( child );
return child->pid;
}

int free_process( process* proc ) {
// we set the status to ZOMBIE, but cannot destruct its vm space immediately.
// since proc can be current process, and its user kernel stack is currently in use!
// but for proxy kernel, it (memory leaking) may NOT be a really serious issue,
// as it is different from regular OS, which needs to run 7x24.
proc->status = ZOMBIE;
remove_from_blocked_queue(proc);
remove_from_ready_queue(proc);

return 0;
}

// syscall.c
long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, long a6, long a7){
insert_to_ready_queue(current);
}

// sched.c
void schedule() {
// 检查父进程等待的子进程是否已经zombie,如果是,则将父进程重新投入运行(重置waiting_pid, status)
if (blocked_queue_head){
for(process* p = blocked_queue_head; p != NULL; p = p->queue_next){
if(procs[p->waiting_pid].status == ZOMBIE){
p->waiting_pid = -1;
from_blocked_to_ready(p);
}
}
}

if ( !ready_queue_head ){
int should_shutdown = 1;

for( int i=0; i<NPROC; i++ )
if( (procs[i].status != FREE) && (procs[i].status != ZOMBIE) ){
should_shutdown = 0;
sprint( "ready queue empty, but process %d is not in free/zombie state:%d\n",
i, procs[i].status );
}

if( should_shutdown ){
sprint( "no more ready processes, system shutdown now.\n" );
shutdown( 0 );
}else{
panic( "Not handled: we should let system wait for unfinished processes.\n" );
}
}

current = ready_queue_head;
assert( current->status == READY );
ready_queue_head = ready_queue_head->queue_next;

current->status = RUNNING;
sprint( "going to schedule process %d to run.\n", current->pid );
switch_to( current );
}

文件查找

PKE 实现了一个具体文件系统 RFS,并将其挂载在虚拟文件系统 VFS 的 /RAMDISK0 下面,所以,在不实现 spike 对宿主文件系统 hostfs 的操作的情况下,我们只对 /RAMDISK 下的文件有读写能力。

我们通过改写 lookup_final_dentry() 函数使得 VFS 拥有处理相对路径的功能。

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
struct dentry *lookup_final_dentry(const char *path, struct dentry **parent, char *miss_name) {
// sprint("lookup_final_dentry: %s\n", path);
int hartid = (int)read_tp();

char path_copy[MAX_PATH_LEN];
strcpy(path_copy, path);

// 处理直接文件名,转为相对路径
if(path_copy[0] != '.' && path_copy[0] != '/'){
str_insert_to_head(path_copy, "./");
}

// split the path, and retrieves a token at a time.
// note: strtok() uses a static (local) variable to store the input path
// string at the first time it is called. thus it can out a token each time.
// for example, when input path is: /RAMDISK0/test_dir/ramfile2
// strtok() outputs three tokens: 1)RAMDISK0, 2)test_dir and 3)ramfile2
// at its three continuous invocations.
int flag = 0;
char *token = strtok(path_copy, "/");
struct dentry* this = *parent;
struct dentry* cwd = NULL;
if(current[hartid] != NULL){
cwd = current[hartid]->pfiles->cwd;
}

while (token != NULL) {
if(strcmp(token, "..") == 0){
if(cwd == NULL){
panic("lookup_final_dentry: proc not init\n");
return NULL;
}
if(flag == 0){
if(cwd == vfs_root_dentry){
panic("already in root\n");
return NULL;
}
(*parent) = cwd->parent;
flag = 1;
}else{
if((*parent) == vfs_root_dentry){
panic("already in root\n");
return NULL;
}
(*parent) = (*parent)->parent;
}
this = (*parent);
}else if(strcmp(token, ".") == 0){
if(cwd == NULL){
panic("lookup_final_dentry: proc not init\n");
return NULL;
}
if(flag == 0){
(*parent) = cwd;
flag = 1;
}
this = (*parent);
}else{
// function return subdir's dentry
this = hash_get_dentry((*parent), token);

if (this == NULL) {
// function return subdir's dentry
this = alloc_vfs_dentry(token, NULL, *parent);

// get subdir's vinode
struct vinode *found_vinode = viop_lookup((*parent)->dentry_inode, this);

// dentry not in hash && vinode not in disk
// return miss
if (found_vinode == NULL) {
// not found in both hash table and directory file on disk.
free_page(this);
strcpy(miss_name, token);
return NULL;
}

// dentry not in hash && vinode in disk
// ensure dentry and vinode in hash
struct vinode *same_inode = hash_get_vinode(found_vinode->sb, found_vinode->inum);
if (same_inode != NULL) {
// the vinode is already in the hash table (i.e. we are opening another hard link)
this->dentry_inode = same_inode;
same_inode->ref++;
free_page(found_vinode);
} else {
// the vinode is not in the hash table
this->dentry_inode = found_vinode;
found_vinode->ref++;
hash_put_vinode(found_vinode);
}
hash_put_dentry(this);
}
flag = 1;
*parent = this;
}

// get next token
token = strtok(NULL, "/");
}
return this;
}

malloc 与堆

我的 malloc 实现非常的不优雅,但为了记录用,仍然提一下。

对于数据结构,我们维护两个二元表:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct page_dentry{
uint64 va_page;
uint64 pa_page;
}page_dentry;

page_dentry* page_dir;

typedef struct malloc_dentry{
uint64 va_start;
uint64 va_end;
}malloc_dentry;

malloc_dentry* malloc_dir;

page_dentry 表用来记录用户程序调用 alloc_page 申请并 map 过的页,同时记录页虚拟地址和物理地址,相当于一个小型页表;

malloc_dentry 表用来记录用户程序调用 malloc 时从已 alloc 的页中取走的空间,表项是 malloc 空间的起止虚拟地址。

按照以下规则来进行运算:

  1. do_malloc 时,首先检查有没有分配页面,如果没有分配页面,则申请需要的页数,添加 malloc_dir 后返回;如果至少有一个已经分配的页,在 malloc_dentry 中从头往尾寻找是否有可用空隙供插入;如果没有空隙,则申请页面

  2. 对于 page_dir 和 malloc_dir,需要保证增序,保证[1]中从头往尾查找可用空隙的正确性

  3. free 时,更新 malloc_dir, 在 malloc 放生后,我们先不返回页面,只有在进程 exit 后,统一 free 所有 page 并清空两个 dir

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
// syscall.c
uint64 do_malloc(int n){
// 在没有已分配页面时
if(current->num_page == 0){
// 需要num_page页,alloc后map,加入表中
int page_need = ROUNDUP(n, PGSIZE) / PGSIZE;
if(alloc_n_page(page_need) == 0){
panic("page alloc error\n");
}

// 添加malloc表
uint64 va_start = current->page_dir[0].va_page;
if(current->num_malloc + 1 > MAX_MALLOC_IN_HEAP){
panic("malloc error\n");
}
add_to_malloc_dir(va_start, va_start + n);
// print_malloc_dir();
return va_start;
}

uint64 va_page_start = current->page_dir[0].va_page;
uint64 va_page_end = current->page_dir[current->num_page - 1].va_page + PGSIZE;

// 首先检查头是否可用
int front_offset = current->malloc_dir[0].va_start - va_page_start;
if(front_offset >= n){
add_to_malloc_dir(va_page_start, va_page_start + n);
// print_malloc_dir();
return va_page_start;
}

// 检查空隙是否可用
if(current->num_malloc >= 2){
for(int i = 0; i < current->num_malloc - 1; i++){
int gap = current->malloc_dir[i + 1].va_start - current->malloc_dir[i].va_end;
if(gap >= n){
uint64 new_va_start = current->malloc_dir[i].va_end;
add_to_malloc_dir(new_va_start, new_va_start + n);
// print_malloc_dir();
return new_va_start;
}
}
}

// 使用末尾,先填入malloc表,(不够时)再去alloc page并map
uint64 new_va_start = current->malloc_dir[current->num_malloc - 1].va_end;
add_to_malloc_dir(new_va_start, new_va_start + n);
// print_malloc_dir();

int rear_offset = va_page_end - new_va_start;
if(rear_offset < n){
int page_need = ROUNDUP((n - rear_offset), PGSIZE) / PGSIZE;
if(alloc_n_page(page_need) == 0){
panic("page alloc error\n");
}
}
return new_va_start;
}

void do_free(uint64 va){
int index;
if((index = find_malloc_dir(va)) == -1){
panic("free error\n");
}
remove_from_malloc_dir(index);

// 注意到这里 free 的处理时简化过的,即 malloc 放生后,我们先不返回页面,只有在进程 exit 后,我们统一 free 所有被 malloc 占用的 page 并清空两个 dir。
}

int alloc_n_page(int n){
if(current->num_page + n > MAX_PAGE_IN_HEAP){
return 0;
}
for(int i = 0; i < n; i++){
uint64 pa = (uint64)alloc_page();
user_vm_map((pagetable_t)current->pagetable, g_ufree_page, PGSIZE, pa, prot_to_type(PROT_WRITE | PROT_READ, 1));
add_to_page_dir(g_ufree_page, pa);
g_ufree_page += PGSIZE;
}
return 1;
}

void add_to_page_dir(uint64 va_page, uint64 pa_page);
void sort_page_dir();
void add_to_malloc_dir(uint64 va_start, uint64 va_end);
void sort_malloc_dir();
int find_malloc_dir(uint64 va);
void remove_from_malloc_dir(int index);

这里的 malloc 纯粹是为满足功能和已经编写好的系统的逆天设计,关于 Linux 中的 malloc 是如何实现的,可以参考:

https://jacktang816.github.io/post/mallocandfree/

PKE 内存的管理

另一个逆天的事情是,PKE 内存存在泄露问题,变为 ZOMBIE 态的进程,并没有被重新利用,同时进程空间中除了因 malloc 而申请的页,其他申请过的页面都没有被 free,这样内存会越用越少…

3. 吐槽

因为最近事情很多,其他的功能也不想在往 shell 里面迁移了,说一点自己的想法。

我认为尽管 PKE 是 HUST 的老师投入较多且算得上用心的课程实验,但实在算不上设计良好。实际上,在做后续实验的时候,已经感觉并不像在“学习”一个操作系统,而是在往一个乱七八糟的软件上添加各种简陋的功能模块,并且这些功能模块还经不起测试,这实在是一件 demanding 且 unworthy 的事情,还不如去看看 XV6 或者备考下 GRE…

Now, in 2024/3/17, this blog is put into user, it is for briefly recording my life and study.

现在是 2024 年 3 月 17 日,这个博客开始投入使用,它用于简要记录我的生活和学习。

0%