OS-lab4

OS-lab4

上机

lab4-1

exam

发现这届课程组确实很贴心,担心大家过不了exam,每次都给伪代码。lab3上机要自己实现异常处理函数,lab4是实现系统调用。这次exam特别特别简单(尤其还给伪代码,想错都难),实现进程组通讯。

extra

实现父进程给所有的后代进程(子进程、子子进程、…),可以在用户态实现,也可以写新的系统调用。

要遍历所有的后代进程可以用dfs,我是在用户态实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ipc_broadcast(u_int val, void *srcva, u_int perm)
{
dfs(syscall_getenvid(), val, srcva, perm);
}

void dfs(u_int id, u_int val, void *srcva, u_int perm)
{
for (int i = 0; i < 10; i++)
{
if (envs[i].env_status != ENV_FREE && envs[i].env_parent_id == id && envs[i].env_parent_id!=0)
{ debugf("i'm in\n");
dfs(envs[i].env_id, val, srcva, perm);
ipc_send(envs[i].env_id, val, srcva, perm);
}
}
}

md气死我了,我 dfs(syscall_getenvid(), val, srcva, perm);调用syscall_getenvid的时候没加括号,结果相当于传的是syscall_getenvid这个函数的地址,气死了,气死了,到最后也没找出来bug

lab4-2

exam

实现父进程,阻塞所有的后代进程。准确来说,父进程申请一个信号量,可以最多阻塞n个进程,然后后代进程会阻塞,当阻塞的进程数达到n的时候,解除阻塞,同时信号量失效。

保证

  • 只有一个根进程,其他都是后代进程,所以我们的信号量可以直接定义一个全局变量,但如果有多个父进程,就需要把信号量设成进程的属性,在fork的时候父进程把它传给子进程
  • 只会进行一次申请信号量,保证先申请,才会fork

我的阻塞实现在用户态,首先在ipc.c中定义void barrier_alloc(int n) ,void barrier_wait(void)

1
2
3
4
5
6
7
8
9
10
11
void barrier_alloc(int n){
syscall_barrier(0, n);

}
void barrier_wait(void){
int r;
syscall_barrier(2,0);
while((r = syscall_barrier(1,0)) != 0){
syscall_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
int sys_barrier(int op, int sum)
{
if (op == 0) // 申请
{
ba = sum;
return 0;
}
if (op == 2) //try_wait
{
ba--;
return 0;
}
if (op == 1) // 查询
{
if (ba <= 0)
{

return 0;
}
else
{
return 1;
}
}
}

但其实我的实现是错的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void barrier_wait(void){
int r;
syscall_barrier(2,0); //如果执行完这一句之后时间片用完了,切换下一个进程,会出问题
while((r = syscall_barrier(1,0)) != 0){
syscall_yield();
}
}

void barrier_wait(void){
int r;
while((r = syscall_barrier()) != 0){ //这样才是正确的
syscall_yield();
}
}

实验报告

一、thinking

thinking 4.1

  • k0寄存器在MIPS规范中是留给内核操作的,保存现场的时候,对于状态寄存器可以用k0寄存器做最中介进行传递
  • 如果切换内核态的时候没有修改a0-a3应该是可以访问的,但do_syscall传递了tf这个参数,会修改寄存器的值,所以不能
  • do_syscall 的时候我们保存了系统调用前的现场,按照SAVEALL的规格,便可以找到传递的参数
  • 4
    • EPC+=4,返回到系统调用的下一条指令
    • 如果系统调用号不存在,通过修改v0返回错误值

thinking 4.2

进程号是独一无二的,801和001返回的是同一个进程块,而001是非法的,所以必须判断一下

thinking 4.3

envid2env函数中,如果进程号为0,则返回当前进程块

thinking 4.4

C,子进程并没有调用fork,但其调度时返回的现场和父进程相同,除了返回值是0

thinking 4.5

USTACKTOP以上的用户空间不能映射,包括每个进程的页表和页目录,每个进程都不一样。

pages,envs在进程申请的时候进行了映射,也不需要

thinking 4.6

  • vpd和vpt是页目录和页表的基地址,我们取某个页表项的时候可以vpt[index]即可
  • 进程申请的时候完成了页表的自映射
  • e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V;
  • 由上述自映射我们发现,我们只有有效位,并没有PTE_D,所以不能修改

thinking 4.7

真正的异常处理函数在用户空间,我们必须保证在异常处理完之后回到异常发生的现场

如果有多个异常进程,如果保存的现场都在内核态同一个位置,可能会出差错,保存在用户空间解决了这个问题

thinking 4.8

更符合微内核的设计,能在用户空间处理的问题就在用户空间处理,但是在COW处理的过程中,进行了多次系统调用,为什么不在一次系统调用中完成呢,切换内核态不是很大的开销吗?

thinking 4.9

我们在exofork系统调用中,已经设置了子进程的写入异常函数,所以应在exofork之前就进行set_tlb_mod_entry系统调用

二、实验难点分析

1. 系统调用是特殊的异常

都是保存现场进入特定的异常处理函数do_syscall,然后根据传进来的系统调用号再进入特定的系统调用函数。除此,系统调用并不会切换进程,可以理解为用户程序执行了一段内核的代码

2. envid2env

了解创建系统调用号的过程,确保了所有的进程的进程号都不同,所以才有ENVX这个宏。在envid2env中我们特地检查了e的进程id是否和传进来的参数一样,若一样则没问题。如果不一样,确实会有不一样的情况:801和001这两个返回的进程块一样,但001的进程id是非法的。

3. 进程通信

在try_send系统调用中,我一开始使用了sys_mem_map函数,这样是不对的,因为sys_mem_map中envid2env传入了检查位。

4. 写入异常函数

写入异常的函数在用户空间中,所以处理完异常后需要恢复现场。

COW机制确保了父子进程能够最大共享数据

三、实验感想

  • 总的来说,lab4的难度又有提升,工作量很大,需要在内核代码和用户代码来回切换
  • 初始了进程通信和fork,明白了简化的实现机制,只有先理解了简单的机制,才能有优化提升的空间,在进程尝试发消息的时候一直处于轮询状态,这样很浪费资源

OS-lab4
https://etherialize.github.io/2023/05/13/OS-lab4/
作者
HZY
发布于
2023年5月13日
许可协议