Media Log

파일을 열 때 파일 포인터는 0으로 셋팅된다. 이후 해당 파일에 ReadFile이나 WriteFile등의 함수를 통해서 I/O를 하게 되면 파일 포인터가 자동으로 증가하게 된다. 물론 윈도우는 사용자가 직접 오프셋을 조정할 수 있는 인터페이스도 제공해주는데 SetFilePointer 함수가 바로 파일 포인터를 이동 시키는 인터페이스이다.
파일 포인터는 각 핸들별로 따로 관리된다. 즉 같은 파일이라 할지라도 2번을 열어서 핸들을 2개 가지고 있다면 각 핸들에 연결된 파일 포인터는 각각 독립적으로 움직인다.

이 SetFilePointer는 너무 복잡하게 만들어진 함수이다. 그래서 제대로 사용하기가 어렵다. 지금까지 내가 SetFilePointer 함수를 사용하는 코드를 보았던 곳에서는 제대로 작성된 코드가 거의 없었던 것 같다. 그렇다면 어떤 부분이 그렇게 SetFilePointer의 사용을 힘들게 만드는 것일까?

SetFilePointer 함수는 다음과 같이 생겼다. 32비트와 64비트를 동시에 지원하기 위해 2번째 인수와 3번째 인수를 통해 각 4바이트씩 총 64비트 만큼의 오프셋 정보를 전달할 수 있도록 만들어졌다.

DWORD WINAPI SetFilePointer(

  __in         HANDLE hFile,

  __in         LONG lDistanceToMove,

  __inout_opt  PLONG lpDistanceToMoveHigh,

  __in         DWORD dwMoveMethod

);

첫번째로 많이 하는 실수는 오프셋이 32비트 크기를 넘어갈 수 있는 경우에도 항상 lpDistanceToMoveHigh 에 NULL을 넣고 있는 경우이다. 4기가보다 큰 파일에 대해서 제대로 지원하지 못하는 경우인데 오래 전에 작성된 코드에서 흔히 볼 수 있다.
두번째. SetFilePointer의 리턴값은 변경된 오프셋 값이며 함수가 실패할 경우에는 INVALID_SET_FILE_POINTER 를 돌려주게 된다. INVALID_SET_FILE_POINTER의 값은 -1로 정의되어 있고, 이 값은 DWORD로 받아지기 때문에 0xFFFFFFFF가 된다. 그런데 만약 내가 변경하고 싶었던 위치가 0xFFFFFFFF(4기가) 였다면? 사용자는 0xFFFFFFFF위치로 오프셋을 옮겨줄 것을 요청했고 함수는 사용자가 원한 동작을 제대로 수행한 뒤 0xFFFFFFFF를 리턴했다. 이제 이 값이 에러인지 정상적인 오프셋 값인지 어떻게 구분해야할까? 사용자는 이를 확인해보기 위해서 반드시 GetLastError를 호출해야 한다. 만일 함수가 성공했고 제대로된 오프셋이라면 LastError가 ERROR_SUCCESS로 셋팅되어 있을 것이다. 지난 번에 윈도의 LastError값은 오직 함수가 실패할 때만 셋팅된다고 했었는데, SetFilePointer와 같은 몇몇 특별한 함수에서는 성공시에도 값을 0으로 만들어 준다. 물론 그렇게 하는 이유는 위처럼 리턴값만으로는 모든 정보를 전달해줄 수가 없기 때문이다.

따라서 SetFilePointer를 사용하는 곳에서는 다음 표에 있는 것처럼 리턴값을 확인해야 한다.

  If lpDistanceToMoveHigh == NULL If lpDistanceToMoveHigh != NULL
If success retVal != INVALID_SET_FILE_POINTER retVal != INVALID_SET_FILE_POINTER || GetLastError() == ERROR_SUCCESS 
If failed retVal == INVALID_SET_FILE_POINTER retVal == INVALID_SET_FILE_POINTER && GetLastError() != ERROR_SUCCESS
많은 사람들이 틀리게 사용할 만도 하다.

이제 내가 하고 싶었던 말을 정리하면,
  • SetFilePointer 함수를 사용한 곳을 보게 되면 위 내용을 유심히 살펴보는 것도 재미있다. 그리고 코드가 틀렸다면 바르게 고쳐라.
  • 위 표에 나온대로 고치려고 하지말고, SetFilePointerEx를 사용해서 고치는 것이 좋다.
  • GetFileSize 함수도 역시 비슷한 문제가 있다. GetFileSizeEx만 사용해라

다음은 프로그래밍 센스를 확인해 볼 수 있는 간단한 퀴즈이다.
윈도에는 SetFilePointer와 SetFilePointerEx라는 함수는 존재하지만 GetFilePointer라는 함수는 존재하지 않는다. 그렇다면 윈도에서 현재 가지고 있는 핸들의 파일 포인터의 오프셋은 어떻게 구할 수 있을까?

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. win32API공부학생 at 2013.12.31 20:56 신고 [edit/del]

    SetFilePointer함수의 리턴값을 잘못사용하고 있었네요ㅠㅜ 좋은정보감사합니다!

    Reply

submit
GetLastError는 윈도 Api를 호출 한 뒤 해당 함수의 Win32 에러 코드를 받아오기 위한 함수이다. 이 오류 정보는 쓰레드별로 하나만 저장되기 때문에 함수가 실패한 후 다른 함수를 실행하기 전에 에러 값을 읽어와야 한다. 다른 함수들이 호출된 이후에는 에러 값이 덮어 씌워져 버릴 수 있다.

보통은 아래와 같이 사용한다.
HANDLE h = CreateFile(...);
if (h == INVALID_HANDLE_VALUE)
{
  DWORD dw = GetLastError();
  ... Do something
}

경험이 많지 않거나 주의 깊지 않은 프로그래머들은 프로그램을 유지보수 하면서 이미 잘 만들어져있던 위와 같은 코드를 별 생각 없이 아래처럼 바꾸기도 한다.
HANDLE h = CreateFile(...);
if (h == INVALID_HANDLE_VALUE)
{
  DoSomethingElse(); // 뭔가 예외를 처리하기 위해 추가적인 코드를 여기에 쑤셔넣는다. 아니, 왜 하필 여기에.
  DWORD dw = GetLastError();
  ... Do something
}
처음에 말했듯이 DoSomethingElse()안에서 윈도 Api를 사용한다면 쓰레드 저장소에 있던 LastError 코드가 다른 값으로 바뀌어버릴 수 있다는 것을 예상할 수 있다. 항상 코드를 읽으면서 GetLastError를 호출하는 부분이 에러값을 확인하려고 했던 함수의 바로 아래에 붙어있지 않다면 섬뜩함을 느껴야 한다. 하지만 잘 모르고 있으면 보이지 않는 법.

HANDLE h = CreateXXX(...);
DWORD dw = GetLastError();
if (dw == ERROR_SUCCESS)
{
  ... 핸들을 가지고 다른 무엇인가를 한다.
}
else
{
  ... 함수의 실패처리를 한다.
}
이번에는 한 Api를 호출 한 뒤에 바로 GetLastError를 호출해서 에러값을 얻어왔다. 얼핏보면 맞는 것도 같지만 역시 틀린 코드이다. 함수의 성공 실패 여부는 함수의 스펙에 따라 리턴 값 등으로 확인해야지 GetLastError 값으로 확인해서는 안된다. 왜냐하면 Win32에서 제공되는 대부분의 Api들이 함수가 성공했을 때는 LastError 값을 건드리지 않기 때문이다. 위 코드에서는 함수가 성공할 때는 에러 값도 0(ERROR_SUCCESS)으로 셋팅시켜 줄 것이라 굳게 믿고 있다. 실상은 그렇지 않다. GetLastError는 오직 함수가 실패했을 때만(그 바로 직후에) 호출해야 한다.

대부분의 함수들은 그 성공 여부를 리턴값으로 가르쳐준다. 리턴 값으로 성공과 실패 여부를 호출자에게 전달해주기로 했다면 뭐하러 또 SetLastError(ERROR_SUCCESS) 와 같은 추가적인 코드를 호출하겠는가.
하지만 어떤 함수들은 성공시에도 SetLastError(ERROR_SUCCESS)를 정확히 호출해주기도 하는데, 이것에 대한 이야기는 다음 포스트에서 해보려고 한다.
저작자 표시 비영리 동일 조건 변경 허락
신고

submit
윈도의 레지스트리에 무엇인가를 기록하면 항상 영구적으로 저장된다고 생각하는 사람들이 많이 있는 것 같다.
윈도는 일회용으로 레지스트리에 정보를 기록할 수 있는 방법 또한 제공한다.

RegCreateKeyEx 함수를 통해 레지스트리의 키를 생성할 때 REG_OPTION_VOLATILE 옵션을 주면 데이터를 일회용으로 저장할 수 있다.

컴퓨터가 켜져있는 동안 일회성으로 정보를 기록하고 나중에 종료될 때 지워야 할 경우를 종종 맞닥드리게 되는데, 이럴 때 해당 옵션을 사용하면 편리하다. 마치 CreateFile의 DeleteOnClose 옵션과 비슷하다고도 할 수 있겠다. 이런 옵션을 사용하지 않으면 응용이 종료될 때에 직접 데이터를 정확히 지워줘야 하며, 응용이 비정상 종료된다거나 하면 더욱 골치 아파지고 에러 처리를 하기 위해서 쓸데없는 로직을 집어 넣게 된다.
저작자 표시 비영리 동일 조건 변경 허락
신고

submit
MoveFileEx 함수는 파일 이름 변경이나 삭제를 컴퓨터가 재시작할 때 까지 지연시킬 수 있는 상당히 유용한 옵션이 있는데 꽤 많은 사람들이 잘 모르고 있는 것 같다.
이 옵션은 스마트 업데이터 같은 프로그램이 DLL을 교체 시켜야 한다거나 언인스톨러시 파일을 삭제해야 하는데 다른 곳에서 이미 파일이 사용중이어서 삭제할 수 없는 경우에 유용하게 쓸 수 있다.

MoveFile 함수는 내부적으로 CreateFile 함수를 통해 파일을 오픈하는데 이 때 DesiredAccess로 DELETE을 사용한다. 파일이 잘 열렸다면 RenameInformation IRP를 날린 후 핸들을 닫고 성공으로 반환하지만, 이미 다른 위치에서 파일이 열려있었다면 먼저 파일을 연쪽에서 FILE_SHARE_DELETE를 함께 주지 않았었을 경우 파일 열기가 ERROR_SHARING_VIOLATION 으로 실패하게 되어 MoveFile 함수 또한 실패로 리턴해버리게 되는 것이다.

재부팅 시에라도 dll 등을 교체시켜주거나 깨끗하게 삭제하기를 원한다면 MoveFileEx함수를 호출 할 때 세번째 파라메터로 MOVEFILE_DELAY_UNTIL_REBOOT 옵션을 주면 되는데, 이렇게 하면 MoveFileEx함수는 레지스트리의 HKLM\System\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations 위치에 어떤 오퍼레이션이었는지 정보를 적어 놓기만 하고 리턴한다. 시스템이 재부팅 되고 나서 응용프로그램들이 실행되기 전 운영체제에서 레지스트리를 확인해 보고 해당 동작을(이름변경 혹은 삭제) 수행해 주기 때문에 어떤 파일이던지 삭제가 가능하다. HKLM 위치에 써야 하기 때문에 관리자 권한은 필요하다.

이와 관련된 몇 가지 알아두면 좋을 지식들이 있다.
  • 다른 곳에서 파일을 열고 있다고 이름 변경을 못하는 것은 아니다. 먼저 파일을 연쪽에서 어떤 공유 모드로 파일을 열었는지가 중요하다. 파일을 먼저 오픈 하는 쪽에서 FILE_SHARE_DELETE옵션을 주어서 CreateFile을 하면 다른 위치에서 해당 파일의 이름을 변경 할 수 있다. 심지어는 삭제도 가능한데(DeleteFile을 호출하면 성공한다) 이때는 파일이 삭제 상태로만 마킹 되며 파일 시스템 드라이버는 해당 파일을 열어 놓은 모든 핸들이 닫힐 때 실제로 삭제를 수행한다. 이렇게 삭제 상태로 마킹되어 있는 동안에는 또 다른 곳에서 파일 오픈 시도가 생겼을 때 ERROR_ACCESS_DENIED 에러가 발생하게 된다. 파일 핸들을 닫기 전까지는 이런 DELETE_PENDING 상태의 파일을 삭제되지 않은 상태의 파일로 다시 돌리는 것 또한 가능하다.
  • 다른 한 쪽에서 파일 삭제를 허용하지 않고 먼저 파일을 열어두었을 시에, MoveFileEx에 MOVEFILE_DELAY_UNTIL_REBOOT 옵션을 주어 함수를 호출하더라도 파일 열기시 ERROR_SHARING_VIOLATION에러를 받게 되지만 이 때는 MoveFileEx 함수가 실패로 리턴하지 않고 레지스트리에 기록을 해주기 때문에, 어떤 파일이던지 간에 이름 변경이나 삭제를 할 수가 있는 것이다.
  • 함수 모양을 봤을 때 MoveFileEx나 DeleteFile처럼 HANDLE을 인자로 전달받지 않고 파일 경로를 전달 받는 함수는 모두 내부적으로 파일을 오픈한다.
  • SetFileInformationByHandle 함수를 사용하면 추가적으로 파일을 다시 열지 않고 Rename, Delete등의 작업을 할 수 있다. 이 함수는 파일 시스템 드라이버에 전달되는 IRP와 거의 비슷하게 매핑되는 아주 강력한 함수이다. 파일 속성에 대한 모든 조작은 이 함수를 통해서 할 수 있다.
    하지만 워낙 저수준의 함수이기 때문에 사용법이 조금 어렵게 느껴질 수도 있다.
    아래 글에 해당 함수를 사용하여 이름 변경을 하는 코드에 대한 설명이 있다.
    하위 디렉터리의 파일이 변경 되었는지 감지하는 법
     
저작자 표시 비영리 동일 조건 변경 허락
신고

submit
프로그램을 짜다보면, 특정 디렉토리 내에서 파일 혹은 디렉터리가 변경되었음을 감지해내야 하는 경우가 가끔 생긴다.
윈도우즈에서는 FindFirstChangeNotification과 그 패밀리 함수들을 통해서 이를 쉽게 확인할 수 있다.
감시하고 싶은 디렉터리의 바로 하위 디렉터리 뿐만 아니라, 모든 하위 디렉터리까지 알림을 받을 수 있도록 API가 설계되어져 있다.
FindFirstChangeNotification은 파일 변경 알림을 위한 커널 오브젝트를 만들어서 돌려주는 함수이며, 다른 여느 커널 오브젝트들을 사용하듯이, 그저 생성한 뒤 시그널 되기를(파일이 변경되기를) 기다리면 된다. -WaitForSingleObject 따위의 함수들을 이용해서 말이다.

그럼 디렉터리의 어떤 파일이 어떻게 변경되었는지도 알 수 있을까?
FindFirstChangeNotification 함수로는 이를 알 수 없지만 ReadDirectoryChangesW 함수를 이용하면 알 수 있다.

ReadDirectoryChangesW 함수는 다른 함수들과는 다르게 이름 뒤에 W가 붙은 유니코드용 함수만 제공된다.
처음에 막상 이 함수를 써보려고 하면 몇 가지 어려움에 부딪치게 되는데, 알고 나면 그렇게 어렵지 않은 함수이다.

2가지의 지식만 알고 있으면 되는데 첫번째는 디렉터리의 핸들을 얻는 방법이고, 두번째는 FILE_NOTIFY_INFORMATION 데이터 구조를 이해하는 것이다.

CreateFile 함수는 파일을 생성하는 것 뿐만아니라, 파일을 열 수도 있으며 디렉터리를 열 수도 있다. 사실 CreateFile에서 File이란 의미는 VirtualFile을 의미하며, 실제 파일이 아닌 장치들도 CreateFile을 통해 열어서 I/O를 하게 된다.
CreateFile을 통해 디렉터리를 열 때는 꼭 FILE_FLAG_BACKUP_SEMANTICS 플래그를 넣어주어야 한다.

FILE_NOTIFY_INFORMATION 구조체는 다음처럼 생겼다. 한 개의 파일 변경에 대한 정보를 담을 수 있는 구조체이며, 내가 넣어준 버퍼에 여러 개의 아래 구조체가 담겨온다.
첫번째 필드인 NextEntryOffset을 통해 다음 구조체의 오프셋을 가르쳐주는데. 다음 엔트리가 없을 때까지(NextEntryOffset이 0) 하나씩 쭉쭉 읽어오면 되는 것이다.
typedef struct _FILE_NOTIFY_INFORMATION {
  DWORD NextEntryOffset;
  DWORD Action;
  DWORD FileNameLength;
  WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;

마지막에 FileName[1] 이라고 적혀있는 것은 가변 크기 데이터를 한 덩어리로 메모리를 할당해서 쓰기 위해 C언어에서 종종 사용되는 기법이다. 이런 경우 항상 가변 길이 변수(여기서는 FileName[1])의 크기를 나타내는 변수가 하나 더 존재한다.(여기서는 FileNameLength이다)

커널 모드의 많은 서비스 함수들과 유저모드로 노출된 몇몇 API 들에서 저런 데이터 구조를 사용하는데, 이상하게 생기고 어려워 보인다고 그냥 넘어가면 꼭 필요할 때 효율적인 데이터 구조를 만들 수 없을 뿐만 아니라, 남이 만들어 놓은 함수들조차 사용할 수 없다.

아래 블로그 포스트에 이에 대한 약간의 설명이 더 있으니 참고하자.
char data[1]의 역할은?

SetFileInformationByHandle 함수는 비스타 부터 제공되는 강력한 파일 조작 API인데 위와 같은 데이터 구조를 알아야 사용할 수 있다. 이 함수를 통해서 Rename을 하는 부분만 살펴보자. FIELD_OFFSET 매크로를 어떻게 사용하는지 주목해서 봐야한다.

이 함수에서 입력으로 사용되는 FILE_RENAME_INFO 구조체는 다음과 같이 생겼다.
typedef struct _FILE_RENAME_INFO {
  BOOL   ReplaceIfExists;
  HANDLE RootDirectory;
  DWORD  FileNameLength;
  WCHAR  FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

std::wstring newFileName = L"D:\\newfilename";
HANDLE h = CreateFileW(L"D:\\originfilename", GENERIC_READ|GENERIC_WRITE|DELETE,
    FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
 
DWORD cbBuffer = FIELD_OFFSET(FILE_RENAME_INFO, FileName[newFileName.size() + 1]);
 
PFILE_RENAME_INFO pRenameInfo = (PFILE_RENAME_INFO)malloc(cbBuffer);
pRenameInfo->ReplaceIfExists = FALSE;
pRenameInfo->FileNameLength = newFileName.size() * sizeof(WCHAR);
pRenameInfo->RootDirectory = 0;
 
StringCchCopyNW(pRenameInfo->FileName,
    newFileName.size() + 1, newFileName.c_str(), newFileName.size());
 
SetFileInformationByHandle(h, FileRenameInfo, pRenameInfo, cbBuffer);

이제 ReadDirectoryChangesW 함수도 이해할 수 있다. 바로 코드를 살펴보자. 잡스런 처리는 하지 않았다.

HANDLE hDir = CreateFileW(L"D:\\", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,
    0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
CONST DWORD cbBuffer = 1024*1024;
BYTE* pBuffer = (PBYTE)malloc(cbBuffer);
BOOL bWatchSubtree = FALSE;
DWORD dwNotifyFilter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
    FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE |
    FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION;
DWORD bytesReturned;
WCHAR temp[MAX_PATH] = { 0 };
 
for(;;)
{
    FILE_NOTIFY_INFORMATION* pfni;
    BOOL fOk = ReadDirectoryChangesW(hDir, pBuffer, cbBuffer,
        bWatchSubtree, dwNotifyFilter, &bytesReturned, 0, 0);
    if(!fOk)
    {
        DWORD dwLastError = GetLastError();
        printf("error : %d\n", dwLastError);
        break;
    }
 
    pfni = (FILE_NOTIFY_INFORMATION*)pBuffer;
 
    do {
        printf("NextEntryOffset(%d)\n", pfni->NextEntryOffset);
        switch(pfni->Action)
        {
        case FILE_ACTION_ADDED:
            wprintf(L"FILE_ACTION_ADDED\n");
            break;
        case FILE_ACTION_REMOVED:
            wprintf(L"FILE_ACTION_REMOVED\n");
            break;
        case FILE_ACTION_MODIFIED:
            wprintf(L"FILE_ACTION_MODIFIED\n");
            break;
        case FILE_ACTION_RENAMED_OLD_NAME:
            wprintf(L"FILE_ACTION_RENAMED_OLD_NAME\n");
            break;
        case FILE_ACTION_RENAMED_NEW_NAME:
            wprintf(L"FILE_ACTION_RENAMED_NEW_NAME\n");
            break;
        default:
            break;
        }
        printf("FileNameLength(%d)\n", pfni->FileNameLength);
 
        StringCbCopyNW(temp, sizeof(temp), pfni->FileName, pfni->FileNameLength);
 
        wprintf(L"FileName(%s)\n", temp);
 
        pfni = (FILE_NOTIFY_INFORMATION*)((PBYTE)pfni + pfni->NextEntryOffset);
    } while(pfni->NextEntryOffset > 0);
}

위와 같이 변경된 파일의 이름과 어떤 식으로 변경되었는지(파일이 새로 생성되었는지, 시간이 바뀐건지)등의 정보를 모두 얻어낼 수 있다.

함수를 사용하는 법 이외에도 몇 가지 더 알고 있어야 하는 것들이 있다.

ReadDirectoryChangesW를 호출해서 한번 통지를 받은 후 다시 루프를 도는 동안 파일들이 변경된다면 그 사이 변경된 파일들은 모두 놓치게 되는 것인가?
함수를 통해 통지를 받을 때, 꼭 하나의 파일(혹은 디렉터리)만 튀어나오는 것은 아니라는 점을 명심해야 한다.
파일 시스템 드라이버는 내부에서 버퍼를 따로 할당해서 이 버퍼에 그 동안 변경된 파일들을 계속 모아둔다. 그리고 사용자 쪽에서 통지를 기다리면, 이 내부 버퍼에 쌓인 것들을 전부 사용자 버퍼로 복사 한뒤 I/O를 완료시켜서 사용자 쪽으로 돌려주게 된다. 따라서 혹시 루프가 천천히 돌더라도 그 사이에 변경되는 파일들은 다음 번 호출시에 모두 받을 수 있게된다. 그렇기 때문에 두번째 인자로 제공되는 버퍼에 FILE_NOTIFY_INFORMATION 구조를 여러개 담아 주도록 설계한 것이다.

또한 이 파일 시스템 드라이버의 내부 버퍼는 핸들을 닫을 때까지 유지된다. 즉, 한번 ReadDirectoryChangesW 함수를 호출하고 핸들을 닫지 않은채 그 다음 호출을 안하고 멍하니 있는다면 그 동안 드라이버 내의 내부 버퍼에 변경된 파일 정보들이 계속 쌓이게 될 것이다. 물론 얼마나 쌓이느냐는 파일 시스템 드라이버의 구현에 달려있을 것이고 NTFS가 어떻게 구현했는지는 모른다.

ReadDirectoryChangesW 함수의 모양을 보면 알 수 있지만 이 함수는 비동기 I/O도 지원을 한다.
디렉터리를 1개만 감시하고 싶을 때는 위에서 한 것 처럼 동기적으로 호출해도 되겠지만, 1개의 쓰레드만 사용하면서 여러 개의 디렉터리들을 감시하고 싶다면 비동기 I/O를 사용하는 것을 고려해봐야 할 것이다.
비동기로 함수를 호출하는 방법은 따로 설명하지 않는다.

파일 시스템 드라이버나 네트워크 리디렉터를 만들 때는 위 기능을 직접 구현해주어야 하는데 필수적으로 구현해야 하는 것은 아니다. 물론 구현하지 않으면 파일이 변경되었을 때 애플리케이션들이 보여주는 UI에서, 변경되는 파일들이 자동으로 갱신되지 않을 것이므로(ReadDirectoryChangesW가 실패할 것이다) 구현 하는 쪽이 더 나은 사용자 경험을 제공해 줄 수 있는 파일 시스템 드라이버가 될 것이다.
저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 재호님 팬 at 2010.12.29 12:11 신고 [edit/del]

    재호님~ 소스코드 폰트색상이 굉장히 예쁘네요
    폰트 rgb 값좀 알려주시면 안되나요?

    Reply
  2. Ji at 2011.02.10 17:48 신고 [edit/del]

    여기저기 쓸데없는 포스트들만 엄청 찾아보다가 드디어 오아시스 같은 글을 만나네요.
    잘 배우고 갑니다. 고맙습니다.

    Reply
  3. 오곡 at 2012.12.05 00:57 신고 [edit/del]

    정말 좋은 내용 잘보고 갑니다 ㅠㅠ

    Reply
  4. at 2013.07.21 21:49 신고 [edit/del]

    저기 비동기식으로 감시한다는게 무슨 뜻인가요?

    Reply
  5. Mr.K at 2014.06.26 11:37 신고 [edit/del]

    감사합니다. 파일과 관련된 처리 하다가 찾았습니다. 유용할거 같네요

    Reply
  6. BlogIcon builder at 2017.05.12 10:51 신고 [edit/del]

    안녕하세요. 궁금한 게 있어 덧글 남깁니다ㅠ
    cbBuffer = 1048576 초기화 이유가 따로 있나용?

    Reply

submit
윈도우즈 API를 사용하다보면 종종 reserved라는 파라메터를 접하게 된다.

LONG WINAPI RegQueryValueEx(
  __in         HKEY hKey,
  __in_opt     LPCTSTR lpValueName,
  __reserved   LPDWORD lpReserved, //This parameter is reserved and must be NULL.
  __out_opt    LPDWORD lpType,
  __out_opt    LPBYTE lpData,
  __inout_opt  LPDWORD lpcbData
);

도대체 마이크로소프트는 이딴 수법을 왜 쓰는 것일까?

첫번째는 나중에 함수의 기능이 추가되거나 쉽게 확장될 수 있도록 하기 위해서이다. 이 추가적인 파라메터로 인해 인터페이스를 변경하지 않고도 쉽게 기능을 넣을 수 있다.
문서상에서 많은 reserved 파라메터들이 반드시 NULL을 넣어야 한다고 쓰여있는데, 이렇게 해두면 추후 함수가 변경될 때 이전 클라이언트 코드들과 구분을 하기가 용이해진다.

두번째는 윈도우즈 내부에서 호출하는 경우이다. 외부에서 노출된 winapi를 사용하는 클라이언트들에게는 NULL을 넣도록 하고, 내부에서는 다른 용도로 특별한 값을 넣어서 사용하는 것이다.

세번째 이유는 첫번째 이유와 반대이다.
처음 해당 함수가 생길 당시에는 reserved 파라메터는 실제 다른 어떤 용도로 쓰이고 있었다. 시간이 한참 지나고 해당 필드의 의미가 퇴색되고 더 이상 필요 없게 되어 버리자, 파라메터를 제거하는 대신 이름을 reserved로 바꾸어 버렸다. 물론 이전 코드들과의 호환성을 지켜주기 위함이다.

또 다른 이유는 구조체에 불필요한 패딩 데이터를 포함시키지 않고 차라리 reserved용도로 사용하려는 것이다.
struct IconDirectoryEntry {
    BYTE  bWidth;
    BYTE  bHeight;
    BYTE  bColorCount;
    BYTE  bReserved;
    WORD  wPlanes;
    WORD  wBitCount;
    DWORD dwBytesInRes;
    DWORD dwImageOffset;
};
위 구조체에서는 4번째 필드인 bReserved가 패딩 데이터를 채우는 대신 그 자리에 차라리 reserved용 데이터를 집어 넣었다는 것이 명백히 드러난다.

저작자 표시 비영리 동일 조건 변경 허락
신고

submit
SysInternals의 대부분의 유틸리티들은 하나의 실행파일로 배포된다.

프로세스 모니터 같은 도구는 시스템 내에서 일어나는 모든 I/O등을 잡아채서 보여주는데 이 역시 EXE 파일 하나로 배포된다. 그렇다면 마크 러시노비치가 이것들을 유저모드 애플리케이션에서 구현했다는 말인가? 아니, 심지어 이런 것들을 유저모드에서 구현할 수나 있는 것일까?
물론 그렇지는 않다. 프로세스 모니터는 파일 시스템 레이어 상위에 장착되는 필터 드라이버와 응용 프로그램으로 구성되어져 있다. 아마도 필터 드라이버가 열심히 I/O를 엿본 다음에 그 정보를 잘 정리해서 응용 프로그램에게 전달한 뒤 응용 프로그램이 GUI로 출력하도록 구현되어져 있을 것이다.

그렇다면 프로세스 모니터의 드라이버 이미지는 어디에 숨어있는 것일까?
드라이버는 바로 EXE 파일 내의 리소스에 있다.


응용 프로그램 내에 커스텀 리소스를 하나 만들어서 그 곳에 바이너리 파일을(여기서는 프로세스 모니터 드라이버 이미지) 쑤셔 넣어둔다.
응용 프로그램이 처음 실행되면 FindResource, LoadResource, LockResource, SizeofResource 함수들을 사용해서 이 리소스를 읽어 오고 CreateFile과 WriteFile 같은 함수를 통해서 새로운 파일을(바로 그 프로세스 모니터 드라이버) 디스크 상에 만들어낸다.
이런 식으로 말이다.
HRSRC h = FindResourceW(0, MAKEINTRESOURCEW(IDR_BIN), L"BIN");
HANDLE hRes = LoadResource(0, h);
CONST CHAR* p = (CONST CHAR*)LockResource(hRes);
HANDLE hFile = CreateFileW(L"D:\\bin", GENERIC_ALL, 0, 0, CREATE_ALWAYS, 0, 0);
DWORD cb = SizeofResource(0, h);
DWORD dw;
WriteFile(hFile, p, cb, &dw, 0);
UnlockResource(hRes);
너무 간단하지 않은가?

이제 드라이버 파일이 생겼으므로 응용 프로그램 코드에서는 CreateServiceStartService 함수들을 통해서 드라이버를 설치하고 구동시킬 수 있다.
응용 프로그램이 종료 될 때는 물론 드라이버를 잘 멈추고 제거하고 파일을 지워버리는 등의 작업 또한 해주어야 할 것이다.

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 관우 at 2011.01.05 23:15 신고 [edit/del]

    너무 궁금했는데 이렇게 간단할 줄은 몰랐네요. ㅎㅎ 잘 배워 갑니다.

    Reply

submit
오늘 일을 하는데 윈도우즈 세션이 종료될 때 애플리케이션들에게 보내지는 윈도우 메세지가 뭐였는지 기억이 나질 않는 것이다. 옛날에 김상형씨의 winapi 사이트에서 공부할 때 봤던 것 같은 생각이 언뜻 들어 오랜만에 winapi.co.kr을 찾았다.

들어가자마자 메인 페이지에 구인 광고가 있길래 찬찬히 읽어보다가 '예쁜 여성분 우대' 에서 뿜어버리고 말았다.


winapi의 공지사항들도 한번 읽어보면 더욱 즐거워 질 것이다.

유머 감각이 있는 사람들은 아름답다.
훌륭한 해커를 가졌고, 예쁜여자까지 선호하는 저런 바람직한 회사에 많은 인재들이 지원해보기를 바란다.

아참, 궁금했던 그 메세지는 WM_QUERYENDSESSION 이었다.
신고

'에세이' 카테고리의 다른 글

자판기 동전 교환의 비밀  (7) 2010.08.19
예스24에서 중고책 팔기  (2) 2010.05.27
김상형씨의 구인 광고  (2) 2010.05.13
인터넷 서점에서 중고책 사고 팔기  (0) 2010.05.04
제 5회 알라딘 리뷰 대회  (3) 2009.11.01
임요환의 아름다운 열정  (4) 2009.07.14
  1. 락의전사 at 2010.05.18 18:15 신고 [edit/del]

    왜 주소가 바낐찌?
    먼가 구린게 있나보군요...
    구려 구린내가 나...

    Reply

submit