경쟁 상태(Race Condition) : 여러 태스크가 서로 자원을 차지하려고 경쟁하는 상황
임계 영역(Critical Section) : 코드 중에서 반드시 한 개의 태스크만 수행되어야 하는 영역
상호 배제(Mutual Exclusion) : 한 개의 태스크만이 코드를 수행하는 것을 보장한다는 의미
태스크 사이의 동기화 문제를 해결하는 방법은 뮤텍스와 세머포어가 대표적이다.
동기화를 처리하는 방법
① 인터럽트를 제어하는 방법
② 동기화 객체를 사용하는 방법
싱글코어 환경에서는 인터럽트만 제어하면, 동기화를 수행할 수 있다.
// 시스템 전역에서 사용하는 데이터를 위한 잠금 함수
BOOL kLockForSystemData( void )
{
return kSetInterruptFlag( FALSE );
}
// 시스템 전역에서 사용하는 데이터를 위한 잠금 해제 함수
void kUnlockForSystemData( BOOL bInterruptFlag )
{
kSetInterruptFlag( bInterruptFlag );
}
(1) 스케줄러 함수에 동기화 코드 적용
스케줄러 함수에 동기화 함수를 적용하여, 태스크 생성 과정에서 발생하는 문제부터 해결하고, 키보드 함수를 같은 방식으로 수정하여 동기화 처리 부분을 하나로 통일한다.
스케줄러 함수는 내부에서 사용하는 함수와 외부에서 사용하는 함수로 구분된다.
kCreateTask( )
TCB를 할당받은 부분, 태스크 정보를 설정하는 부분, 준비 리스트에 태스크 삽입 부분 등의
3부분으로 구성된다.
임계 영역은 다른 태스크나 인터럽트 핸드러와 데이터를 공유하는 부분으로, TCB 풀에서 TCB를 할당받은 부분과 준비 리스트에 태스크를 삽입하는 부분이 해당된다.
// 동기화 처리가 완료된 태스크 생성 함수의 코드
TCB* kCreateTask( QWORD qwFlags, QWORD qwEntryPointAddress )
{
TCB* pstTask;
void* pvStackAddress;
BOOL bPreviousFlag;
// 임계 영역 시작
bPreviousFlag = kLockForSystemData();
pstTask = kAllocateTCB();
if( pstTask == NULL )
{
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return NULL;
}
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
// 태스크 ID로 스택 어드레스 계산, 하위 32비트가 스택 풀의 오프셋 역할 수행
pvStackAddress = ( void* ) ( TASK_STACKPOOLADDRESS + ( TASK_STACKSIZE * GETTCBOFFSET( pstTask->stLink.qwID ) ) );
// TCB를 설정한 후, 준비 리스트에 삽입하여 스케줄링 될 수 있도록 함
kSetUpTask( pstTask, qwFlags, qwEntryPointAddress, pvStackAddress, TASK_STACKSIZE );
// 임계 영역 시작
bPreviousFlag = kLockForSystemDAta();
// 태스크를 준비 리스트에 삽입
kAddTaskToReadyList( pstTask );
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return pstTask;
}
kSchedule() 함수와 kScheduleInInterrupt( ) 함수는 전체 코드가 스케줄러 자료구조 즉, 공유 데이터를 수정하는 코드이다.
// 다른 태스크를 찾아서 전환
// 인터럽트나 예외가 발생했을 때 호출하면 안됨
void kSchedule( void )
{
TCB* pstRunningTask, * pstNextTask;
BOOL bPreviousFlag;
// 전환할 태스크가 있어야 함
if( kGetReadyTaskCount() < 1 )
{
return ;
}
// 전환하는 도중 인터럽트가 발생하여 태스크 전환이 또 일어나면 곤란하므로 전환하는
// 동안 인터럽트가 발생하지 못하도록 설정
// 임계 영역 시작
bPreviousFlag = kLockForSystemData();
// 실행할 다음 태스크를 얻음
pstNextTask = kGetNextTaskToRun();
if( pstNextTask == NULL )
{
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return ;
}
// 현재 수행중인 태스크의 정보를 수정한 뒤 콘텍스트 전환
pstRunningTask = gs_stScheduler.pstRunningTask;
gs_stScheduler.pstRunningTask = pstNextTask;
// 유휴 태스크에서 전환되었다면 사용한 프로세서 시간을 증가시킴
if( ( pstRunningTask->qwFlags & TASK_FLAGS_IDLE ) == TASK_FLAGS_IDLE )
{
gs_stScheduler.qwSpendProcessorTimeInIdleTask +=
TASK_PROCESSORTIME - gs_stScheduler.iProcessorTime;
}
// 태스크 종료 플래그가 설정된 경우 콘텍스트를 저장할 필요가 없으므로, 대기 리스트에
// 삽입하고 콘텍스트 전환
if( pstRunningTask->qwFlags & TASK_FLAGS_ENDTASK )
{
kAddListToTail( &( gs_stScheduler.stWaitList ), pstRunningTask );
kSwitchContext( NULL, &( pstNextTask->stContext ) );
}
else
{
kAddTaskToReadyList( pstRunningTask );
kSwitchContext( &( pstRunningTask->stContext ), &( pstNextTask->stContext ) );
}
// 프로세서 사용 시간을 업데이트
gs_stScheduler.iProcessorTime = TASK_PROCESSORTIME;
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
}
// 인터럽트가 발생했을 때, 다른 태스크를 찾아 전환
// 반드시 인터럽트나 예외가 발생했을 때 호출해야 함
BOOL kScheduleInInterrupt( void )
{
TCB* pstRunningTask, * pstNextTask;
char* pcContextAddress;
BOOL bPreviousFlag;
// 임계 영역 시작
bPreviousFlag = kLockForSystemData();
// 전환할 태스크가 없으면 종료
pstNextTask = kGetNextTaskToRun();
if( pstNextTask == NULL )
{
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return FALSE;
}
//=============================================================================
// 태스크 전환 처리
// 인터럽트 핸들러에서 저장한 콘텍스트를 다른 콘텍스트로 덮어쓰는 방법으로 처리
//=============================================================================
pcContextAddress = ( char* ) IST_STARTADDRESS + IST_SIZE - sizeof( CONTEXT );
// 현재 수행 중인 태스크의 정보를 수정한 뒤, 콘텍스트 전환
pstRunningTask = gs_stScheduler.pstRunningTask;
gs_stScheduler.pstRunningTask = pstNextTask;
// 유휴 태스크에서 전환되었다면, 사용한 Tick Count를 증가시킴
if( (pstRunningTask -> qwFlags & TASK_FLAGS_IDLE ) == TASK_FLAGS_IDLE )
{
gs_stScheduler.qwSpendProcessorTimeInIdleTask += TASK_PROCESSORTIME;
}
// 프로세서 사용 시간을 업데이트
gs_stScheduler.iProcessorTime = TASK_PROCESSORTIME;
// 태스크 종료 플래그가 설정된 경우 콘텍스트를 저장하지 않고, 대기 리스트에만 삽입
if( pstRunningTask->qwFlags & TASK_FLAGS_ENDTASK )
{
kAddListToTail( &( gs_stScheduler.stWaitList ), pstRunningTask );
}
// 태스크가 종료되지 않으면, IST에 있는 콘텍스트를 복사하고 현재 태스크를 준비 리스트로 옮김
else
{
kMemCpy( &(pstRunningTask -> stContext ), pcContextAddress, sizeof(CONTEXT));
kAddTaskToReadyList( pstRunningTask );
}
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
// 전환해서 실행할 태스크를 Running Task로 설정하고 콘텍스트를 IST에 복사해서
// 자동으로 태스크 전환이 일어나도록 함
kMemCpy( pcContextAddress, &( pstNextTask->stContext ), sizeof( CONTEXT ) );
return TRUE;
}
(2) 키보드 함수에 동기화 코드 적용
// 스캔 코드를 내부적으로 사용하는 키 데이터로 바꾼 후 키 큐에 삽입
BOOL kConvertScanCodeAndPutQueue( BYTE bScanCode )
{
KEYDATA stData;
BOOL bResult = FALSE;
BOOL bPreviousInterrupt;
// 스캔 코드를 키 데이터에 삽입
stData.bScanCode = bScanCode;
// 스캔 코드를 ASCII 코드와 키 상태로 변환하여 키 데이터에 삽입
if( kConvertScanCodeToASCIICode( bScanCode, &( stData.bASCIICode ),
&( stData.bFlags ) ) == TRUE )
{
// 임계 영역 시작
bPreviousInterrupt = kLockForSystemData();
// 키 큐에 삽입
bResult = kPutQueue( &gs_stKeyQueue, &stData );
// 임계 영역 끝
kUnlockForSystemData( bPreviousInterrupt );
}
return bResult;
}
// 키 큐에서 키 데이터를 제거
BOOL kGetKeyFromKeyQueue( KEYDATA* pstData )
{
BOOL bResult;
BOOL bPreviousInterrupt;
// 임계 영역 시작
bPreviousInterrupt = kLockForSystemData();
bResult = kGetQueue( &gs_stKeyQueue, pstData );
// 임계 영역 끝
kUnlockForSystemData( bPreviousInterrupt );
return bResult;
}