Microsoft MVP성태의 닷넷 이야기
C/C++: 171. C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE [링크 복사], [링크+제목 복사],
조회: 5933
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13885정성태2/14/2025256닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/2025398닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/2025507닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/2025967스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20251077닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/2025911오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20251147오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20251021오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20251121닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20251310닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20251317스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20251262스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20251147디버깅 기술: 217. WinDbg - PCI 장치 열거
13872정성태1/23/20251149오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20251231Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20251308개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20251348개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20251303Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
13867정성태1/17/20251426오류 유형: 943. Hyper-V에 Windows 11 설치 시 "This PC doesn't currently meet Windows 11 system requirements" 오류
13866정성태1/16/20251418개발 환경 구성: 739. Windows 10부터 바뀐 device driver 서명 방법
13865정성태1/15/20251521오류 유형: 942. C# - .NET Framework 4.5.2 이하의 버전에서 HttpWebRequest로 https 호출 시 "System.Net.WebException" 예외 발생
13864정성태1/15/20251475Linux: 114. eBPF를 위해 필요한 SELinux 보안 정책
13863정성태1/14/20251558Linux: 113. Linux - 프로세스를 위한 전용 SELinux 보안 문맥 지정
13862정성태1/13/20251560Linux: 112. Linux - 데몬을 위한 SELinux 보안 정책 설정
13861정성태1/11/20251855Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...