728x90

기본적인 파일 처리 함수들

Windows 기반 파일 입출력 함수

 

파일 열기 : CreateFile

HANDLE CreateFile(

  LPCTSTR lpFileName,

  DWORD dwDesiredAccess,

  DWORD dwShareMode, 

  LPSECURITY_ATTRIBUTES lpSecurityAttributes,

  DWORD dwCreationDisposition,

  DWORD dwFlagsAndAttributes,

  HANDLE hTemplateFile

);

1.lpFileName : 개방(Open)할 파일 이름을 지정한다.

2.dwDesiredAccess : 읽기/쓰기 모드를 지정한다. or(|)연산으로 결합 가능하다.

 - GENERIC_READ 읽기모드 지정

 - GENERIC_WRITE 쓰기모드 지정

3.dwShareMode : 파일 공유방식을 지정한다.

 - 0 : 다른 프로세스에 절대 공유 불가, 이미 개방된 파일은 중복 개방 불가

 - FILE_SHARE_READ : 다른 프로세스에서 이 파일에 동시 읽기 접근 가능

 - FILE_SHARE_WRITE : 다른 프로세스에서 이 파일에 동시 쓰기 접근 가능, 단 동시에 같은 영역에 데이터를 쓰는 문제를 피해야 함.

4.lpSecurityAttributes : 보안 속성을 지정한다. 핸들을 자식 프로세스에게 상속할 것인지 말 것인지를 결정하기 위한 용도로 사용할 수 있다. 디폴트 보안 속성을 지정하고자 하는 경우 NULL 전달

5.dwCreationDisposition : 파일이 생성되는 방법을 지정한다.

- CREATE_ALWAYS : 항상 새 파일을 생성한다.

- CREATE_NEW : 새 파일 생성, 같은 이름의 파일이 존재하면 생성 실패!

- OPEN_ALWAYS : 기존 파일 개방, 없으면 새로 생성

- OPEN_EXISTING : 기존 파일 개방, 존재하지 않으면 함수 호출

6.dwFlagsAndAttributes : 파일의 특성정보를 설정한다. 둘이상의 특성 정보는 OR(|) 연산자를 통해서 지정할 수 있으며, 기본적으로 FILE_ATTRIBUTE_NORMAL 값 사용

7.hTemplateFile : 기존에 존재하는 파일과 동일한 특성을 가지는 새파일을 만들 때 사용되는 전달인자이다.

 

위 함수 호출시, 파일의 핸들이 반환된다.

 

CreateFile로 파일을 열고 파일을 종료할 때에는 여느 커널 오브젝트의 핸들과 마찬가지로 CloseHandle 함수를 호출하면 된다.

CreateFile 함수는 핸들을 반환한다. 해당 핸들의 커널오브젝트에는 파일에 대한 정보로 가득 차있을 것이다.

 

파일 읽기 & 쓰기 함수

파일 읽기 함수

BOOL ReadFile(

   HANDLE hFile,

   LPVOID lpBuffer,

   DWORD nNumberOfBytesToRead,

   LPDWORD lpNumberOfBytesRead,

   LPOVERLAPPED lpOverlapped

);

1.hFile : 데이터를 읽을 파이르이 핸들을 지정한다.

2.lpBuffer : 읽어들인 데이터를 저장할 버퍼(배열, 메모리)의 주소(포인터)를 지정한다.

3.nNumberOfBytesToRead  : 파일로 부터 읽고자 하는 데이터의 크기를 바이트 단위로 지정한다.

4.lpNumberOfBytesRead : 실제 읽어들인 데이터 크기를 얻기위한 변수의 주소를 지정한다.

 

파일 쓰기 함수

BOOL WriteFile(

  HANDLE hFile,

  LPCVOID lpBuffer,

  DWORD nNumberOfBytesToWrite,

  LPDWORD lpNumberOfBytesWritten,

  LPOVERLAPPED lpOverlapped

);

1.hFile : 데이터를 저장할 파일의 핸들을 지정한다.

2.lpBuffer : 데이터를 저장하고 있는 버퍼(배열, 메모리)의 주소(포인터)를 지정한다.

3.nNumberOfBytesToWrite : 파일에 저장하고자 하는 데이터 크기를 바이트 단위로 지정한다.

4.lpNumberOfBytesWritten : 파일에 실제 저장된 데이터 크기를 얻기위해 변수의 주소를 지정한다.

 

파일의 시간 정보 얻어오기

Windows에서는 파일의 만든날짜, 수정한 날짜(마지막으로 수정한 날짜), 엑세스한 날짜(마지막으로 엑세스한 날짜)등의 정보를 확인할 수 있다.

 

프로그램상에서 위와같은 정보를 얻거나 변경하기 위한 함수

BOOL GetFileTime(

  HANDLE hFile,

  LPFILETIME lpCreationTime,

  LPFILETIME lpLastAccessTime,

  LPFILETIME lpLastWriteTime

);

1. hFile : 시간 관련 정보를 얻을 대상 파일의 핸들을 지정한다.

2. lpCreationTime : 파일이 생성된 시간을 얻기 위해 FILETIME 구조체 변수의 주소 값을 전달한다. NULL을 전달하는 것도 가능하다.

3. lpLastAccessTime : 파일의 마지막 접근 시간을 얻기위해 FILETIME 구조체 변수의 주소값을 전달한다. NULL을 전달하는 것도 가능하다.

4.lpLastWriteTime : 파일의 마지막 데이터 갱신(덮어쓰기 포함) 시간을 얻기위해 FILETIME 구조체 변수의 주소값을 전달한다. NULL을 전달하는 것도 가능하다.

 

FILTIME 구조체

typedef struct _FILETIME{

  DWORD dwLowDataTime,

  DWORD dwHighDataTime,

}FILETIME, *PFILETIME

 

FILETIME 구조체는 시간 정보를 나타내는 8바이트(DWORD *2)자료형이다. 이 구조체는 UTC 기반으로 시간을 표현한다.

 

UTC:Coordinated Universal Time의 간략한 표현으로써 세계시간의 기준을 만들기 위해 정의된 시간이다.

 

FileTimeToSystemTime 함수의 호출로 변경되는것은 단지 포맷이다.

SYSTEMTIME 구조체 선언

typedef struct _SYSTEMTIME{

  WORD wYear,

  WORD wMonth,

  WORD wDayOfWeek,

  WORD wDay,

  WORD wHour,

  WORD wMinute,

  WORD wSecond,

  WORD wMilliseconds

}SYSTEMTIME, *PSYSTEMTIME;

FileTimeToSystemTime 함수를 통해 FILETIME 표맷을 SYSTEMTIME포맷으로 변경한다.

 

SystemTimeToTzSpecificLocalTime 함수는 UTC를 지역별, 국가별 시간대(Time zone)로 변경하는 기능을 지닌다.

SystemTimeToTzSpecificLocalTime(NULL, &stCreateUTC, &StCreateLocal)

첫번째 인자는 변경하고자 하는 시간대에 대한 정보이다. NULL이 전달되면 현재 시스템의 시간대 정보가 기준이 된다. 두번째 전달인자는 변경할 대상이 되는 UTC 기반정보이고, 세번째 전달인자는 변환된 시간정보가 채워질 변수의 주소값이다. 

 

GetFileTime 함수를 이용하여 파일의 시간정보를 얻어올 수 있고, SetFileTime 함수를 이용하여 파일의 시간정보를 변경할 수도 있다.

 

파일 사이즈 얻어오기

일반적으로 파일 사이즈를 얻어오는 코드

FILE * fp = fopen("abc.dat", "rb");

 

fseek(fp, 0, SEEK_END);

DWORD sizeofFile = ftell(fp);

위 방법은 fseek 함수로 파일 포인터를 끝으로 이동시킨 다음, ftell 함수를 호출해서 현재 위치정보를 얻어온다. 파일 포인터가 파일의 끝으로 설정되어 있기 때문에 현재 위치 정보는 파일의 크기가 된다.

 

Windows 시스템함수는 파일 크기를 직접 계산해서 반환하는 함수를 제공한다.

DWORD GetFileSize(

  HANDLE hFile,

  LPDWORD lpFileSizeHigh

);

1.hFile : 파일 핸들을 지정한다. 이 핸들이 가리키는 파이르이 크기정보를 얻게된다.

2.lpFileSizeHigh : 반환 타입을 보면 4바이트 DWORD로 선언되어있다. 따라서 4G바이트 이상의 파일 크기를 반환값으로 얻는것은 불가능하다. 4G바이트 이상되는 파일의 크기를 얻을 때 사용된다. 이 전달인자를 통해 4G바이트를 넘는 파일의 상위 4바이트 정보를 얻을 수 있다.

 

GetFileSize 함수는 4G바이트 이상의 파일에 대해서 상위 4바이트와 하위 4바이트를 각각 다른 경로를 통해서 얻어야 한다. 그러나 GetFileSizeEx함수를 사용하면 한번에 얻을 수 있다.

BOOL GetFileSizeEx(

  HANDLE hFile,

  PLARGE_INTEGER lpFileSize

);

1.hFile : 크기를 얻고자 하는 파일의 핸들을 지정한다.

2.lpFileSize : 파일 크기를 저장하기 위한 변수의 포인터(주소 값)을 인자로 전달한다. PLARGE_INTEGER는 LARGE_INTEGER의 포인터 타입이고, LARGE_INTEGER는 다음과 같이 선언되어 있다. 중요한 사실은 4바이트가 아니라 8바이트 자료형이라는 것이다.

 

typedef union _LARGE_INTEGER

{

   struct {

   DWORD LowPart;

   LONG HighPart;

   };

   LONGLONG QuadPart;

}LARGE_INTEGER, *PLARGE_INTEGER;

728x90
728x90

보통 malloc을 호출하였다면 반환되는 주소가 NULL이 아닌지 확인해야 하고, 프로세스나 쓰레드, 그리고 동기화 오브젝트들을 생성할 때에도 해당 함수 호출 후에 오류가 발생했는지 확인하는 코드를 삽입해야한다.

 

프로그램 실행시 발생하는 문제점 대부분을 예외라고 인식하자.

처리 불가능한 문제가 발생하면 프로그램은 종료하게 되는데, 문제가 발생한다고 해서 프로그램이 종료된다면 이 또한 문제가 아닐 수 없다. 외부적인 요소에 의한 문제점이든 내부적인 요소에 의한 문제점이든 구조적 예외처리 기법 관점에서 대부분 해결 가능해야한다.

프로그램 실행시 예측 가능한 대부분의 문제점을 예외로 간주하고, 처리가능하도록 프로그램을 구현해야 한다.

 

하드웨어 예외와 소프트웨어 예외

하드웨어 예외 : 하드웨어에서 인식하고 알려주는 예외

ex) 정수를 0으로 나누는 연산은 하드웨어 예외를 발생시킨다.

사칙연산을 수행하는 주체는 CPU이다. 연산을 수행하는 과정에서 정수를 0으로 나누는 요청이 들어올 경우 문제가 있다는 신호를 운영체제에 전달할 것이다. 이를 통해 운영체제는 예외 상황이 발생했음을 알고, 구조적 예외 처리 메커니즘에 의해 예외상황이 처리되도록 일을 진행시킨다.

 

소프트웨어 예외 : 소프트웨어에서 감지하는 예외이다.

작성한 프로그램 뿐만아니라 운영체제도 포함한다. 소프트웨어 예외는 프로그래머가 직접 정의할 수 있는 예외이다.

 

종료 핸들러(Termination Handler)

SEH(Structured Exception Handling), 즉 구조적 예외처리 메커니즘은 기능적 특성에 따라 크게 두가지로 나뉜다.

1.종료 핸들러(Termination Handler)

2.예외 핸들러(Exception Handler)

 

종료 핸들러의 기본 구성과 동작 원리

종료핸들러에서 사용되는 키워드 : __try, __finally

 

__try는 예외 핸들러에서도 사용된다.

__try와 __finally는 다음과 같은 형태로 구성된다.

 

__try

{

     //code

}

__finally

{

  //종료 처리

}

__try만 올 수 없고, __finally만 올 수도 없다. 이 둘 사이에는 다른 어떠한 코드도 삽입될 수 없다.

 

위 소스코드는 __try블록을 실행하면 반드시 __finally블록을 실행하라는 의미를 가진 코드이다.

__try 내부에 return문이 존재해도 return이 실행되기에 앞서서 __finally를 실행시키는것이 컴파일러에 의해 보장되어 있다.

컴파일러는 return문에 의해 반환되는 값을 임시 변수(컴파일러가 만들어내는 변수)에 저장하고, __finally블록을 실행한다음, 값을 반환하도록 바이너리 코드를 구성한다.

 

ExitProcess, ExitThread, exit 함수에 의한 프로세스 또는 쓰레드의 강제 종료는 __finally블로그이 실행으로 이어지지 않는다.

 

종료 핸들러 활용

종료핸들러를 보면 문제의 발생 유무에 상관없이 반드시 실행되어야 하는 코드를 실행시킬 수 있다.

예를들어 파일의 개방과 이에따른 종료상황이다.

개방된 파일을 적절히 닫아주지 못한다면, 데이터의 일부가 손실될 수도 있기 때문이다.

동적할당도 같은 맥락에서 생각할 수 있다. 동적할당한 메모리를 해제하지 않고 함수를 빠져나가게 된다면 할당된 메모리는 유출된 상태로 남아있게 된다.

 

종료 핸들러의 경우 "무조건 실행"이라는 특성을 가진다.

 

예외 핸들러(Exception Handler)

예외 핸들러는 "예외 상황 발생시 선별적 실행"

 

예외 핸들러와 필터(Exception Handler & Filter)

예외 처리 핸들러 기본 구성

__try

{

    //예외 발생 경계

}

__except(예외 처리 방식)

{

   //예외 처리를 위한 코드

}

 

__try 블록은 예외 상황이 발생 가능한 영역을 묶는데 사용된다.

__try 블록에서 예외 상황이 발생하면 __except블록에서 이 상황을 처리하게 된다.

 

__except문의 "예외 처리 방식"부분을 가리켜 예외 필터(Exception Filter)라 한다.

예외 필터를 통해 예외처리 메커니즘의 동작 방식을 결정한다.

예외 필터값은 총 세가지 이다. 즉 예외처리 핸들러가 동작하는 방식이 세가지라는 것이다.

 

예외 필터를 EXCEPTION_EXECUTE_HANDLER 로 선언할 경우, __try 블록의 예외 발생 라인 이후를 건너 뛰게 된다.

 __try블록의 범위는 예외 발생 가능영역 뿐만 아니라, 예외처리 이후에 실행위치도 고려해서 결정해야만 한다.

 

__try블록내에서 호출하는 함수내에서 발생하는 예외도 __try블록 내에서 발생하는 예외로 인식한다.

 

예외의 종류와 예외를 구분하는 방법

예외 상황에 따라서 처리하는 방식이 달라져야 하기 때문에 발생한 예외의 종류를 구분할 수 있어야 한다.

어떠한 종류의 예외가 발생했는지를 확인하기 위해서 GetExceptionCode함수를 호출하면 된다.

DWORD GetExceptionCode(void)

 

예외 정보

EXCEPTION_ACCESS_VIOLATION과 EXCEPTION_INT_DIVIDE_BY_ZERO는 시스템에서 발생할 수 있는 예외로서 이미 약속된 예외의 종류에 해당한다.

EXCEPTION_ACCESS_VIOLATION : 메모리 참조 오류

EXCEPTION_INT_DIVIDE_BY_ZERO : 정수를 0으로 나누는 예외

EXCEPTION_STACK_OVERFLOW :  스택 메모리가 넘쳤을 때(부족할 때)발생하는 예외

 

GetExceptionCode 함수는 __except 블록 내에서나 예외 필터 표현식을 지정하는 위치에서만 호출이 가능하다는 특징이 있다.

 

EXCEPTION_CONTINUE_EXECUTION & EXCEPTION_CONTINUE_SEARCH 예외 필터 표현식

EXCEPTION_CONTINUE_EXECUTION의 표현식의 경우 __except 블록의 내부코드를 실행하지 않고 예외가 발생한 그 위치로 이동하여 실행을 이어나간다.

 

EXCEPTION_CONTINUE_SEARCH 란 다른곳에 있는 예외 핸들러를 통해서 예외를 처리하라는 것이다.

예외 핸들러를 찾을 때는 함수가 호출된 순서(스택이 쌓여있는 순서)를 바탕으로 예외핸들러를 찾게된다.

EXCEPTION_CONTINUE_SEARCH은 예외가 처리되어야 하는 위치를 별도로 지정하기 위한 용도로 사용된다.

 

소프트웨어 예외(Software Exception)의 발생

void RaiseException(

    DWORD dwExceptionCode,

    DWORD dwExceptionFalgs,

    DWORD nNumberOfArguments,

    const ULONG_PTR * lpArguments

);

1.dwExceptionCode : 발생시킬 예외의 형태를 지정한다.

2.dwExceptionFlags : 예외 발생 이후의 실행방식에 있어서 제한을 둘때 사용한다.

3.nNumberOfArguments : 추가정보의 개수를 지정한다.

4.lpArguments : 추가정보를 전달한다.

위 함수는 예외 발생을 알리기 위한 용도로 사용된다.

이 함수가 호출되면 SEH 메커니즘이 작동되면서, 예외처리가 전개된다.

 

GetExceptionCode 함수호출을 통해 얻게되는 예외코드를 통해서 부가적인 정보를 얻을 수 있다.

 

RaiseException의 첫번째 인자로 EXCEPTION_NONCONTINUEABLE이 올수있다.

EXCEPTION_NONCONTINUABLE은 예외가 발생하면 프로그램을 종료시키는 인자이다. EXCEPTION_CONTINUE_EXECUTION은 예외처리흐름을 막는 용도로 사용한다.

 

GetExceptionInformation

예외발생시 GetExceptionCode가 반환해 주는 정보보다 더 많은 정보를 얻기 원한다면, GetExceptionInformation 함수호출을 고려할 수 있다. 이 함수는 "예외 필터 표현식을 지정하는 부분"에서만 호출 가능하다.

 

LPEXCEPTION_POINTERS GetExceptionInformation(void);

GetExceptionInformation 함수가 호출되면, EXCEPTION_POINTERS 구조체 변수의 주소값이 반환된다.

 

EXCEPTION_POINTERS 구조체

typedef struct _EXCEPTION_POINTERS

{

    PEXCEPTION_RECORD ExceptionRecord;

    PCONTEXT ContextRecord;

 }EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

 

EXCEPTION_POINTERS 구조체는 EXCEPTION_RECORD구조체 포인터 PEXCEPTION_RECORD와 CONTEXT의 구조체 포인터 PCONTEXT로 구성되어 있음을 알 수 있다.

즉 예외발생후 GetExceptionInformation 함수가 호출되면 다음과 같은 구조로 구성된 예외 관련 정보를 얻게된다.

EXCEPTION_POINTERS 구조체 변수에는 두가지 정보가 채워진다. 하나는 예외 자체에 대한 정보가, 또 하나는 프로세서(CPU)의 레지스터 데이터를 비롯한 프로세서의 종속적인 정보가 채워진다.

728x90
728x90

1.물리 주소(Physical Address)

임베디드 시스템과 범용 시스템의 가장 큰 차이점은 하드디스크의 존재유무 이다.

범용 시스템은 windows 운영체제에서부터 각종 소프트웨어를 하드디스크에 저장해 놓고, 전원이 인가되면 저장된 소프트웨어를 기반으로 동작하게 한다.

 

임베디드 시스템은 하드디스크가 없다. 하드디스크가 없어서 운영체제를 비롯한 각종 소프트웨어를 플래쉬 메모리에 저장하고 전원이 들어오면 플래쉬 메모리 데이터를 RAM에 올린다.

 

프로그램이 메인메모리에 올라갈때 메인메모리의 크기에 따라 접근 가능한 영역이 정해진다. 

ex) 램 크기가 16M라면 CPU가 접근가능한 메인 메모리 영역은 0번지~(16 * 1024 *1024)-1 번지이다.

 

위와같은 주소는 실제 물리적인 메인 메모리의 주소 범위에 해당하며, 이렇게 주소를 할당하는 것을 가리켜 물리적 주소 지정(Physical Addressing)이라 한다.

 

물리적 주소 지정은 메인메모리 크기에 따라 지정가능한 주소의 범위가 결정된다.

물리적 주소 지정은 CPU 입장에서 접근 가능한 주소의 범위가 제한된다는 뜻이다. 또한 프로그래머가 할당할 수 있는 주소 범위가 제한적이라는 뜻도된다.

 

메인 메모리에는 항상 운영체제가 돌아가고 있다. 메인 메모리의 시작부터 일정영역을 커널영역이라 하고 컴퓨터가 켜지면 해당 영역에 운영체제(커널 포함)가 적재된다.

그렇기 때문에 메인 메모리 크기가 16M바이트 라면 이 범위 안에서 운영체제와 프로그램을 로딩(Loading)하고 프로그램 실행과정에서 메모리를 할당해야만 한다.

 

가상주소(Virtual Address)시스템 1

32비트 시스템에서 프로세스 생성시 4G바이트의 메모리를 할당받을 수 있다. 할당 받을 수 있는 메모리에 비해 메인 메모리는 매우 작다.

따라서 프로세스 생성시 할당받는 4G바이트는 실제로 존재하지 않는 가사으이 주소이다.

이렇게 주소를 지정하는 것을 가상 주소 지정(Virtual Addressing)이라 하며, 가상 주소 지정을 통해서 할당받는 4G바이트를 가리켜 가상 메모리 공간(Virtual Address Space)이라 한다.

 

MMU(Memory Management Unit) : MMU는 CPU와 하나로 패키징 되어있는 장치이다. MMU는 16K바이트 밖에 존재하지 않는 메모리를 64K바이트가 존재하는 것처럼 CPU가 느끼도록 컨트롤 하는 역할을 한다.(가상 주소 시스템)

 

페이지&페이지 프레임

프로그램의 스페이셜 로컬리티 특성을 반영하여 블록 단위로 메모리에 매핑된다. 블록을 메인 메모리 입장에서는 페이지 프레임(Page Frame)이라 하고, 소프트웨어 입장에서는 페이지(Page)라 한다.

페이지 프레임은 실제 메인 메모리를 의미하고, 페이지는 가상 메모리 블록을 의미한다. 페이지 프레임과 페이지의 크기는 일치한다.

 

 위 그림을 보면 현재 가상메모리 0K-4K가 물리메모리에 매핑되어있다.(MMU 에 의해 매핑)CPU가 0K - 4K 사이에 존재하는 데이터를 요구할 경우 MMU는 매핑된 물리 메모리를 참조해서 데이터를 전송해준다.

 

가상 주소(Virtual Address) 시스템 2

메모리 부족 문제 해결

하드디스크도 램(RAM)과 비교해서 속도를 제외하면 그 기능에 있어서 부족함이 없는 메모리이다.

하드디스크를 이용하여 램의 메모리 부족 문제를 해결할 수 있다. 스왑파일(Swap File)이라는 개념은 램(RAM)에 해당하는 메인 메모리를 하드 디스크로 까지 확장한 개념이다.

스왑파일을 통해 메인메모리 부족 문제를 해결해 줄 수 있다.

 

메인메모리의 부족한 부분은 스오바파일을 통해 하드디스크로 해결할 수 있다.

 

 

 여러가지 프로세스를 실행시킬때 각 프로세스의 전환을 프로세스에 해당하는 스왑파일 전환으로 실행시킨다.

프로세스 A실행중 프로세스 B 를 실행시킬 때 램에 존재하는 프로세스 A의 실행을 위한 데이터 모두를 프로세스 A의 스스왑파일에 저장하고, 프로세스 B 실행을 위한 데이터를 프로세스 B의 스왑파일로 부터 램에 가져다 놓는다.

페이징 기법은 하나의 프로세스 내에서 개별 페이지들을 관리하는 기법이다.

스왑핑은 메인 메모리에 실행중인 프로세들을 관리하는 기법이다.

 

페이징기법을 통해 작은 메인메모리가 프로세스의 모든 주소에 접근할 수 있게 하였다.

추가적으로 메모리 스왑핑(Swapping)을 통해서 여러 프로세스가 작은 메인메모리에서 실행될수 있게 하였다.

위 두 기법을 통해 효율적으로 메모리를 참조할 수 있다.

 

*메모리 스왑핑 : 프로세스 실행에 필요한 데이터를 하드디스크에 저장(Store)하고 실행시킬 프로세스를 스왑파일에서 로드(LOAD)함

  

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

파일 I/O 와 디렉터리 컨트롤(1)  (0) 2020.08.16
SEH(Structured Exception Handling)  (0) 2020.08.10
캐쉬(cache)와 캐쉬 알고리즘  (0) 2020.08.07
메모리 계층  (0) 2020.08.07
쓰레드 풀링(Pooling)  (0) 2020.08.05
728x90

컴퓨터 프로그램의 일반적인 특성

컴퓨터 프로그램을 유심히 관찰해 보면, 공통적으로 지니는 일반적인 특성이 하나 존재한다.

 

대부분의 함수가 특정 연산을 하기위해 지역변수를 선언한다. 이러한 지역변수의 특성은 선언 및 초기화 이후 다양한 값으로 변경도 되고, 값을 얻기 위한 참조도 빈번하게 일어난다는 것이다. 이러한 특성을 템퍼럴 로컬리티(Temporal Locality)라 한다.

 

템퍼럴 로컬리티(Temporal Locality)란, 프로그램 실행시 한번 접근이 이뤄진 주소의 메모리 영역은 자주 접근하게 된다는 프로그램 특성을 표현할 때 사용하는 말이다.

 

스페이셜 로컬리티(Spatial Locality)란, 프로그램 실행 시 접근하는 메모리 영역은 이미 접근이 이루어진 영여그이 근처일 확률이 높다는 프로그램 성격을 표현할 때 사용하는 말이다.

즉 0x12번지 메모리에 접근 했다면, 다음번 메모리 접근은 그 주소(0x12 번지)에서 멀리 떨어지지 않은 곳일 확률이 높다는 프로그램의 성격을 표현한다.

 

위와같은 특성들을 고려하여 구현된 코드를 캐쉬 프렌드리 코드(Cache Friendly Code)라 한다.

 

캐쉬 알고리즘

 ALU연산 과정중 필요한 데이터가 있다면 이를 레지스터로 이동시켜야 한다. 필요한 데이터가 0x1000번지에 존재하는 데이터라면 이 주소에 해당하는 데이터를 레지스터로 가져오기 위해서 데이터가 존재하는 곳을 찾아봐야 한다. L1캐쉬에 해당 데이터가 존재하는 곳을 찾아봐야한다.

 

 L1캐쉬에 찾는 데이터가 존재할 경우 캐쉬 힛(Cache Hit)이 발생했다고 하며, 이 데이터를 레지스터로 이동시킨다.

L1캐쉬에 데이터가 존재하지 않을 경우 이를 가리켜 캐쉬 미스(Cache Miss)가 발생했다 하고, 캐쉬 미스가 발생하면 L2캐쉬에서 해당 데이터를 가져오게 된다.

(L2캐쉬에 존재하지 않으면 메인메모리에서, ->하드디스크까지 단계별로 탐색한다.)

 

 캐쉬와 메인메모리, 하드디스크 간에 데이터의 이동은 블록단위로 진행이 된다.

블록단위로 전송을 함으로써 스페이셜 로컬리티(Spatial Locality)의 특성을 성능 향상에 십분 활용하게 된다.

->블록 단위란 필요한 주소의 데이터만을 가져오는 것이 아니라 근처의 주소들을 포함하여 하나의 '블록'의 단위로 데이터를 전송하는 것이다.

 

위의 캐쉬 알고리즘 그림을 보면 메모리의 피라미드 구조상 아래로 내려갈수록 블록 크기는 커지게 된다. 이를 통해 아래에 존재하는 메모리 일수록 접근횟수를 줄이는 효과가 있다. 아래에 존재하는 메모리 일수록 속도가 느리기 때문에 접근 횟수를 줄이는 것이 성능향상에 많은 도움이 된다.

 

캐쉬미스가 발생할때 L1캐쉬에서  L2캐쉬로 부터 데이터 블록을 읽어 들일 때 저장할 공간이 없는 경우가 있다.

꽉차 있는 L1캐쉬에 데이터를 저장하려면 당연히 기존에 저장한 데이터를 밀어내야 하는데 이때 블록 교체 알고리즘에 의해서 데이터를 밀어내게 된다.

블록교체 알고리즘은 캐쉬 교체 정책(Cache's Replacement Policy)에 따라서 달라질 수 있다.

가장 보편적인 블록교체 알고리즘은 LRU(Least-Recently Used)알고리즘 이다. 이 알고리즘은 가장 오래전에 참조된 블록을 밀어내는 알고리즘이다.

 

캐쉬 프렌드리 코드(Cache Friendly Code)작성 기법

템퍼럴 로컬리티와 스페이셜 로컬리티의 특성을 이용해야한다.

ex)

...

int total = 0;

 

for(int i=0; i<10; i++)

{

    for(int j=0; j<10;j++)

    {

        total += arr[j][i];

    }

}

...

위 코드를 보면 10x10배열을 모두 더하는 코드라는 것을 알 수 있다.

 

변수 total의 값이 빈번하게 갱신되고 있다는 점에서 템퍼럴 로컬리티는 만족이된다.

그러나 배열의 접근이 열 단위로도 이루어지는 점에서 스페이셜 로컬리티를 만족시키지 못한것이다.

행 단위접근이던 열단위 접근이던 스페이셜 로컬리티를 어느정도 만족하지만, 배열의 크기가 커질수록 스페이셜 로컬리티의 만족도는 떨어지게 된다.

그러나 배열의 크기와 상관없이 행단위로 접근한다면 그것은 스페이셜 로컬리티를 만족시킨다.

 

 

 

 

 

 

 

 

 

 

 

 

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

SEH(Structured Exception Handling)  (0) 2020.08.10
가상 메모리(Virtual Memory)  (0) 2020.08.08
메모리 계층  (0) 2020.08.07
쓰레드 풀링(Pooling)  (0) 2020.08.05
타이머(Timer) 기반 동기화  (0) 2020.08.03
728x90

메모리의 범위와 종류

1.메인(Main) 메모리

메인 메모리인 램(RAM)은 정확하게 이야기 하자면 D램(D-RAM)계열의 메모리이다.

 

2.레지스터(Register)

CPU내부에 내장되어 있으며 연산을 위한 저장소의 역할을 한다.

 

3.캐쉬(Cache)

캐쉬는 D램보다 빠른 S램(S-RAM)으로 구성하는데 램이라는 단어는 이미 메인 메모리에서 사용되므로 그냥 캐쉬라고 표현한다. 캐쉬 메모리는 CPU에 가장 근접해 있는 메모리 이다.

(CPU 내부에 있는 것이 아니라 바로 옆에 있다는 것이다.)

 

4.하드 디스크(Hard Disk), 외의 저장 장치들

하드 디스크는 크고 작은 파일들을 저장하기 위한 용도로 사용되지만, 프로그램 실행에 있어서도 중요한 의미를 지닌다. 또한 SD카드, CD-ROM과 같은 I/O장치들도 메모리에 해당한다.

 

메모리 계층 구조

프로그램이 실행되는 동안 메모리의 역할은 데이터의 입력 및 출력이다.

 

메모리 사이의 가장 큰 차이점은 CPU로 부터의 거리이다.

레지스터는 CPU내부에 존재하는 메모리이다. CPU외부에서 가장 가까이 있는것이 캐쉬메모리이고, 그 다음이 메인메모리 이다. 가장 멀리있는 것이 하드디스크이다.

 

CPU와 가까이 있을 수록 속도가 빠르고, 멀리 있을 수록 속도가 느리다.

레지스터의 경우 CPU내부에 있으므로 접근에 있어서 별다른 절차가 필요없다. 그러나 메인 메모리에 접근하기 위해서는 몇몇 복잡한 과정을 거쳐야한다. 대표적인 것이 버스(BUS)인터페이스 컨트롤 이다. 데이터 입출력을 위해 버스를 거쳐야하기 때문에 그만큼 더 느리다.

 

하드디스크를 CPU가까이 가져간다면  속도도 빠르고 용량도 클것이다.

그러나 CPU근처로 대용량 메모리를 가져갈수록 기술적인 문제들도 발생하고 비용도 훨씬 많이 든다.

->하드디스크 용량을 수십기가바이트 늘리는 것보다 캐쉬 메모리 1M바이트 늘리는데 드는 비용이 훨씬 크다.

 

 

 

1.하드디스크에 있는 내용은 프로그램의 실행을 위해서 메인 메모리로 이동한다.

2.메인 메모리에 있는 데이터 일부도 실행을 위해서 L2캐쉬로 이동한다.

3.L2캐쉬에 있는 데이터 일부는 L1캐쉬로 이동을 하고, L1캐쉬에 있는 데이터 중에서 연산에 필요한 데이터가 레지스터로 이동한다.

 

 모든 메모리의 역할이 피라미드 구조에서 자신보다 아래에 있는 메모리를 캐쉬(자주 사용되는 메모리의 일부를 저장해서 속도를 향상시키는것)하기 위해서 존재한다.

 

1.연산에 필요한 데이터가 레지스터에 존재하지 않는다면 L1캐쉬를 확인한다.

2.L1캐쉬가 가지고 있지 않는다면 L2캐쉬를 확인한다.

3.L2캐쉬에도 없다면 메인메모리를 확인한다.

4.메인 메모리에도 존재하지 않는다면 결국은 하드디스크에서 읽어들이게 된다.

 

실제로는 메인메모리를 제외한 L1캐쉬와 L2캐쉬에, 연산에 필요한 데이터가 존재할 확률이 90%이상 된다. 캐쉬는 속도를 많이 향상시킨다.

 

Level1 캐쉬와 Level2 캐쉬

L1캐쉬는 CPU내부가 아니라 시스템 보드(메인 보드)에 존재하는 메모리 였지만, 성능향상을 위해 CPU내부로 들어가게 됐다. 그리고 CPU외부에 또 다른 캐쉬가 등장했는데 이것이 L2 캐쉬이다.

 

시스템의 성능을 좌우하는 클럭의 속도는 항상 느린쪽에 맞춰지게 되어있다.

예를들어 CPU는 상당히 고속화 되었지만, 메인메모리의 처리속도는 CPU의 속도를 따라가지 못하고있다. CPU속도가 두배 빨라져도 주변장치의 속도가 그대로 라면, 결코 기대하는 만큼의 속도 향상을 경험할 수 없다.

 

<메인 메모리 캐쉬간 병목현상>

ALU는 연산을 위해서 레지스터에 저장된 데이터를 가지고 와야하는데, 필요한 데이터가 존재하지 않을 경우 메인메모리에서 데이터를 읽어와야만 한다. 자주 사용되는 주소 번지의 데이터는 캐쉬 메모리에 저장해 두어 메인 메모리까지 가야만 하는 빈도수를 줄여준다.

 

필요로 하는 데이터가 캐쉬에 존재하지 않는다면 메인 메모리에서 데이터를 가져와야한다. 느린 메인메모리가 데이터를 전송해 줄 때까지 CPU는 쉬고 있어야만 한다. 여기가 '병목 현상'이 발생하는 부분이다.

 

병목현상을 최소화 하기 위해서는 캐쉬 사이즈를 키우면 된다. 그러나 이는 기술적으로 힘들기 때문에 캐쉬 메모리를 하나 더 설치한다.

 

L2 캐쉬의 역할

L2캐쉬의 개입으로 병목현상의 주체가 L1캐쉬에서 L2캐쉬로 이동하였다. L2캐쉬는 CPU 가까이 존재하므로 당연히 메인메모리보다 접근이 빠르다. CPU입장에서는 클럭속도의 차이에 의해 발생하는 병목현상의 부담을 그만큼 줄인 것이다.

 

병목현상의 발생위치가 L1에서 L2로 이동했을 뿐 CPU의 처리속도가 빨라졌다고 말할 수 있겠는가?

 

물론 빨라졌다. L2캐쉬는 L1캐쉬보다 크기가 크게 구성되기 때문에 기존의 L1캐쉬보다 필요로 하는 데이터를 훨씬 더 많이 저장할 수 있다. 그렇기 때문에 병목현상이 줄어들고 CPU의 처리속도가 향상되었다.

 

 

728x90
728x90

쓰레드의 생성과 소멸은 시스템에 많은 부담을 준다.

컴퓨터의 쓰레드 생성 및 소멸로 인해 성능이 저하되는 것을 피하기 위해 존재하는 것이 쓰레드 풀 이다.

쓰레드 풀의 기본 원리는 쓰레드 재활용이다.

할당된 일을 마친 쓰레드를 소멸시키지 않고, 쓰레드 풀에 저장해 뒀다가 필요할 때 다시 꺼내 쓰는 개념이다.

 

 

쓰레드 풀은 처리할 일(Work)이 등록되기 전에 생성된다.

풀이 생성됨과 동시에 쓰레드들도 생성되어 풀에서 대기하게 된다.

쓰레드 풀이 생성된 상태에서 처리해야 할 일(Work)이 하나 등록되었다면 쓰레드 풀에 존재하는 쓰레드 하나를 임의로 할당해서 일을 처리한다.

 

쓰레드 풀에 존재하는 쓰레드 수 보다 처리해야할 일의 수가 많다면, 일이 순서대로 처리되도록 디자인 할 수 있고, 추가적인 쓰레드가 생성되도록 풀을 디자인 할 수도 있다.

 

쓰레드 풀링은 한번 생성한 쓰레드를 재활용해서 시스템의 부담을 덜어주기 위한 기법이다.

쓰레드 풀은 여러개의 쓰레드를 생성한다. 그리고 실행해야 할 일이 등록될 때마다 미리 생성해 놓은 쓰레드 중 하나를 할당한다. 그리고 일이 끝나면 쓰레드는 소멸시키지 않고 다음 일을 위해서 보관한다.

728x90
728x90

Timer 기반 동기화 오브젝트는 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지닌다.

 

타이버 기반으로 동기화 하는 것은 임계영역 문제 해결을 위한 동기화가 아니라 쓰레드의 실행시간 및 실행 주기를 결정하겠다는 의미이다.

 

수동 리셋 타이머 : 가장 일반적인 타이머로, 알람시계와 같은 특성이다.

주기적 타이머 : 수동리셋 타이머에 주기적인 특성이 가해진 형태이다. 6시에 알람을 맞춰놓으면 6시에도 알람이 울리고 30분마다 한번씩 주기적으로 알람이 울리도록 설정하는 기능이 있다.

 

수동 리셋 타이머(Manual-Reset Timer)

타이머 오브젝트는 정해진 시간이 지나야 Signaled 상태가 되는 커널 오브젝트이다.

 

타이머 오브젝트 생성 함수

HANDLE CreateWaitableTimer(

    LPSECURITY_ATTRIBUTES IpTimerAttributes,

    BOOL bManualReset,

    LPCTSTR lpTimerName

);

1.lpTimerAttributes : 보안 속성 지정, 핸들을 자식 프로세스에게 상속하고자 할 경우 NULL이 아닌 다른 값

2.bManualReset : 타이머 오브젝트를 수동 리셋(Manual-Reset)모드로 생성 할 것인지, 자동 리셋(Auto-Reset)모드로 생성할 것인지 결정

3.lpTimerName : 타이머 오브젝트에 이름을 붙여줄 경우에 사용되는 전달 인자 NULL 전달 시 이름없는 타이머 오브젝트 생성

 

타이머 오브젝트는 무조건 Non-Signaled 상태로 생성된다.

시간이 지나서 Signaled 상태가 되어야 한다.

 

타이머 시간 설정 함수

BOOL SetWaitableTimer

   HANDLE hTimer,

   const LARGE_INTEGER * pDueTime,

   LONG lPeriod,

   PTIMERAPCROUTINE pfnCompletionRoutine,

   LPVOID lpArgToCompletionRoutine,

   BOOL fResume

);

1.hTimer : 알람을 설정할 타이머 오브젝트 핸들

2.pDueTime : 알람이 울리는 시간을 지정하는 매개변수, +값이 전달되면 절대 시간, -값이 전달되면 상대시간

"A시 B분 알람"-> +값

"지금으로부터 A초후 알람" ->? -값

1000만 분의 1초(100 Nanoseconds)단위로 시간 설정

3.lPeriod : 타이머가 주기적으로 알람을 울리게 할 때 사용하는 전달인자. 1/1000초(Milliseconds)단위로 전달한다. 0을 전달할 경우 주기적인 알람을 사용하지 않겠다는 의미이다.

4,5 pfnCompletionRoutine 과 lpArgToCompletionRoutine은 타이머를 생성하는 용도의 함수이다.

6.fResume은 전원관리와 관련이 있는 매개변수인데, 기본적으로 FALSE 전달을 원칙으로 한다.

 

LARGE_INTEGER는 64비트 정수를 표현하지 못하는 시스템에서 64비트 정수를 표현하기 위해 선언된 자료형이다.

 

주기적 타이머(Periodic-Timer)

SetWaitableTimer(

 hTimer, &liDueTime, 5000, NULL, NULL, FALSE);

위와 같이 3번째 인자에 5000을 넣으면 5초간격으로 타이머가 Signaled 상태가 된다.

 

CancleWaitableTimer 함수

가동중에 있는 타이머를 중지시키는 기능의 함수

타이머를 소멸시키거나 할당된 자원을 반환하는 종류가 아니다.

 

BOOL CancelWaitableTimer(

   HANDLE hTimer

);

1.hTimer : 알람을 해제할 타이머 오브젝트의 핸들을 전달한다.

전달된 타이머는 알람이 해제된다.

 

타이머의 자원을 반환하기 위해서는 CloseHandle을 사용해야한다.

 

 

 

 

 

 

728x90
728x90

쓰레드의 실행 순서를 동기화 한다는 것은 메모리에 접근하는 쓰레드의 실행 순서를 동기화 한다는 것이다. 즉 "실행 순서 동기화"는 "메모리 접근 동기화"를 포함하는 개념이다. 다만 초점이 실행 순서에 맞춰져 있고 이러한 부분을 부각시키기 위해 "실행 순서 동기화" 표현을 사용한다.

 

생산자 / 소비자 모델

생산자 / 소비자 모델은 실행되는 쓰레드의 순서가 중요한 상황을 설명할 때 종종 소개되는 모델이다.

생산자가 물건을 만들어야만 소비자가 소비할 수 있다. 이 순서가 바뀌어버린다면 소비자는 빈테이블에서 빵을 찾게되고, 소비자가 빈 테이블임을 알고 떠난 뒤에야 생산자가 빵을 가져다 놓는 일이 발생하게 된다. 당연히 이 빵은 소비되지 않고 남아있다.

 

이벤트(Event) 기반 동기화

세마포어나 뮤텍스와 같이 이 기법에서도 동기화를 위한 오브젝트가 사용된다. 이러한 오브젝트를 "이벤트(Event) 오브젝트"라고 표현한다. 그리고 이러한 오브젝트를 보편적으로 '이벤트'라고 부른다.

 

이벤트에는 이벤트를 생성 및 소멸시키는 함수와, 이벤트를 소유 및 반환하는 함수가 전부이다.

 

이벤트 오브젝트 생성 함수

HANDLE CreateEvent(

    LPSECURITY_ATTRIBUTES lpEventAttributes,

    BOOL bManualReset,

    BOOL bInitialState,

    LPCTSTR lpName

);

1.lpEventAttributes : 보안 속성을 지정할 때 싸용한다.

2.bManualReset : 수동 리셋(Manual-Reset)모드로 이벤트 오브젝트를 생성하느냐, 자동 리셋(Auto-Reset)모드로 이벤트 오브젝트를 생성하느냐를 결정짓는다. TRUE가 전달될 경우 수동 리셋(Manual-Reset) 모드 이벤트 오브젝트가, FALSE가 전달될 경우 자동 리셋(Auto - Reset) 모드 이벤트 오브젝트가 생성된다.

3.bInitialState : 이벤트 오브젝트의 초기 상태 결정, TRUE가 전달될 경우 Signaled 상태로 생성되고, FALSE 일 경우 Non-Signaled 상태의 이벤트가 생성된다.

4.lpName : 이벤트 오브젝트에 이름을 줄 경우에 사용하는 전달인자이다.

 

이벤트 오브젝트를 소멸시킬 때에는 다른 커널 오브젝트와 마찬가지로 CloseHandle 함수를 사용하면 된다.

 

쓰레드나 프로세스의 커널 오브젝트의 경우, Non-Signaled 상태로 생성되고 쓰레드나 프로세스가 종료될 경우 해당 커널 오브젝트는 Signaled상태로 자동 변경된다.

이벤트 오브젝트의 경우 자동으로 Signaled 상태가 되는 상황이없다.

특정 함수 호출을 통해서 Signaled 상태로 변경해주어야 한다. 이벤트 오브젝트가 Signaled 상태가 되어 대기중이던 쓰레드가 블로킹 상태를 빠져 나왔을 때(WaitForSingleObject 함수를 호출 완료) 이벤트 오브젝트의 상태가 그대로Signaled 상태라면, 수동 리셋 모드(Manual-Reset 모드) 이벤트 오브젝트이고, 자동으로 Non-Signaled 상태로 변경됐다면, 자동 리셋 모드(Auto - Reset 모드) 이벤트 오브젝트이다.

 

벤트 오브젝트 실행순서 동기화

1.프로그래머 요청에 의해 이벤트는 Signaled 상태가 된다.

2.Non-Signaled 상태의 이벤트 오브젝트에 의해 WaitForSingleObject 함수 호출이 블로킹이 되면, Signaled 상태가 되는 순간 블로킹된 함수를 빠져나오게 된다. 이때 자동 리셋 모드 이벤트 오브젝트라면, Non-Signaled 상태로의 변경은 자동으로 이뤄진다.

 

이벤트 오브젝트의 경우 자동 리셋 모드일때 WaitForSingleObject함수에 의해 자동으로 Signaled->Non-Signaled 로 변경된다.

 

이벤트 오브젝트의 상태를 변경시키는 함수

BOOL ResetEvent(

   HANDLE hEvent

);

1.hEvent : 이벤트 오브젝트의 핸들을 인자로 전달한다. 전달된 핸들의 오브젝트는 Non-Signaled 상태가 된다.

 

수동 리셋 모드 이벤트 동기화의 경우 동시에 여러 쓰레드가 접근이 가능하도록 만들 수 있다.

 

BOOL SetEvent(

   HANDLE hEvent

);

1.hEvent : 이벤트 오브젝트 핸들을 인자로 전달한다. 전달된 핸들의 오브젝트가 Signaled 상태가 된다.

728x90

+ Recent posts