Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

윈도 파일 프로그래밍

뭐니 뭐니 해도 시스템 프로그래밍의 핵심은 파일. 리눅스 프로그래밍에 익숙한 관계로 리눅스(:12) 파일 프로그래밍과의 비교가 많을 것이다.

윈도에서 파일은 커널 Object로 관리된다. 파일을 포함한 모든 커널 Object는 Create로 시작하는 함수로 handle을 얻을 수 있다. 핸들은 커널 Object를 가리킨다. 파일외에 관리하는 커널 객체로는 아래와 같은 것들이 있다.
  1. 파이프 객체
  2. 이벤트 객체
  3. 쓰레드 객체
  4. 뮤텍스 객체
  5. 프로세스 객체
  6. I/O 포트 객체
  7. 세마포어 객체

파일 생성과 읽고 쓰기

파일 만들기/열기

CreateFile(:4100)함수로 파일이나 입출력 장치를 열 수 있다. 입출력 장치는 파일, 파일 스트림, 디렉토리, 물리적인 디스크, 볼륨, 콘솔 버퍼, 테이프 드라이버, mailslot(:4100), pipe(:12)등이다. 16비트 버전 윈도에서는 OpenFile함수를 사용할 수 있는데, 지금은 거의 사용하지 않는다.

리눅스의 파일 열기 함수인 open(2)에 비해 많은 매개 변수를 필요로 한다.
HANDLE WINAPI CreateFile(
  __in      LPCTSTR lpFileName,
  __in      DWORD dwDesiredAccess,
  __in      DWORD dwShareMode,
  __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  __in      DWORD dwCreationDisposition,
  __in      DWORD dwFlagsAndAttributes,
  __in_opt  HANDLE hTemplateFile
);
lpfileName으로 열고자 하는 파일 이름을 지정한다.

dwDesiredAccess로 일반 접근 방법을 지정할 있다. GENERIC_READGENERIC_WRITE를 사용한다. 전자는 읽기 전용, 후자는 쓰기전용으로 열기 위해서 사용한다. 읽기와 쓰기 모두를 위해서는 GENERIC_READ | GENERIC_WRITE를 사용하면 된다. 또한 상세 접근 방법을 지정할 수 있다. 상세 접근 방법은 MSDN문서를 참고한다.

dwShareMode는 열린 파일에 다른 프로세스나 쓰레드로 부터의 접근권한을 설정 하기 위해서 사용한다. 0을 사용한다면 다른 프로세스나 쓰레드는 읽기, 쓰기, 삭제를 할 수 없다. FILE_SHARED_DELETE, FILE_SHARED_READ, FILE_SHARED_WRITE를 사용할 수 있다.

lpSecurityAttribute는 보안속성을 지정하기 위해 사용한다. NULL을 지정하면 기본 보안속성이 사용되는데, 이 경우 파일은 자식 프로세스에서 상속할 수 없게 된다.

dwCreationDisposition은 생성 방법을 결정하기 위해서 사용한다. 새로이 생성하려면 CREATE_NEWCREATE_ALWAYS를 사용할 수 있다. CREATE_NEW는 파일이 존재하지 않을 경우 생성하고 CREATE_ALWAYS는 파일이 존재할경우 덮어쓴다는 차이점이 있다. 존재하는 파일을 열기 위해서는 OPEN_ALWAYSOPEN_EXISTING를 사용할 수 있다. OPEN_ALWAYS는 파일이 존재하든 그렇지 않든 참을 반환한다. 만약 파일이 존재하지 않는다면, 새로 파일을 만든다. OPEN_EXISTING는 존재하는 파일만 연다. 만약 파일이 존재하지 않는다면 거짓을 반환한다. TRUNCATE_EXISTING는 존재하는 파일의 크기를 0으로 해서 연다. 이 값을 사용하기 위해서는 반드시 GENERIC_WRITE가 설정되어 있어야 한다.

성공적으로 수행되면 파일 HANDLE을 반환한다. 실패하면 INVALID_HANDLE_VALUE를 반환한다.

읽기 쓰기

ReadFile함수와 WriteFile(:4100)함수로로 읽고 쓸 수 있다.
BOOL WINAPI ReadFile(
  __in         HANDLE hFile,
  __out        LPVOID lpBuffer,
  __in         DWORD nNumberOfBytesToRead,
  __out_opt    LPDWORD lpNumberOfBytesRead,
  __inout_opt  LPOVERLAPPED lpOverlapped
);
파일 핸들 hFile로 부터 읽은 데이터를 lpBuffer에 저장한다. 읽을 데이터의 크기는 nNumberOfBytesToRead로 지정할 수 있다. 실제 읽은 데이터의 크기는 lpNumberOfBytesRead로 알아낼 수 있다. 마지막 매개 변수인 lpOverlapped는 파일이 FILE_FLAG_OVERLAPPED (비동기 모드)로 열렸을 때 사용한다. 자세한 내용은 비동기 입출력에서 다루도록 한다. 동기 모드로 열린 파일이라면 NULL을 지정한다.

BOOL WINAPI WriteFile(
  __in         HANDLE hFile,
  __in         LPCVOID lpBuffer,
  __in         DWORD nNumberOfBytesToWrite,
  __out_opt    LPDWORD lpNumberOfBytesWritten,
  __inout_opt  LPOVERLAPPED lpOverlapped
);
파일 핸들 hFile에 lpBuffer의 데이터를 쓴다. 쓸 데이터의 크기는 nNumberOfBytesToWrite로 지정한다. 실제 쓴 데이터의 크기는 lpNumberOfBytesWritten으로 알아낼 수 있다. lpOverlapped는 비동기 모드에서 사용한다. 동시 모드로 열린 파일이라면 NULL을 지정한다.

닫기

더이상 CloseHandle(:4100) 사용하지 않는 파일은 CloseHandle로 닫아준다.

리눅스 함수의 차이

파일 생성 매개 변수가 많을 뿐더러, 매개 변수에 사용할 수 있는 값들도 많아서 리눅스의 open(:2)함수에 비교해서 복잡하다는 느낌을 받는다. 리눅스에서 파일의 접근은 공유가 아닌 유저별 파일 관리 권한의 형태로 달성하는데, 리눅스의 유저별 권한 부여 방식이 좀 더 명확해 보인다.

예컨데 매개 변수 dwShareMode는 파일이 열린 동안 다른 프로세스와 공유여부를 결정하기 위해서 사용하는데, 파일 잠금과 같은 효과를 가져올 수 있다. 이런 값을 굳이 번거롭게 파일 생성시 매개 변수로 받아올 필요가 있나라는 생각이 든다. fcntl(2)류의 함수로 분리시키는게 낫지 않을까 ? 작은게 아름답다라는 철학을 함수에도 구현하고 있는 리눅스 환경에 익숙해져있기 때문일지도 모르겠다.

파일 접근 방식은 윈도와 리눅스 동일하다.
윈도 리눅스 설명
GENERIC_READ O_RDONLY 읽기 전용
GENERIC_WRITE O_WRONLY 쓰기 전용
GENERIC_READ|GENERIC_WRITE O_RDWR 읽기/쓰기 전용
기타 파일 생성 방법들이다.
윈도 리눅스 설명
CREATE_ALWAYS O_CREAT 항상 파일을 연다.
CREATE_EXISTS O_CREAT | O_EXCL 에러 값으로 파일의 존재여부를 확인할 수 있다. 주로 실수로 존재하는 파일을 덮어쓰지 않기 위한 목적으로 사용한다.
TRUNCATE_EXISTING O_TRUNC 파일크기를 0으로 한다.
FILE_APPEND_DATA O_APPEND 파일을 추가 모드로 연다. 파일의 위치는 마지막으로 설정 된다.
읽기 / 쓰기 리눅스의 read(2) write(2)함수가 반환 값으로 실제 읽고 쓴 데이터의 크기를 알아오는 것에 반해, 윈도의 함수는 매개 변수로 읽고 쓴 데이터의 크기를 가져 온다는 것 외에는 동일하다.

예제

파일에 내용을 추가하는 프로그램 a.txt의 내용을 읽어서 b.txt의 마지막에 추가하는 프로그램이다. a.txt는 읽기 모드로 열고, b.txt는 쓰기모드로 연다. 이때 FILE_APPEND_DATA를 매개 변수 값으로 줘서 파일 핸들의 위치를 마지막으로 만들어준다.

#include <windows.h>
#include <stdio.h>

#define MAX_LINE 1024

int main(int argc, char **argv)
{
	char buf[MAX_LINE];
	HANDLE source_fh,dest_fh;
	int readn, writen;
	if (argc != 3)
	{
		printf("Usge : %s [source file] [dest file]\n", argv[0]);
		return 0;
	}

	source_fh= CreateFile(
		argv[1],
		GENERIC_READ,
		0,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL,
		NULL
		);
	if(source_fh == INVALID_HANDLE_VALUE)
	{
		printf("FILE Open Error %d\n", GetLastError);
		return 1;
	}
	dest_fh = CreateFile(
		argv[2],
		FILE_APPEND_DATA,  // append 모드로 연다.
		0,
		NULL,
		OPEN_EXISTING,       // 파일이 존재하는지 확인한다.
		FILE_ATTRIBUTE_NORMAL,
		NULL
		);
	if (dest_fh == INVALID_HANDLE_VALUE)
	{
		if (GetLastError() == ERROR_FILE_NOT_FOUND) 
			printf("Dest File not found\n");
		else 
			printf("Dest File Open Error\n");
		return 1;
	}
	

	while(1)
	{
		memset(buf, 0x00, MAX_LINE);
		ReadFile(source_fh, buf, MAX_LINE-1, &readn, NULL);
		if(readn <= 0)
			break;
		WriteFile(dest_fh, buf, readn, &writen, NULL);
		printf("%d\n", writen);
	}
	return 0;
}

파일 제어

파일 찾기 및 파일 정보 확인

CreateFile로도 파일 존재를 확인할 수 있지만 가능하면 전용 함수를 사용하는 걸 권장한다.

FindFirstFile
HANDLE WINAPI FindFirstFile
{
  __in   LPCTSTR lpFileName,
  __out  LPWIN32_FIND_DATA lpFindFileData
};
winapi함수로 링크 없이 사용할 수 있다는 장점이 있다. 매개 변수 lpFindFileData로 찾은 파일의 정보를 알 수 있다. 파일 정보는 WIN32_FIND_DATA(:4300)구조체를 참고한다.

#include <windows.h>
#include <stdio.h>

int main(int argc, char **argv)
{
	WIN32_FIND_DATA FindFileData;
	HANDLE fh;
	SYSTEMTIME SystemTime;

	if(argc != 2)
	{
		fprintf(stderr, "Usage : %s [file name]\n", argv[0]);
		return 1;
	}

	fh = FindFirstFile(argv[1], &FindFileData);
	if(fh == INVALID_HANDLE_VALUE)
	{
		printf("Error %d\n", GetLastError());
		return 1;
	}
	printf("File Name : %s\n", FindFileData.cFileName);
	printf("File Size : %d\n", FindFileData.nFileSizeLow);
	FileTimeToSystemTime(&FindFileData.ftCreationTime,&SystemTime);
	printf("File Creeation time : %d/%d/%d \n", 
            SystemTime.wYear, SystemTime.wMonth, SystemTime.wDay);

	FindClose(fh);
}
단 파일 핸들을 반환하기 때문에 FindClose(:4100)함수로 핸들을 닫아줘야 한다.

파일 링크

윈도는 CreateSymbolicLink(:4100)와 CreateHardLink(:4100)함수로 심볼릭 링크와 하드 링크(:12)파일을 만들 수 있다.

심볼릭 링크 만들기
BOOLEAN WINAPI CreateSymbolicLink(
  __in  LPTSTR lpSymlinkFileName,
  __in  LPTSTR lpTargetFileName,
  __in  DWORD dwFlags
);
lpTargetFileName 이름을 가지는 원본 파일을 가리키는 lpSymlinkFileName이름의 심볼릭 링크 파일을 만든다. 원본 파일이 디렉토리일 경우 dwFlags의 값을 SYMBOLIC_LINK_FLAG_DIRECTORY로 하면 된다. 일반 파일이라면 0으로 한다.

windows vista, windows server 2008 이상에서 사용할 수 있다.
#include <stdio.h>
#include <windows.h>

int main(int argc, char **argv)
{
	if(argv != 3)
	{
		fprintf(stderr, "Usage : %s [target file name] [symbolic name]\n", argv[0]);
		return 1;
	}

	if(CreateSymbolicLink(argv[2], argv[1], 0) == 0)
	{
		printf("Error %d\n", GetLastError());	
	}
	return 0;

}

하드 링크 만들기
BOOL WINAPI CreateHardLink(
  __in        LPCTSTR lpFileName,
  __in        LPCTSTR lpExistingFileName,
  __reserved  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
lpExistingFileName이름을 가지는 원본파일의 lpFileName 하드 링크를 만든다. lpSecurityAttributes는 사용하지 않는다. 반드시 NULL을 쓴다.

windows 2000 professional, windows 2000 server 이상에서 사용할 수 있다.

유닉스에서 링크는 매우 오래전 부터 사용되었는데, 윈도는 비교적 최근들어 지원되었다. 이유를 확인해 봐야 할 것 같다. 예제 프로그램은 xp에서 실행되지 않았다.

파일 카피

  • 링크와 카피의 다른 점에 대해서 알아본다.
  • .. 그냥 ReadFile, WriteFile함수로 구현하는게 낫지 않을까란 생각이 든다.

디렉토리 관리

  • 리눅스의 opendir(:3)에 해당하는 함수가 있으리라 생각된다. 알아보자.

기타

STDIN_FILENO에 대한 입출력 다중화

채팅 클라이언트 프로그램을 만든다고 가정해보자. 채팅은 표준입력(:12)과 소켓 입출력을 동시에 처리해야 한다. 리눅스(:12)는 소켓도 파일이니 select(:2)를 이용한 입출력:::다중화(:12)로 간단히 문제흘 해결할 수 있는데, 아 윈도는 그게 안된다.

윈도는 STDIN_FILENO를 비 봉쇄로 할 수 없다. 일단 파일과 소켓을 다르게 취급하기 때문에 select(:2)를 이용할 수도 없고, 비 봉쇄로 할 수도 없으니 결국 쓰레드(:12)로 처리하는 수 밖에 없을 것 같다. 다른 방법이 있을려나 ?

참고로 표준입력은 ReadFileEx에서도 블럭되며, 중첩 입출력 에서도 블럭된다고 한다. 쩝. 좀 더 테스트를 해봐야 겠는데, 우선은 쓰레드로 구현 하련다.