실수 연산에 사용하는 레지스터는 FPU, MMX, XMM 레지스터 이다.
FPU 레지스터 : 실수 연산 장치가 사용하는 레지스터로, 486 프로세스 이전에 사용되던 x87 코프로세서가 통합된 것이다.
TS비트 : 태스크 전환이 발생했다는 것을 알리는 비트이며, 콘텍스트 저장을 최대한 미루는데 핵심적인 역할을 수행한다.
FPU 콘텍스트와 FPU 사용 여부가 추가된 태스크 자료구조 Code
// FPU 콘텍스트가 추가됐기 때문에 자료구조의 크기가 16의 배수로 정렬되어야 함
typedef struct kTaskControlBlockStruct
{
// 다음 데이터의 위치와 ID
LISTLINK stLink;
// 플래그
QWORD qwFlags;
// 프로세스 메모리 영역의 시작과 크기
void* pvMemoryAddress;
QWORD qwMemorySize;
//================================================================
// 이하 스레드 정보
//================================================================
// 자식 스레드의 위치와 ID
LISTLINK stThreadLink;
// 부모 프로세스의 ID
QWORD qwParentProcessID;
// FPU 콘텍스트는 16의 배수로 정렬되어야 하므로 앞으로 추가할 데이터는
// 현재 라인 아래에 추가해야 함
QWORD vqwFPUContext [ 512 / 8 ];
// 자식 스레드의 리스트
LIST stChildThreadList;
// 콘텍스트와 스택
CONTEXT stContext;
void* pvStackAddress;
QWORD qwStackSize;
// FPU 사용 여부
BOOL bFPUUsed;
// TCB 전체를 16바이트 배수로 맞추기 위한 패딩
char vcPadding [ 11 ];
} TCB;
태스크 생성 함수 코드에 FPU 사용 여부를 0으로 초기화 하는 코드 추가
TCB* kCreateTask( QWORD qwFlags, void* pvMemoryAddress, QWORD qwMemorySize, QWORD qwEntryPointAddress )
{
TCB* pstTask, * pstProcess;
void* pvStackAddress;
BOOL bPreviousFlag;
// 임계 영역 시작
bPreviousFlag = kLockForSystemData();
pstTask = kAllocateTCB();
if( pstTask == NULL ){
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return NULL;
}
``` 생략 ```
// 자식 스레드 리스트를 초기화
kInitializeList( &( pstTask->stChildThreadList ) );
// FPU 사용 여부를 사용하지 않은 것으로 초기화
pstTask->bFPUUsed = FALSE;
// 임계 영역 시작
bPreviousFlag = kLockForSystemData();
// 태스크를 준비 리스트에 삽입
kAddTaskToReadyList( pstTask );
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
return pstTask;
}
FPU 콘텍스트를 저장하고 복원하는 과정에서 스케줄러의 역할은 FPU를 마지막으로 사용한 태스크 ID와 CR0 컨트롤 레지스터의 TS 비트를 관리하는 것이다.
마지막으로 FPU를 사용한 태스크 ID를 관리하도록 수정된 스케줄러 코드
// 헤더
// 스케줄러의 상태를 관리하는 자료구조
typedef struct kSchedulerStruct{
// 현재 수행 중인 태스크
TCB* pstRunningTask;
// 현재 수행 중인 태스크가 사용할 수 있는 프로세서 시간
int iProcessorTIme;
``` 생략 ```
// 유휴 태스크에서 사용한 프로세서 시간
QWORD qwSpendProcessorTimeInIdleTask;
// 마지막으로 FPU를 사용한 태스크의 ID
QWORD qwLastFPUUsedTaskID;
} SCHEDULER;
// 함수 선언
QWORD kGetLastFPUUsedTaskID ( void );
void kSetLastFPUUsedTaskID( QWORD qwTaskID );
// 함수
// 스케줄러를 초기화
// 스케줄러를 초기화하는데 필요한 TCB 풀과 init태스크도 같이 초기화
void kInitializeScheduler( void ){
int i;
TCB* pstTask;
// 태스크 풀 초기화
kInitializeTCBPool();
``` 생략 ```
// 프로세서 사용률을 계산하느데 사용하는 자료구조 초기화
gs_stScheduler.qwSpendProcessorTimeInIdleTask = 0;
gs_stScheduler.qwProcessorLoad = 0;
// FPU를 사용한 태스크 ID를 유효하지 않은 값으로 초기화
gs_stScheduler.qwLastFPUUsedTaskID = TASK_INVALIDID;
}
// 마지막으로 FPU를 사용한 태스크 ID를 반환
QWORD kGetLastFPUUsedTaskID( void ){
return gs_stScheduler.qwLastFPUUsedTaskID;
}
// 마지막으로 FPU를 사용한 태스크 ID를 설정
void kSetLastFPUUsedTaskID( QWORD qwTaskID ){
gs_stScheduler.qwLastFPUUsedTaskID = qwTaskID;
}
스케줄러 함수의 코드 수정
// 다른 태스크를 찾아서 전환
// 인터럽트나 예외가 발생했을 때 호출하면 안됨
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;
}
// 다음에 수행할 태스크가 FPU를 쓴 태스크가 아니라면 TS 비트 설정
if( gs_stScheduler.qwLastFPUUsedTaskID != pstNextTask->stLink.qwID ){
kSetTS(); //CRO 컨트롤 레지스터의 TS 비트를 1로 설정하는 함수
} else{
kClearTS(); //CRO 컨트롤 레지스터의 TS 비트를 0로 설정하는 함수
}
// 프로세서 사용 시간을 업데이트
gs_stScheduler.iProcessorTime = TASK_PROCESSORTIME;
// 태스크 종료 플래그가 설정된 경우 콘텍스트를 저장할 필요가 없으므로, 대기 리스트에
// 삽입하고 콘텍스트 전환
if( pstRunningTask->qwFlags & TASK_FLAGS_ENDTASK )
{
kAddListToTail( &( gs_stScheduler.stWaitList ), pstRunningTask );
kSwitchContext( NULL, &( pstNextTask->stContext ) );
}
else
{
kAddTaskToReadyList( pstRunningTask );
kSwitchContext( &( pstRunningTask->stContext ), &( pstNextTask->stContext ) );
}
// 임계 영역 끝
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;
}
// 태스크 종료 플래그가 설정된 경우, 콘텍스트를 저장하지 않고 대기 리스트에만 삽입
if( pstRunningTask->qwFlags & TASK_FLAGS_ENDTASK )
{
kAddListToTail( &( gs_stScheduler.stWaitList ), pstRunningTask );
}
// 태스크가 종료되지 않으면 IST에 있는 콘텍스트를 복사하고, 현재 태스크를 준비 리스트로 옮김
else
{
kMemCpy( &( pstRunningTask->stContext ), pcContextAddress, sizeof( CONTEXT ) );
kAddTaskToReadyList( pstRunningTask );
}
// 임계 영역 끝
kUnlockForSystemData( bPreviousFlag );
// 다음에 수행할 태스크가 FPU를 쓴 태스크가 아니라면 TS 비트 설정
if( gs_stScheduler.qwLastFPUUsedTaskID != pstNextTask->stLink.qwID ){
kSetTS();
}else{
kClearTS();
}
// 전환해서 실행할 태스크를 Running Task로 설정하고 콘텍스트를 IST에 복사해서
// 자동으로 태스크 전환이 일어나도록 함
kMemCpy( pcContextAddress, &( pstNextTask->stContext ), sizeof( CONTEXT ) );
// 프로세서 사용 시간을 업데이트
gs_stScheduler.iProcessorTime = TASK_PROCESSORTIME;
return TRUE;
}
FPU와 관련된 FINIT, FXSAVE, FXRSTOR 명령어나 컨트롤 레지스터의 비트를 제어하는 기능은 C코드만으로 처리하기 어렵다.
FPU 초기화, FPU 콘텍스트 저장과 복원을 수행하는 어셈블리어 함수의 코드
; FPU 초기화
; PARAM: 없음
KInitializeFPU:
finit ; FPU 초기화 수행
ret
; FPU 관련 레지스터를 콘텍스트 버퍼에 저장
; PARAM: Buffer Address
kSaveFPUContext:
fxSave [ rdi ] ; 첫번째 파라미터로 전달된 버퍼에 FPU 레지스터를 저장
ret
; FPU 관련 레지스터를 콘텍스트 버퍼에서 복원
; PARAM: Buffer Address
kLoadFPUContext:
fxrstor [ rdi ] ; 첫번째 파라미터로 전달된 버퍼에 FPU 레지스터를 복원
ret
컨트롤 레지스터
FPU를 제대로 사용하려면, CR0 컨트롤 레지스터의 TS비트, EM비트, MP 비트와 CR4 컨트롤 레지스터의 OSFXSR 비트, OSXMMEXCPT 비트를 올바르게 설정해야 한다.
CR0 컨트롤 레지스터의 TS비트, EM비트, MP 비트를 설정하는 코드
; TS비트(비트 3) = 1, EM비트(비트 2) = 0, MP 비트(비트 1) = 1로 설정하여 FPU 사용 준비
mov eax, cr0
; 비트 3, 2, 1를 모두 1로 만든 후, 비트 2만 1로 XOR하여 0으로 설정
or eax, 0x0E
xor eax, 0x04
mov cr0, eax
CR4 컨트롤 레지스터의 OSFXSR 비트, OSXMMEXCPT 비트 설정하는 코드
( 두 비트를 1로 설정해야, FXSAVE와 FXRSTOR 명령어가 XMM 레지스터까지 처리한다.)
; OSFXSR 비트(비트9)와 OSXMMEXCPT 비트(비트10)를 1로 설정하여 FPU 사용 준비
mov eax, cr4
; 비트 9와 비트 10을 1로 설정
or eax, 0x0600
mov cr4, eax
TS 비트를 제어하는 어셈블리어 함수 코드
; CR0 컨트롤 레지스터의 TS 비트를 1로 설정
; PARAM: 없음
kSetTS:
push rax ; 스택에 RAX 레지스터의 값을 저장
mov rax, cr0 ; CR0 컨트롤 레지스터 값을 RAX 레지스터에 저장
or rax, 0x08 ; TS 비트(비트 7)을 1로 설정
mov cr0, rax ; TS 비트가 1로 설정된 값을 CR0 컨트롤 레지스터로 저장
pop rax ; 스택에서 RAX 레지스터의 값을 복원
ret
; CR0 컨트롤 레지스터의 TS 비트를 0으로 설정
; PARAM: 없음
kClearTS:
clts ; CR0 컨트롤 레지스터에서 TS 비트를 0으로 설정
ret
예외 7번 (#NM, Device Not Available)
현재 FPU의 상태를 저장하는 부분
void kDeviceNotAvailableHandler( int iVectorNumber )
{
TCB* pstFPUTask, * pstCurrentTask;
QWORD qwLastFPUTaskID;
// CR0 컨트롤 레지스터의 TS 비트를 0으로 설정
kClearTS();
// 이전에 FPU를 사용한 태스크가 있는지 확인하여, 있다면 FPU 상태를 태스크에 저장
qwLastFPUTaskID = kGetLastFPUUsedTaskID();
pstCurrentTask = kGetRunningTask();
// 이전에 FPU를 사용한 것이 자신이면 아무것도 안 함
if( qwLastFPUTaskID == pstCurrentTask->stLink.qwID ){
return;
}
// FPU를 사용한 태스크가 있으면 FPU 상태를 저장
else if( qwLastFPUTaskID != TASK_INVALIDID ){
pstFPUTask = kGetTCBInTCBPool( GETTCBOFFSET( qwLastFPUTaskID ) );
if( (pstFPUTask != NULL ) && ( pstFPUTask->stLink.qwID == qwLastFPUTaskID ) ){
kSaveFPUContext( pstFPUTask->vqwFPUContext );
}
}
``` FPU를 초기화하거나 복원하는 코드 ```
}
FPU를 초기화하거나 이전 상태로 복원하는 방법
void kDeviceNotAvailableHandler( int iVectorNumber )
{
``` FPU 상태를 저장하는 코드 ```
// 현재 태스크가 FPU를 사요한 적이 있는지 확인하여 FPU를 사용한 적이 없다면 초기화하고,
// 사용한 적이 있다면 저장된 FPU 콘텍스트를 복원
if( pstCurrentTask->bFPUUsed == FALSE ){
kInitializeFPU();
pstCurrentTask->bFPUUsed = TRUE;
}else{
kLoadFPUContext( pstCurrentTask->vqwFPUContext );
}
// FPU를 사용한 태스크 ID를 현재 태스크로 변경
kSetLastFPUUsedTaskID( pstCurrnetTask->stLink.qwID );
}