Synchronisation Exercise

From COMP15212 Wiki
Revision as of 10:22, 9 August 2019 by gravatar W81054ch [userbureaucratinterface-adminsysopPHRhYmxlIGNsYXNzPSJ0d3BvcHVwIj48dHI+PHRkIGNsYXNzPSJ0d3BvcHVwLWVudHJ5dGl0bGUiPkdyb3Vwczo8L3RkPjx0ZD51c2VyPGJyIC8+YnVyZWF1Y3JhdDxiciAvPmludGVyZmFjZS1hZG1pbjxiciAvPnN5c29wPGJyIC8+PC90ZD48L3RyPjwvdGFibGU+] (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
On path: Exercises 0: Exercises • 1: Pointer Exercise • 2: Arguments Exercise • 3: Malloc Exercise • 4: Structs Exercise • 5: Processes Exercise • 6: Shared memory Exercise • 7: Pipes Exercise • 8: Exceptions Exercise • 9: Synchronisation Exercise • 10: Files Exercise • 11: Threads Exercise • 12: Unix proc Exercise
Depends on Unix SignalsSleep

Download exercise files

The purposes of this exercise are:

Before properly starting this exercise, a quick illustration of process operation. The exact results you get may vary slightly according to the system, but in a Unix shell (terminal) type: ps which should print a list of the processes running there. One will be the shell (such as sh, ksh, bash etc.); another should be ps which is doing the printing. (There may be others if you have run background jobs from that shell.) Alongside there should be the PID (Process IDentifier), a unique process number. (The process name is inadequate as you can run multiple copies of one program. Try ps -A.)

Now try ps again in the same shell. The shell will have the same PID but ps will be different because it is a new process. It will probably have a similar but higher number allocated.

This reveals something of the operation of executing an application from a shell: the shell creates a new (largely independent) process, then waits for it to finish, then continues.

If you don’t want the shell to wait, add & on the end of the command line and it won’t – but with applications like ps it will then tangle any ‘command prompt’ with the application’s output.

To illustrate, these exercises run a ‘parent’ process (under the shell) which starts its own family of child processes.

Part 1

Compile and execute synchronisation.c

As a bonus, this prints its own and its parent PID. Processes know who created them: there is only ever one parent.

You should have already seen that a parent receives its child’s PID when it forks: one parent can have many child processes. That happens again here and the processes are directed down different code sequences by the if. It is not deterministic which will run first but pretty much certain that the sleep() call will make the children terminate later. (The sleep() might represent some time-consuming processing in another application.)

Have a few goes: you will probably see some differences in scheduling.

This is merely messy here because the parent terminating will wake up the shell which will print the prompt before the child says “Goodbye”. In other circumstances it may be more important to ensure there is ordering.

Uncomment the waitpid() call; you can ignore the syntax, at least for now. This call waits for a change in state of the selected child process – in this case its termination. If you run this version you will see that the selected child always terminates before its parent. Similarly, the parent process termination signals to a similar wait in the shell which puts the prompt back again. The shell can be scheduled any time after the parent’s termination.

Note: the sleep() call suspends a process for a time; This is another scheduling issue and is examined in more detail elsewhere.

Part 2

Change the argument of the sleep from ‘1’ to something more interesting like i or (FAMILY - i). This will show up the causality better. Try it (a couple of times).

Now replace the waitpid() call with the code from barrier.c. This introduces a synchronisation barrier in the parent process. In this case the wait() is non-specific: it responds to a SIGCHLD signal from any child process. The signal source is given as the return value and the variable status (passed by reference) is used to indicate any reason the process went wrong. An exit(0); will return a NULL status; anything else will return a non-NULL code – which is not distinguished in this code fragment.

Here, the barrier is implemented by collecting the same number of SIGCHLD signals – in any order – as there were processes created before proceeding. This is a bit crude – it would be better to check off separate, legitimate terminations for each child – but is serves to illustrate the principle.

Part 3

“Prolicide” is the act of killing one’s own children.

Compile and run prolicide.c. This has a badly behaved child, which does not terminate. As the parent is waiting, it can’t terminate either.

If you ran that you’d better try ^C.

Uncomment the /* */ line with the kill() call, and try again. This time everything should terminate correctly and in a well defined order because the parent still waits for the child’s termination.

Although appropriate in this case, kill() is really a call which sends a ‘signal’ of any form. The signal here is SIGTERM – the same (default) signal sent by ^C from a shell – and the result is the same. This example traps that exception to print a final, accusatory message.

If you want to experiment further, try killing using SIGKILL instead; that signal is not – and indeed cannot – be intercepted. (This is used, for example, by kill -9 in a command line.)

If/when you have to employ these techniques prefer signals such SIGTERM if possible as it allows interception and the orderly release of any resources the process might have. SIGKILL can bypass this and may thus leave things in an untidy state.   For example, if there is a shared memory it does not specifically belong to the process so the operating system can’t free it if that would be appropriate and it can therefore remain ‘forever’.


Results

Try to record the outputs of the various parts above into text files. You should have more than one run of each to show some differences in ordering (as the process scheduler is liable to have made different choices) and include something to identify the various parts. Leave any shell prompts in place, to show where that process runs too.

Here’s a partial example suggestion:

----------------------------------------
 Part 1:
 
 ./synchronisation
 Hello from ./synchronisation with PID 11468 whose parent is 11462
 I am 11469, child 0 of 11468
 I am 11470, child 1 of 11468
 I am 11471, child 2 of 11468
 I am 11473, child 4 of 11468
 I am 11472, child 3 of 11468
 I am 11474, child 5 of 11468
 I am 11475, child 6 of 11468
 I am 11476, child 7 of 11468
 I am 11477, child 8 of 11468
 I am 11478, child 9 of 11468
 [11468]: I now have 10 children:
    11469
    11470
    11471
    11472
    11473
    11474
    11475
    11476
    11477
    11478
 
 [11468]: Goodbye from parent process.
 jdg-synchronisation-) 
 Goodbye from child 0 with PID 11469
 
 Goodbye from child 1 with PID 11470
 
 Goodbye from child 2 with PID 11471
 
 ... etc ...