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.
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 says | Use | Covered in |
|---|---|---|
| "signals that it is ready", "gives a sign" | kill + a handler, sigsuspend to wait | Signals |
| "sends through a pipe", "tells", "asks" | a pipe per direction | Pipes and message queues |
| "sends through a message queue", "reports back" | msgsnd / msgrcv with an mtype | Pipes and message queues |
| "writes to shared memory", "stores in a variable" | a struct in shmget memory | Shared memory and semaphores |
| "protect it with a semaphore" | semget, SETVAL, P/V around the access | Shared 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.
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.
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.
- 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.
- 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.
- 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.
- 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
- Skeleton: handler, mask, queue, semaphore, pipes; fork Ursula, wait for her signal; fork Csőrmester, wait.
- Split the count, write one int into each pipe.
- Children: read the int, print it; draw a random number per patient; send two messages.
- Parent: four msgrcv calls (two of each type), print the sums.
- 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 onefor (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 mtypesemaphoreOperation(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 pathTask 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.
- The office opens registration. The student and the supervisor sign in by sending a signal; the office prints that both are present.
- 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.
- The supervisor has a question for the student: "Which technology stack would you use?" It goes through a message queue; the student prints it.
- 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
- Skeleton with queue, segment, semaphore (value 1) and pipes.
- Student: write the struct into its pipe. Office: read it, write it into the supervisor's pipe.
- Supervisor: read and print the form; write the decision into shared memory; then msgsnd the question.
- 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 enoughsemaphoreOperation(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 msgrcvTask 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.
- 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.
- 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.
- 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.
- 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
- Skeleton with two pipes, queue, segment, semaphore (value 1).
- Moderator: write the question, read the answer, print both.
- Spokesperson: P, write the value, V – then msgsnd the statement.
- 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.
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.
- 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.
- 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!"
- 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.
- 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
- Skeleton with four pipes, queue, segment, semaphore (value 1); "Masks up" is SIGUSR1 under another name.
- Players: write a random name (fixed 10 bytes); game master: read and print both.
- Game master: generate the question set, pick one, write it to both players.
- Players: read, answer, print; P, store the answer in shared memory, V; then write the answer to the pipe.
- Game master: read both answers, evaluate them; msgrcv the second player's bet and compare it with the first player's result.
- 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 charstruct 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 itSeed 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
ipcsafter each run.