Nos services

Nos provides several services:  semaphore, monitor, message passing, timer

Process communication

There are two ways to communicate (passing some values) between processes:

1  by share variables
2  by message passing

Share variables

A semaphore is used to provide mutual exclusion of access to share variables.  The share variable will be accessed by only one process at a time. (remember that processes can be concurrent therefore at any time there can be more than one process trying to access the same variable). (read OS concept)   send and signal are used in order to access a semaphore.  The following example shows how to share a variable between two processes in synchronisation. The semaphore "mutex" is used to protect the shared variable.  Two semaphores: empty, full, are used to synchronise two processes.

// --- use semaphore to protect share resource ------

empty, full, mutex   // semaphores
shareVar

writer()
  i = 0
  while( i < 3 )
    wait(empty)
    wait(mutex)
    shareVar = shareVar + 1
    signal(mutex)
    signal(full)
    i = i + 1

reader()
  i = 0
  while( i < 3 )
    wait(full)
    wait(mutex)
    print(shareVar)
    signal(mutex)
    signal(empty)
    i = i + 1

p1, p2            // user process

main()
  ...
  shareVar = 0
  empty = initsem()
  full = initsem()
  mutex = initsem()
  p1 = run(writer())
  p2 = run(reader())
  bootnos()

Message passing

The message passing in Nos is implemented as a blocking protocol where the sender and receiver wait until the exchange is completed before continuing.  This is done using two mail-boxes: in-box and await-box. 

send p mess
  if there is a process p wait for it
    put mess to p's buffer
    wakeup p
  else
    block itself
    append itself to p's in-box

receive p
  if there is a process p mail in in-box
    take the message from p's buffer
    wakeup p
  else
    block itself
    append itself to p's await-box

There are two buffers, one in the sender and other in the receiver. The process descriptor is attached to the in-box/await-box so that waking up a process associated with the mail is simple.  The behaviour of mail from a producer/consumer cycle is as follows:

Example of use of send/receive message

// send 2..n to p2 ended with -1
produce()
  n = 10
  i = 2
  while( i < n )
    send(p2,i)
    i = i + 1
  send(p2,-1)

// receive 2..n from p1 ended with -1
consume()
  m = 1
  while( m > 0 )
    m = receive(p1)
    print(m)


Create and run producer and consumer.

main()
  ...
  p1 = run(produce())
  p2 = run(consume())
  bootnos()


Suppose a producer streams the messages (integers) 2..n to a consumer.  The producer's output is marked "!n" and the receiver's output is marked " @n ". The task-switched is marked "*".  The trace is:

!2  * @2  * !3 !4  * @3 @4  * !5 !6  * @5 @6  * !7 !8  * @7 @8  * !9  *  @9 . . .

This behaviour can be explained as follows:

The following is the trace of sending/receiving messages between two processes: s, r.

notation

sM send in-box
sA send await
rM receive in-box
rA receive await
sB sender block
rB receiver block

The trace is:

1 producer: sM sB *
2 consumer: rM rA rB *
3 producer: sA sM sB *
4 consumer: rM rA rB * ...

The first line says that the sender just sent a message to the receiver's in-box then itself is blocked.   The second line is quite interesting.  It says the receiver retrieves the message from the sender's buffer and then continue to execute it's program which does "receive p".  This call makes the receiver to send itself to the sender's await-box, then itself is blocked.  This mean "r" is waiting for a message from "s".  Once "s" wakeups "r", "r" will have its message in its buffer.  Line 3, 4 can be similarly explained.

Timer

To facilitate a real-time system, some operating system functions needed to be supported.  In our system, the real-time clock is the clock of running the processor.

gettime() returns the real-time clock.

timer(t)

set a timer to be time-out at t cycles in the future, not earlier than gettime+t.  timer is used to schedule a task according to some real-time.

How a timer is implemented?

A timer stores its time value as a field in PD. A timer list keeps track of the processes that have been scheduled to time-out in the future by "timer". The time value in PD is an absolute time. When a timer is set to t, the time value in PD is set to gettime+t.  The process that executes "timer" is blocked.  It is removed from the process queue and it is added to the timer list.  The timer list is sorted according to the time values from earliest time to the latest.  This list will be processed by a timer process.

Timer process

The time value in the list is compared to the master time (the global variable clock in the processor simulator).  If it is less than the master time, the owner process of this timer is awaken. As the timer list is sorted in ascending order of time value, only the first one is consulted if it is time-out then the next one is consulted and so on.

The time-out timer process will be queued either at the front of the process queue or the back depends on the scheduling policy. What to do when there is no more process in the process queue?  To simulate the real-time, if the timer list is not empty, and the process queue is empty, then the first process in the timer list should be scheduled to be run.  The master time should be updated to advance to the time value of that process.  This is similar to an ordinary event-driven simulation based on time.  The timer process is run after "switchp".

Granuality of timer

How precise the timer be depends on how often the timer process is scheduled to run.  The overhead depends on this rate.  It is reasonable to have the granuality at most the same as "quanta".  Then, the timer process can be scheduled to run after the task switcher.


last update 26 Jan 2013