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

비밀번호

댓글 작성자
 




... 76  77  78  79  [80]  81  82  83  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11936정성태6/10/201918364Math: 58. C# - 최소 자승법의 1차, 2차 수렴 그래프 변화 확인 [2]파일 다운로드1
11935정성태6/9/201919928.NET Framework: 843. C# - PLplot 출력을 파일이 아닌 Window 화면으로 변경
11934정성태6/7/201921260VC++: 133. typedef struct와 타입 전방 선언으로 인한 C2371 오류파일 다운로드1
11933정성태6/7/201919602VC++: 132. enum 정의를 C++11의 enum class로 바꿀 때 유의할 사항파일 다운로드1
11932정성태6/7/201918782오류 유형: 544. C++ - fatal error C1017: invalid integer constant expression파일 다운로드1
11931정성태6/6/201919299개발 환경 구성: 441. C# - CairoSharp/GtkSharp 사용을 위한 프로젝트 구성 방법
11930정성태6/5/201919830.NET Framework: 842. .NET Reflection을 대체할 System.Reflection.Metadata 소개 [1]
11929정성태6/5/201919394.NET Framework: 841. Windows Forms/C# - 클립보드에 RTF 텍스트를 복사 및 확인하는 방법 [1]
11928정성태6/5/201918170오류 유형: 543. PowerShell 확장 설치 시 "Catalog file '[...].cat' is not found in the contents of the module" 오류 발생
11927정성태6/5/201919394스크립트: 15. PowerShell ISE의 스크립트를 복사 후 PPT/Word에 붙여 넣으면 한글이 깨지는 문제 [1]
11926정성태6/4/201919931오류 유형: 542. Visual Studio - pointer to incomplete class type is not allowed
11925정성태6/4/201919767VC++: 131. Visual C++ - uuid 확장 속성과 __uuidof 확장 연산자파일 다운로드1
11924정성태5/30/201921399Math: 57. C# - 해석학적 방법을 이용한 최소 자승법 [1]파일 다운로드1
11923정성태5/30/201921030Math: 56. C# - 그래프 그리기로 알아보는 경사 하강법의 최소/최댓값 구하기파일 다운로드1
11922정성태5/29/201918531.NET Framework: 840. ML.NET 데이터 정규화파일 다운로드1
11921정성태5/28/201924387Math: 55. C# - 다항식을 위한 최소 자승법(Least Squares Method)파일 다운로드1
11920정성태5/28/201916052.NET Framework: 839. C# - PLplot 색상 제어
11919정성태5/27/201920304Math: 54. C# - 최소 자승법의 1차 함수에 대한 매개변수를 단순 for 문으로 구하는 방법 [1]파일 다운로드1
11918정성태5/25/201921147Math: 53. C# - 행렬식을 이용한 최소 자승법(LSM: Least Square Method)파일 다운로드1
11917정성태5/24/201922131Math: 52. MathNet을 이용한 간단한 통계 정보 처리 - 분산/표준편차파일 다운로드1
11916정성태5/24/201919946Math: 51. MathNET + OxyPlot을 이용한 간단한 통계 정보 처리 - Histogram파일 다운로드1
11915정성태5/24/201923058Linux: 11. 리눅스의 환경 변수 관련 함수 정리 - putenv, setenv, unsetenv
11914정성태5/24/201922042Linux: 10. 윈도우의 GetTickCount와 리눅스의 clock_gettime파일 다운로드1
11913정성태5/23/201918761.NET Framework: 838. C# - 숫자형 타입의 bit(2진) 문자열, 16진수 문자열 구하는 방법파일 다운로드1
11912정성태5/23/201918726VS.NET IDE: 137. Visual Studio 2019 버전 16.1부터 리눅스 C/C++ 프로젝트에 추가된 WSL 지원
11911정성태5/23/201917491VS.NET IDE: 136. Visual Studio 2019 - 리눅스 C/C++ 프로젝트에 인텔리센스가 동작하지 않는 경우
... 76  77  78  79  [80]  81  82  83  84  85  86  87  88  89  90  ...