0%

Lab Thread

Lab Thread

前置知识

一、进程切换中的状态保存

在常规的强制进程切换时,需要kernel的调配。所以需要先切换到内核态,然后通过scheduler函数来切换到目标进程对应的内核态,然后再切换到user。在这个过程中,进程的切换需要保存的是context,而user和kernel的切换需要保存的是trapframe。

Task: Uthread: switching between threads

这个task要求我们实现一个用户级的进程切换。注意在这种情况下,进程自愿放弃cpu并转让给另一个需要cpu的进程。这个过程中kernel是完全不知道的,因此不需要保存trapframe,stack之类的麻烦东西,只需要一个类似context的数据结构来保存部分寄存器就好。

  1. 首先,模仿context创建一个结构体来保存寄存器状态。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct ucontext {
    uint64 ra;
    uint64 sp;

    // callee-saved
    uint64 s0;
    uint64 s1;
    uint64 s2;
    uint64 s3;
    uint64 s4;
    uint64 s5;
    uint64 s6;
    uint64 s7;
    uint64 s8;
    uint64 s9;
    uint64 s10;
    uint64 s11;
    };

    struct thread {
    char stack[STACK_SIZE]; /* the thread's stack */
    int state; /* FREE, RUNNING, RUNNABLE */
    struct ucontext ucontext;
    };
  2. 模仿swtch中的汇编代码进行寄存器保存(其实是直接拷贝过来)
    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
    	.text

    /*
    * save the old thread's registers,
    * restore the new thread's registers.
    */

    .globl thread_switch
    thread_switch:
    sd ra, 0(a0)
    sd sp, 8(a0)
    sd s0, 16(a0)
    sd s1, 24(a0)
    sd s2, 32(a0)
    sd s3, 40(a0)
    sd s4, 48(a0)
    sd s5, 56(a0)
    sd s6, 64(a0)
    sd s7, 72(a0)
    sd s8, 80(a0)
    sd s9, 88(a0)
    sd s10, 96(a0)
    sd s11, 104(a0)

    ld ra, 0(a1)
    ld sp, 8(a1)
    ld s0, 16(a1)
    ld s1, 24(a1)
    ld s2, 32(a1)
    ld s3, 40(a1)
    ld s4, 48(a1)
    ld s5, 56(a1)
    ld s6, 64(a1)
    ld s7, 72(a1)
    ld s8, 80(a1)
    ld s9, 88(a1)
    ld s10, 96(a1)
    ld s11, 104(a1)
    /* YOUR CODE HERE */
    ret /* return to ra */
  3. 在thread_schedule里填空,即调用thread_switch。
    1
    thread_switch((uint64)&t->ucontext, (uint64)&current_thread->ucontext);
  4. 在thread_create函数中填空,记录当前进程再次进行加载时的地址,记录当前栈的顶端(注意,栈向下增长),最后设置状态为RUNNABLE
    1
    2
    3
    4
    t->ucontext.ra = (uint64)func;
    t->ucontext.sp = (uint64)t->stack + STACK_SIZE;
    t->state = RUNNABLE;
    // YOUR CODE HERE

Task: Using threads

这个task旨在加深我们对freelist中因为并行导致的list丢失问题的理解。为了避免这种情况,我们需要适当加锁。

  1. 首先来简答一下问题。key丢失是因为当两个cpu同时将不同的key指向链表的next时,链表的head只会指向最后的做修改head.next操作的key,另一个key就丢失了。
  2. 首先,定义lock数组并初始化(注意,你不能把锁定义在entry结构体里,但是你或许可以把它和key定义在一起)
    1
    2
    3
    4
    5
    6
    7
    pthread_mutex_t lock[NKEYS];

    void lockinit()
    {
    for (int i = 0; i < NKEYS; i++)
    pthread_mutex_init(&lock[i], NULL);
    }
  3. 通过浏览源码可知,对key的插入在put函数中,对key的读取在get函数中,因此我们应该在这两个函数里加锁。但我并没有修改get,因为key使用头插法,在整个插入过程中最后修改的是head,在并行中唯一可能导致的问题就是读不到正在写的那个key。
    1
    2
    3
    4
    // put()
    pthread_mutex_lock(&lock[i]);
    insert(key, value, &table[i], table[i]);
    pthread_mutex_unlock(&lock[i]);

Task: barrier

这个task要求我们写一个barrier函数,当有进程呼叫这个函数的时候就让这个进程sleep,直到呼叫这个函数的进程数量累计达到一定量后再统一唤醒。

  1. 首先在main中初始化锁。
    1
    pthread_mutex_init(&bstate.barrier_mutex, NULL); // init the lock
  2. 完善barrier函数,如果有进程呼叫就添加计数并沉睡进程,当计数累计到指定数量就全部清零,增加”barrier的轮数”,并唤醒所有进程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    static void 
    barrier()
    {
    // YOUR CODE HERE
    //
    // Block until all threads have called barrier() and
    // then increment bstate.round.
    //
    pthread_mutex_lock(&bstate.barrier_mutex);
    bstate.nthread ++;
    if (bstate.nthread == nthread) {
    bstate.nthread = 0;
    bstate.round++;
    pthread_cond_broadcast(&bstate.barrier_cond);
    }
    else {
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
    }
    pthread_mutex_unlock(&bstate.barrier_mutex);

    }