When a C program begins execution, the operating system environment is responsible for opening three files and providing file pointers to them:
Examples:
fprintf(stderr, "An error has occurred.\n"); fprintf(stdout, "The output is %d\n", sum); printf("The output is %d\n", sum); /* equivalent to above */
What is the output of the following program?
#include <stdio.h> main(int argc, char *argv[]) { printf("hello there, "); if (fork()) printf("this is parent process %d\n", getpid()); else printf("this is child process %d\n", getpid()); }
Because the output is buffered and then duplicated on the fork(), the output is
hello there, this is parent process 895 hello there, this is child process 8139Buffer will flush when the process exits or when the routine fflush() is used. For example, fflush(stdout), or fflush(stderr). If debugging, always use fflush().
Process coordination or concurrency control deals with mutual exclusion and synchronization.
Mutual exclusion--ensure that two concurrent activities do not access shared data (resource) at the same time, critical region--set of instructions that only one process can execute.
Synchronization--using a condition to coordinate the actions of concurrent activities. A generalization of mutual exclusion.
When considering process coordination, we must keep in mind the following situations:
Suppose processes A and B each need two tape drives to continue, but only one drive has been assigned to each of them. If the system has only 2 drives, neither process can ever proceed.
Consider two cpu bound jobs, one running at a higher priority than the other. The lower priority process will never be allowed to execute. As we shall see, some synchronization primitives lead to starvation.
int x = 0; /* global shared variable (not Unix!) */ ProcessA() { IncrementX(); printf(``X = %d\n'', x); } ProcessB() { IncrementX(); printf(``X = %d\n'', x); } IncrementX() { int Temp; /* local variable */ Temp = x; Temp = Temp + 1; x = Temp; }
If x starts with an initial value of 0, what will its ending value be?
In our example, the three statements in IncrementX are a critical section; once execution in the critical section begins, we must insure that it completes before any other activity executes that critical section.
Because Temp is a variable local to IncrementX, the variable will be different for the two processes.
How to ensure that IncrementX is executed as a critical region? Need primitives to enforce execution as a critical region: BeginRegion() and EndRegion. With mutual exclusion the program is
int x = 0; /* global shared variable */ ProcessA() { IncrementX(); printf(``X = %d\n'', x); } ProcessB() { IncrementX(); printf(``X = %d\n'', x); } IncrementX() { int Temp; /* local variable */ BeginRegion(); /* enter critical region */ Temp = x; Temp = Temp + 1; x = Temp; EndRegion(); /* exit critical region */ }
BeginRegion() { DisableInterrupts(); } EndRegion() { EnableInterrupts(); }
Disabling interrupts has the following disadvantages:
Another approach is to define a boolean variable that is set to ``true'' if some activity is currently executing the critical region, ``false'' otherwise. One (shortsighted!) solution might be:
#define TRUE 1 #define FALSE 0 int mutex; /* also called lock variable */ BeginRegion() /* Loop until safe to enter */ { while (mutex) ; /* do nothing until FALSE */ mutex = TRUE; } EndRegion() /* Exit critical section */ { mutex = FALSE; } BeginRegion(); /* code for critical section */ EndRegion();
Code for BeginRegion() compiles to
lw $14, mutex ; load mutex into register 14 beq $14, 0, $33 ; branch to $33 if mutex is 0 (FALSE) $32: lw $15, mutex ; load mutex into register 15 bne $15, 0, $32 ; branch to $32 if mutex is 1 (TRUE) $33: li $24, 1 ; load a 1 (TRUE) into register 24 sw $24, mutex ; store value into mutex
Do our routines work correctly? No! A process that finds mutex set to FALSE may get past the bne statement but be rescheduled before it actually changes the value of mutex. While it sits on the ready list, another process that test the value of mutex will find its value set to FALSE.
Solution: we need a mechanism for atomically fetching and setting the value of mutex. That is, we want to fetch the value, and if it is FALSE, set it to TRUE, all in one instruction. If such a step takes more than one instruction, a process could be interrupted or rescheduled before it has a chance to finish the job.
int test_and_set(int *pVar, int value) { int temp; temp = *pVar; *pVar = value; return(temp); }
BeginRegion and EndRegion can now be rewritten as:
BeginRegion() /* Loop until safe to enter */ { while (test_and_set(&mutex, TRUE)) ; /* Loop until return value is false */; } EndRegion() { mutex = FALSE; }
Advantages of above approach:
Disadvantage of above approach: CPU busy-waits until it can enter the critical region, wasting resources.
int n = 0; /* shared by all processes */ main() { int produce(), consume(); CreateProcess(produce); CreateProcess(consume); /* wait until done */ } produce() /* ``produce'' values of n */ { int i; for (i=0; i<2000; i++) n++; /* increment n by 1 */ } consume() /* ``Consume'' and print values of n */ { int i; for (i=0; i<2000; i++) printf("n is %d\n", n); /* print value of n */ }
Assume goal is for the consumer to print each value produced. What output does the program generate?
Problem: producer and consumer need to synchronize with each other.
Counting Semaphores are an abstract entity provided by an operating system (not the hardware). Semaphores:
First introduced by Dijkstra (1965) as binary semaphores and the operations were P (wait) and V (signal).
Can be used to implement mutual exclusion:
int sem; sem = semcreate(1); BeginRegion() { wait(sem); } EndRegion() { signal(sem); }
int produced, consumed; /* semaphores */ int n = 0; main() { int produce(), consume(); consumed = semcreate(0); produced = semcreate(1); CreateProcess(produce); CreateProcess(consume); /* wait until done */ } produce() { int i; for (i=0; i<2000; i++) { wait(consumed); n++; /* increment n by 1 */ signal(produced); } } consume() { int i; for (i=0; i<2000; i++) { wait(produced); printf("n is %d\n", n); /* print value of n */ signal(consumed); } }
Now consumer prints all values of n (0-1999).
Things to note:
Advantages of semaphores:
Disadvantages of semaphores:
/* in /cs/cs3013/public/example/prodcons.c */ #include <stdio.h> #include "/cs/cs3013/example/lib/sem.h" int produced, consumed; /* semaphores */ int *pn; main() { int produce(), consume(); pn = (int *)shmcreate(sizeof(int)); *pn = 0; consumed = semcreate(0); produced = semcreate(1); CreateProcess(produce); consume(); semdelete(consumed); semdelete(produced); shmdelete((char *)pn); } produce() { int i; for (i=0; i<5; i++) { semwait(consumed); (*pn)++; /* increment n by 1 */ semsignal(produced); } } consume() { int i; for (i=0; i<5; i++) { semwait(produced); printf("n is %d\n", *pn); /* print value of n */ semsignal(consumed); } }
CreateProcess(int (*pFunc)()) { int pid; if ((pid = fork()) == 0) { (*pFunc)(); exit(0); } return(pid); }