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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1756정성태9/23/201427486기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434280오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424642VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420610오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438944.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423832.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423732.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425384개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428473오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426106.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423035개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431062.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426971개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421334.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432508.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426538.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422047.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419765VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425576VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418200.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419861오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426362.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434476Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427081개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...