Priority Inversions... The curse of multitasking.
Posted: Fri Nov 06, 2020 5:10 am
I have just completed major rewrite/clean up of my OS code, to allow me to implement proper message passing.
My tasks are scheduled to run preemptively in a simple Round Robin depending upon a priority sorted list, higher priority tasks will always be scheduled before lower one, any task can request to be removed from the ready list and moved to the waiting list where they will remain until the waiting tasks receive a "signal" to run. Each task has 64 signal bits (dynamically assigned), and this signalling system has been sufficient for early tests, but now I have added a higher level message passing system build on the signal system so task can communicate with each other. This was needed as the input task (which monitors the keyboard and mouse) must send more complex events to tasks now, rather than just flag that "something has changed" (which is easy with signals).
A consequence of this was some graphical user interface house keeping code which was temporarily running in a timed interrupt, has now been moved to its proper place in the input task.
This is when I stumbled into a problem I've been expecting for a while, the code running in the interrupt was using data structures protected (to some degree) from concurrent access by other tasks due to the fact the interrupt would never be interrupted by a task. But now I needed to protect these data structures with a lock. MY first attempt was to have simple spin lock as such:
This is were I hit the priority inversion. The input task runs at a much high priority than the normal tasks on the system... So when the input task encountered a lock which was set by lower priority task, it would just sit and never obtain the lock, since the lower priority task would never be scheduled in to complete it's work and release the lock.
Here is the solution I have implemented, it might be be useful to others facing similar issues... And more experience members here might be able to critique/spot issues.
Now any task waiting for a lock to be freed will wait at the same priority as the locking task, allowing all tasks to get some CPU time. This works for now, and I can't foresee any issues... yet
My tasks are scheduled to run preemptively in a simple Round Robin depending upon a priority sorted list, higher priority tasks will always be scheduled before lower one, any task can request to be removed from the ready list and moved to the waiting list where they will remain until the waiting tasks receive a "signal" to run. Each task has 64 signal bits (dynamically assigned), and this signalling system has been sufficient for early tests, but now I have added a higher level message passing system build on the signal system so task can communicate with each other. This was needed as the input task (which monitors the keyboard and mouse) must send more complex events to tasks now, rather than just flag that "something has changed" (which is easy with signals).
A consequence of this was some graphical user interface house keeping code which was temporarily running in a timed interrupt, has now been moved to its proper place in the input task.
This is when I stumbled into a problem I've been expecting for a while, the code running in the interrupt was using data structures protected (to some degree) from concurrent access by other tasks due to the fact the interrupt would never be interrupted by a task. But now I needed to protect these data structures with a lock. MY first attempt was to have simple spin lock as such:
Code: Select all
bool lock;
void Lock(bool* lock){
while(*lock){
Reschedule(); // This causes the task to give up its time slice, and wait to be scheduled back in during the round robin.
}
}
Here is the solution I have implemented, it might be be useful to others facing similar issues... And more experience members here might be able to critique/spot issues.
Code: Select all
typedef struct{
bool isLocked;
int lockingPriority;
}lock_t;
void Lock(lock_t* lock){
if(lock->isLocked){
int taskPri = thisTask->priority; //Temporarily save the original task priority for restoration when the lock is released
if(lock->lockingPriority < taskPri){ //Only change the priority if the locking task is lower priority
thisTask->priority = lock->lockingPriority;
}
while(lock->isLocked){
Reschedule();
}
thisTask->priority = taskPri;
}
lock->isLocked = true;
lock->lockingPriority = thisTask->priority;
}
Now any task waiting for a lock to be freed will wait at the same priority as the locking task, allowing all tasks to get some CPU time. This works for now, and I can't foresee any issues... yet