Этого мало.
Первое. Нужны особые операции, которые гарантированно выполняются атомарно. Например (из исходников Delphi)
function InterlockedAdd(var Addend: Integer; Increment: Integer): Integer;
asm
MOV ECX,EAX
MOV EAX,EDX
LOCK XADD [ECX],EAX
ADD EAX,EDX
end;
Здесь префикс LOCK (блокирование шины) и даёт атомарность. Также используют операцию XCHG (exchange) — единственная атомарная без префикса LOCK.
На основе этого можно устроить объект, который называется spinlock. Крутим цикл, пока система не скажет: свободно. Из Википедии.
mov eax, spinlock_address
mov ebx, SPINLOCK_BUSY
wait_cycle:
xchg [eax], ebx ; xchg - единственная инструкция, являющаяся атомарной без префикса lock
cmp ebx, SPINLOCK_FREE
jnz wait_cycle
; < критическая секция захвачена данным потоком, здесь идёт работа с разделяемым ресурсом >
mov eax, spinlock_address
mov ebx, SPINLOCK_FREE
xchg [eax], ebx ; используется xchg для атомарного изменения
; последние 3 инструкции лучше заменить на mov [spinlock_address], SPINLOCK_FREE -
; это увеличит скорость за счёт отсутствия лишней блокировки шины, а mov и так выполнится атомарно
; (но только если адрес spinlock_address выровнен по границе двойного слова)
От спинлока до настоящего мьютекса остаётся один небольшой шаг. Спинлок потребляет ресурс процессора, не производя полезной работы. После того, как спинлок проработал несколько микросекунд и не освободился, мы говорим: не судьба — и передаём работу другому процессу. Надеюсь, вы в своей мини-ОС как-то научились переключать процессы.
В однопроцессорном ядре никаких спинлоков нет. Если надо захватить ресурс, а он чей-то — сразу отдаём управление другому.
UPD1. Почему не сразу отдавать работу другому процессу. Хороший тон — защищать мьютексом не системные вызовы (которые действительно долги) а какие-нибудь структуры данных вроде связных списков и атомарных struct. Так что вероятность, что объект просидит занятым долго, крайне мала. В настоящем мьютексе есть очередь с приоритетом, которая защищается, как ни странно, спинлоком. И этого достаточно.