반응형

애니메이션

기본적으로 유니티의 애니메이션은 다음으로 구성되어 있다.

Pose : 각 한 모션에 대한 애니메이션의 키 값(각 프레임별로 메쉬가 어느 위치에 있는지)들로 구성된 확장자.

pose구성도

Pose Controller : 각 Pose들을 FSM식으로 구성하여 하나의 모델 애니메이션 사이클을 나타내는 정보. 

Pose Controller 구성도

Animator Component : Pose Controller를 참조해 모션을 직접 컨트롤 하는 개체.

Animator Component 구성

 

반응형

'프로그래밍 > 게임제작' 카테고리의 다른 글

[WinAPI] 유피의 소원  (2) 2024.10.07
[연습작] 콘솔 오목게임  (0) 2024.04.03
[연습작] 던파 플래시 모작  (1) 2024.02.12
[연습작] 비행기 게임  (1) 2024.02.12
반응형

내가 inline키워드를 명시적으로 쓰는 타입의 프로그래머인데, 링커에러가 뜬다? 그러면 이 글을 보시길 바란다.

 

3D자체엔진을 제작하려고 3D리소스 래핑을 하는 도중에 inline에 대하여 엄청난 오용을 하고 있었다는 사실을 깨달았다.

// Material.h
// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture);
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType);
private:
	MaterialMapArray mMaterialMaps;
};

// Material.cpp
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
inline void MaterialResource::SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture)
{
	mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture;
}
inline std::shared_ptr<Texture> MaterialResource::GetMaterialMap(eMaterialMapType _mapType)
{
	return mMaterialMaps[static_cast<UINT>(_mapType)];
}

이런 클래스가 있다.

이 클래스는 일단 SetMaterialMap() 함수를 사용하면 에러를 이렇게 낸다.

개같은...링커에러

그렇다. 그 악명높은 링커에러가 뜬다.

근데 정의와 구현부도 제대로 되어있고, 헤더가 꼬인거도 아니고....당연하게도 main문은 있다.

애초에 SetMaterialMap() 함수를 사용한 코드에서 나는 에러다. 해당 코드를 지우면 제대로 빌드가 된다.

 

뭐가 문제임? 하는 사람도 있을거고,

왜 이따구로 짬? 하는 사람도 있을거다...

일단 확실한건, 대부분은 "뭐가 문제임?" 혹은 "inline이 뭐임?" 하는 반응이 대부분일 것이다.

내가 아는 inline의 역할은 컴파일 단계에서 함수 호출을 없애고, 호출 위치에 함수의 코드를 그대로 붙여넣어 함수 호출비용을 줄이는 것으로 알고있었다.

물론 이것도 맞다. 하지만 보조적으로 한 가지 역할이 더 있다.

그건 바로 여러 파일에서 #include한 함수가 중복 정의되어도 괜찮다고 링커에게 알려주는 역할이라는 것이다......

약간 처음 들으면 애매할 수 있는데, 이해하고 보면 당연한 말이다.

이해를 돕기 위해 gpt쌤의 말을 가져오면,

더보기

inline을 쓰면 헤더 파일에서 구현해야 하는 이유는 컴파일러와 링커의 작동 방식 때문입니다. 이를 차근차근 설명드릴게요.


1. 컴파일러는 각 번역 단위에서 따로 작동

  • C++에서 코드를 컴파일할 때 **각 소스 파일(.cpp)**은 독립적인 "번역 단위"로 처리됩니다.
  • inline 함수는 컴파일러에게 "이 함수는 어디에서든 중복 정의될 수 있지만, 중복되어도 괜찮다"는 뜻을 전달합니다.

중요: inline 함수는 중복 정의를 허용하지만, 컴파일러는 이를 보장하기 위해 번역 단위마다 해당 함수의 정의를 알고 있어야 합니다.


2. 헤더 파일은 모든 번역 단위에 포함됨

  • 헤더 파일에 inline 함수를 정의하면 이를 포함하는 모든 번역 단위에서 동일한 함수 정의가 생깁니다.
  • 이 정의가 동일하다면, 링커는 이를 하나로 합쳐 문제가 없게 처리합니다.

예제:

cpp
코드 복사
// header.h inline int Add(int a, int b) { return a + b; }
  • 이 코드가 포함된 각 .cpp 파일마다 Add 함수의 정의가 복사됩니다.
  • 링커는 "이건 모두 동일한 함수다!"라고 인식하고 하나로 합칩니다.

3. CPP 파일에 정의하면 발생하는 문제

  • inline 함수는 헤더 파일에 정의되지 않으면 다른 번역 단위에서 해당 정의를 볼 수 없습니다.
  • 링커는 다른 번역 단위에서 함수의 정의를 찾을 수 없고, "이 기호를 찾을 수 없다"는 에러를 발생시킵니다.

문제 상황:

cpp
코드 복사
// header.h inline int Add(int a, int b); // 선언만 있고 구현 없음 // source.cpp #include "header.h" int Add(int a, int b) { // 구현이 cpp 파일에만 존재 return a + b; }
  • 링커는 Add 함수가 헤더에 정의되어 있어야 한다고 가정하지만, 실제 정의는 CPP에 있으므로 충돌합니다.

4. inline의 의미와 동작

  • inline의 의미: 함수가 호출될 때 **코드 삽입(inline expansion)**으로 대체될 수 있다.
  • 컴파일러가 함수의 코드를 호출자 위치에 직접 삽입하려면, 함수의 **정의(body)**를 컴파일 시점에 알아야 합니다.
  • 따라서 inline 함수는 반드시 모든 번역 단위에서 동일한 정의를 볼 수 있도록 헤더 파일에 구현해야 합니다.

5. 중복 정의를 피하기 위한 설계

  • 일반적인 함수는 여러 번 정의되면 링커가 에러를 발생시킵니다(One Definition Rule 위반).
  • 그러나 inline 함수는 특별히 "같은 정의라면 여러 번 있어도 괜찮다"는 규칙이 있습니다.
  • 이 규칙을 제대로 활용하려면, 모든 번역 단위에서 동일한 정의가 필요하므로 헤더 파일에 구현해야 합니다.

요약: 헤더에 구현해야 하는 이유

  1. 모든 번역 단위에서 동일한 함수 정의를 보장하기 위해.
  2. 함수 호출 위치에 직접 코드 삽입하려면, 컴파일러가 함수의 정의를 알고 있어야 하기 때문에.
  3. 중복 정의로 인한 링킹 에러를 방지하기 위해.

따라서, inline 함수는 헤더 파일에 정의를 작성하고, CPP 파일에서는 구현을 배제해야 안정적으로 동작합니다! 😊

따라서, 쉽게 말하면 템플릿이랑 컴파일과정이 비슷하다고 볼 수 있습니다.

템플릿도 헤더에 구현을 하지 않으면 에러가 뜨므로.....

 

그래서 이렇게 바꾸니까 되었습니다.

class Texture;

// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture) {
		mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture; 
	}
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType) { 
		return mMaterialMaps[static_cast<UINT>(_mapType)];
	}
private:
	MaterialMapArray mMaterialMaps;
};
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
MaterialState::MaterialState(MaterialResource* _pMaterialResource)
	: mMaterialResource(_pMaterialResource)
{
	for (int mapType = 0; mapType < static_cast<UINT>(eMaterialMapType::SIZE); ++mapType)
	{
		mCBuffer.UseMap[mapType] = TRUE;
	}
}

cpp에 구현을 없애고 h에 구현을 했습니다.

 

근데 걍 느낀게....

처음에 inlline 굳이 쓴건, 어차피 컴파일러가 자동으로 inline화를 판단하지만, inline키워드를 씀으로써 const키워드처럼 필요없지만 (물론 const는 항상 필요없는건 아니다) 명시적으로 넣어주는 습관을 가지려고 쓴건데... 이렇게 복잡한 키워드인지 몰랐다.

하...걍 inline써도 컴파일러가 inline화 무시할 때도 있고, inline안써도 컴파일러가 알아서 inline화 시킬 때도 있는데,
걍 쓰지 맙시다.....

 

반응형
반응형

 

사진출처: IPC


IPC가 뭘까?

예전에 프로세스에 관해 포스팅을 했었을 때, 각 프로세스는 독립적인 주소공간을 가진다고 했었습니다. 즉 프로세스끼리는 서로의 메모리를 공유할 수 없다는 뜻인데요. 진짜일까요?

당연하게도 프로세스는 서로 데이터를 공유할 수 있는 방법이 있습니다. 그걸 바로 IPC통신이라고 하는데요.

커널은 IPC자원(커널 객체)와 IPC메서드 등을 관리 및 제공하며, 개발자는 운영 체제가 제공하는 IPC자원과 API, 인터페이스를 사용하여 프로세스간 통신을 구현할 수 있습니다.


IPC의 종류

IPC통신에도 많은 종류가 있으며, 상황에 따라 맞는 방법을 선택해 사용하는 것이 중요합니다.


익명 파이프(Anonymous Pipe)

특징

  • 부모-자식 프로세스 간의 단방향 통신을 위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 간의 파이프 연결이 다른 IPC설계보다 쉽습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 각 프로세스가 부모-자식 관계가 아니면 사용이 불가능합니다.

사용법

더보기

CreatePipe 함수(namedpipeapi.h) - Win32 apps | Microsoft Learn

 

CreatePipe 함수(namedpipeapi.h) - Win32 apps

익명 파이프를 만들고 파이프의 읽기 및 쓰기 끝에 핸들을 반환합니다.

learn.microsoft.com

익명 파이프 생성 예시

#include <iostream>
#include <Windows.h>

int main()
{
    HANDLE hRead, hWrite;

    // 이 예제는 부모-자식 프로세스에 대한 구분이 없으므로 실제로 사용할 땐, CreateProcess로 부모 프로세스가 자식 프로세스를 생성해야한다.
    // 익명 파이프 생성
    if (CreatePipe(&hRead, &hWrite, NULL, 0))
    {
        std::cout << "익명 파이프 생성 성공" << '\n';
    }
    else
    {
        std::cerr << "익명 파이프 생성 실패" << '\n';
        return 1;
    }

    // 쓰기 쓰레드: 데이터를 파이프에 쓰기
    const char* data = "Hello, Anonymous Pipe!";
    DWORD bytesWritten;

    if (WriteFile(hWrite, data, strlen(data) + 1, &bytesWritten, NULL))
    {
        std::cout << "파이프에 데이터 쓰기 성공: " << bytesWritten << " 바이트" << '\n';
    }
    else
    {
        std::cerr << "파이프 쓰기 실패" << '\n';
    }

    // 읽기 쓰레드: 데이터를 파이프에서 읽기
    char buffer[128] = { 0 };
    DWORD bytesRead;

    if (ReadFile(hRead, buffer, sizeof(buffer) - 1, &bytesRead, NULL))
    {
        std::cout << "파이프에서 데이터 읽기 성공: " << buffer << '\n';
    }
    else
    {
        std::cerr << "파이프 읽기 실패" << '\n';
    }

    // 커널 객체 반납
    CloseHandle(hRead);
    CloseHandle(hWrite);

    return 0;
}

이름 있는 파이프(Named Pipe)

특징

  • 부모-자식 프로세스가 아닌 프로세스와의 단방향 통신을위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 이름을 통해 특정 파이프를 식별합니다.
  • 여러 프로세스가 동일한 파이프에 접근 가능합니다.
  • 기본적으로 단방향 통신이지만, 운영체제에 따라 양방향이 가능합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 관계에 제한되지 않습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 파이프의 설정과 관리가 익명 파이프보다 복잡합니다.

사용법

더보기

CreateNamedPipeA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateNamedPipeA 함수(winbase.h) - Win32 apps

ANSI(CreateNamedPipeA) 함수(winbase.h)는 명명된 파이프의 인스턴스를 만들고 후속 파이프 작업에 대한 핸들을 반환합니다.

learn.microsoft.com

네임드 파이프 생성 예시 (읽기, 쓰기는 없음)

#include "pch.h"
#include <iostream>

int main()
{
	return 0;
	HANDLE hPipe = CreateNamedPipe(
		L"\\.\pipe\MyPipe",			 // 파이프 이름(\\.\pipe\의 뒤에 이름)
		PIPE_ACCESS_DUPLEX,          // 양방향 통신
		PIPE_TYPE_BYTE | PIPE_WAIT,  // 바이트 스트림과 동기식 통신
		1,                           // 인스턴스 개수
		512, 512,                    // 출력/입력 버퍼 크기
		0,                           // 기본 타임아웃
		NULL                         // 보안 속성
	);
	CloseHandle(hPipe);
	return 0;
}

메시지 큐 (Message Queue)

특징

  • 커널이 관리하는 메시지 큐를 통해 데이터를 주고받습니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 송신과 수신이 독립적으로 동작하는 비동기성으로 동작합니다.
  • 운영체제가 메시지 큐의 생성 및 관리를 담당합니다. 

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 비동기적 데이터 통신방식이므로 송신자와 수신자가 동시에 동작할 필요 없음.

단점

  • 운영체제마다 API가 다르므로 주의해야 합니다.
  • 메시지 타입과 큐 ID등의 메세지를 명확히 전송해야 합니다.

사용법


공유 메모리 (Shared Memory)

특징

  • 한 메모리 영역을 여러 프로세스가 공유하여 사용하는 방식입니다.
  • 이는 서로의 메모리 주소공간을 공유한다는 것이 아니라, OS가 공유가능한 메모리 공간을 할당하여 여러 프로세스에게 접근 가능하게 해줍니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

사용법

더보기

명명된 공유 메모리 만들기 - Win32 apps | Microsoft Learn

 

명명된 공유 메모리 만들기 - Win32 apps

데이터를 공유하기 위해 여러 프로세스는 시스템 페이징 파일이 저장하는 메모리 매핑 파일을 사용할 수 있습니다.

learn.microsoft.com

쓰기

#include <windows.h>
#include <iostream>

// 쓰는 프로세스
int main() 
{
    // 공유 메모리 생성
    HANDLE hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,          // 물리적 파일이 아닌 메모리 사용
        nullptr,                       // 기본 보안 속성
        PAGE_READWRITE,                // 읽기/쓰기 가능
        0,                             // 고위 메모리 크기 (사용 안 함)
        256,                           // 공유 메모리 크기 (256 바이트)
        L"SharedMemoryExample");       // 공유 메모리 이름

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);                      // 전체 영역 매핑

    // 데이터 쓰기
    const char* message = "Hello!";
    CopyMemory(pBuf, message, strlen(message) + 1);
    Sleep(5000);
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

읽기

#include <windows.h>
#include <iostream>

// 읽는 프로세스
int main() 
{
    Sleep(1000);
    // 공유 메모리 열기
    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        FALSE,                         // 자식 프로세스 상속 여부 (아니오)
        L"SharedMemoryExample");       // 공유 메모리 이름 (생성 시와 동일해야 함)

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);

    // 데이터 읽기
    std::cout << "공유 메모리에서 데이터를 읽었습니다: " << static_cast<char*>(pBuf) << std::endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
}

소켓 (Socket)

해당 사진은 TCP에 대한 예시사진

특징

  • 네트워크 인터페이스를 통해 두 프로세스가 데이터를 주고받는 방식입니다.
  • 데이터를 양방향으로 주고받을 수 있습니다.
  • TCP(연결 지향)방식과 UDP(비연결 지향)방식이 있습니다.

장점

  • 같은 시스템 내에서의 통신 뿐만이 아니라, 네트워크를 통한 다른 컴퓨터와의 통신이 가능합니다.
  • 대상이 서로 다른 운영체제여도 통신이 가능합니다.
  • 실시간으로 데이터 전송이 가능합니다.

단점

  • 소켓 통신은 패킷의 처리, 예외 처리 등에 대한 구현이 필요하므로 복잡합니다.
  • 네트워크 자원을 사용하기 때문에 연결 수가 많아질수록 리소스 사용이 급격히 많아집니다.
  • 데이터를 암호화하지 않으면 보안에 취약할 수 있습니다.

사용법

더보기

socket 함수(winsock2.h) - Win32 apps | Microsoft Learn

 

socket 함수(winsock2.h) - Win32 apps

소켓 함수는 특정 전송 서비스 공급자에 바인딩된 소켓을 만듭니다.

learn.microsoft.com

 

소켓 서버/클라이언트 예시 코드

 

*서버

#include<iostream>
#include<string>
#include<winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define BUFFER_SIZE 100

SOCKET gListen, gAccept;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = { 0 };

// 서버
int main()
{
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    if ((gListen = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    gAddress.sin_family = AF_INET;
    gAddress.sin_addr.s_addr = INADDR_ANY;
    gAddress.sin_port = htons(PORT);

    // 소켓 바인딩
    if (bind(gListen, (PSOCKADDR)&gAddress, sizeof(gAddress)) == SOCKET_ERROR)
    {
        printf("bind() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    // 연결 요청 수신 대기
    if (listen(gListen, 5))
    {
        printf("listen() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Waiting for a connection..." << std::endl;

    // 연결 수락
    if ((gAccept = accept(gListen, NULL, NULL)) == INVALID_SOCKET)
    {
        printf("accept() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Connection!" << std::endl;

    // 받기
    if (recv(gAccept, buffer, sizeof(buffer), 0) <= 0)
    {
        printf("WSARecv() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    std::cout << "Message received: " << buffer << std::endl;

    // 종료
    closesocket(gListen);
    closesocket(gAccept);
    WSACleanup();
    return 0;
}

 

*클라이언트

#include<iostream>
#include<string> 
#include<WinSock2.h> 
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define ADDR "127.0.0.1"
#define BUFFER_SIZE 100

SOCKET gSocket;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = {0};

// 클라
int main()
{
    Sleep(1000);
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    gAddress.sin_family = AF_INET;
    gAddress.sin_port = htons(PORT);

    if (inet_pton(AF_INET, ADDR, &(gAddress.sin_addr)) != 1)
    {
        printf("Invalid IP address %d\n", WSAGetLastError());
        return -1;
    }
    else
    {
        std::cout << "Server IP address: " << ADDR << std::endl;
        if ((gSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
        {
            printf("socket() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        if (connect(gSocket, (SOCKADDR*)&gAddress, sizeof(gAddress)) < 0)
        {
            printf("connect() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        std::cout << "Connected to server!" << std::endl;
        std::string sendMessage;

        std::cout << "Input massge: ";
        std::cin >> sendMessage;

        /* 5. 메시지 전송 */

        send(gSocket, sendMessage.c_str(), sendMessage.length(), 0);

        closesocket(gSocket);
    }
    WSACleanup();
    return 0;
}

메모리 맵 (Memory Map)

특징

  • 파일을 프로세스에 매핑시켜 파일의 메모리에 접근하여 읽거나 쓰는 기법입니다.
  • 파일을 매핑할 때 실제 물리 메모리 페이지에 해당 파일 데이터를 로드하고, 이 물리 메모리를 가상 메모리 주소로 매핑합니다.
  • 공유 메모리와 비슷하나 다른 점은, OS가 메모리 공간을 할당해주는 것이 아닌, 파일의 메모리를 읽고 쓴다는 것입니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

번외1) 메모리 맵 vs 파일 I/O

파일을 읽고 쓰는거면 파일IO(std::filesystem 등)랑 다른게 뭘까요? 몇몇 차이점이 존재합니다.

1. 메모리 맵은 실제 메모리 공간을 사용하는 기법이나, 파일I/O는 파일의 데이터를 버퍼로 복사하고, 버퍼로 씁니다.

2. 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드하지만, 파일IO는 모든 데이터를 직접 로드하고 쓰기 때문에 느립니다.

번외2) 그러면 수십 기가바이트의 파일을 어떻게 로드할까?

앞서, 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드한다고 했는데요. 어떤 식으로 그렇게 로드할 수 있는걸까요?

간단합니다! 필요한 데이터가 있는 페이지만 로드하게 됩니다. 때문에 파일의 용량이 아무리 커도 속도에 영향을 받지 않습니다.

 

 

사용법

더보기

CreateFileMappingA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateFileMappingA 함수(winbase.h) - Win32 apps

지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 엽니다. (CreateFileMappingA)

learn.microsoft.com

MapViewOfFile 함수(memoryapi.h) - Win32 apps | Microsoft Learn

 

MapViewOfFile 함수(memoryapi.h) - Win32 apps

호출 프로세스의 주소 공간에 파일 매핑 보기를 매핑합니다.

learn.microsoft.com

 

메모리 맵 사용 예시

#include <iostream>
#include <windows.h>

#define FILE_PATH "example.txt"
#define BUFFER_SIZE 256

int main() {
    // 파일 열기(없으면 생성)
    HANDLE hFile = CreateFileA(
        FILE_PATH,                       // 파일 경로
        GENERIC_READ | GENERIC_WRITE,    // 읽기/쓰기 권한
        0,                               // 공유 모드 없음
        NULL,                            // 보안 속성
        OPEN_ALWAYS,                     // 파일이 없으면 생성
        FILE_ATTRIBUTE_NORMAL,           // 일반 파일 속성
        NULL                             // 템플릿 파일 없음
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        std::cerr << "CreateFile failed: " << GetLastError() << std::endl;
        return -1;
    }

    // 파일 매핑 생성
    HANDLE hMapFile = CreateFileMappingA(
        hFile,                // 파일 핸들
        NULL,                 // 보안 속성
        PAGE_READWRITE,       // 읽기/쓰기 권한
        0,                    // 파일 크기 상위 32비트
        BUFFER_SIZE,          // 파일 크기 하위 32비트
        NULL                  // 이름 없음
    );

    if (hMapFile == NULL) {
        std::cerr << "CreateFileMapping failed: " << GetLastError() << std::endl;
        CloseHandle(hFile);
        return -1;
    }

    // 매핑된 메모리 접근
    LPVOID pBuf = MapViewOfFile(
        hMapFile,               // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,    // 접근 권한
        0,                      // 오프셋 상위 32비트
        0,                      // 오프셋 하위 32비트
        BUFFER_SIZE             // 매핑 크기
    );

    if (pBuf == NULL) {
        std::cerr << "MapViewOfFile failed: " << GetLastError() << std::endl;
        CloseHandle(hMapFile);
        CloseHandle(hFile);
        return -1;
    }

    // 데이터 쓰기
    strcpy_s((char*)pBuf, BUFFER_SIZE, "Hello, File Mapping!");

    std::cout << "Data written to file via mapping: " << (char*)pBuf << std::endl;

    // 메모리 해제
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
    CloseHandle(hFile);

    return 0;
}

 

 


 

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04
반응형

참고 : [운영체제] 페이지 교체 알고리즘

저번에 페이징에 대해서 포스팅을 했던적이 있는데요?

페이지 교체 알고리즘에 대해선 안다뤘었습니다.

그래서 이번 기회에 한번 다뤄보려고 합니다.

바로 ㄱㄱ


페이지 교체 알고리즘

페이지 교체 알고리즘은 운영체제가 가상 메모리를 효율적으로 관리하기 위해 사용하는 방법 중 하나로, 메모리 부족 상황에서 어떤 페이지를 교체할지 결정하는 역할을 합니다. 페이지 교체 알고리즘은 무수히 많지만, 하지만 오늘은 그중 많이 다루는 몇가지만 다루려고 합니다.

페이지 교체 알고리즘들.png


 

1. FIFO (First In, First Out)

  • 개념: 가장 먼저 들어온 페이지를 가장 먼저 교체합니다. (큐를 사용해서 구현)
  • 장점: 구현이 간단합니다.
  • 단점: 오래된 페이지가 여전히 많이 사용되는 경우 성능 저하 유발합니다. 페이지 수가 늘어날 수록 page fault가 발생하는 빈도가 늘어나는 경우(Belady's Anomaly)가 생길 수 있습니다.

2. OPT (Optimal)

4번 시점에서 이후에 A와 B 페이지가 사용될 것을 알고 있기 때문에 C 페이지를 대상 페이지로 선정해 스왑 아웃시킨다.
  • 개념: 앞으로 가장 오래 사용되지 않을 페이지를 교체합니다.
  • 장점: 가장 적은 페이지 폴트를 보장합니다. (이론상 최강).
  • 단점: 미래의 페이지 접근 패턴을 알아야 하는데, 이는 실질적으로 구현이 불가능하다고 합니다.

3. LRU (Least Recently Used)

  • 개념: 가장 오랫동안 사용되지 않은 페이지를 교체합니다.
  • 장점: FIFO보다 성능이 좋습니다.
  • 단점: 최근 사용 기록을 저장하고 갱신하는 비용이 발생합니다.

4. LFU (Least Frequently Used)

  • 개념: 사용 빈도가 가장 낮은 페이지를 교체합니다.
  • 장점: 자주 사용되는 페이지를 보호할 수 있습니다.
  • 단점: 페이지의 사용 빈도를 저장하고 갱신하는 비용이 발생하게 됩니다. 최신 페이지가 비교적 불리한 방향이므로 특정 상황에서 성능 저하가 생길 수 있습니다.

5. Clock (Second Chance)

위 그림과 같이 대상 페이지를 가리키는 포인터를 사용하는데, 이 포인터가 큐의 맨 아래로 내려가면 시계처럼 다음번에는 다시 큐의 처음을 가리키게 된다.
교체 시, 비트가 0일 경우 교체, 1일 경우 다음 포인터로 넘어가서 다시 검사

  • 개념: FIFO 변형으로, 각 페이지에 사용 비트를 사용합니다. (1이면 사용 중)
  • 장점: 최근에 사용된 페이지는 교체되지 않습니다. LRU처럼 과거 사용 기록을 저장하지 않아도 됩니다.
  • 단점: 참조 비트만으로는 페이지가 얼마나 자주 사용되었는지, 최근에 사용되었는지 구체적으로 알 수 없습니다. 구현이 비교적 복잡하고 페이지의 사용 비트를 저장 및 갱신하는 비용이 발생합니다.

현대 OS는 어떤 알고리즘을 주로 사용할까?

일반적으로 운영체제는 LRU와 Clock을 실제로 사용하는 경우가 많습니다.

LRU와 Clock을 혼용하여 가장 오랫동안 사용되지 않은 페이지를 교체하며, 최근에 참조된 페이지를 보존하는데 효과적이도록 페이지 교체를 수행한다고 합니다.

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04

+ Recent posts