Operating System Implementation: Synchronization and Thread-Safe I/O
In multi-threaded environments, concurrent access to video memory and ports can cause GP exceptions due to incorrect calculation of write offsets. To address this issue, we implement semaphore and synchronization mechanisms.
When a thread waits for a semaphore, it voluntarily blocks until another thread unblocks it. Only a thread can block itself; one thread cannot force another to block.
void OS_ThreadBlock(enum THREAD_STATE newState) {
uint8_t interruptFlag;
struct THREAD_CTRL *currentThread;
KASSERT(newState == THREAD_STATE_BLOCKED || newState == THREAD_STATE_WAITING || newState == THREAD_STATE_HANGING);
interruptFlag = DisableInterrupts();
currentThread = GetCurrentThread();
currentThread->state = newState;
OS_Schedule();
RestoreInterrupts(interruptFlag);
}
void OS_ThreadUnblock(struct THREAD_CTRL *thread) {
uint8_t interruptFlag;
KASSERT(thread->state == THREAD_STATE_BLOCKED || thread->state == THREAD_STATE_HANGING ||
thread->state == THREAD_STATE_WAITING);
interruptFlag = DisableInterrupts();
if (thread->state != THREAD_STATE_READY) {
if (ListContains(&readyQueue, &thread->queueNode))
KERNEL_PANIC("OS_ThreadUnblock: blocked thread in Ready Queue");
ListInsertHead(&readyQueue, &thread->queueNode);
thread->state = THREAD_STATE_READY;
}
RestoreInterrupts(interruptFlag);
}
To implement mutual exclusion, we can use binary semaphores:
struct BINARY_SEMAPHORE {
BOOL isAvailable;
struct LIST waitingThreads;
};
void OS_InitBinarySemaphore(struct BINARY_SEMAPHORE *semaphore, BOOL initialValue);
void OS_AcquireSemaphore(struct BINARY_SEMAPHORE *semaphore);
void OS_ReleaseSemaphore(struct BINARY_SEMAPHORE *semaphore);
BINARY_SEMAPHOREstructure: Represents a binary semaphoreisAvailable: Boolean value (0 or 1) indicating if the semaphore is availablewaitingThreads: Queue of threads blocked waiting for the semaphore
- Initialization (
OS_InitBinarySemaphore): All binary semaphores must be initialized with this function - Acquire (
OS_AcquireSemaphore): Adds current thread to waiting queue and blocks the thread - Release (
OS_ReleaseSemaphore): Removes a thread from the waiting queue and wakes it up - Strict mutual exclusion: The value can only switch between 0 and 1
The MUTEX_LOCK structure is a simple wrapper around a binary semaphore that implements a recursive mutex:
struct MUTEX_LOCK {
struct THREAD_CTRL *owner;
struct BINARY_SEMAPHORE semaphore;
uint32_t recursionCount;
};
void OS_InitMutex(struct MUTEX_LOCK *mutex);
void OS_AcquireMutex(struct MUTEX_LOCK *mutex);
void OS_ReleaseMutex(struct MUTEX_LOCK *mutex);
The recursionCount field allows the same thread to acquire the lock multiple times without causing deadlock. Other threads must wait until the lock is complete released.
The console device implementation uses locks to prevent GP exceptions in multi-threaded environments:
#ifndef __OS_CONSOLE_H
#define __OS_CONSOLE_H 1
void Console_Initialize(void);
void Console_Lock(void);
void Console_Unlock(void);
void Console_PrintString(const char *str);
void Console_PrintLine(const char *str);
void Console_PrintChar(char ch);
void Console_PrintAddress(void *addr);
void Console_PrintInt(int val, int base);
#endif
Keyboard Input Handling
The following discussion is based on traditional PS/2 keyboards. Most modern keyboards connect via USB and follow the HID protocol. However, if a computer supports PS/2 compatible ports, the following code remains applicable.
Three sets of keyboard scan codes have existed historically. Regardless of which set the keyboard uses, the i8042 compatible chip converts them to Scan Code Set 1, which is then processed by the operating system's keyboard interrupt handler.
When a user presses any key, a make code is sent. If the key is held down, the make code is repeated. When the key is released, a break code is sent. For Scan Code Set 1:
- Most keys have one-byte scan codes, where make code + 0x80 = break code
- Some extended keys use two bytes: make code is 0xe0, 0x?? and break code is 0xe0, 0x?? + 0x80
- Special keys might use four bytes (which we won't handle)
- The highest bit (8th bit) is always 0 for make codes and 1 for break codes
- Most operating systems don't process break codes directly
For 8086-compatible systems, keyboard interrupt signals come from the master 8259A PIC's IR1 interface (interrupt number 0x21):
; 8259A Interrupt Vector Table
VECTOR 0x20,KEYBOARD_HANDLER
VECTOR 0x21,KEYBOARD_HANDLER
VECTOR 0x22,KEYBOARD_HANDLER
VECTOR 0x23,KEYBOARD_HANDLER
VECTOR 0x24,KEYBOARD_HANDLER
VECTOR 0x25,KEYBOARD_HANDLER
VECTOR 0x26,KEYBOARD_HANDLER
VECTOR 0x27,KEYBOARD_HANDLER
VECTOR 0x28,KEYBOARD_HANDLER
VECTOR 0x29,KEYBOARD_HANDLER
VECTOR 0x2a,KEYBOARD_HANDLER
VECTOR 0x2b,KEYBOARD_HANDLER
VECTOR 0x2c,KEYBOARD_HANDLER
VECTOR 0x2d,KEYBOARD_HANDLER
VECTOR 0x2e,KEYBOARD_HANDLER
VECTOR 0x2f,KEYBOARD_HANDLER
The keyboard interrupt handler reads the scan code from port 0x60:
int scanCode = inb(KEYBOARD_DATA_PORT);
<p>The port transfers one byte at a time, so pressing certain special keys (extended keys) may trigger two interrupts (and four when released, including breaks).</p>
<p>We don't handle four-byte keys or break codes. The implementation processes scan codes as follows:</p>
- Read the keyboard buffer port (0x60) to get the raw scan code
- If the scan code is 0xe0, mark the next scan code as an extended key and set an extension flag
- If the extension flag is set, mark the scan code with 0xe000 in the high byte and reset the flag
- Determine if it's a press (Make Code) or release (Break Code) using the highest bit (0x80)
- For release events, update the global state of modifier keys (Shift/Ctrl/Alt) to FALSE
- Determine the correct character based on Shift and CapsLock states
Circular Buffer Implementation
The circular buffer implementation in iocbuf.c provides a thread-safe, fixed-size (128-byte) circular buffer that supports producer-consumer patterns.
A circular buffer is a fixed-size contiguous memory area accessed as a circular queue. When data is written, the head pointer moves forward; when data is read, the tail pointer moves forward.
The circular buffer ensures thread safety through:
- Lock mechanism: Ensures atomic operations on buffer state, preventing data inconsistency
- Blocking and waking:
- When a producer finds the buffer full, it blocks itself, waiting for consumers to read data
- After consumers read data, they check if any producers are waiting and wake them up
- Conversely, when consumers find the buffer empty, they block themselves, waiting for producers to write data
For the keyboard device, the keyboard interrupt handler acts as the producer, while future implementations like Shell and user programs will act as consumers:
// keyboard.h
extern struct CIRCULAR_BUFFER keyboardBuffer;
// keyboard.c
char character = keyMap[scanCode][shiftState];
if (character) {
if (!CB_IsFull(&keyboardBuffer)) {
// The keyboard interrupt handler acts as a producer,
// placing characters into the buffer for consumption by other threads
CB_PutChar(&keyboardBuffer, character);
}
return;
}