본문 바로가기

Anything

Spinlock과 Read-Write Spinlock을 구현해보았다.


위 그림이 나타내는 것은 가장 단순한 형태의 상호 배제 개체입니다. 커널에서 특정 리소스를 보호하기 위해서 사용하는 가장 기본적인 방법입니다.


Spinlock

  스핀락은 위 상태 머신에서 나타내는 동작을 구현한 개체입니다. 단순한 카운터가 동시에 실행되면 안되는 어떤 흐름을 차단하기 위해서 사용된 케이스죠. 대표적으로 어떤 메모리 영역에 대해서 동시에 읽기와 쓰기가 일어나게 되면 흐름이 깨지게 되는 경우를 막기 위해 사용됩니다. 스핀 락을 얻기 위한 과정은 아주 간단하게 아래처럼 구현할 수 있습니다.

acquire:

push ebp

mov ebp, esp


push ebx

push esi

push edi


mov ebx, 1

mov esi, [ss:ebp + 8]

mov edi, [ss:ebp + 8]

xor eax, eax


.loop:

cmp eax, dword [ds:esi]

jne .loop


mov dword [ds:edi], ebx


pop edi

pop esi

pop ebx


xor eax, eax

mov esp, ebp

pop ebp

ret


release:

push ebp

mov ebp, esp


push edi

mov edi, [ss:ebp + 8]

xor eax, eax


mov dword [ds:edi], eax


pop edi


mov esp, ebp

pop ebp

ret

뭐, 물론 위 구현은 어셈블러로 짠다면을 가정한 거죠. 어셈블러로 짜여져 있는게 C나 C++ 코드로 짜는 것 보다 안전합니다. 왜냐하면 확실히 한큐에 실행되는 코드를 만들 수 있기 때문이지요. (뭐, 물론 위 코드는 더 최적화할 수 있습니다) 아니면, 이러한 구현으로 C/C++ 구현을 덧붙혀서 좀 더 정밀한 스핀 락을 만드는 것도 가능하죠. 그러면 아래 C++ 코드를 보도록 하지요. (어셈블리 구현은 cli, sti 외에 덧붙히지 않았습니다)

bool Spinlock::acquire(lock_t& token, ELockFlags lockFlags) {

if(lockFlags == E_LOCK_NOTHING)

return false;


while(m_lockOwner != &token && m_lockCount > 0);


__asm ("cli"); // 인터럽트를 잠급니다.

m_lockOwner = &token;

m_lockCount++;

__asm ("sti"); // 인터럽트를 풀어줍니다.


return true;

}


bool Spinlock::release(lock_t& token, ELockFlags lockFlags) {

if(lockFlags == E_LOCK_NOTHING)

return false;


if(m_lockOwner == &token) {

__asm ("cli"); // 인터럽트를 잠급니다.


m_lockCount--;

if(!m_lockCount)

m_lockOwner = 0;


__asm ("sti"); // 인터럽트를 풀어줍니다.

return true;

}


return false;

}

어때요? 간단하지요? 뭐, 물론, 당연한 이야기지만 cli와 sti는 반드시 irq 비활성 동작을 포함하는 코드로 교체되어야 합니다. cli와 sti는 인터럽트 마스킹만 하기 때문에 실질적으로 인터럽트가 완전히 막히지 않는 상황이 생깁니다. PIC나 APIC를 그때그때 다시 프로그래밍할 필요는 없습니다. 그냥 단순하게 blockInterrupt라는 전역 변수를 만들고 걔를 1으로 만들면 인터럽트가 실질적인 루틴을 돌지 않도록 만들어 주면 되는 것이죠.


Spinlock 클래스는 상위 클래스를 가집니다. ILockable이라는 놈입니다.

class ILockable {

public:

virtual ~ILockable() { }


public:

virtual bool isLocked(ELockFlags lockFlags) = 0;

virtual bool acquire(lock_t& token, ELockFlags lockFlags) = 0;

virtual bool release(lock_t& token, ELockFlags lockFlags) = 0;

};

그리고 이것을 상속받은 또다른 Lock 개체로 Read-Write 스핀 락이 있습니다. RW 스핀락의 경우에는 일반 스핀락 보다 퍼포먼스가 좀 더 잘 나옵니다. 일반 스핀락은 흐름을 "모든 경우"에 락을 잡아버립니다. 반면에 RW 스핀락은 "쓰기"가 포함된 락을 잡은 경우가 아니라면 동시에 읽을 수 있습니다. 당연한 이야기일지도 모르지만, RW 스핀락은 읽기 빈도가 쓰기 빈도보다 높을 때 강력한 성능을 발휘하지요. 코드를 봅시다.

bool RwSpinlock::isLocked(ELockFlags lockFlags) {

switch(lockFlags) {

case E_LOCK_READ:

case E_LOCK_WRITE:

case E_LOCK_BOTH:

return m_writeLock.isLocked(E_LOCK_WRITE);


default:

break;

}


return false;

}


bool RwSpinlock::acquire(lock_t& token, ELockFlags lockFlags) {

if(lockFlags != E_LOCK_READ)

return m_writeLock.acquire(token, lockFlags);


while(m_writeLock.isLocked(E_LOCK_WRITE));

return true;

}


bool RwSpinlock::release(lock_t& token, ELockFlags lockFlags) {

if(lockFlags != E_LOCK_READ)

return m_writeLock.release(token, lockFlags);


return true;

}

예, 위에서 구현한 스핀락을 기반으로 구현되었고, 쓰기 락이 잡히지 않은 경우에는 무조건 락이 잡히거나, 무조건 통과하게 됩니다. 정말 간단합니다. 당연한 이야기인데, 이 코드들은 더욱 최적화될 수 있습니다. 가상함수로서 구현되는 것 보다 일반 함수로 구현되는 것이 더 빠르지만 C++ 개체지향 페러다임에 맞춘다면 이런 코드가 나오지 않을까? 라는 생각으로 이렇게 짰습니다. 또한 학습을 위해 운영체제 만들기를 공부하는 경우에는 속도 쯤이야 아무 상관이 없지요. 기본적으로 SMP를 구현하지 않는다는 전제하에 Legacy PIC인 i8259A를 프로그래밍할텐데 말입니다. (그건 즉, CPU 코어중 단 하나만 사용하겠다는 의미겠지요)


위에서 소개한 코드는 아래 압축파일로 함께 올려두었습니다. (어셈블리 코드 제외)

Spinlock.zip