The programming project is to implement user-level threads to simulate threads operations in Linux. The project consists of 4 parts. Part 1 presents the base code of the project to help the reader get started. The base code implements a multitasking system, which supports independent executions of tasks within a Linux process. It is the same MT multitasking system presented in Chap. 3, but adapted to the user-level threads environment. Part 2 is to extend the base code to implement support for task join operation. Part 3 is to extend the base code to support mutex operations. Part 4 is to implement counting semaphores to support task cooperation, and demonstrate semaphores in the multitasking system.
1. Project Base Code: A Multitasking System
Programming Project PART 1: Base Code: The following shows the base code of the programming project, which will be explained in latter sections.
(1). ts.s file in 32-bit GCC assembly code:
(2). type.h file: This file defines the system constants and the PROC structure
(3). queue.c file, which implements queue operation functions
// same as in MT system of Chapter 3
(4). t.c file, which is the main file of the program
#include <stdio.h>
#include “type.h”
int init()
{
int i, j;
PROC *p;
for (i=0; i<NPROC; i++){
p = &proc[i]; p->pid = i;
p->priority = 0;
p->status = FREE;
p->event = 0;
p->joinPid = 0;
p->joinPtr = 0;
p->next = p+1;
}
proc[NPROC-1].next = 0;
freeList = &proc[0]; // all PROCs in freeList
readyQueue = 0;
sleepList = 0;
// create P0 as initial running task
running = p = dequeue(&freeList);
p->status = READY;
p->priority = 0;
printList(“freeList”, freeList);
printf(“init complete: P0 running\n”);
}
int texit(int value)
{
printf(“task %d in texit value=%d\n”, running->pid, running->pid);
running->status = FREE;
running->priority = 0;
enqueue(&freeList, running);
printList(“freeList”, freeList);
tswitch();
}
int do_create()
{
int pid = create(func, running->pid); // parm = pid
}
int do_switch()
{
tswitch();
}
int do_exit()
{
texit(running->pid); // for simplicity: exit with pid value
}
void func(void *parm)
{
int c;
printf(“task %d start: parm = %d\n”, running->pid, (int)parm);
while(1){
printf(“task %d running\n”, running->pid);
printList(“readyQueue”, readyQueue);
printf(“enter a key [c|s|q] : “);
c = getchar();
getchar();
switch (c){
case ’c’ : do_create(); break;
case ’s’ : do_switch(); break;
case ’q’ : do_exit(); break;
}
}
}
int create(void (*f)(), void *parm)
{
int i;
PROC *p = dequeue(&freeList); if (!p){
printf(“create failed\n”);
return -1;
}
p->status = READY;
p->priority = 1;
p->joinPid = 0;
p->joinPtr = 0;
// initialize new task stack for it
for (i=1; i<13; i++)
p->stack[SSIZE-i] = 0;
p->stack[SSIZE-1] = (int)parm;
p->stack[SSIZE-2] = (int)do_exit;
p->stack[SSIZE-3] = (int)f;
p->ksp = (int)&p->stack[SSIZE-12];
enqueue(&readyQueue, p);
printList(“readyQueue”, readyQueue);
printf(“task %d created a new task %
return p->pid;
}
int main()
{
printf(“Welcome to the MT User-Level Threads System\n”);
init();
create((void *)func, 0);
printf(“P0 switch to P1\n”);
while(1){
if (readyQueue)
tswitch();
}
}
int scheduler()
{
if (running->status == READY)
enqueue(&readyQueue, running);
running = dequeue(&readyQueue);
printf(“next running = %d\n”, running->pid);
}
To compile and run the base code under Linux, enter
gcc -m32 t.c ts.s
Then run a.out. While running the program, the reader may enter the commands
‘c’: create a new task
‘s’: switch to run the next task from readyQueue
‘q’ : let the running task terminate
to test and observe task executions in the system. Figure 4.6 shows the sample outputs of running the base code program.
2. User-Level Threads
The entire base code program runs as a Linux process in user mode. Inside the Linux process are independent execution entities, which are equivalent to conventional threads. In order not to be confused with either Linux process or threads, we shall refer to the execution entities as tasks. In the system, tasks run concurrently by multitasking. Figure 4.7 shows the model of concurrent tasks inside a Linux process.
Since all the tasks execute inside a Linux process, the Linux kernel does not know their existence. To the Linux kernel, the entire MT system is a single Linux process, but we subdivide the CPU’ s execution time to run different tasks inside the Linux process. For this reason, the tasks are known as user-level threads. The purpose of this programming project is to create a multitasking environment in a Linux process to support user-level threads. The same technique can also be used to create user-level threads under any operating system. Next, we explain the base code in more detail by the following steps.
- Task PROC structure: A major difference between processes and threads is that the former obey a parent-child relation but the latter do not. In the threads model, all threads are equal. Relations between threads are peer-to-peer, rather than parent to child. So there is no need for parent task PID and the task family tree. Therefore, these fields are deleted from the PROC structure. The new fields joinPid and joinPtr are for implementing the threads join operation, which will be discussed later.
- init(): When the system starts, main() calls init() to initialize the system. Init() initializes the PROC structures and enter them into a freeList. It also initializes the readyQueue to empty. Then it uses proc[0] to creates P0 as the initial running task. P0 has the lowest priority 0. All other tasks have a priority 1, so that they will take turn to run from the readyQueue.
- P0 calls create() to create a task P1 to execute the func(parm) function with a parameter parm=0, and enters it into the readyQueue. Then P0 calls tswitch() to switch task to run P1.
- tswitch(): The tswitch() function implements task context switching. Instead of pushl/popl individual CPU registers, it uses the PUSHAL and POPAL instructions. tswitch() acts as a task switch box, where one task goes in and, in general, another tasks emerges. It consists of 3 separated steps, which are explained in more detail below.
-
- SAVE part of tswitch(): When a task calls tswitch(), it saves the return address on its own stack and enters tswitch() in assembly code. In tswitch(), the SAVE part saves CPU registers into the calling task’s stack, save the stack pointer into proc.ksp. The Intel x86 CPU in 32-bit mode has many registers, but only the registers eax, ebx, ecx, edx, esp, ebp, esi, edi and eflags are visible to user mode processes. So we only need to save and restore these registers while a (Linux) process is executing. The following diagram shows the stack contents and the saved stack pointer of the calling task after executing the SAVE part of tswitch(), where xxx denote the stack contents before calling tswitch().
In the Intel x86 based PCs in 32-bit mode, every CPU register is 4 bytes wide and stack operations are always in units of 4 bytes. Thus, we may define the PROC stack as an integer array.
-
- scheduler(): After executing the SAVE part of tswitch(), the task calls scheduler() to pick the next running task. In scheduler(), if the calling task is still READY to run, it calls enqueue() to put itself into the readyQueue by priority. Otherwise, the task will not be in readyQueue, which makes it non-runnable. Then it calls dequeue(), which returns the first ready PROC removed from readyQueue as the next running task.
- RESUME part of tswitch(): When execution returns from scheduler(), running may have changed to point at the PROC of a different task. The RESUME part of tswitch() sets the CPU’s stack pointer to the saved stack pointer of the current running task. Then it pops saved registers, followed by RET, causing the current running task return to where it called tswitch() earlier.
- create(): The create(func, parm) function creates a new task and enters it into the readyQueue.
The new task will begin execution from the named func() function with parm as parameter. Although the new task never existed before, we may pretend it not only existed before but also ran before. The reason why it is not running now is because it called tswitch() to give up CPU earlier. If so, its stack must contain a frame saved by the SAVE part of tswitch(), and its saved ksp must point at the stack top. Furthermore, when a new task begins execution from func(), which is written as func(void *parm), it must have a return address and a parameter parm on the stack. When func() finishes, the task shall “return to where func() was called earlier”. Thus, in create(), we initialize the stack of the new task as follows.
where the index -i means SSIZE-i. These are done by the following code segment in create().
When the new task begins to run, it executes the RESUME part of tswitch(), causing it to return to the entry address of func(). When execution enters func(), the stack top contains a pointer to do_exit () and a parameter parm, as if it was called as func(parm) from the entry address of do_exit(). In practice, the task function rarely returns. If it does, it would return to do_texit(), which causes the task to terminate.
- func(): For demonstration purpose, all tasks are created to execute the same function, func(parm). For simplicity, the function parameter is set to the pid of the creator task, but the reader may pass any parameter, e.g. a structure pointer, to the function. While executing in func(), a task prompts for a command char = [c | s | q], where
‘c’ : create a new task to execute func(parm), with parm=caller pid
‘s’ : switch task;
‘q’ : terminate and return the PROC as FREE to freeList
- The Idle Task P0: P0 is special in that it has the lowest priority among all the tasks. After system initialization, P0 creates P1 and switches to run P1. P0 will run again if and only if there are no runnable tasks. In that case, P0 simply loops. The reader may enter Control-C to terminate the (Linux) process. It is noted that the stack of P0 is actually the user mode stack of the Linux process.
3. Implementation of Thread Join Operation
PART 2: Extend Base Code to support task join operation. The extensions are listed in order below.
- tsleep(int event): Whenever a task must wait for something that’s not available, it calls tsleep() to
go to sleep on an event value, which represents the reason of the sleep. The algorithm of tsleep() is
A sleeping task is not runnable until it is woken up by another task. So in tsleep(), the task calls tswitch() to give up the CPU.
- int twakeup(int event): twakeup() wakes up ALL tasks that are sleeping on the specified event value. If no task is sleeping on the event, it does nothing. The algorithm of twakeup() is
/*********** Algorithm of twakeup(int event) **********/
for each PROC *p in sleepList do{ // assume sleepers are in a global sleepList
if (p->event == event){
delete p from sleepList;
p->status = READY; // make p READY to run again
enqueue(&readyQueue, p); // enter p into readyQueue by priority
}
}
- texit(int status): Modified texit() in the base code for tasks to terminate. In the process model, every process has a unique parent (which may be the INIT process P1), which always waits for the process to terminate. So a terminating process can become a ZOMBIE and wakes up its parent, which will dispose of the ZOMBIE child. If a terminating process has children, it must give away the children to either the INIT process or a subreaper process before becoming a ZOMBIE. In the threads model, threads are all equal. A thread does not have a parent or children. When a thread terminates, if there is no other threads waiting for it to terminate, it must exit as FREE. If there is any thread waiting for it to terminate, it should become a ZOMBIE, wake up all such threads, allowing one of them to find and dispose of the ZOMBIE. In the threads model, the thread termination algorithm is
- join(int targetPid, int *status): The join operation waits for a thread with targetPid to terminate. In the process model, every process can only be waited by a unique parent, and a process never waits for its own parent to terminate. So the waiting sequence is always a unidirectional chain. In the threads model, a thread is allowed to join with any other thread. This may cause two problems. First, when a thread tries to join with a target thread, the target thread may no longer exist. In this case, the joining thread must return with an error. If the target thread exists but has not terminated yet, the joining thread must go to sleep on the target thread PID, waiting for the target thread to terminate. Second, since threads do not obey the parent-child relation, a sequence of joining threads may lead to a deadlock waiting loop. In order to prevent such deadlocks, every joining thread must record the identity of the target thread in its PROC structure to allow other joining threads to check whether the join request would lead to a deadlock. When the targeted thread terminates, it can check whether there are any threads waiting for it to terminate. If so, it would become a ZOMBIE and wake up all such joining threads. Upon waking up, the joining threads must execute the join operation again since the ZOMBIE may already be freed by another joining thread. Thus, the algorithm of the thread join operation is as follows.
/********* Algorithm of join(int targetPid, int *status) *********/
while(1)
{
-
- if (no task with targetPid) return NOPID error;
- if (targetPid’s joinPtr list leads to this task) return DEADLOCK error;
- set running->joinPid = targetPid, running->joinPtr ->targetPid’s PROC;
- if (found targetPid’s ZOMBIE proc *p){
*status = p->exitStatus; // extract ZOMBIE exit status
p->status = FREE;
p->priority = 0;
enqueue(&freeList, p); // release p to freeList
return p->pid;
}
-
- tsleep(targetPid); // sleep on targetPID until woken up by target task
}
Steps 2 of the join() algorithm prevents deadlock due to either crossed or circular joining requests, which can be checked by traversing the targetPid’s joinPtr pointer list. Each joining task’s joinPtr points to a target PROC it intends to join with. If the list points back to the current running task, it would be a circular waiting list, which must be rejected due to a potential deadlock.
Programming Project PART 2: Part 2 of the programming project is for the reader to implement the tsleep(), twakeup() and join() functions. Then test the resulting system by the following program code, which uses task creation and join operations of the programming project.
/************ part 2 of Programming Project ***********/
#include <stdio.h>
#include <stdlib.h>
#include “type.h”
PROC proc[NPROC];
PROC *freeList;
PROC *sleepList;
PROC *readyQueue;
PROC *running;
/****** implement these functions ******/
int tsleep(int event){ }
int twakeup(int event){ }
int texit(int status){ }
int join(int pid, int *status){ }
/****** end of implementations *********/
int init(){ } // SAME AS in PART 1
int do_exit()
{
// for simplicity: exit with pid value as status
texit(running->pid);
}
void task1(void *parm) // task1: demonstrate create-join operations
{
int pid[2]; int i, status;
//printf(“task %d create subtasks\n”, running->pid);
for (i=0; i<2; i++){ // P1 creates P2, P3
pid[i] = create(func, running->pid);
}
join(5, &status); // try to join with targetPid=5
for (i=0; i<2; i++){ // try to join with P2, P3
pid[i] = join(pid[i], &status);
printf(“task%d joined with task%d: status = %d\n”,
running->pid, pid[i], status);
}
}
void func(void *parm) // subtasks: enter q to exit
{
char c;
printf(“task %d start: parm = %d\n”, running->pid, parm);
while(1){
printList(“readyQueue”, readyQueue);
printf(“task %d running\n”, running->pid);
printf(“enter a key [c|s|q|j]: “);
c = getchar(); getchar(); // kill \r
switch (c){
case ’c’ : do_create(); break;
case ’s’ : do_switch(); break;
case ’q’ : do_exit(); break;
case ’j’ : do_join(); break;
}
}
}
int create(void (*f)(), void *parm)
{
int i;
PROC *p = dequeue(&freeList); if (!p){
printf(“create failed\n”);
return -1;
}
p->status = READY; p->priority = 1;
p->joinPid = 0;
p->joinPtr = 0;
for (i=1; i<13; i++)
p->stack[SSIZE-i] = 0;
p->stack[SSIZE-1] = (int)parm;
p->stack[SSIZE-2] = (int)do_exit;
p->stack[SSIZE-3] = (int)f;
p->ksp = &p->stack[SSIZE-12];
enqueue(&readyQueue, p);
printList(“readyQueue”, readyQueue);
printf(“task%d created a new task%d\n”, running->pid,
return p->pid;
}
int main()
{
int i, pid, status;
printf(“Welcome to the MT User-Threads System\n”);
init();
create((void *)task1, 0);
printf(“P0 switch to P1\n”); tswitch();
printf(“All tasks ended: P0 loops\n”);
while(1);
}
int scheduler()
{
if (running->status == READY)
enqueue(&readyQueue, running);
running = dequeue(&readyQueue);
}
In the test program, P0 creates a new task P1, which executes the function task1(). When P1 runs, it creates two new tasks P2 and P3 to execute the function func(). P1 first tries to join the targetPid =5. Then it tries to join with both P2 and P3. Figure 4.8 shows the sample outputs of running Part 2 of the Project Program, which demonstrates task join operations. As the figure shows, when P1 tries to join with P5, it results in a join error due to invalid targetPid. Then P1 tries to join with task 2, which causes it to wait. When P2 runs, it tries to join with P1, which is rejected because this would lead to a deadlock. Then P2 exits, which switches task to run P3. When P3 exits, P1 is still waiting to join with P2, so P3 exits as FREE, allowing P1 to complete its join operation with P2. When P1 tries to join with P3, it gets an invalid targetPid error since P3 no longer exists. Instead of letting P3 exit first, the reader may let P3 switch to P1, which will dispose of the ZOMBIE P2 and then tries to join with P3. In that case, when P3 exits, P1 will complete its join operation with P3 successfully. The reader may modify the test program to create more tasks and enter different command sequences to test the system.
4. Implementation of Mutex Operations
Programming Project PART 3: Extend Part 2 of the project to support mutex operations. The extensions are described below.
- Mutex Structure
typedef struct mutex{
int lock; // mutex lock state: 0 for unlocked, 1 for locked
PROC *owner; // pointer to owner of mutex; may also use PID
PROC *queue; // FIFO queue of BLOCKED waiting PROCs
}MUTEX;
MUTEX *mutex_create() // create a mutex and initialize it
{
MUTEX *mp = (MUTEX *)malloc(sizeof(MUTEX));
mp->lock = mp->owner = mp->queue = 0;
return mp;
}
void mutex_destroy(MUTEX *mp){ free(mp); }
int mutex_lock(MUTEX *mp){// implement mutex locking operation }
int mutex_unlock(MUTEX *mp){// implement mutex unlocking operation }
/******** Algorithm of mutex_lock() ************/
(1) if (mutex is in unlocked state){
change mutex to locked state;
record caller as mutex owner;
}
(2). else{
BLOCK caller in mutex waiting queue;
switch task;
}
/******* Algorithm of mutex_unlock() *********/
(1). if (mutex is unlocked ||(mutex is locked && caller NOT owner))
return error;
(2). // mutex is locked && caller is owner
if (no waiter in mutex waiting queue){
change mutex to unlocked state;
clear mutex owner field to 0;
}
(3). else{ // mutex has waiters
PROC *p = dequeue a waiter from mutex waiting queue;
change mutex owner to p; // mutex remains locked
enter p into readyQueue;
}
5. Test Project with Mutex by Concurrent Programs
Part 3: Implement mutex_lock() and mutex_unlock() functions for the MT system. Test mutex operations by the following program. The test program is a modified version of the Example 4.3 program using Pthreads. It computes the sum of the elements of a matrix by concurrent tasks and mutex of the MT system. In the test program, P0 initializes a 4 x 4 matrix, creates P1 to execute task1(). P1 creates 4 working tasks to execute the function func(parm). Each working task computes the partial sum of a row and updates the global variable total using mutex for synchronization. The outputs should look like Fig. 4.9.
/************ test program for mutex operations ************/
#include <stdio.h>
#include <stdlib.h>
#include “type.h”
PROC proc[NPROC], *freeList, *sleepList, *readyQueue, *running;
#include “queue.c” // queue operation functions
#include “wait.c” // tsleep/twakeup/texit/join functions
#include “mutex.c” // mutex operation functions
#define N 4
int A[N][N];
MUTEX *mp;
int total;
int init()
{
int i, j;
PROC *p;
for (i=0; i<NPROC; i++){
p = &proc[i];
p->pid = i;
p->ppid = 1;
p->priority = 0;
p->status = FREE;
p->event = 0;
p->next = p+1;
}
proc[NPROC-1].next = 0;
freeList = &proc[0];
readyQueue = 0;
sleepList = 0;
p = running = dequeue(&freeList);
p->status = READY;
p->priority = 0;
printList(“freeList”, freeList);
printf(“P0: initialize A matrix\n”);
for (i=0; i<N; i++){
for (j=0; j<N; j++){
A[i][j] = i*N + j + 1;
}
for (i=0; i<N; i++){ // show the matrix
for (j=0; j<N; j++){
printf(“%4d “, A[i][j]);
}
printf(“\n”);
}
mp = mutex_create(); // create a mutex total = 0;
printf(“init complete\n”);
}
int myexit() // for task exit
{
texit(0);
}
void func(void *arg)
{
int i, row, s;
int me = running->pid;
row = (int)arg;
printf(“task %d computes sum of row %d\n”, me, s = 0;
for (i=0; i < N; i++){
s += A[i];
}
printf(“task %d update total with %d\n”, me, s) mutex_lock(mp);
total += s;
printf(“[total = %d] “, total); mutex_unlock(mp);
}
void task1(void *par^)
{
int pid[N];
int i, status;
int me = running->pid;
printf(“task %d: create working tasks : “, me);
for(i=0; i < N; i++) {
pid[i] = create(func, (void *)i);
printf(“%d “, pid[i]);
}
printf(” to compute matrix row sums\n”); for(i=0; i<N; i++) {
printf(“task %d tries to join with task %d\n” running->pid, pid[i]);
join(pid[i], &status);
printf(“task %d : total = %d\n”, me, total);
}
int create(void (*f)(), void *parm)
{
int i;
PROC *p = dequeue(&freeList); if (!p){
printf(“fork failed\n”);
return -1;
}
p->ppid = running->pid;
p->status = READY;
p->priority = 1;
p->joinPid = 0;
p->joinPtr = 0;
for (i=1; i<12; i++){
p->stack[SSIZE-i] = 0;
p->stack[SSIZE-1] = (int)parm;
p->stack[SSIZE-2] = (int)myexit;
p->stack[SSIZE-3] = (int)f;
p->ksp = &p->stack[SSIZE-12];
}
enqueue(&readyQueue, p); return p->pid;
}
int main()
{
printf(“Welcome to the MT User-Level Threads System\n”);
init();
create((void *)task1, 0);
//printf(“P0 switch to P1\n”);
tswitch();
printf(“all task ended: P0 loops\n”);
while(1);
}
int scheduler()
{
if (running->status == READY)
enqueue(&readyQueue, running);
running = dequeue(&readyQueue);
}
Figure 4.9 shows the sample outputs of running the test program of mutex.
6. Implementation of Semaphores
Programming Project PART 4: Implement counting semaphores as described in Sect. 4.6.5. Demonstrate semaphores by the following test program, which is the producer-consumer problem of Example 4.5 adapted to the MT system.
7. Producer-Consumer Problem using Semaphores
/********** test program for semaphore operations ********/
#include <stdio.h>
#include “type.h”
PROC proc[NPROC], *freeList, *sleepList, *readyQueue, *running;
#include “queue.c” // queue operation function
#include “wait.c” // tsleep/twakeup/texit/join functions
#define NBUF 4
#define N 8
int buf[NBUF], head, tail; // buffers for producer-consumer
typedef struct{
int value;
PROC *queue;
}SEMAPHORE;
SEMAPHORE full, empty, mutex; // semaphores
int P(SEMAPHORE *s)
{ // implement P function }
int V(SEMAPHORE *s)
{ // implement V function }
void producer() // produce task code
{
int i;
printf(“producer %d start\n”, running->pid);
for (i=0; i<N; i++){
P(&empty);
P(&mutex); buf[head++] = i + 1;
printf(“producer %d: item = %d\n”, running->pid, i + 1);
head %= NBUF;
V(&mutex);
V(&full);
}
printf(“producer %d exit\n”, running->pid);
}
void consumer() // consumer task code
{
int i, c;
printf(“consumer %d start\n”, running->pid);
for (i=0; i<N; i++) {
P(&full);
P(&mutex);
c = buf[tail++];
tail %= NBUF;
printf(“consumer %d: got item = %d\n”, running->pid, c);
V(&mutex);
V(&empty);
}
printf(“consumer %d exit\n”, running->pid);
}
int init()
{
int i, j;
PROC *p;
for (i=0; i<NPROC; i++){
p = &proc[i];
p->pid = i;
p->ppid = 1;
p->priority = 0;
p->status = FREE;
p->event = 0;
p->next = p+1;
}
proc[NPROC-1].next = 0;
freeList = &proc[0];
readyQueue = 0;
sleepList = 0;
p = running = dequeue(&freeList);
p->status = READY; p->priority = 0;
printList(“freeList”, freeList);
// initialize semaphores full, empty, mutex head = tail = 0;
full.value = 0; full.queue = 0;
empty.value=NBUF; empty.queue = 0;
mutex.value = 1; mutex.queue = 0;
printf(“init complete\n”);
}
int myexit(){ texit(0); }
int task1()
{
int status;
printf(“task %d creates producer-consumer tasks\n”, running->pid);
create((void *)producer, 0);
create((void *)consumer, 0);
join(2, &status);
join(3, &status);
printf(“task %d exit\n”, running->pid);
}
int create(void (*f)(), void *par^)
{
int i;
PROC *p = dequeue(&freeList); if (!p){
printf(“fork failed\n”);
return -1;
}
p->ppid = running->pid;
p->status = READY;
p->priority = 1;
p->joinPid = 0;
p->joinPtr = 0;
for (i=1; i<12; i++){
p->stack[SSIZE-i] = 0;
p->stack[SSIZE-1] = (int)parm;
p->stack[SSIZE-2] = (int)myexit;
p->stack[SSIZE-3] = (int)f;
p->ksp = &p->stack[SSIZE-12];
}
enqueue(&readyQueue, p); return p->pid;
}
int main()
{
printf(“Welcome to the MT User-Level Threads System\n”); init();
create((void *)task1, 0);
printf(“P0 switch to P1\n”); tswitch();
printf(“all task ended: P0 loops\n”);
while(1);
}
int scheduler()
{
if (running->status == READY)
enqueue(&readyQueue, running);
running = dequeue(&readyQueue);
}
Figure 4.10 shows the sample outputs of the producer-consumer problem using semaphores.
Solutions to the programming project are available online for download. Source code of the programming project is available for instructors upon request.
Source: Wang K.C. (2018), Systems Programming in Unix/Linux, Springer; 1st ed. 2018 edition.