﻿# Simulation tasks: everything together

> Four practice tasks in the style of the practical test – a vaccination day, a thesis registration, a press conference and a masked quiz – each a story whose characters are processes that signal, talk through pipes and message queues and share memory under a semaphore.

Source: https://laszlonemes.com/university/operating-systems/simulation-tasks · Author: László Nemes

Practice · Operating Systems

Four practice tasks in the style of the practical test – a vaccination day, a thesis registration, a press conference and a masked quiz – each a story whose characters are processes that signal, talk through pipes and message queues and share memory under a semaphore.

15 min read `Story → processes` `The common skeleton` `Merged signals` `Write, then announce` `Four practice tasks`

The end of the semester puts every tool together. A simulation task tells a small story – a clinic, a quiz show, a press conference – and every character in it is a process. The characters signal, talk through pipes and message queues, and share data in shared memory guarded by a semaphore. Your program plays the story out.

The four tasks here are practice tasks in the style of the practical test, not the test itself. Each has the same structure: a story, then numbered task blocks that build on each other. Try each one with a timer before opening the hints; the hints give the architecture, the order of the steps and a few key lines – never the whole program.

After this session you can

- turn a story into processes: who is the parent, who are the children, who talks to whom
- pick the right channel for each step: signal, pipe, message queue or shared memory with a semaphore
- write the common skeleton – handler, blocked signal, IPC objects before fork, cleanup – from memory
- avoid the two classic traps: merged signals and reading shared memory before it was written
- finish the task blocks in order, each one complete and runnable

## How a simulation task works

You get a story and a few task blocks. The first block is the basic task; every further block that is correct and **complete** raises the grade by one, so a finished block 2 is worth more than a half-done block 3.

Read the story once for the characters and once for the verbs. Characters become processes: usually one parent and two children. Verbs become channels:

| The story says | Use | Covered in |
| --- | --- | --- |
| "signals that it is ready", "gives a sign" | `kill` + a handler, `sigsuspend` to wait | [Signals](https://laszlonemes.com/university/operating-systems/signals) |
| "sends through a pipe", "tells", "asks" | a `pipe` per direction | [Pipes and message queues](https://laszlonemes.com/university/operating-systems/pipes-message-queues) |
| "sends through a message queue", "reports back" | `msgsnd` / `msgrcv` with an `mtype` | [Pipes and message queues](https://laszlonemes.com/university/operating-systems/pipes-message-queues) |
| "writes to shared memory", "stores in a variable" | a struct in `shmget` memory | [Shared memory and semaphores](https://laszlonemes.com/university/operating-systems/shared-memory-semaphores) |
| "protect it with a semaphore" | `semget`, `SETVAL`, P/V around the access | [Shared memory and semaphores](https://laszlonemes.com/university/operating-systems/shared-memory-semaphores) |

## The common skeleton

All four solutions share one skeleton (Fig. 1). Before any `fork`, the parent installs the signal handler, blocks the signal and creates every pipe and IPC object, so the children inherit them. Each child is a function that forks and returns the PID in the parent, and does its job and exits in the child.

> **Fig. 1** · Set up, fork one child at a time, wait for its signal, work, clean up.
>
> *Diagram:* The skeleton shared by all four tasks: the parent installs the handler and blocks the signal, creates every pipe and IPC object, then forks the children one at a time and waits for each one's ready signal. After the task's steps it waits for both children and removes the IPC objects.

the parent, before any fork

```c
volatile sig_atomic_t ready = 0;
void starthandler(int sig) { if (sig == SIGUSR1) ready++; }

struct sigaction sa;
sa.sa_handler = starthandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);

sigset_t mask, oldmask;              // block SIGUSR1 BEFORE fork: a signal that
sigemptyset(&mask);                  // arrives early stays pending, it isn't lost
sigaddset(&mask, SIGUSR1);
sigprocmask(SIG_BLOCK, &mask, &oldmask);
```

the start of every child function

```c
pid_t process = fork();
if (process > 0)
    return process;                  // parent: back to main with the child's PID

sigset_t empty;                      // child: the blocked mask was inherited
sigemptyset(&empty);
sigprocmask(SIG_SETMASK, &empty, NULL);
kill(mainProcessValue, SIGUSR1);     // "I am ready"
```

the parent: one child at a time

```c
pid_t child1_pid = ursula(io_pipes[0], io_pipes1[1]);
while (ready < 1)
    sigsuspend(&oldmask);            // unblock and sleep, atomically

pid_t child2_pid = csormester(io_pipes2[0], io_pipes3[1]);
while (ready < 2)
    sigsuspend(&oldmask);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
```

the parent, at the end

```c
do {
    finished = waitpid(child1_pid, &child_status, 0);
} while (finished == -1 && errno == EINTR);
/* ... the same for child 2, close the pipes ... */
msgctl(messageQueue, IPC_RMID, NULL);
shmctl(sh_mem_id, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
```

In your assignment

**Write the skeleton first**

In the practical test, write and compile the skeleton before reading block 2: handler, mask, two children that signal, `waitpid`, cleanup. If that runs – both "ready" lines appear, the program ends, `ipcs` is empty – block 1 is mostly done, and every later block only adds channels.

## Two traps

Both traps make a program that works on most runs and fails on some – the worst kind of bug in a timed test.

> **Fig. 2** · Standard signals are not counted: two of the same kind can arrive as one.
>
> *Diagram:* Two identical standard signals do not add up. If both children send SIGUSR1 while the parent has it blocked, the kernel keeps a single pending SIGUSR1, the handler runs once, ready stays 1, and a loop waiting for ready to reach 2 waits forever.

Common mistake

**Two children, one signal, ready never reaches 2**

While `SIGUSR1` is blocked, the kernel remembers only *that* it is pending, not how many times it was sent (Fig. 2). If both children were forked in a row and signal at nearly the same moment, the handler may run once, and `while (ready < 2)` waits forever. Fork the second child only after the first one's signal has arrived – as in the skeleton – or give each child its own signal.

> **Fig. 3** · The message carries the order; the semaphore only prevents overlap.
>
> *Diagram:* A semaphore initialised to 1 only keeps the two processes from using the shared memory at the same time. The order comes from the message: the writer fills the shared memory first and sends the message after it, and the reader's msgrcv blocks until the message is there – so its read always sees the new value.

Common mistake

**Announcing before writing**

A semaphore with initial value 1 keeps two processes out of the shared memory at the same time, but it does not decide who goes first. If a child sends its message and only then writes the shared memory, the parent can receive the message, take the lock first, and read a value that isn't there yet (Fig. 3). Write the shared memory first, then send the message or pipe data that the other side waits for.

Good to know

**msgsnd counts the text, not the type**

The size argument of `msgsnd` and `msgrcv` is the size of the data *after* the `long mtype` field – `strlen(text) + 1` for a string, `sizeof(char)` for a single character – never `sizeof(struct)`.

## Task 1 – Vaccination

Task 1

**Dr. Bubó's vaccination day**

A virus is spreading among the animals too. Thanks to Lion, every animal clinic has the vaccine, so Dr. Bubó (the parent), his assistant Ursula (a child) and his ever-loyal helper Csőrmester (another child) organise a fast vaccination day.

1. Dr. Bubó opens the surgery. He asks Ursula and Csőrmester to take their places (the children start) and to signal back that vaccination can begin. Dr. Bubó gets the number of waiting patients as a command-line argument and splits them fairly: through a pipe each assistant learns how many patients to vaccinate – with an odd number, Csőrmester gets one more. Both read their number from the pipe, print it as a confirmation and finish. Dr. Bubó waits until both have finished, then finishes too.
2. With the numbers received, the assistants start vaccinating. Before each shot they check the patient for fever: with 20% probability a patient is ill and cannot be vaccinated. At the end of the day each assistant prints how many patients came to them, then sends Dr. Bubó through a message queue how many were ill and how many actually got the vaccine, and finishes. Dr. Bubó prints these numbers and closes the clinic.
3. Dr. Bubó also writes a report for Lion: he appends to a file on which day, of the patients who came, how many did and how many didn't get the vaccine.
4. Dr. Bubó's clinic is not the only one writing its report into this file, so protect his file writing with a semaphore.

**Architecture:**

One parent, two children. Two pipes (one per assistant, parent → child), one message queue that both assistants write to, one semaphore for the file (Fig. 4). Distinguish "ill" and "vaccinated" messages by their `mtype`; the parent only needs the sums, so it doesn't matter which assistant's message it reads first.

**Step order:**

1. Skeleton: handler, mask, queue, semaphore, pipes; fork Ursula, wait for her signal; fork Csőrmester, wait.
2. Split the count, write one int into each pipe.
3. Children: read the int, print it; draw a random number per patient; send two messages.
4. Parent: four msgrcv calls (two of each type), print the sums.
5. Parent: P, append the report, V. Then waitpid both, remove queue and semaphore.

**Key lines:**

```c
int patients = atoi(argv[1]);
int ursulanum = patients / 2;                  // integer division rounds down …
int csormesternum = patients - ursulanum;      // … so the second gets the extra one
```

```c
for (int i = 0; i < toVaccinate; i++)
    if (rand() % 100 < 80)                     // 80 %: no fever
        vaccinated++;
```

```c
struct Message { long mtype; char mtext[1024]; };

struct Message msg;
msg.mtype = 5;                                 // 5 = sick, 6 = vaccinated
sprintf(msg.mtext, "%d", toVaccinate - vaccinated);
msgsnd(messageQueue, &msg, strlen(msg.mtext) + 1, 0);   // size WITHOUT mtype
```

```c
semaphoreOperation(semid, -1);
FILE *f = fopen("data.txt", "a");
if (f != NULL) {
    time_t t = time(NULL);
    struct tm tm = *localtime(&t);
    fprintf(f, "Ezen a napon: %d-%02d-%02d\n", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
    /* ... the counts ... */
    fclose(f);
}
semaphoreOperation(semid, 1);                  // also on the error path
```

> **Fig. 4** · Task 1: who talks to whom, and through what. Open it after your own attempt.
>
> *Diagram:* Vaccination: Dr. Bubó, the parent, gets a ready signal from both assistants and sends each one its number of patients through its own pipe. Both assistants send their sick and vaccinated counts back through one message queue. The parent appends the day's report to a shared file under a semaphore.

Good to know

**Block 4 and a shared key**

A semaphore only protects the file from processes that use the *same* semaphore, so the "other clinics" must compute the same key. With `ftok(argv[0], 2)` that means the same program file. For a real multi-program setup you would pick a fixed file for `ftok` – and only the creator should set the initial value (`IPC_CREAT | IPC_EXCL`), or one program's `SETVAL` resets the lock another one holds.

## Task 2 – Thesis registration

Task 2

**Registering a thesis topic**

A student registers a thesis topic. The thesis office (the parent) coordinates; the student and the supervisor are its two children.

1. The office opens registration. The student and the supervisor sign in by sending a signal; the office prints that both are present.
2. The student fills in a registration form – student name, thesis title, year, supervisor name – and sends it to the office through a pipe. The office forwards it to the supervisor through another pipe, and the supervisor prints every field.
3. The supervisor has a question for the student: "Which technology stack would you use?" It goes through a message queue; the student prints it.
4. The supervisor decides whether to accept the topic (80% yes) and writes the answer into shared memory, protected by a semaphore. The student reads the answer and prints it. The office waits for both and cleans up.

**Architecture:**

The form travels student → office → supervisor through two pipes; the question and the decision go directly from supervisor to student, through a queue and through shared memory (Fig. 5). The form is a struct of fixed-size arrays, so it can be written as raw bytes.

**Step order:**

1. Skeleton with queue, segment, semaphore (value 1) and pipes.
2. Student: write the struct into its pipe. Office: read it, write it into the supervisor's pipe.
3. Supervisor: read and print the form; write the decision into shared memory; then msgsnd the question.
4. Student: msgrcv the question, print it; then P, read the decision, V.

**Key lines:**

```c
struct Thesis {
    char studentName[64];
    char Title[128];
    int Year;
    char SupervisorName[64];
};
write(pipe_id_send, &thesis, sizeof(struct Thesis));   // no pointers: bytes are enough
```

```c
semaphoreOperation(semid, -1);
strcpy(s->text, accepted ? "Elfogadom a tema vezeteset"
                         : "Sajnos nem tudom elfogadni a tema vezeteset");
semaphoreOperation(semid, 1);

msgsnd(messageQueue, &ms, strlen(ms.mtext) + 1, 0);   // only now: the student
                                                       // reads shm after msgrcv
```

> **Fig. 5** · Task 2: the form goes through the office; question and decision go directly.
>
> *Diagram:* Thesis registration: both children signal that they are ready. The student sends the registration form to the office through a pipe, the office forwards it to the supervisor through another pipe. The supervisor asks the student a question through a message queue and leaves the decision in shared memory guarded by a semaphore.

## Task 3 – Press conference

Task 3

**The press conference**

A press conference begins. The moderator (the parent), the spokesperson (a child) and the expert (another child) answer the press's questions. The moderator runs the whole conference and keeps it going until every question is answered – the parent keeps the connections open and waits for both children to end.

1. The moderator waits until both participants have taken their seats and signalled that they are ready (print it). Then, through a pipe, the moderator asks the expert for a confirmation: "Do you have all the documents?" The expert answers "Yes" through a pipe. Print the whole exchange.
2. Next the spokesperson adds to the answer through a message queue, to the moderator: "Yes, we have measurements, and we will publish them." The moderator prints it.
3. The spokesperson also mentions a measured value (any variable you like), which it writes into shared memory. The moderator reads it from there and prints it.
4. Protect the use of the shared memory with a semaphore.

**Architecture:**

Two pipes with the expert – question down, answer up – and two channels from the spokesperson: a message queue for the statement, shared memory for the value (Fig. 6). The spokesperson needs no pipe at all.

**Step order:**

1. Skeleton with two pipes, queue, segment, semaphore (value 1).
2. Moderator: write the question, read the answer, print both.
3. Spokesperson: P, write the value, V – then msgsnd the statement.
4. Moderator: msgrcv the statement, print; P, read the value, V, print.

**Key lines:**

```c
const char *question = "Megvan minden dokumentuma?";   // 26 characters
write(io_pipes1[1], question, strlen(question) + 1);   // + 1: the '\0' travels too

/* expert */
char puffer[27];
if (read(pipe_id_rec, puffer, sizeof(puffer)) != sizeof(puffer)) { /* error */ }
```

The spokesperson's order – shared memory first, message second – is the one from Fig. 3.

> **Fig. 6** · Task 3: a pipe pair with the expert, a queue and shared memory from the spokesperson.
>
> *Diagram:* Press conference: both children signal that they are ready. The moderator asks the expert through one pipe and gets the answer through a second pipe. The spokesperson sends a statement through a message queue and leaves a measured value in shared memory guarded by a semaphore; the moderator reads both.

## Task 4 – Masked quiz

Task 4

**The masked quiz show**

In a masked quiz show the game master (the parent) plays with two players (two children). The game master always waits for the game to end.

1. The game master calls the players on stage (creates the children) and waits until they are up and signal that they are ready with the "Masks up" signal. Both introduce themselves: they send their name through a pipe. They choose the name at random, behind the mask, from three predefined names (for example Pig, Donkey, Zebra). The game master repeats the names aloud – prints them.
2. The game master has prepared 3 questions and the 3 right answers (simple data; an answer is a number from 1 to 5). He picks one question at random and sends it through a pipe; both players get it and answer through a pipe with a random number from 1 to 5, and both print their answer. The game master reads the answers and prints them too. A right answer earns the player a chocolate Santa, a wrong one a birch rod – the game master prints, for example, "Wrong answer, you get a big birch rod!"
3. In the second round the first player sits out. The second player bets blindly whether the first player's answer to the previous question was right or wrong, and sends the bet to the game master through a message queue. The game master prints the first player's question and answer once more, whether it was right, and the second player's bet. The reward is again a Santa or a birch rod.
4. The game master keeps the players' answers in a simple variable. Protect it with a semaphore, so that nobody accidentally accesses the values while they are being changed. Print the result.

**Architecture:**

A pipe pair with each player – name and answer up, question down – a message queue for the bet, and a struct in shared memory for the answers (Fig. 7). "A simple variable" that several processes change can only live in shared memory: a normal variable is copied by `fork`, and each process would change its own copy.

**Step order:**

1. Skeleton with four pipes, queue, segment, semaphore (value 1); "Masks up" is SIGUSR1 under another name.
2. Players: write a random name (fixed 10 bytes); game master: read and print both.
3. Game master: generate the question set, pick one, write it to both players.
4. Players: read, answer, print; P, store the answer in shared memory, V; then write the answer to the pipe.
5. Game master: read both answers, evaluate them; msgrcv the second player's bet and compare it with the first player's result.
6. Game master: P, print both stored answers, V; wait for both players, clean up.

**Key lines:**

```c
char randomNames[][10] = {"Malac", "Szamar", "Zebra"};
srand(getpid());                         // a different seed in each child
int r = rand() % 3;
write(pipe_id_send, randomNames[r], 10);
```

```c
struct Message { long mtype; char mguess; };     // 'j' = right, 'h' = wrong

struct Message ms = {5, rand() % 100 < 50 ? 'h' : 'j'};
msgsnd(messageQueue, &ms, sizeof(char), 0);       // the payload is one char
```

```c
struct sharedData { int firstPlayerAnswer; int secPlayerAnswer; };

semaphoreOperation(semid, -1);
s->firstPlayerAnswer = answer;           // into shared memory first …
semaphoreOperation(semid, 1);
write(pipe_id_send, &answer, sizeof(answer));   // … then announce it
```

> **Fig. 7** · Task 4: two pipe pairs, a queue for the bet, shared memory for the answers.
>
> *Diagram:* Masked quiz: both players signal masks up. Each player has a pipe pair with the game master – name and answer up, question down. The second player's blind bet goes through a message queue, and both players' answers are kept in shared memory guarded by a semaphore, which the game master reads at the end.

> **Seed each child separately.** Children forked from the same parent inherit its random state. Without its own `srand(getpid())`, each child draws the same "random" numbers as its sibling – both players pick the same name and give the same answer.

## Summary

- Characters are processes, verbs are channels: signal for "ready", pipe or queue for messages, shared memory with a semaphore for stored values.
- Set up everything before fork: handler, blocked signal, pipes, queue, segment, semaphore.
- Fork one child at a time and wait for its signal – two identical signals can merge into one.
- Write shared memory first, announce it second: the message orders the processes, the mutex only keeps them apart.
- Every child is waited for, every IPC object removed – check with `ipcs` after each run.
