Practice · Operating Systems

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.

15 min readStory → processesThe common skeletonMerged signalsWrite, then announceFour 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.

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 saysUseCovered in
"signals that it is ready", "gives a sign"kill + a handler, sigsuspend to waitSignals
"sends through a pipe", "tells", "asks"a pipe per directionPipes and message queues
"sends through a message queue", "reports back"msgsnd / msgrcv with an mtypePipes and message queues
"writes to shared memory", "stores in a variable"a struct in shmget memoryShared memory and semaphores
"protect it with a semaphore"semget, SETVAL, P/V around the accessShared memory and 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.

parentchildrensigaction, block SIGUSR1create pipes, queue, shm, semfork() child 1sigsuspend until ready ≥ 1fork() child 2sigsuspend until ready ≥ 2the steps of the taskwaitpid both, remove IPCchild 1unblock, kill(parent, SIGUSR1), workchild 2unblock, kill(parent, SIGUSR1), workforkSIGUSR1forkSIGUSR1pipes, queue, shared memory
Fig. 1 · Set up, fork one child at a time, wait for its signal, work, clean up.

the parent, before any fork

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

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

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

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);

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.

child 1kill(parent, SIGUSR1)child 2kill(parent, SIGUSR1)pending: SIGUSR1one bit, not a counterhandler runs onceready = 1while (ready < 2) sigsuspendwaits foreverfix: fork child 2 only after child 1's signal has arrived (ready ≥ 1)or give each child its own signal: SIGUSR1 and SIGUSR2
Fig. 2 · Standard signals are not counted: two of the same kind can arrive as one.
writerP · write shared memory · Vmsgsnd()readermsgrcv(): blockedmsgrcv() returnsP · read shm · Vthe message arrivesthe other way round – msgsnd first, write after – the reader may look before the value is there
Fig. 3 · The message carries the order; the semaphore only prevents overlap.

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
int patients = atoi(argv[1]);
int ursulanum = patients / 2;                  // integer division rounds down …
int csormesternum = patients - ursulanum;      // … so the second gets the extra one
for (int i = 0; i < toVaccinate; i++)
    if (rand() % 100 < 80)                     // 80 %: no fever
        vaccinated++;
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
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
Dr. Bubóparentreport file4 · append, semaphore3Ursulachild 1Csőrmesterchild 21 · count1 · ready1 · count1 · readymessage queue2 · sick, vaccinatedsolid blue: pipe · dashed: signal · numbers: the task block
Fig. 4 · Task 1: who talks to whom, and through what. Open it after your own attempt.

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
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
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
thesis officeparentstudentchild 1supervisorchild 22 · form1 · ready2 · form1 · readymessage queue3 · questionshared memory4 · decision, semaphoresolid blue: pipe · dashed: signal · numbers: the task block
Fig. 5 · Task 2: the form goes through the office; question and decision go directly.

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
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.

moderatorparentexpertchild 1spokespersonchild 21pipes1 · readymsg queue2 · statementshared mem3, 4 · value1 · readysolid blue: pipe (question down, answer up) · dashed: signal · numbers: the task block
Fig. 6 · Task 3: a pipe pair with the expert, a queue and shared memory from the spokesperson.

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
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);
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
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
game masterparentmessage queue3 · the betplayer 1child 1player 2child 21, 2pipesmasks up1, 2pipesmasks upshared memory4 · answers, semaphoresolid blue: pipe (question down; name, answer up) · dashed: signal · numbers: the task block
Fig. 7 · Task 4: two pipe pairs, a queue for the bet, shared memory for the answers.
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.