Lab4 - 进程同步

实验要求

  • 实现信号量

  • 封装系统调用

  • 实现共享变量

  • 解决同步问题

  • 基于XCHG指令的自旋锁实现(选做)

实现格式化输入函数

在lab2中,我们已经实现了格式化输出函数(如printf()),但并未实现功能完备的格式化输入函数(如scanf())。这是为什么呢?

格式化输入函数的难点

前序实验未涉及进程同步机制,因此 scanf() 的实现更具挑战性。

本次实验我们先实现一个简化版的 scanf() 函数,并通过测试代码验证其功能。这不仅是格式化输入机制的实践,也为后续进程同步的学习奠定基础。

#include "lib.h"
#include "types.h"

int uEntry(void) {
    int dec = 0;
    int hex = 0;
    char str[6];
    char cha = 0;
    int ret = 0;
    while(1){
        printf("Input:\" Test %%c Test %%6s %%d %%x\"\n");
        ret = scanf(" Test %c Test %6s %d %x", &cha, str, &dec, &hex);
        printf("Ret: %d; %c, %s, %d, %x.\n", ret, cha, str, dec, hex);
        if (ret == 4)
            break;
    }
  return 0;
}

输入Test a Test oslab 2025 0xadc后,屏幕上输出为Ret: 4; a, oslab, 2025, adc.

要求

非格式化字符原样输入

其它字符(输入和要求格式不符、是否回显等)可自行斟酌决定如何处理

实现信号量相关的系统调用

实现SEM_INITSEM_POSTSEM_WAITSEM_DESTROY系统调用,使用以下用户程序测试,并在实验报告中说明实验结果

#include "lib.h"
#include "types.h"

int uEntry(void) {
    int i = 4;
    int ret = 0;
    int value = 2;

    sem_t sem;
    printf("Parent Process: Semaphore Initializing.\n");
    ret = sem_init(&sem, value);
    if (ret == -1) {
        printf("Parent Process: Semaphore Initializing Failed.\n");
        exit();
    }

    ret = fork();
    if (ret == 0) {
        while( i != 0) {
            i --;
            printf("Child Process: Semaphore Waiting.\n");
            sem_wait(&sem);
            printf("Child Process: In Critical Area.\n");
        }
        printf("Child Process: Semaphore Destroying.\n");
        sem_destroy(&sem);
        exit();
    }
    else if (ret != -1) {
        while( i != 0) {
            i --;
            printf("Parent Process: Sleeping.\n");
            sleep(128);
            printf("Parent Process: Semaphore Posting.\n");
            sem_post(&sem);
        }
        printf("Parent Process: Semaphore Destroying.\n");
        sem_destroy(&sem);
        exit();
    }
    
    return 0;
}

实现共享变量

增加进程间共享变量相关的系统调用和对应的库函数

    printf("==============TEST SHAREDVARIABLE=============\n");
    int number = 114514;
    sharedvar_t svar;
    ret = createSharedVariable(&svar, number);
    printf("Parent Process: create Shared Variable: %d  with value: %d\n", svar, number);
    if (ret == -1) exit();

    ret = fork();
    if (ret == 0)
    {
        number = readSharedVariable(&svar);
        printf("Child Process: readShared Variable: %d get value: %d\n", svar, number);
        sleep(128);

        number = readSharedVariable(&svar);
        printf("Child Process: readShared Variable: %d get value: %d\n", svar, number);
        number = 2333;
        writeSharedVariable(&svar, number);
        printf("Child Process: writeShared Variable: %d with value: %d\n", svar, number);

        exit();
    }

基于信号量解决进程同步问题

我们需要基于信号量机制解决进程同步问题。以下是具体要求和实现步骤:

  1. 实现验证

    • uEntry() 中编写代码,验证信号量在进程同步中的正确性。

    • 可以自行定义所需的功能性函数,并直接实现在 lab4/app/main.c 文件中。

  2. 代码提交要求

最终提交的 lab4/app/main.c 文件应包含完整的实现代码。

注意

在完成 lab4.4 实验后,请删除 lab4.3 中父进程的 exit() 操作,以确保代码符合实验要求。

#include "lib.h"
#include "types.h"

int uEntry(void) {
    // For lab4.1
    // Test 'scanf'
    ...
    
    // For lab4.2
    // Test 'Semaphore'
    ...

    // For lab4.3
    // Test 'Shared Variable'
    ...

    // For lab4.4
    // TODO: You need to design and test the problem.
    // Note that you can create your own functions.
    // Requirements are demonstrated in the guide.
    ...

    return 0;
}

基于XCHG指令的自旋锁实现

实现一个基于XCHG指令的自旋锁,用于保护信号量的P()V()操作中对信号量值和阻塞队列的修改,确保这些操作在临界区内的安全性。

单核环境的特殊性

由于不是多核CPU环境,也不容易测试临界区保护的作用

相关资料

信号量

相信课上大家已经对信号量有了一定的了解,这里以另一个角度介绍信号量

信号是一种抽象数据类型,由一个整型(sem)变量和两个原子操作组成;

P()(Prolaag,荷兰语尝试减少)

  • sem减1
  • sem < 0,进入等待,否则继续

V()(Verhoog,荷兰语增加)

  • sem加1
  • sem <= 0,唤醒一个等待进程

信号量的实现(伪代码):

struct Semaphore {
    int state;
    int value;
    struct ListHead pcb; // link to all pcb ListHead blocked on this semaphore
};

int sem_wait(sem_t *sem) {
    sem->value -= 1;
    if(sem->value <= 0) {
        // Add this process to Queue
        // block(current)
    }
}


int sem_post(sem_t *sem) {
    sem->value += 1;
    if(sem->value < 0) {
        // Remove this process from Queue
        // wakeup(current)
    }
}

信号量的简单应用

用信号量实现临界区的互斥访问

每类资源设置一个信号量,其初值为1

sem_init(&mutex, 1);
mutex->P();
// Critical Section;
mutex->V();

这里要注意必须成对使用P()操作和V()操作

  • P()操作保证互斥访问临界资源
  • V()操作在使用后释放临界资源
  • PV操作不能次序错误、重复或遗漏

用信号量实现条件同步

条件同步设置一个信号量,其初值为0

sem_init(&sem, 0);
process A                               process B

... M ...

... N ...                                ... X ...

                                         ... Y ...

AMN模块,BXY模块,这里为了保证B执行到X后,A才能执行N,可以使用信号量实现条件同步

process A                                 process B

... M ...
sem->P();        -----------+
... N ...                   |            ... X ...
                            +--------->  sem->V();
                                         ... Y ...

经典进程同步问题

友情提示

下面的三个问题是进程同步中非常经典的问题,相信课上老师已经着重讲述

虽然我们的实验中只涉及到部分问题,但是其他问题也贴出来,希望大家弄懂

哲学家就餐问题

场景描述

  • 5 位哲学家围坐在一张圆桌旁。

  • 桌上有 5 支叉子,每两位哲学家之间放一支。

哲学家行为

  • 思考:哲学家不需要叉子,处于思考状态。

  • 进餐:

    • 需要同时拿到左右两支叉子才能进餐。

    • 进餐结束后,将两支叉子放回原处,继续思考。

核心问题

如何设计一种机制,确保哲学家们的动作有序进行,避免以下问题:

  • 死锁:所有哲学家同时拿起左边的叉子,导致无法拿到右边的叉子。

  • 饥饿:某些哲学家始终无法获得叉子,无法进餐。

方案1:

#define N 5                // 哲学家个数
semaphore fork[N];         // 信号量初值为1

void philosopher(int i) {  // 哲学家编号:0-4
  while(TRUE) {
    think();               // 哲学家在思考
    P(fork[i]);            // 去拿左边的叉子
    P(fork[(i+1) % N]);    // 去拿右边的叉子
    eat();                 // 吃面条
    V(fork[i]);            // 放下左边的叉子
    V(fork[(i+1) % N]);    // 放下右边的叉子
  }
}

极端情况下不正确,可能导致死锁

方案2:

#define N 5                // 哲学家个数
semaphore fork[N];         // 信号量初值为1
semaphore mutex;           // 互斥信号量,初值1

void philosopher(int i) {  // 哲学家编号:0-4
  while(TRUE) {
    think();               // 哲学家在思考
    P(mutex);              // 进入临界区
    P(fork[i]);            // 去拿左边的叉子
    P(fork[(i+1)%N]);      // 去拿右边的叉子
    eat();                 // 吃面条
    V(fork[i]);            // 放下左边的叉子
    V(fork[(i+1)%N]);      // 放下右边的叉子
    V(mutex);              // 退出临界区
  }
}

互斥访问正确,但是每次只允许一个人就餐

方案3:

#define N 5                // 哲学家个数
semaphore fork[5];         // 信号量初值为1

void philosopher(int i){   // 哲学家编号:0-4
  while(TRUE){
    think();               // 哲学家在思考
    if(i%2==0){
      P(fork[i]);          // 去拿左边的叉子
      P(fork[(i+1)%N]);    // 去拿右边的叉子
    } else {
      P(fork[(i+1)%N]);    // 去拿右边的叉子
      P(fork[i]);          // 去拿左边的叉子
    }
    eat();                 // 吃面条
    V(fork[i]);            // 放下左边的叉子
    V(fork[(i+1)%N]);      // 放下右边的叉子
  }
}

没有死锁,可以实现多人同时就餐

Questions?

有没有更好的方式处理这个就餐问题?

生产者-消费者问题

生产者—->缓冲区—->消费者

有界缓冲区的生产者-消费者问题描述:

  • 一个或多个生产者在生产数据后放在一个缓冲区里
  • 单个消费者从缓冲区取出数据处理
  • 任何时刻只能有一个生产者或消费者可访问缓冲区

问题分析:

  • 任何时刻只能有一个进程(线程)操作缓冲区(互斥访问)
  • 缓冲区空时,消费者必须等待生产者(条件同步)
  • 缓冲区满时,生产者必须等待消费者(条件同步)

用信号量描述每个约束:

  • 二进制信号量mutex
  • 资源信号量fullBuffers
  • 资源信号量emptyBuffers

伪代码描述一下:

class BoundedBuffer {
    mutex = new Semaphore(1);
    fullBuffers = new Semaphore(0);
    emptyBuffers = new Semaphore(n);
}
BoundedBuffer::Deposit(c){               BoundedBuffer::Remove(c){
  emptyBuffers->P();                       fullBuffers->P();
  mutex->P();                              mutex->P();
  Add c to the buffer;                     Remove c from buffer;
  mutex->V();                              mutex->V();
  fullBuffers->V();                        emptyBuffers->V();
}                                        }

读者-写者问题

读者-写者问题主要出现在数据库等共享资源的访问当中

问题描述

  • 共享数据的两类使用者
    • 读者:只读取数据,不修改
    • 写者:读取和修改数据
  • 对共享数据的读写
    • “读-读”允许,同一时刻,允许有多个读者同时读
    • “读-写”互斥,没有写者时读者才能读,没有读者时写者才能写
    • “写-写”互斥,没有其他写者,写者才能写

用信号量描述每个约束:

  • 信号量WriteMutex,控制读写操作的互斥,初始化为1
  • 读者计数Rcount,正在进行读操作的读者数目,初始化为0
  • 信号量CountMutex,控制对读者计数的互斥修改,初始化为1,只允许一个进程(线程)修改Rcount计数

写者进程

P(WriteMutex);
write;
V(WriteMutex);

读者进程

P(CountMutex);
if (Rcount == 0)
  P(WriteMutex);
++Rcount;
V(CountMutex);
read;
P(CountMutex);
--Rcount;
if (Rcount == 0)
  V(WriteMutex);
V(CountMutex);

相关系统调用

sem_init

sem_init系统调用用于初始化信号量,其中参数value用于指定信号量的初始值.

初始化成功则返回0,指针sem指向初始化成功的信号量;否则返回-1

int sem_init(sem_t *sem, uint32_t value);

sem_post

sem_post系统调用对应信号量的V操作,其使得sem指向的信号量的value增一,若value取值不大于0,则释放一个阻塞在该信号量上进程(即将该进程设置为就绪态),若操作成功则返回0,否则返回-1

int sem_post(sem_t *sem);

sem_wait

sem_wait系统调用对应信号量的P操作,其使得sem指向的信号量的value减一,若value取值小于0,则阻塞自身,否则进程继续执行,若操作成功则返回0,否则返回-1

int sem_wait(sem_t *sem);

sem_destroy

sem_destroy系统调用用于销毁sem指向的信号量,销毁成功则返回0,否则返回-1,若尚有进程阻塞在该信号量上,可带来未知错误

int sem_destroy(sem_t *sem);

自旋锁

  • 自旋锁是一种轻量级的锁机制,线程在尝试获取锁时会不断循环检查锁的状态(自旋),直到成功获取锁。
  • 自旋锁适用于临界区较短的场景,因为自旋会占用 CPU 资源。

XCHG指令

  • XCHG 是一种原子操作指令,可以在不被中断的情况下交换两个值。
  • 通过 XCHG 实现的自旋锁可以避免中断或线程切换导致的并发问题。
单核环境的特殊性
  • 在单核 CPU 环境中,虽然不会有多个线程同时执行,但中断可能导致线程切换,从而破坏临界区的完整性。
  • 因此,临界区保护仍然是必要的,尽管在单核环境中不容易测试其作用。

实验攻略

在攻略之前,先带大家看一看实验4新增的或修改的数据结构等:

新增数据结构

struct Semaphore {
    int state;
    int value;
    struct ListHead pcb; // link to all pcb ListHead blocked on this semaphore
};
typedef struct Semaphore Semaphore;

struct Device {
    int state;
    int value;
    struct ListHead pcb; // link to all pcb ListHead blocked on this device
};
typedef struct Device Device;

struct SharedVariable {
    int state;
    int value;
};
typedef struct SharedVariable SharedVariable;

Semaphore sem[MAX_SEM_NUM];
Device dev[MAX_DEV_NUM];
SharedVariable shared_var[MAX_SHARED_VAR_NUM];

对于信号量Semaphore,相信大家在看完相关资料后能理解。因为我们将信号量定义成数组,所以添加了一个state成员,表示当前信号量是否正在使用;1表示正在使用,0表示未使用

那么Device是干啥用的,为什么和信号量的定义这么相似?

说这个之前我们需要想一下stdin标准输入。在操作系统中,我们不可能一直监听键盘中断来进行输入,因为过于占用系统资源,所以我们需要一个键盘输入缓冲区与类似信号量的机制,以此实现条件同步。

在键盘中断将输入存入缓冲区后再让用户程序读取,所以代码中定义了Device,他其实就是信号量,只不过不能由用户通过系统调用控制,而是直接和硬件绑定

在键盘中断的处理中,输入内容会先被存入缓冲区,随后由用户程序读取。为了实现这一机制,代码中定义了 Device 结构体。

#define STD_OUT 0
#define STD_IN 1

实际上,stdout是非阻塞式的,stdin上才会有进程阻塞.

struct ListHead {
    struct ListHead *next;
    struct ListHead *prev;
};

ListHead是一个双向链表

如下两个函数用于初始化semdev

void initSem() {
    int i;
    for (i = 0; i < MAX_SEM_NUM; i++) {
        sem[i].state = 0; // 0: not in use; 1: in use;
        sem[i].value = 0; // >=0: no process blocked; -1: 1 process blocked; -2: 2 process blocked;...
        sem[i].pcb.next = &(sem[i].pcb);
        sem[i].pcb.prev = &(sem[i].pcb);
    }
}

void initDev() {
    int i;
    for (i = 0; i < MAX_DEV_NUM; i++) {
        dev[i].state = 1; // 0: not in use; 1: in use;
        dev[i].value = 0; // >=0: no blocked; -1: 1 process blocked; -2: 2 process blocked;...
        dev[i].pcb.next = &(dev[i].pcb);
        dev[i].pcb.prev = &(dev[i].pcb);
    }
}

修改的数据结构:PCB中添加对应的双向链表结构

 struct ProcessTable {
     uint32_t stack[MAX_STACK_SIZE];
     struct TrapFrame regs;
     uint32_t stackTop;
     uint32_t prevStackTop;
     int state;
     int timeCount;
     int sleepTime;
     uint32_t pid;
     char name[32];
+	struct ListHead blocked; // sempahore, device, file blocked on
 };
 typedef struct ProcessTable ProcessTable;

这样将current进程加到信号量i的阻塞列表可以通过以下代码实现

        pcb[current].blocked.next = sem[i].pcb.next;
        pcb[current].blocked.prev = &(sem[i].pcb);
        sem[i].pcb.next = &(pcb[current].blocked);
        (pcb[current].blocked.next)->prev = &(pcb[current].blocked);

以下代码可以从信号量i上阻塞的进程列表取出一个进程:

        pt = (ProcessTable*)((uint32_t)(sem[i].pcb.prev) -
                    (uint32_t)&(((ProcessTable*)0)->blocked));
        sem[i].pcb.prev = (sem[i].pcb.prev)->prev;
        (sem[i].pcb.prev)->next = &(sem[i].pcb);

irqHandle.c中的sysWrite也有一些变化

 void sysWrite(struct TrapFrame *sf) {
     switch(sf->ecx) { // file descriptor
-		case 0:
-			sysPrint(sf);
+		case STD_OUT:
+			if (dev[STD_OUT].state == 1) {
+				sysWriteStdOut(sf);
+			}
             break; // for STD_OUT
         default:break;
     }
 }

另外对sysWrite进行了重命名

-void sysPrint(struct TrapFrame *sf) {
+void sysWriteStdOut(struct TrapFrame *sf) {

实现格式化输入函数

为了降低实验难度,syscall.c中的scanf()已经完成,同学们只需要完成对应的中断处理例程

irqHandle.c中添加了sysRead()内核函数处理各种读数据:

void sysRead(struct StackFrame *sf) {
    switch(sf->ecx) {
        case STD_IN:
            if (dev[STD_IN].state == 1)
                sysReadStdIn(sf);
            break; // for STD_IN
        default:
            break;
    }
}

在这一节主要关注的就是sysReadStdIn(),同学们需要去完成它。那么如何完成呢,它是和键盘中断有条件同步的,所以的这一步还要结合keyboardHandle()一起完成

在实验2中,有很多同学不知道下面的代码是干啥的

extern uint32_t keyBuffer[MAX_KEYBUFFER_SIZE];
extern int bufferHead;
extern int bufferTail;

其实这就是键盘输入的缓冲区,把所有零碎的知识拼凑在一起,keyboardHandle()要做的事情就两件:

  1. 将读取到的keyCode放入到keyBuffer
  2. 唤醒阻塞在dev[STD_IN]上的一个进程

接下来安排sysReadStdIn(),它要做的事情也就两件:

  1. 如果dev[STD_IN].value == 0,将当前进程阻塞在dev[STD_IN]
  2. 进程被唤醒,读keyBuffer中的所有数据
注意

最多只能有一个进程被阻塞在dev[STD_IN]上,多个进程想读,那么后来的进程会返回-1,其他情况scanf()的返回值应该是实际读取的字节数

和实验2中printf()的处理例程类似,以下代码可以将读取的字符character传到用户进程

int sel = sf->ds;
char *str = (char *)sf->edx;
int i = 0;
asm volatile("movw %0, %%es"::"m"(sel));
asm volatile("movb %0, %%es:(%1)"::"r"(character),"r"(str + i));

完成这一步后请测试scanf(),并在实验报告展示结果

实现信号量

这一部分也只需要完善处理例程,其它部分已经实现,所有的信号量相关调用有一个总的处理:

void sysSem(struct StackFrame *sf) {
    switch(sf->ecx) {
        case SEM_INIT:
            sysSemInit(sf);
            break;
        case SEM_WAIT:
            sysSemWait(sf);
            break;
        case SEM_POST:
            sysSemPost(sf);
            break;
        case SEM_DESTROY:
            sysSemDestroy(sf);
            break;
        default:break;
    }
}

需要完成的是4个子例程:sysSemInitsysSemWaitsysSemPostsysSemDestroy

在实现时,因为信号量以数组形式存在,所以只要一个下标就可以定位信号量

完成后请进行测试,并在实验报告中展示结果

实现共享变量

我们需要在kernel和lib部分,实现进程间的共享变量,并增加进程间共享变量的相关系统调用和对应的故函数

库函数声明参考如下:

  • int createSharedVariable(sharedvar_t *svar, int value); //成功创建时返回对应的共享变量描述符
  • int destroySharedVariable(sharedvar_t *svar); //销毁共享变量描述符对应的共享变量
  • int readSharedVariable(sharedvar_t *svar); //返回共享变量的值
  • int writeSharedVariable(sharedvar_t *svar, int value); //修改共享变量的值

我们需要自行实现共享变量的数据结构以及相应的操作

解决进程同步问题(lab4.4)

以下问题,请你自行完成相应的实现需求,并在lab4.4中撰写相应的测试代码,以此展示功能

生产者-消费者问题

同学们需要在lab4/app/main.c中实现生产者-消费者问题

生产者-消费者问题:

  • 4个生产者,1个消费者同时运行
  • 生产者生产,printf("Producer %d: produce\n", id);
  • 消费者消费,printf("Consumer : consume\n");
  • 任意P、V及生产、消费动作之间添加sleep(128);

读者-写者问题

问题描述:

  • 多个读者进程可以同时读取共享变量,但写者进程必须独占访问。

  • 需要实现两种同步策略:

    • 读者优先(Reader-Priority):只要还有读者在读,写者就必须等待。

    • 读写公平(Fair):避免读者或写者无限等待,确保公平竞争。

Tips

你需要实现SharedVariable以及相应的操作函数,然后完成本问题

哲学家就餐问题

哲学家就餐问题:

  • 5个哲学家同时运行
  • 哲学家思考,printf("Philosopher %d: think\n", id);
  • 哲学家就餐,printf("Philosopher %d: eat\n", id);
  • 任意P、V及思考、就餐动作之间添加sleep(128);
选择

生产者-消费者问题与哲学家就餐问题,二选一完成

基于XCHG指令的自旋锁实现(可选)

下面是一段nasm实现的自旋锁代码

section .text
global _lock
global _unlock

_lock: 
    mov rax, 1
    xchg rax, [rdi]
    cmp rax, 0
    jne _lock

    ret


_unlock:
    mov long[rdi], 0

    ret

这段代码是用 NASM 汇编语言 实现的一个简单的 自旋锁,用于在多线程或多进程环境中保护临界区,确保只有一个线程或进程能够进入临界区。

下面是自旋锁的使用

#include<stdio.h>
#include<pthread.h>

#define NUM 500000

extern void _lock(long* l);
extern void _unlock(long* l);

long l = 0;

int count = 0;

void * worker_func(void *arg)
{

    for (int i = 0; i < NUM; i++) { 
   
        _lock(&l); 
        
        count++;
    
        _unlock(&l);
        
    }

    return NULL;
}

int main(void)
{
    pthread_t worker1, worker2;
    void *worker1_status;
    void *worker2_status;

    pthread_create(&worker1, NULL, worker_func, NULL);
    pthread_create(&worker2, NULL, worker_func, NULL);

    pthread_join(worker1, &worker1_status);
    pthread_join(worker2, &worker2_status);

    printf("Count: %d\n", count);

    return 0;
}

虽然我们的实验没有引入线程的概念,但是也可以使用自旋锁,保护PV操作中关于信号量修改和队列修改时的临界区保护

请你根据课堂上对自旋锁的理解,在内核区域完成自旋锁的实现(开中断的条件下)

代码框架与结果演示

提供的框架结构

lab4-STUID
├── lab
│   ├── app
│   │   ├── main.c
│   │   └── Makefile
│   ├── bootloader
│   │   ├── boot.c
│   │   ├── boot.h
│   │   ├── Makefile
│   │   └── start.S
│   ├── kernel
│   │   ├── include
│   │   │   ├── common
│   │   │   │   ├── assert.h
│   │   │   │   ├── const.h
│   │   │   │   └── types.h
│   │   │   ├── common.h
│   │   │   ├── device
│   │   │   │   ├── disk.h
│   │   │   │   ├── keyboard.h
│   │   │   │   ├── serial.h
│   │   │   │   ├── timer.h
│   │   │   │   └── vga.h
│   │   │   ├── device.h
│   │   │   ├── x86
│   │   │   │   ├── cpu.h
│   │   │   │   ├── io.h
│   │   │   │   ├── irq.h
│   │   │   │   └── memory.h
│   │   │   └── x86.h
│   │   ├── kernel
│   │   │   ├── disk.c
│   │   │   ├── doIrq.S
│   │   │   ├── i8259.c
│   │   │   ├── idt.c
│   │   │   ├── irqHandle.c
│   │   │   ├── keyboard.c
│   │   │   ├── kvm.c
│   │   │   ├── serial.c
│   │   │   ├── timer.c
│   │   │   └── vga.c
│   │   ├── lib
│   │   │   └── abort.c
│   │   ├── main.c
│   │   └── Makefile
│   ├── lib
│   │   ├── lib.h
│   │   ├── syscall.c
│   │   └── types.h
│   ├── Makefile
│   └── utils
│       ├── genBoot.pl
│       └── genKernel.pl
└── report
    └── 231220000.pdf

结果演示

scanf()测试结果 scanf

Semaphore测试结果 sem

SharedVariable测试结果 sharedvariable

作业提交

  • 本次作业需提交可通过编译的实验相关源码与报告,提交前请确认make clean过.
  • 请大家在提交的实验报告中注明你的邮箱, 方便我们及时给你一些反馈信息.
  • 其他问题参看Introduction中的作业规范与提交一章
  • 本实验最终解释权归助教所有

results matching ""

    No results matching ""