Mit6.828 lab4 Part C:Preeptive Multitasking and Inter-Process communication

环境

deepin 20
实验地址:mit6.828 2018 lab4 partC
很重要:不知道是不是我的代码实现有些问题,请把lib/syscall.c中的sys_ipc_recv()中的 return syscall()的第二个参数由1改为0。具体看文末

正文

在lab4的最后一部分,你将会修改内核的代码来从uncooperative的进程中抢回cpu,同时我们的内核还将会支持让进程之间互相通信。

Clock Interrupts and Preemption

运行user/spin程序。这个程序会创建一个子线程,这个子线程在得到CPU后会运行一个死循环。无论是父进程还是子进程都不能再次得到CPU。从保护系统远离bug和用户程序中的恶意代码来说,这并不是一个好的情况,因为任一一个用户程序能够独占CPU。为了使得内核能够抢回CPU的执行,强制终端当前程序的执行,所以我们必须让JOS能够支持外部中断。

Interrupt discipline

外部中断(也就是设备中断,比如说键盘的中断,时钟的中断等等) ,称为IRQs. 一般来说呢有16个外部中断(比如说级联的8259A芯片),从0到15。将IRQ映射到IDT中并不是固定的。在picirq.c中的pic_init()函数我们将IRQs的0-15映射到了IRQ_OFFSET到IRQ_OFFSET+15.

在inc/trap.h中,IRQ_OFFSET就是十进制的32。因此IDT中的32-47对应的就是IRQ的0-15。比如说,时钟中断就是IRQ 0,于是呢 IDT[IFQ_OFFSET+0]就是时钟中断的中断处理程序地址。IRQ_OFFSET之所以这样选择是因为这样一来和处理的异常(exceptions,比如说除0异常)之间不会存在overlap(这也就是说,比如说将IRQ_OFFSET设置为0,这样就会造成冲突0)。原文说到早期的MS-DOS系统就是这样做的,不知道到底咋实现的。

在JOS中,和xv6比起来做了一点简化。外部中断在内核当中的时候总是关闭了(也就是说在内核中不响应外部中断),与xv6相似的是,在用户程序中是开启外部中断的。外部中断通过eflags中的FL_IF bit来控制。当这个bit被设置的时候,外部中断开启。虽然设置这个bit有多种方法,简单起见,我们就通过保存以及恢复eflags的方法来设置。(eflags += FL_IF,通过或运算可以设置某bit)
你需要确保在用户程序当中FL_IF的设置,这样一来当用户程序运行的时候可以通过外部中断将CPU交给内核。如果不这样做的话,那么中断就被屏蔽了。我们在bootloader当中使用cli指令屏蔽了外部中断,到目前为止都没有开启外部中断。

Exercise 13:

修改kern/trapentry.S和kern/trap.c的代码来初始化IDT中的成员。修改kern/env.c中的env_alloc()函数,确保用户程序执行的时候可以响应外部中断。
shced_halt()中的sti语句取消注释,这样一来其他idle的CPU也可以响应中断。
在完成了这些练习后,如果你运行一些测试程序(比如说spin),你应该可以看到内核输出了trap frames for hardware interrupts. 虽然现在中断已经被interrupted了,但是JOS还没有处理他们,所以会出错。

代码实现:
这个不难。经过我们前面的lab,修改IDT很容易实现。代码如下,我将几个涉及到的地方放到一起,具体看注释的内容。

  //trap.c中的trap_init()
    SETGATE(idt[IRQ_OFFSET + IRQ_TIMER],    0, GD_KT, timer_handler, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_KBD],      0, GD_KT, kbd_handler,     0);
    SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL],   0, GD_KT, serial_handler,  0);
    SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, spurious_handler, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_IDE],      0, GD_KT, ide_handler,     0);
    SETGATE(idt[IRQ_OFFSET + IRQ_ERROR],    0, GD_KT, error_handler,   0);


/*
external interrupt,trapentry.S
*/
TRAPHANDLER_NOEC(timer_handler, IRQ_OFFSET + IRQ_TIMER);
TRAPHANDLER_NOEC(kbd_handler, IRQ_OFFSET + IRQ_KBD);
TRAPHANDLER_NOEC(serial_handler, IRQ_OFFSET + IRQ_SERIAL);
TRAPHANDLER_NOEC(spurious_handler, IRQ_OFFSET + IRQ_SPURIOUS);
TRAPHANDLER_NOEC(ide_handler, IRQ_OFFSET + IRQ_IDE);
TRAPHANDLER_NOEC(error_handler, IRQ_OFFSET + IRQ_ERROR);

    // LAB 4: Your code here.在env.c中的env_alloc()
    e->env_tf.tf_eflags |= FL_IF;
        

sched.c,将sti注释取消了,这样就开启了中断

    asm volatile (
        "movl $0, %%ebp\n"
        "movl %0, %%esp\n"
        "pushl $0\n"
        "pushl $0\n"
        // Uncomment the following line after completing exercise 13
        "sti\n"
        "1:\n"
        "hlt\n"
        "jmp 1b\n"
    : : "a" (thiscpu->cpu_ts.ts_esp0));

Handling Clock Interrupts

在user/spin程序当中,当子程序轮到执行的时候,陷入到了死循环当中,于是内核永远也得不到CPU。我们需要program硬件让它周期性的产生时钟中断,使得将CPU控制权还给内核,经调度程序之后可以在多个用户程序之间交替。

Exercise 14:

修改内核的trap_dispatch()的代码,当时钟中断发生的时候可以让内核调用sched_yield()来寻找然后运行不同的用户程序。
现在你应该可以让user/spin正常运行了。

PS: 我们在每一次响应时钟中断的时候,应该主动告诉外设,表示中断结束了(End of Interrupt, e.g. EOI)。关于外设的一些IO port的意思我没有详细的去了解,希望以后有时间补上这个坑吧。
关于EOI的一点资料:8259-PIC-EOI

思路:
响应时钟中断十分简单,只需要新加入一个case就可以。然后在代码实现中,调用sys_yield(),别忘了发送EOI

        case (IRQ_OFFSET + IRQ_TIMER):
            // cprintf("hello world!\n");

            lapic_eoi();
            sched_yield();

            // break;

Inter-Process communication(IPC)

(从技术来说,在JOS中叫IEC更合适。因为Inter-Environemnt commuications,感觉像冷笑话)。
我们过去一直专注于操作系统的隔离(isolation,各个进程的执行不会影响对方),这使得每一个程序都感觉他在独享整个电脑。另外一个重要的东西操作系统需要提供的服务就是让每一个进程之间相互通信。Unix pipe就是一个经典的例子。
现有非常多的模型用于进程通信。甚至在今天还有争论说到底哪个一个才是最好的。我们不讨论这个,我们只是实现一个最简单的IPC。

IPC on JOS

你将会实现几个JOS的系统调用,这几个系统调用共同来实现进程之间通讯(inter-process communiation).你将会实现两个系统调用,sys_ipc_recv()sys_ipc_try_send().然后实现两个wrapper,ipc_recv()ipc_send()

Sending and Receiving Messages

为了接受一个消息,一个进程调用sys_ipc_recv()。这个系统调用de-schedules 当前正在运行的进程并且不在运行它直到数据已经接收到了。当一个进程等待接受数据的时候,任何其他的都可以向他发送数据,并不是说只有某个进程专属的,也不需要发送者和接收者之间有某种联系,比如说它俩是父子进程。换句话说,在part A中实现的权限校验并不适用于IPC,因为IPC system call 被精心设计过,从而把是相当安全的,一个进程不能通过发送数据使得另外一个进程出问题。

为了发送一个数据,一个进程调用sys_ipc_try_send(),参数是接收者的ID以及所有发送的内容。如果指定的接收者正在准备接收消息(也就是说调用了sys_ipc_recv()且并未返回),那么发送数据给它并且返回0.否则的话,返回-E_IPC_NOT_RECV来表明接收者现在并不想接收消息。这里可能比较难懂,反正我一开始的时候没有理解其中的意思,待会到代码实现的时候在解释下。

Transferring Pages

当一个进程调用sys_ipc_recv()并且带有参数dstva的时候,这就表明这个用户进程可以接受一个page mapping。如果发送者发送了一个pgae,那么这个page should be mapped at dstva in receiveris address space. 如果接收者已经在dstva已经有一个页映射了,那么之前的就需要unmaped.

当一个进程调用sys_ipc_try_send()并且带有参数srcva,这就意味着发送者希望将当前地址空间srcva映射的page发送到接收者,权限为perm。在IPC成功以后,发送者继续保持srcva在自己的地址空间内的映射关系,但是接收者也获得了这个同样的映射关系。(这也就是说在两个不同的进程中srcva映射到了相同的物理页)。这样一来结果是sender和receiver共享了一个物理页。

Implementing IPC

Exercise 15:

实现sys_ipc_recv 和 sys_ipc_try_send在 kern/syscall.c中。在实现他们之前阅读一下注释。当你调用envid2env的时候checkperm设置为0.另外实现ipc_recv 和 ipc_sned在lib/ipc.c中。
运行user/pingpong和user/primes程序来测试你的IPC实现。

这几个函数的逻辑是:系统调用sys_ipc_recv和sys_ipc_try_send分别是接收数据和发送数据。sys_ipc_send的几个参数分别是目标进程ID(envid),要发送的值(value), 要发送的页如果需要的话(srcva),页的权限(perm)。sys_ipc_recv的参数dstva表示当前进程想要接收到的数据放到dstva处。

在实现这些代码之前。先来看一下Env这个结构当中新加入的一些结构:

  • env_ipc_recving
    当前进程的状态,表明当前进程是否处于接受状态。
  • env_ipc_dstva
    当前进程要把接收到的数据放到哪儿(如果需要用页来传递数据)。
  • env_ipc_value
    当前进程接收到的数据(如果用页来传递数据)。
  • env_ipc_from
    当前进程接收到的数据来自谁。
  • env_ipc_perm
    当前进程接收到的数据的页的权限。

sys_ipc_recv():
某个进程调用这个系统调用可以来接收数据。所以我们在这个函数内部必然要做的是切换当前的接受状态,放弃CPU(如果不放弃CPU,那么发送者永远得不到CPU了)。在结合注释里面的信息,我们还需要校验一下地址是否是页对齐的(page-aligned)下面是具体的代码实现:

static int
sys_ipc_recv(void *dstva)
{
    // LAB 4: Your code here.
    // panic("sys_ipc_recv not implemented");
        uint32_t dst_addr = (uint32_t)dstva;
    if(dst_addr < UTOP && PGOFF(dst_addr) > 0) {
        // not page-aligned, return -E_INVAL;
        return -E_INVAL;
    }
    curenv->env_ipc_recving = 1; // 表示当前进程正在接受信息
    curenv->env_ipc_dstva = dstva; //表明想接收数据到dstva这个虚拟地址
    curenv->env_status = ENV_NOT_RUNNABLE; // block until a message has been received
    sched_yield();
    return 0;
}

sys_ipc_try_send():
这个系统调用的实现,需要进行一大堆的条件判断来剔除异常情况。除此之外,值得我们关注的就是注释里面提到的
这里的意思是:如果目标进程没有被挂起(放弃CPU),即env_ipc_recving == 0的时候,因为env_ipc_recving == 1说明进程等待接受数据。那么应该返回-E_IPC_NOT_RECV。此外还需要关注的就是,我们往目标进程插入页,接收者和发送者共享一个页。
代码如下:

static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
    // LAB 4: Your code here.
    int result;
    struct Env* proc; //目标进程
    uint32_t src_addr = (uint32_t)srcva; // the page  address that will be sent to target process
    struct PageInfo *page; //phyiscal page 
    pte_t *pg_table_entry; // page table entry
    if ((result = envid2env(envid, &proc, 0)) < 0)
        return result;  
    if(proc->env_ipc_recving == 0) {
        // sys_ipc_recv()中设置了recving=1来表明这个进程想接受数据, 如果target process的recving == 0
        // 说明target process并不想接收数据,所以return -E_IPC_NOT_RECV;
        return -E_IPC_NOT_RECV;
    }
    if(src_addr < UTOP && PGOFF(src_addr) > 0) {
        return -E_INVAL;
    }
    if (((uint32_t)srcva < UTOP) && ((perm | PTE_SYSCALL) != PTE_SYSCALL)) {
        return -E_INVAL;
    }
    page = page_lookup(curenv->env_pgdir,srcva,&pg_table_entry);
    if(src_addr < UTOP && page == NULL) {
        return -E_INVAL;
    } 
    if((perm & PTE_W) && !(*pg_table_entry & PTE_W)) {
        //(*pg_table_entry & PTE_W)如果这个条件成立,说明是writetable的
        //所以如果我们要判断它是否是一个read-only的,只需要!即可
        return -E_INVAL;
    }

    proc->env_ipc_perm = 0;
    if ((src_addr< UTOP) && ((uint32_t) proc->env_ipc_dstva < UTOP)){
        // 如果src_addr < UTOP,才可以使用页来传递数据
        result = page_insert(proc->env_pgdir,page,proc->env_ipc_dstva,perm);
        if(result < 0) {
            return -E_NO_MEM;
        }
        proc->env_ipc_perm = perm;
    }
    proc->env_ipc_recving = 0; //表示接受完毕
    proc->env_ipc_value = value;
    proc->env_ipc_from = curenv->env_id;
    proc->env_status = ENV_RUNNABLE; //接收数据完毕后设置为RUNNABLE,接受调度
    return 0;
    // panic("sys_ipc_try_send not implemented");
}

ipc_send():
相对来说ipc_send()和系统调用比起来就没有那么困难了。他只是一个对于系统调用sys_ipc_try_send()的包装。结合注释,他说this function keeps trying until it succeeds 这就是意味着我们需要用一个循环来判断目标进程是否准备好接收消息。如果sys_ipc_try_send()的返回值不是-E_IPC_NOT_RECV那么就panic。另外我们要发送消息,肯定要让当前进程放弃CPU的,因此在最后还需要调用sys_yield()来放弃CPU。其他的一些信息就卸载注释里面了,代码如下:

void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
    // LAB 4: Your code here.
    int result;
    if(pg == NULL) {
        //如果不需要以页来发送数据,那么就将pg设置为一个合适的值
        //让sys_ipc_try_send理解这个不是一个合法的地址
        //根据sys_ipc_try_send里面的注释,很多条件都需要 < UTOP,那就意味着说如果我们传入UTOP
        //将会被视为不合法的地址
        pg = (void*)UTOP;
    }
    while((result = sys_ipc_try_send(to_env,val,pg,perm)) == -E_IPC_NOT_RECV);
    if(result != -E_IPC_NOT_RECV && result < 0) {
        panic("ipc_send():send message to %d failed",to_env);
    }
    sys_yield();
    
}

ipc_recv():
这个函数是对系统调用sys_ipc_recv()的封装。根据注释里面的解释,主要是进行返回值的判断。不必多说,代码如下。

int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
    // LAB 4: Your code here.
    if(pg == NULL) {
        // 系统调用中的条件都是要求pg < UTOP,所以传入UTOP就会被视为
        // no page
        pg = (void*)UTOP;
    }
    int result;
    result = sys_ipc_recv(pg);
    if(result < 0) {
        if(from_env_store != NULL) {
            *from_env_store = 0;
        }
        if(perm_store != NULL) {
            *perm_store = 0;
        }
    }

    if(from_env_store != NULL) {
        *from_env_store = thisenv->env_ipc_from;
    }
    if(perm_store != NULL) {
        *perm_store = thisenv->env_ipc_perm;
    }
    
    // return the value sent by sender
    return thisenv->env_ipc_value;

}

修改lib/syscall.c的代码:
如果不修改,我一直无法通过pingpong和primes测试点。并且运行的时候会报错,将原来的代码修改为:

int
sys_ipc_recv(void *dstva)
{
    return syscall(SYS_ipc_recv, 0, (uint32_t)dstva, 0, 0, 0, 0); 
        //原来为return syscall(SYS_ipc_recv, 1, (uint32_t)dstva, 0, 0, 0, 
}c

往kern/syscall.c中加入新的case:
虽然这个很简单,但是我在这里找了很久的bug。真是低级的错误


实验结果

运行make run-pingpong,出现以下结果:

make run-pingpong

运行make grade,结果如下,我们通过了所有的测试点。

make grade

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容