System call tracing(moderate)
没有特别卡住,可以参考网上别人的实现
Sysinfo(moderate)
memory part
阅读题目可知,我们需要知道空闲内存和进程数量,并且提示我们前往kernel/kalloc.c
添加一个获取空闲内存量的函数,于是推断关于内存分配的代码在kalloc.c
中,于是去查看。
//kerlnel/kalloc.c
// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"
void freerange(void *pa_start, void *pa_end);
extern char end[]; // first address after kernel.
// defined by kernel.ld.
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
uint64
kfreemem(void) {
int counter = 0;
struct run *p;
p = kmem.freelist;
while(p) {
p = p->next;
counter++;
}
return (uint64)counter * PGSIZE;
}
我们来阅读代码,看看能得到什么。
先定义了一个指针end,由end
的定义可知,end
指向内核定义结束后的第一个地址,也就是下图的Kernel data
和Free memory
的交界处。
接着定义了两个结构体,run
和kmem
,但是我们现在不知道这是在干嘛,接着往下看吧。
-
kinit()
,看上去是在初始化,通过freerange(end, (void*)PHYSTOP);
不难推断freerange
接受一段内存的头尾指针,然后把这段内存标记为可用,所以我们就得重点看一下freerange
是如何让一段内存变得可用的。 -
freerange(void *pa_start, void *pa_end)
,根据函数名和参数名可以推测这是一个用于释放物理地址空间的函数。
我们发现函数内部的指针p对传进来的pa_start
参数进行了一个PGROUDUP
操作,我们跳转到PGROUDUP
,发现定义如下
#define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
可知,这是一个对齐内存地址的宏,工作原理如下:
• PGSIZE:代表一个页的大小,通常为 4096(即 2 的 12 次方)。
• sz:要对齐的大小或地址。
-
加上 PGSIZE - 1:首先加上 PGSIZE - 1,确保如果 sz 不是整页的大小,它会向上取整到下一个页边界。例如,如果 sz 为 4097 字节,那么加上 PGSIZE - 1 后,结果是 8192(即 2 页大小),这是下一页边界。
-
按页大小对齐:使用位与操作 & ~(PGSIZE - 1) 来清除掉低于页大小的偏移部分,从而使结果对齐到页边界。~(PGSIZE - 1) 的作用是构造一个掩码,将所有低于页大小的位清零。
示例
假设 PGSIZE = 4096 (即 4KB):
• 输入 sz = 5000:
• 5000 + 4096 - 1 = 5000 + 4095 = 9095
• 9095 & ~4095 = 9095 & 0xFFFFF000 = 8192 (向上取整到 8192,即 2 页)
• 输入 sz = 8192(刚好是页边界):
• 8192 + 4095 = 12287
• 12287 & ~4095 = 8192 (不变,因为已经是页大小的整数倍)
接着,我们在用一个循环,以一个分页作为单位释放内存。可以观察到kfree()
用于释放一个分页。
kfree(void *pa)
,这里便是我们努力看了这么多代码想看到的关键信息出现的地方!因为我要明白空闲内存是怎么样定义的!首先定义了一个run
的结构体指针r,目前还不知道这个结构体是干嘛的,不管接着看,接着用一个if来检测传入的地址是否合法,主要进行了三个判断。1.是否对齐。2.检查 pa 是否小于 end,end 表示内核在内存中的结束位置,因此不能释放内核自己正在使用的内存。3. 检查 pa 是否大于 PHYSTOP,表示不能释放超出物理内存范围的内存。
然后调用memset()
往这一页中填入一些“垃圾”,目的是为了捕获错误的引用,帮助调试内存管理问题。(我问gpt的,我现在也不知道)
然后进行了一些锁相关的操作,因为还没学到,跳过!
接着就到了我们最最关心的部分了!结构体将next
指针指向kmem.freelist
,而这个freelist
又指向了我们刚刚释放的页区域r。意思就是在空闲链表中添加了一页的大小!所以我们想要获取空闲内存量,只需要数一数链表上有多少节点再乘PGSIZE
就可以了!
process part
想要获得进程相关信息,先去proc.c
里面看看能搞到什么信息。
一开头就定义了一个结构体数组proc
,点进去观察这个struct proc
结构体,观察到state
状态是一个枚举类型,所以我们只需要遍历结构体数组proc
,找到每一个不等于UNUSED的进程统计下来即可。
用户和内核态的传递
lab给了提示,让我们观察sys_fstat()
(kernel/sysfile.c)和filestat()
(kernel/file.c)
//kernel/sysfile.c
uint64
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
return -1;
return filestat(f, st);
}
//kernel/file.c
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
观察到在sys_fstat
函数先用arg
函数获取从用户程序传进来的参数,再降参数传入filestat
函数,下面的语句便是传输的关键逻辑。
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
我们跳转到copyout
函数一探究竟
//kernel/vm.c
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
由参数和注释大概可以了解这个函数的作用,是将src
位置长度为len
的数据拷贝到dstva
这个目的虚拟地址根据pagetable
计算得到的对应的物理地址。
于是我们可以大概的了解我们要干嘛了。
我在写sys_sysinfo的时候有一个突然疑惑的点,就是我不知道我的目的地址到底是哪里,查看了用户级程序代码之后,才知道sysinfo里面是有一个参数的,那个就是一个用户态定义的地址,所以我们只需要用arg函数获取到从用户程序传进来的参数就可以了。