[NSWI004] A04 grading and notes

Vojtech Horky horky at d3s.mff.cuni.cz
Thu Dec 10 07:22:04 CET 2020


Hello,

I will be uploading comments for A04 assignment to your grading 
repository shortly.

Apart from few omissions, the solutions were in a good shape. Our 
comments mostly focused on these and on pointing out places for 
potential race conditions.

As usual - if you have any comments/questions, do not hesitate to 
contact us (this ML for general questions and teachers-nswi004 for 
complaints/comments/questions about your grading).


Follows a short overview of common issues that might be interesting to 
more teams even if not directly relevant to their code. They are in no 
particular order.


Ensure that you check fully the cause of the exception. At the moment, 
you were only interested in interrupts (exc. code 0) from the timer (7). 
Hence, there should be two conditions in the handler when the scheduler 
is supposed to be called.


Do not forget to protect shared data structures but do not disable 
interrupts when it is really not needed. For example, a typical 
thread_create:

thread = kmalloc(...)
thread->stack = kmalloc(...)
...
thread->context = ...
...
scheduler_add_ready_thread(thread)

does not need disabled interrupts at all as individual calls (kmalloc 
and scheduler-related) are already protected. It is completely fine if 
the thread is interrupted (and rescheduled) between the calls to kmalloc.

On the other hand, a typical thread_join shall be protected around the 
following fragment

if (thread->joined_by != NULL) { return EBUSY; }
thread->joined_by = thread_get_current()

to ensure that the thread is not rescheduled between these two lines and 
joined_by changed meanwhile. Even if use of thread_join from multiple 
places is not a typical use case.

It is also generally better to enter critical section for the shortest 
time possible. For example, the following prologue of kfree() does not 
need to be protected at all as it does not access any shared data 
structures:


if (ptr == NULL) {
    return;
}
block_header_t* header = ptr - sizeof(block_header_t);
assert(header->magic == HEADER_MAGIC);


Use the right kind of protection of the critical section. Heap can 
easily use mutex as a higher-level primitive that is supposed to work 
even in multiprocessor environment. On the other hand, it is fine to use 
disabling interrupts for a scheduler ready queue. First of all, we do 
not have to go through the recursive reasoning that scheduler is using 
mutex that is using scheduler in its implementation. And next because 
scheduler typically maintains some kind of per-CPU queue and that can be 
easily (and correctly) protected by disabled interrupts only.


I have to stress once again the need and advantages of using the right 
abstraction layers. As a typical example, following three approaches 
were seen when reacting to the timer interrupts: call 
scheduler_handle_timer_interrupt(), scheduler_schedule_next() and 
thread_yield().

In the end, all of them did the same - find the next ready thread and 
switched to it. However, the first one states that it is up to the 
scheduler to decide what to do on the interrupt (i.e. we clearly mark 
the responsibility to belong to the scheduler without caring about the 
implementation). In the second approach, we still mark the scheduler as 
the responsible entity but the abstraction layer is leaking a little bit 
as we reveal what the scheduler is doing. In the last approach, we leak 
the implementation detail that preemption and (voluntary) yield share 
the same implementation.

As another example, if the mutex_lock() contained the following

if (mutex->locked) {
    list_append(...);
    current_thread->state = state_blocked_mutex;
    thread_suspend();
}

then the thread structure members are used without proper encapsulation 
(e.g. without concurrent-access protection) and we also leak abstraction 
detail about thread states.

On an unrelated note: the state space of thread now contains multiple 
states that are treated the same in multiple places - the thread is 
blocked, the reason is irrelevant to most parts of the code. If we would 
need this information for debugging purposes, it would be probably 
better to add extra suspend call where we could pass the object we are 
blocked on and the type of that object (in the following example we 
would just pass the printk directive for debugging prints).

   ...
   thread_block_on_object(mutex, "%pM");
   ...



I hope it makes sense :-).

Cheers,
- VH


More information about the NSWI004 mailing list