Microsoft MVP성태의 닷넷 이야기
C/C++: 171. C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE [링크 복사], [링크+제목 복사],
조회: 7443
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
C/C++: 170. Windows - STARTUPINFO의 cbReserved2, lpReserved2 멤버 사용자 정의
; https://www.sysnet.pe.kr/2/0/13723

C/C++: 171. C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE
; https://www.sysnet.pe.kr/2/0/13726

C/C++: 172. Windows - C 런타임에서 STARTUPINFO의 cbReserved2, lpReserved2 멤버를 사용하는 이유
; https://www.sysnet.pe.kr/2/0/13728

C/C++: 174. C/C++ - 윈도우 운영체제에서의 file descriptor, FILE*
; https://www.sysnet.pe.kr/2/0/13738

C/C++: 175. C++ - WinMain/wWinMain 호출 전의 CRT 초기화 단계
; https://www.sysnet.pe.kr/2/0/13750




C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE

Windows 운영체제의 핸들(HANDLE) 상속은 지난 글에 이미 설명했습니다.

Win32/C# - 자식 프로세스로 HANDLE 상속
; https://www.sysnet.pe.kr/2/0/13724

C/C++로 만든 프로그램 역시, 결국은 하부에서 Win32 API로 번역되기 때문에 저 원칙을 따릅니다. 그렇다면, 예를 들어 fopen 함수는 파일을 상속 가능한 핸들로 열까요? 아니면 C#처럼 불가능으로 열까요?

WIN03-C. Understand HANDLE inheritance
; https://wiki.sei.cmu.edu/confluence/display/c/WIN03-C.+Understand+HANDLE+inheritance

위의 글에도 설명하고 있지만, fopen은 윈도우의 HANDLE 자원을 상속 가능으로 생성합니다. (이것은, *NIX 시스템에서 file descriptor를 기본 상속하는 것과 유사합니다.)

#include <iostream>
#include <fstream>

int main()
{
    FILE* fs;
    fopen_s(&fs, "test.dat", "a+"); // inherit handle
    
    getchar(); // 이 시점에 Process Explorer로 확인해 보면 inherit 속성이 설정된 것을 볼 수 있습니다.

    fclose(fs);
}

C#의 경우 자식 프로세스에 상속하고 싶은 경우 명시적으로 Win32 API를 호출해야 했지만, C/C++의 경우에는 반대가 된 것입니다. 따라서 상속을 원치 않는다면, 각각의 파일 열기 함수에서 그것을 위한 옵션을 제공해야 합니다.

fopen_s(&fs, "test.dat", "a+N"); // "N"은 상속하지 않음을 의미

fopen_s가 첫 번째 인자를 통해 반환하는 것은 FILE* 타입, 즉 파일에 대한 스트림을 반환하는데요, 사실 이것은 open + fdopen 2가지 동작을 함께 처리해 주고 있는 것이므로 다음과 같이 나눠서 코딩할 수 있습니다.

// #include <io.h>
// #include <fcntl.h>

{
    int fd = 0;

    // 파일을 여는 시점에, 상속 여부를 _O_NOINHERIT 옵션으로 결정
    _sopen_s(&fd, "test.dat", _O_APPEND | _O_NOINHERIT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    FILE* fs = _fdopen(fd, "a+"); // 파일에 대한 스트림 반환

    fclose(fs); // fs가 유효한 경우, 스트림을 닫으면 파일(file descriptor)도 닫힘.
}

여기서 FILE 구조체 정의가 재미있는데요,

// C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt\corecrt_wstdio.h

typedef struct _iobuf
{
    void* _Placeholder;
} FILE;

아무 정보도 없습니다. ^^; 이것은 의도적으로 내부 구현을 노출시키지 않기 위해 전면에 내세운 타입이라서 그런 것이고, 실제 동작에서는 __crt_stdio_stream_data 타입이 사용됩니다. (그래서인지 Visual C++ 내부 소스에서는 저 FILE* 변수를 종종 public_stream이라고 작명합니다.)

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\inc\corecrt_internal_stdio.h

struct __crt_stdio_stream_data
{
    union
    {
        FILE  _public_file;
        char* _ptr;
    };

    char*            _base;
    int              _cnt;
    long             _flags;
    long             _file; // file descriptor
    int              _charbuf;
    int              _bufsiz;
    char*            _tmpfname;
    CRITICAL_SECTION _lock;
};

class __crt_stdio_stream
{
public:

    // ...[생략]...

    explicit __crt_stdio_stream(FILE* const stream) throw()
        : _stream(reinterpret_cast<__crt_stdio_stream_data*>(stream))
    {
    }

    // ...[생략]...

private:

    __crt_stdio_stream_data* _stream;
};

이렇게 숨겨진 FILE 구조체의 값을 접근하려면 함수로 공개된 것이 있어야 합니다. 아래는 FILE 구조체의 _file 필드를 반환해 주는 _fileno의 사용 예입니다.

{
    int fd = 0;
    _sopen_s(&fd, "test.dat", _O_APPEND | _O_NOINHERIT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    FILE* fs = _fdopen(fd, "a+");

    // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\stdio\fileno.cpp
    int fd2 = _fileno(fs);  
    _ASSERT(fd == fd2);
    _close(fd);
}




엄밀히 말해서, file descriptor는 윈도우 운영체제에서는 필요 없는 개념입니다. *NIX에서는 운영체제로부터 open한 결과가 file descriptor라서 꼭 필요하지만, 윈도우의 경우 kernel로부터 반환받는 값은 HANDLE이기 때문입니다.

문제는 C 런타임 라이브러리가 근본적으로 *NIX 운영체제하에서 개발되었고, file descriptor가 HANDLE과 의미는 유사하다고 하지만 실제 동작 방식은 다르기 때문에 호환이 안 됩니다. 가령, file descriptor가 0은 STDIN, 1은 STDOUT, 2는 STDERR를 가리키는데, 이것을 그냥 HANDLE로 바꿔버리면 문제가 발생하게 됩니다. (HANDLE은 0을 쓰지 않고, 다음 HANDLE에 대한 증가는 4단위로 이뤄집니다.)

이 외에도, "리눅스 API의 모든 것, 기초 리눅스 API" 책에 나오듯이,

https://broman.dev/download/The%20Linux%20Programming%20Interface.pdf#page=117

SUSv3 specifies that if open() succeeds, it is guaranteed to use the lowest-numbered unused file descriptor for the process. We can use this feature to ensure that a file
is opened using a particular file descriptor. For example, the following sequence
ensures that a file is opened using standard input (file descriptor 0)


윈도우 환경의 CRT에서도 저 규칙을 지켜 file descriptor 번호가 할당됩니다.

결국 리눅스에서의 file 관련 함수들과 윈도우의 HANDLE 처리의 간극을 해결하기 위해 윈도우는 file descriptor와 HANDLE의 매핑 테이블을 관리합니다. 그래서 open으로 파일을 열 때마다 3번부터 시작하는 fd를 할당한 다음 그것과 HANDLE 정보를 연결합니다. 물론 그러한 처리는 CRT 내부적으로 __pioinfo 포인터 배열에 fd를 인덱스로, __crt_lowio_handle_data 구조체를 참조하는 식으로 관리됩니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\inc\corecrt_internal_stdio.h

extern "C" extern __crt_stdio_stream_data** __piob;

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\inc\corecrt_internal_lowio.h

#define IOINFO_ARRAYS       128
#define IOINFO_L2E          6
#define IOINFO_ARRAY_ELTS   (1 << IOINFO_L2E)

#define _pioinfo(i)          (__pioinfo[(i) >> IOINFO_L2E] + ((i) & (IOINFO_ARRAY_ELTS - 1)))
extern __crt_lowio_handle_data_array __pioinfo;
typedef __crt_lowio_handle_data* __crt_lowio_handle_data_array[IOINFO_ARRAYS];

#define _osfhnd(i)           (_pioinfo(i)->osfhnd)
#define _osfile(i)           (_pioinfo(i)->osfile)

struct __crt_lowio_handle_data
{
    CRITICAL_SECTION           lock;
    intptr_t                   osfhnd;          // underlying OS file HANDLE
    __int64                    startpos;        // File position that matches buffer start
    unsigned char              osfile;          // Attributes of file (e.g., open in text mode?)
    __crt_lowio_text_mode      textmode;
    __crt_lowio_pipe_lookahead _pipe_lookahead;

    uint8_t unicode          : 1; // Was the file opened as unicode?
    uint8_t utf8translations : 1; // Buffer contains translations other than CRLF
    uint8_t dbcsBufferUsed   : 1; // Is the dbcsBuffer in use?
    char    mbBuffer[MB_LEN_MAX]; // Buffer for the lead byte of DBCS when converting from DBCS to Unicode
                                  // Or for the first up to 3 bytes of a UTF-8 character
};

음... 너무 깊이 들어가지 말고 ^^ 그냥 어쩔 수 없이 file descriptor와 HANDLE을 연결하는 방법이 필요했고 Visual C++의 CRT 내부에 그것이 구현돼 있다고 알면 되겠습니다.

그래도 이쯤 되면, file descriptor와 HANDLE, FILE (Stream)의 관계를 대강 이해했을 것입니다. 그리고, 이들 사이의 변환(?)도 이제 다음과 같이 정리해 볼 수 있습니다.

_fileno
    입력: FILE* 
    출력: file descriptor (FILE 구조체 내부에 보관하고 있는 file descriptor를 반환)
_fdopen
    입력: file descriptor 
    출력: FILE* (__piob에서 비어 있거나/할당해서 FILE Stream을 반환, __piob는 일종의 object pool 역할)

_get_osfhandle
    입력: file Descriptor 
    출력: HANDLE (__pioinfo 테이블을 참조해서 file descriptor에 대한 HANDLE을 반환)
_open_osfhandle
    입력: HANDLE 
    출력: file Descriptor (__pioinfo 테이블에 빈 file descriptor를 찾아 반환, __pioinfo는 일종의 object pool 역할)

저걸 연결하면, 예를 들어 FILE 스트림 개체로부터 HANDLE을 가져오는 것도 가능합니다.

#include <iostream>
#include <fstream>

int main()
{
    FILE* fs;
    fopen_s(&fs, "test.dat", "a+"); // inherit handle

    HANDLE hFile = (HANDLE)_get_osfhandle(_fileno(fs));

    fclose(fs);
}

반면, 다른 2개의 함수는 단순히 값을 가져오는 수준이 아니므로 주의를 요합니다. 가령, 동일한 FILE 스트림을 2개 열었을 때 그것을 fclose로 2번 닫으면,

{
    int fd = 0;
    _sopen_s(&fd, "test.dat", _O_APPEND | _O_NOINHERIT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    FILE* fs = _fdopen(fd, "a+");
    FILE* fs2 = _fdopen(fd, "r");
    _ASSERT(fs != fs2);


    fclose(fs);
    fclose(fs2); // ASSERT
}

2번째 fclose에서 ASSERT가 발생합니다.

---------------------------
Microsoft Visual C++ Runtime Library
---------------------------
Debug Assertion Failed!

Program: ...\ConsoleApplication1\x64\Debug\ConsoleApplication1.exe
File: minkernel\crts\ucrt\src\appcrt\lowio\close.cpp
Line: 56

Expression: (_osfile(fh) & FOPEN)

For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.

(Press Retry to debug the application)

왜냐하면, _fdopen이 열은 FILE 스트림은 동일한 HANDLE에 대해 다른 Stream을 생성하는 경우이기 때문입니다. 따라서, 만약 같은 파일에 대해 FILE 스트림을 여러 개 생성하고 싶다면, open 또는 dup을 해야 합니다. (dup은 원본 file descriptor의 HANDLE이 가진 Inherit 속성을 무시하고 무조건 상속 가능 유형의 HANDLE을 생성합니다.)

또한, _open_osfhandle도 (이름에서 get이 아닌 open인 것처럼) 단순히 기존 값을 가져오는 것이 아닌, HANDLE에 대해 빈 file descriptor를 매번 받아오는 식입니다.

int fd = 0;
_sopen_s(&fd, "test.dat", _O_APPEND | _O_NOINHERIT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

FILE* fs = _fdopen(fd, "a+");

int fd2 = _fileno(fs);
_ASSERT(fd == fd2);

{
    HANDLE hHandle = (HANDLE)_get_osfhandle(fd);
    int fd3 = _open_osfhandle((intptr_t)hHandle, 0);
    _ASSERT(fd2 + 1 == fd3);

    int fd4 = _open_osfhandle((intptr_t)hHandle, 0);
    _ASSERT(fd3 + 1 == fd4);
}

따라서, 저런 경우 _close(fd3), _close(fd4)로 닫으면 2번째 닫기에서 (이미 HANDLE을 닫았으므로) 예외가 발생합니다.

저 함수의 진정한 의도는, CreateFile로 열은 파일에 대해 file descriptor를 최초 구할 때만 호출하라고 있는 것입니다. (만약 2번 이상 호출하면 __pioinfo에 닫히지 않을 수밖에 없는 닫히지 않은 file descriptor가 쌓이게 됩니다.)

결국, _fileno, _get_osfhandle 함수만 Pool과 관련돼 있지 않아 안심하고 어느 상황에서든 호출할 수 있습니다.




마지막으로, C++ 파일 클래스의 상속에 관해 살펴보면,

#include <iostream>
#include <fstream>

int main()
{
    std::ofstream outFile("filename.txt");  // inherit handle
    std::cout << "Hello World!\n";

    getchar();
}

기본적으로 상속 가능한 핸들로 열리는 것을 확인할 수 있습니다. 한 가지 재미있는 점은, C++부터는 OS 종속적인 면을 제거하기 위해 file descriptor / HANDLE과의 연관성을 숨겼다는 특징이 있습니다.

심지어, Visual C++의 경우 예전에는 ofstream/ifstream에서 HANDLE을 반환하는 함수를 제공했지만 근래에는 그것마저 없앴다는 기록도 보입니다. 이것을 달리 말하면 ofstream/ifstream에 대해서는 상속을 제어할 방법이 없다는 것입니다. 단지, 꼭 이것을 원한다면 fstream을 FILE*을 거쳐 생성하는 식으로 처리해야 합니다.

{
    FILE* fs;
    fopen_s(&fs, "filename_o.txt", "a+N"); // 상속 불가능으로 stream을 열고,
    std::ofstream outFile(fs); // 그것을 ofstream에 전달

    outFile.close();
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 9/22/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [151]  152  153  154  155  156  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1274정성태4/17/201221414Phone: 4. "Holiday Calendar" 윈폰 응용 프로그램 등록
1273정성태4/6/201224623Phone: 3. 윈도우 폰을 위한 Holiyday Calendar 앱 개발파일 다운로드1
1272정성태4/5/201226171오류 유형: 151. ASP.NET - EcbGetUnicodeServerVariables 코드에서 System.AccessViolationException 예외 발생
1271정성태4/3/201228831Math: 6. 동전을 여러 더미로 나누는 경우의 수 세기 [1]
1270정성태3/29/201222741오류 유형: 150. Visual Studio 2010 원격 디버깅 오류 - Kerberos authentication failed
1269정성태3/27/201236567오류 유형: 149. ODP.NET 오류 - The provider is not compatible with the version of Oracle client
1268정성태3/27/201233075오류 유형: 148. WCF svc 호출 시 HTTP Error 404.17 - Not Found [1]
1267정성태3/16/201231046.NET Framework: 314. C++의 inline asm 사용을 .NET으로 포팅하는 방법 [1]파일 다운로드1
1266정성태3/14/201234258개발 환경 구성: 149. RAID 1 구성 시 하드 디스크 장애 발생 해결에 대한 경험담
1265정성태3/13/201224611VC++: 61. 아이태니엄(IA64: Itanium) 에서 겪은 C++ 포인터 연산 문제 [2]
1264정성태3/10/201243964.NET Framework: 313. WELL512 난수 발생 알고리즘 - C# [5]파일 다운로드1
1263정성태3/9/201222788개발 환경 구성: 148. tinyget 사용법
1262정성태3/8/201243679개발 환경 구성: 147. .keystore 파일에 저장된 개인키 추출 방법과 인증기관으로부터 온 공개키를 합친 pfx 파일 만드는 방법 [1]
1261정성태3/7/201224396Phone: 2. 개발자용 윈도우 폰 7 기기 등록하는 방법
1260정성태3/6/201224220Phone: 1. 윈도폰 7 개발자 (회사) 등록하는 방법 [3]
1259정성태3/4/201235701Windows: 57. 새로 추가된 네트워크 커널 디버깅 및 PowerShell 3.0 [1]
1258정성태3/3/201237330개발 환경 구성: 146. SQL Server 2012에 포함된 LocalDB 기능 소개 [3]파일 다운로드1
1257정성태3/3/201225553.NET Framework: 312. Native 스레드와 Managed 스레드 개체의 상태 관계 [1]파일 다운로드1
1256정성태3/3/201229076Math: 5. Euler's totient function - C#파일 다운로드1
1255정성태3/3/201231450Math: 4. 소수 판정 및 소인수 분해 소스 코드 - C# [1]파일 다운로드1
1254정성태3/1/201226362Windows: 56. Windows 8 Consumer Preview를 사용해 보고... [1]
1253정성태3/1/201227854VS.NET IDE: 71. Visual Studio 11 Ultimate 베타 설치 [3]
1252정성태3/1/201225226Windows: 55. 윈도우 8 베타 설치 과정 [1]
1251정성태2/27/201229170VC++: 60. C/C++ Native 스레드 콜 스택 덤프를 얻는 공개 라이브러리 [2]파일 다운로드1
1250정성태2/27/201231302VC++: 59. C/C++ 프로젝트 빌드 속도 개선 - UnityBuild를 아세요? [3]
1249정성태2/26/201230972.NET Framework: 311. .NET 스레드 콜 스택 덤프 (5) - ICorDebug 인터페이스 사용법 [2]파일 다운로드3
... [151]  152  153  154  155  156  157  158  159  160  161  162  163  164  165  ...