I noticed my previous post about preemptive OS involved many people and started many discussions, most of them theoric.
Someone wrote the synchronization of tasks in preemptive scheduler is not so difficult, after understanding some things. Others suggested to abandon at all preemptive scheduler, considering its pitfalls.
Because I know my limits, I don't think I can produce a well-written preemption system. However I'd like to understand a little more about them. Starting from an example.
Suppose my system is a display where a message is written. The message can be customized by a serial line. In cooperative approach, I would write something:
--- main.c --- ... while(1) { task_display(); task_serial(); }
--- end of main.c ---
--- display.c --- static const char msg[32]; void display_set_message(const char *new_msg) { strncpy(msg, new_msg, sizeof(msg)); } void task_display(void) { if (refresh_is_needed()) { display_printat(0, 0, msg); } }
--- end of display.c ---
--- serial.c --- static unsigned char rxbuf[64]; static size_t rxlen; void task_serial(void) { unsigned char b = serial_rx(); if (b != EOF) { rxbuf[rxlen++] = b; if (frame_is_complete(rxbuf, rxlen)) { char new_msg[32]; /* decode new message from received frame from serial line */ display_set_message(new_msg); rxlen = 0; } } }
--- end of serial.c ---
The display needs to be refreshed. display_printat() is blocking: when it returns, all the display was refreshed. So the display always shows the entire message: there's no risk the display shows a part of the previous message and a part of the new message.
How to convert these two tasks in a preemptive scheduler? Which priority to assign to them?
The simplest approach is...
--- display.c --- static const char msg[32]; void display_set_message(const char *new_msg) { strncpy(msg, new_msg, sizeof(msg)); } void task_display(void) { while(1) { if (refresh_is_needed()) { display_printat(0, 0, msg); } } }
--- end of display.c ---
--- serial.c --- static unsigned char rxbuf[32]; static size_t rxlen; void task_serial(void) { while(1) { unsigned char b = serial_rx(); if (b != EOF) { rxbuf[rxlen++] = b; if (frame_is_complete(rxbuf, rxlen)) { char new_msg[32]; /* decode new message from received frame from serial line */ display_set_message(new_msg); rxlen = 0; } } } }
--- end of serial.c ---
This code works most of the time, but the display sometime can show a mix of old/new messages. This happens if display task is interrupted during refresh by serial task that calls display_set_message(). Or when display_set_message() is interrupted by display task and a refresh occurs.
If I assigned a higher priority to display task, the problem would remain. Indeed display_printat() couldn't be interrupted, but display_set_message() yes.
Here the solution is to take a binary semaphore before using the shared resource (and give the semaphore after the job is done).
void display_set_message(const char *new_msg) { semaphore_take_forever(); strncpy(msg, new_msg, sizeof(msg)); semaphore_give(); }
... if (frame_is_complete(rxbuf)) { char new_msg[32]; /* decode new message from received frame from serial line */ semaphore_take_forever(); display_set_message(new_msg); semaphore_give(); rxlen = 0; } ...
My impression is that a very simple code is cluttered with synchronization things that decrease readability and maintainability and increase complexity. Why? Just to use preemption?
Again my impression is that preemption is NOT GOOD and must be avoided if it isn't required.
So the question is: when a preemption scheduler is needed? Could you give a real example?
From what I have understood, preemption could solve real-time requirement.
Suppose display_printat() takes too much time to finish. This increases the worst-case superloop duration and could delay some system reaction. For example, if display_printat() takes 1 second to finish, the system could react after 1 second from an event (the press of a button, for example).
If this isn't acceptable, preemption could help. Is it correct?