Media Log

이것은 윈도우 프로그래밍 이야기.
길을 걷다 보면 간혹 듣게 되는 이야기. 는 아니고 소프트웨어 회사의 사무실이나 엘레베이터에서 가끔 들을 수 있는 이야기.
메인 쓰레드가 종료되면 프로세스도 종료되는 것 아닌가요?
어잉. 그런가?

답은 아니오이다. 메인 쓰레드가 종료된다고 프로세스가 종료되는 것은 아니다.
ExitProcess 같은 함수를 통해 명시적으로 프로세스를 종료시키거나, 모든 쓰레드가 종료되었을 때 프로세스는 종료된다.

그럼 사람들은 왜 저런 미신을 가지게 되었을까. 아마도 메인 함수에서 리턴할 때 다른 쓰레드들이 존재하더라도 프로세스가 종료되는 것을 봐왔기 때문일 것이다.
그렇다. 메인 함수가 리턴하면 다른 쓰레드들이 잘 살아 있더라도 프로세스가 종료된다.
하지만 메인 쓰레드가 종료되었기 때문에 프로세스가 종료되는 것은 아니다. 이 때 프로세스가 종료되는 이유는 메인 함수가 끝나고 CRT에서 exit 함수를 호출 해주기 때문이다. exit()는 물론 내부적으로 ExitProcess를 호출한다.

다른 쓰레드를 생성해 놓은 뒤 메인 함수 내에서 ExitThread 함수를 사용해 메인 쓰레드만 종료시켜 보면 프로세스가 종료되지 않는다는 것을 살펴 볼 수 있을 것이다.
저작자 표시 비영리 동일 조건 변경 허락
신고

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
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
Win32 에러번호를 간편하게 확인할 수 있는 방법이 있는데도 불구하고 많은 사람들이 비주얼 스튜디오의 Error Lookup 툴을 사용하거나 심지어 툴을 따로 만들어 쓰는 것 까지 보고는 이 방법을 모르는 사람들이 상당히 많다는 것을 알게 되었다.


이보다 더 편할 수가 있겠는가.

응용 프로그램 레벨에서는 Win32 에러를 받게 되지만 커널부에서는 NTSTATUS 에러값을 확인하곤 한다.
다음 링크에서는 커널 코드에서 돌려지는 NTSTATUS 에러가 어떤 Win32 에러 코드로 매핑되어지는지 나와있다.

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 지나가던 at 2011.07.28 15:03 신고 [edit/del]

    헐.. 저런 기능이 있는지 몰랐네요! 감사합니다.
    앞으로 오류코드 보는게 더 편해지겠습니다.

    Reply

submit