Microsoft MVP성태의 닷넷 이야기
C/C++: 174. C/C++ - 윈도우 운영체제에서의 file descriptor, FILE* [링크 복사], [링크+제목 복사],
조회: 2156
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 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, FILE*

지난 글에서,

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

윈도우의 경우 file descriptor는 단순히 __pioinfo 전역 변수에 담긴 정보의 index를 의미한다고 설명했습니다. *NIX가 file open 시 file descriptor를 운영체제로부터 반환받는 것과는 달리, 윈도우는 CreateFile 시 HANDLE을 반환받기 때문에 file descriptor를 별도로 관리하기 위해 Visual C++에서 구현한 것이 __pioinfo 전역 변수입니다.

단순하게 요약하면, *NIX에서는 file descriptor를 직접 사용하지만,

int fd = open("file.txt", O_RDONLY); // *NIX 운영체제 함수
write(fd, buf, len);

(윈도우에서는 HANDLE을 사용하므로) file descriptor와의 인터페이스를 유지하기 위해 다음과 같은 식으로 구현돼 있는 것입니다.

// __pioinfo를 간략해서 fd_to_handle로 표현한 것으로 가정
HANDLE fd_to_handle[8192];

int open(const char* filename, int flags)
{
    HANDLE hHandle = CreateFile(filename, ...);
    int fd = get_available_file_descriptor_number();
    fd_to_handle[fd] = hHandle;
    return fd;
}

int write(int fd, const void* buf, size_t len)
{
    HANDLE hHandle = fd_to_handle[fd];
    WriteFile(hHandle, buf, len, ...);
}

개념만 그렇다고 이해하시면 되는데요, 그래도 심심하니 ^^ 지난 글에서 살펴본 소스코드에서 한 발짝 더 나아가 분석해 보겠습니다.

우선, __pioinfo 전역 변수는 2중 포인터로 다음과 같이 선언돼 있습니다.

#define IOINFO_ARRAYS       128
#define IOINFO_L2E          6
#define IOINFO_ARRAY_ELTS   (1 << IOINFO_L2E) // == 64

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

extern __crt_lowio_handle_data_array __pioinfo;
typedef __crt_lowio_handle_data* __crt_lowio_handle_data_array[IOINFO_ARRAYS];

struct __crt_lowio_handle_data
{
    ...[생략]...
    intptr_t                   osfhnd;          // underlying OS file HANDLE
    ...[생략]...
};

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\lowio\ioinit.cpp

extern "C" __crt_lowio_handle_data* __pioinfo[IOINFO_ARRAYS] = { 0 };

대충 이런 방식으로 운영이 됩니다.

[__pioinfo]

[0] -> [0] -> __crt_lowio_handle_data
       [1] -> __crt_lowio_handle_data
       [2] -> __crt_lowio_handle_data
         ...
       [63] -> __crt_lowio_handle_data
[1] -> [0] -> __crt_lowio_handle_data
       [1] -> __crt_lowio_handle_data
       [2] -> __crt_lowio_handle_data
         ...
       [63] -> __crt_lowio_handle_data
[2] -> nullptr
[3] -> nullptr
[4] -> nullptr
      ...
[127] -> nullptr

IOINFO_ARRAYS(128) 개의 배열이 __pioinfo에 할당돼 있고, 필요할 때마다 각 배열 요소에 IOINFO_ARRAY_ELTS(64) 개의 요소를 담은 테이블 포인터가 할당됩니다. 따라서 총 128 * 64 = 8192 개의 file descriptor를 관리할 수 있는데요, "typedef __crt_lowio_handle_data* __crt_lowio_handle_data_array[IOINFO_ARRAYS];"라고 정의돼 있는 것에서 알 수 있듯이, 이 숫자는 현재 바꿀 수 없고 내부에 매크로로 정의돼 있으며,

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

#define _NHANDLE_ (IOINFO_ARRAYS * IOINFO_ARRAY_ELTS) // _NHANDLE_ == 8192

각종 CRT 함수에서 검증 코드로 사용되고 있습니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\lowio\osfinfo.cpp

extern "C" errno_t __cdecl __acrt_lowio_ensure_fh_exists(int const fh)
{
    _VALIDATE_RETURN_ERRCODE(static_cast<unsigned>(fh) < _NHANDLE_, EBADF);

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

실제로 간단하게 다음과 같은 코드로 테스트할 수 있습니다.

#include <io.h>
#include <stdio.h>
#include <fcntl.h>
#include <Windows.h>

int main()
{
    HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);

    int i;
    for (i = 0; i < 9000; i++)
    {
        int fdTest = _open_osfhandle((intptr_t)hStdIn, _O_TEXT);
        if (fdTest == -1) // 8189번 루프부터 fdTest == -1을 반환
        {
            break;
        }
    }

    printf("%d\n", i);
}

/* 출력 결과: 8189 */

_open_osfhandle은 설명했듯이 동일한 HANDLE이어도 매번 다른 file descriptor를 __pioinfo로부터 할당해 반환하기 때문에 최대 크기인 8192를 넘어가면 이후 EMFILE (Too many open files) 에러가 발생합니다. (위의 결과에서 8189가 나온 것은 3개의 표준 I/O/Error로 이미 0, 1, 2가 점유돼 있기 때문입니다.)

보다시피, __pioinfo는 메모리를 효율적으로 사용하기 위해 자체 배열은 128개뿐이지만, 개별 요소가 64개의 배열을 가리키는 포인터로 구성돼 있습니다.

따라서 이것을 단순히 평면적인 배열 인덱스로 접근할 수는 없으므로 도우미로 매크로 함수가 제공됩니다.

#define _pioinfo(i)          (__pioinfo[(i) >> IOINFO_L2E] + ((i) & (IOINFO_ARRAY_ELTS - 1)))

즉, 0번 file descriptor를 구하고 싶다면 _pioinfo(0)으로 접근하면 되는 식입니다. 그리고 그 file descriptor가 보유한 데이터인 __crt_lowio_handle_data 구조체의 개별 멤버는 직접 접근하기보다는 매크로를 통할 수 있도록 이렇게 정의돼 있습니다.

#define _osfhnd(i)           (_pioinfo(i)->osfhnd) // file descriptor에 해당하는 HANDLE 반환
#define _osfile(i)           (_pioinfo(i)->osfile)
#define _pipe_lookahead(i)   (_pioinfo(i)->_pipe_lookahead)
#define _textmode(i)         (_pioinfo(i)->textmode)
#define _tm_unicode(i)       (_pioinfo(i)->unicode)
#define _startpos(i)         (_pioinfo(i)->startpos)
#define _utf8translations(i) (_pioinfo(i)->utf8translations)
#define _dbcsBuffer(i)       (_pioinfo(i)->dbcsBuffer)
#define _dbcsBufferUsed(i)   (_pioinfo(i)->dbcsBufferUsed)

위의 매크로는 CRT 내부에서 사용되는 것이므로 알 필요가 없습니다. 게다가 public으로 노출할 만한 기능은 별도의 CRT 함수로 제공하고 있으니 그것을 사용하면 됩니다. 가령, _osfhnd(i) 매크로보다는 그것을 감싼 _get_osfhandle 함수를 사용하는 식입니다.

extern "C" intptr_t __cdecl _get_osfhandle(int const fh)
{
    _CHECK_FH_CLEAR_OSSERR_RETURN(fh, EBADF, -1 );
    _VALIDATE_CLEAR_OSSERR_RETURN(fh >= 0 && (unsigned)fh < (unsigned)_nhandle, EBADF, -1);
    _VALIDATE_CLEAR_OSSERR_RETURN(_osfile(fh) & FOPEN, EBADF, -1);

    return _osfhnd(fh);
}




지난 글을 통해 accumulate_inheritable_handles 함수에서 __pioinfo 전역 변수의 값을 직렬화하고 __acrt_initialize_lowio, initialize_inherited_file_handles_nolock에서 복원한다고 했습니다.

__acrt_initialize_lowio 함수를 다시 한번 살펴보면,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp

extern "C" bool __cdecl __acrt_initialize_lowio()
{
    __acrt_lock(__acrt_lowio_index_lock);
    bool result = false;
    __try
    {
        // First, allocate and initialize the initial array of lowio files:
        if (__acrt_lowio_ensure_fh_exists(0) != 0)
            __leave;

        // Next, process and initialize all inherited file handles:
        initialize_inherited_file_handles_nolock();

        // Finally, initialize the stdio handles, if they were not inherited:
        initialize_stdio_handles_nolock();
        result = true;
    }
    __finally
    {
        __acrt_unlock(__acrt_lowio_index_lock);
    }

    return result;
}

크게 3개의 함수, __acrt_lowio_ensure_fh_exists, initialize_inherited_file_handles_nolock, initialize_stdio_handles_nolock이 호출됩니다.

__acrt_lowio_ensure_fh_exists 함수에는 0을 전달해 호출하고 있는데요,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp
 
extern "C" int _nhandle = 0;

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\lowio\osfinfo.cpp

extern "C" errno_t __cdecl __acrt_lowio_ensure_fh_exists(int const fh)
{
    _VALIDATE_RETURN_ERRCODE(static_cast<unsigned>(fh) < _NHANDLE_, EBADF);

    errno_t status = 0;

    __acrt_lock(__acrt_lowio_index_lock);
    __try
    {
        for (size_t i = 0; fh >= _nhandle; ++i)
        {
            if (__pioinfo[i])
            {
                continue;
            }

            __pioinfo[i] = __acrt_lowio_create_handle_array();
            if (!__pioinfo[i])
            {
                status = ENOMEM;
                __leave;
            }

            _nhandle += IOINFO_ARRAY_ELTS;
        }
    }
    __finally
    {
        __acrt_unlock(__acrt_lowio_index_lock);
    }

    return status;
}

저렇게 처음에는 __pioinfo[0]에 64개의 __crt_lowio_handle_data 구조체 테이블을 가리키는 포인터를 보관하고, _nhandle 값을 64로 증가시킵니다. (만약 fh 값이 65였다면 __pioinfo[1]에 추가로 포인터가 보관될 것입니다.)

이후 initialize_inherited_file_handles_nolock은, 부모 프로세스가 lpReserved2에 __pioinfo를 넘겨줬다면 실행되겠지만 (근래의 대부분의 윈도우 콘솔 프로그램에서는 거의 없을 것이므로) 일단은 아무 작업 없이 넘어갈 것입니다.

마지막으로 initialize_stdio_handles_nolock은,

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

#define STDIO_HANDLES_COUNT       3

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp

static void initialize_stdio_handles_nolock() throw()
{
    for (int fh = 0; fh != STDIO_HANDLES_COUNT; ++fh)
    {
        __crt_lowio_handle_data* const pio = _pioinfo(fh);

        // If this handle was inherited from the parent process and initialized
        // already, make sure it has the FTEXT flag and continue:
        if (pio->osfhnd != reinterpret_cast<intptr_t>(INVALID_HANDLE_VALUE) &&
            pio->osfhnd != _NO_CONSOLE_FILENO)
        {
            pio->osfile |= FTEXT;
            continue;
        }

        // Regardless what happens next, the file will be treated as if it is
        // open in text mode:
        pio->osfile = FOPEN | FTEXT;

        // This handle has not yet been initialized, so  let's see if we can get
        // the handle from the OS:
        intptr_t const os_handle = reinterpret_cast<intptr_t>(GetStdHandle(get_std_handle_id(fh)));

        bool const is_valid_handle = 
            os_handle != reinterpret_cast<intptr_t>(INVALID_HANDLE_VALUE) &&
            os_handle != reinterpret_cast<intptr_t>(nullptr);

        DWORD const handle_type = is_valid_handle
            ? GetFileType(reinterpret_cast<HANDLE>(os_handle))
            : FILE_TYPE_UNKNOWN;


        if (handle_type != FILE_TYPE_UNKNOWN)
        {
            // The file type is known, so we obtained a valid handle from the
            // OS.  Finish initializing the lowio file object for this handle,
            // including the flag specifying whether this is a character device
            // or a pipe:
            pio->osfhnd = os_handle;

            if ((handle_type & 0xff) == FILE_TYPE_CHAR)
                pio->osfile |= FDEV;

            else if ((handle_type & 0xff) == FILE_TYPE_PIPE)
                pio->osfile |= FPIPE;
        }
        else
        {
            // We were unable to get the handles from the OS.  For stdin, stdout,
            // and stderr, if there is no valid OS handle, treat the CRT handle
            // as being open in text mode on a device with _NO_CONSOLE_FILENO
            // underlying it.  We use this value instead of INVALID_HANDLE_VALUE
            // to distinguish between a failure in opening a file and a program
            // run without a console:
            pio->osfile |= FDEV;
            pio->osfhnd = _NO_CONSOLE_FILENO;

            // Also update the corresponding stdio stream, unless stdio was
            // already terminated:
            if (__piob)
                __piob[fh]->_file = _NO_CONSOLE_FILENO;
        }
    }
}

GetStdHandle에 의해 3개(STDIO_HANDLES_COUNT)의 표준 핸들을 초기화합니다.

STD_INPUT_HANDLE ((DWORD)-10)
STD_OUTPUT_HANDLE ((DWORD)-11)
STD_ERROR_HANDLE ((DWORD)-12)

따라서, 저 코드가 "/SUBSYSTEM:CONSOLE" 프로그램에서 실행됐다면 __pioinfo[0], __pioinfo[1], __pioinfo[2]에 GetStdHandle로 반환한 핸들이 pio->osfhnd 필드에 저장되는 식으로 초기화됩니다.

재미있는 건, 콘솔 프로그램이 아닌 경우에도 __pioinfo[0..2]에 _NO_CONSOLE_FILENO로 osfhnd 필드를 초기화하고, 이와 함께 "pio->osfile = FOPEN | FTEXT;" 코드를 수행해 "FOPEN" 상태로 점유한다는 점입니다.

바로 이것이, _open_osfhandle을 호출했을 때 무조건 3번부터 file descriptor를 할당받는 이유입니다.

또한, 위의 코드가 ucrtbase.dll의 DllMain에서 실행된다고 했는데요, "/SUBSYSTEM:CONSOLE" 프로그램이라면 Windows Loader에 의해 EXE를 로드하면서 ucrtbase.dll을 함께 로드하는 시점에 저 코드가 실행돼 __pioinfo[0..2]에 표준 핸들이 초기화됩니다.

반면, (콘솔이 없는) "/SUBSYSTEM:WINDOWS" 프로그램이라면, (Windows Loader에 의한) initialize_stdio_handles_nolock 코드는 Console의 존재를 모르고 _NO_CONSOLE_FILENO 값으로 초기화를 수행합니다. 바로 그 이유로 인해, GUI 프로그램에서 AllocConsole을 호출했을 때 일반적인 CRT 함수들이 콘솔과 연동하지 않는 것입니다.

Windows / C++ - AllocConsole로 할당한 콘솔과 CRT 함수 연동
; https://www.sysnet.pe.kr/2/0/13729




지금까지 윈도우가 __pioinfo 전역 변수를 이용해 *NIX의 file descriptor를 HANDLE과 어떻게 연결했는지 살펴봤습니다. 생각보다 간단하죠? ^^

자, 그럼 이제 FILE* (파일 스트림)을 살펴보겠습니다. file descriptor가 *NIX의 운영체제와 밀접한 관련이 있는 반면, FILE*은 C 언어의 표준 라이브러리인 stdio.h에 정의돼 있습니다. 따라서, 윈도우 환경의 Visual C++ 역시 이에 대해 표준을 따르는 식으로 구현됐습니다.

지난 글에 FILE* 정의를 살펴봤듯이, 이것은 대응하는 file descriptor를 가지고 있으면서 buffer 역할을 구현하고 있는 구조체입니다. 뭐랄까, C#과 비교하자면 이런 관계라고 보시면 됩니다.

C C#
file descriptor FileStream
FILE* StreamReader/StreamWriter

FILE*을 위한 데이터는 __piob 전역 변수에 보관하는데요,

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

extern "C" extern int _nstream;

extern "C" extern __crt_stdio_stream_data** __piob;

file descriptor의 관리 변수와 이렇게 대응시켜 이해하시면 됩니다.

관리 변수 최댓값
file descriptor __pioinfo _nhandle
FILE* __piob _nstream

HANDLE과 대응하는 file descriptor와는 달리 FILE* 스트림은 상대적으로 중요도가 떨어지므로 부모/자식 프로세스에서 실행될 때 상속되지는 않습니다. 하지만, 표준 I/O/Error에 대해서는 처음부터 FILE* 스트림을 무조건 마련해 두기는 합니다. 역시나 좀 심심하니, 관련해서 어떻게 코드가 실행되는지 뜯어볼까요? ^^

이에 대한 초기화 코드는 ucrtbase.dll에서 호출하던 __acrt_execute_initializers의 목록 중, (당연히 __pioinfo를 초기화하는 __acrt_initialize_lowio 이후로) 마지막에 배치된 initialize_c에서 이뤄집니다.

initialize_c는 내부적으로 _initterm_e를 호출하고, 다시 그 내부에서 초기화의 한 단계로 __acrt_initialize_stdio가 호출되면서 비로소 __piob가 초기화됩니다.

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

/*
 * Default number of supported streams. _NFILE is confusing and obsolete, but
 * supported anyway for backwards compatibility.
 */
#define _NFILE      _NSTREAM_

#define _NSTREAM_   512

/*
 * Number of entries in _iob[] (declared below). Note that _NSTREAM_ must be
 * greater than or equal to _IOB_ENTRIES.
 */
#define _IOB_ENTRIES 3

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\stdio\_file.cpp

#ifdef CRTDLL
    extern "C" { int _nstream = _NSTREAM_; }
#else
    extern "C" { int _nstream; }
#endif

// Initializes the stdio library.  Returns 0 on success; -1 on failure.
extern "C" int __cdecl __acrt_initialize_stdio()
{
    #ifndef CRTDLL
    // If the user has not supplied a definition of _nstream, set it to _NSTREAM_.
    // If the user has supplied a value that is too small, set _nstream to the
    // minimum acceptable value (_IOB_ENTRIES):
    if (_nstream == 0)
    {
        _nstream = _NSTREAM_;
    }
    else if (_nstream < _IOB_ENTRIES)
    {
        _nstream = _IOB_ENTRIES;
    }
    #endif

    // Allocate the __piob array.  Try for _nstream entries first.  If this
    // fails, then reset _nstream to _IOB_ENTRIES an try again.  If it still
    // fails, bail out and cause CRT initialization to fail:
    __piob = _calloc_crt_t(__crt_stdio_stream_data*, _nstream).detach();
    if (!__piob)
    {
        _nstream = _IOB_ENTRIES;

        __piob = _calloc_crt_t(__crt_stdio_stream_data*, _nstream).detach();
        if (!__piob)
        {
            return -1;
        }
    }

    // Initialize the first _IOB_ENTRIES to point to the corresponding entries
    // in _iob[]:
    for (int i = 0; i != _IOB_ENTRIES; ++i)
    {
        __acrt_InitializeCriticalSectionEx(&_iob[i]._lock, _CORECRT_SPINCOUNT, 0);
        __piob[i] = &_iob[i];

        // For stdin, stdout, and stderr, we use _NO_CONSOLE_FILENO to allow
        // callers to distinguish between failure to open a file (-1) and a
        // program run without a console.
        intptr_t const os_handle = _osfhnd(i);
        bool const has_no_console =
            os_handle == reinterpret_cast<intptr_t>(INVALID_HANDLE_VALUE) ||
            os_handle == _NO_CONSOLE_FILENO ||
            os_handle == 0;

        if (has_no_console)
        {
            _iob[i]._file = _NO_CONSOLE_FILENO;
        }
    }

    return 0;
}

__pioinfo가 크기를 IOINFO_ARRAYS * IOINFO_ARRAY_ELTS로 다룬 것과는 달리, __piob는 단순히 __crt_stdio_stream_data* 포인터 데이터를 담는, 초기 크기가 512인 배열입니다. (대신, 이 크기는 조절할 수 있습니다.)

또한, _IOB_ENTRIES(3개) 수만큼 루프를 돌면서 __pioinfo에 표준 I/O/Error에 대응하는 FILE*을 초기화하는데요, 이마저도 미리 초기화해둔 _iob 테이블의 인스턴스를 할당하는 식으로 이뤄집니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\stdio\_file.cpp

extern "C" __crt_stdio_stream_data _iob[_IOB_ENTRIES] =
{
    // ptr      _base,   _cnt, _flag,                   _file, _charbuf, _bufsiz
    {  nullptr, nullptr, 0,    _IOALLOCATED | _IOREAD,  0,     0,        0}, // stdin
    {  nullptr, nullptr, 0,    _IOALLOCATED | _IOWRITE, 1,     0,        0}, // stdout
    {  nullptr, nullptr, 0,    _IOALLOCATED | _IOWRITE, 2,     0,        0}, // stderr
};

extern "C" FILE* __cdecl __acrt_iob_func(unsigned const id)
{
    return &_iob[id]._public_file;
}

_file 필드가 file descriptor를 의미하는데, 각각 0, 1, 2로 설정해 뒀으므로 표준 I/O/Error에 해당합니다.

이로 인해, 일반적인 파일은 FILE Stream을 별도로 구해야 하지만, 표준 I/O/Error는 이미 초기화돼 있으므로 저 값을 그냥 가져오는 것이 가능합니다. 실제로 CRT에는 표준 I/O/Error에 대한 FILE*을 반환하는 매크로가 정의돼 있습니다.

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

#define stdin  (__acrt_iob_func(0))
#define stdout (__acrt_iob_func(1))
#define stderr (__acrt_iob_func(2))

재미있는 건, (FILE* 스트림이 버퍼 역할을 수행한다는 의미도 있지만) CRT는 표준 I/O 함수를 간략화시키는 대상을 file descriptor가 아닌 FILE* 스트림을 대상으로 했다는 점입니다.

가령, 엄밀하게는 표준 출력을 원한다면 file descriptor를 지정해 다음과 같은 코드를 작성해야 합니다.

write(1, "Hello, World!\n", 14); // 1 == 표준 출력에 대한 file descriptor

하지만, 표준 I/O에 대해 FILE* 스트림을 대상으로 하는 CRT 함수를 정의해 두었기 때문에,

_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL printf(
    _In_z_ _Printf_format_string_ char const* const _Format,
    ...)
#if defined _NO_CRT_STDIO_INLINE
;
#else
{
    int _Result;
    va_list _ArgList;
    __crt_va_start(_ArgList, _Format);
    _Result = _vfprintf_l(stdout, _Format, NULL, _ArgList);
    __crt_va_end(_ArgList);
    return _Result;
}
#endif

간단하게 printf("Hello, World!\n"); 함수를 호출할 수 있었던 것입니다. 만약 저렇게 감싼 함수를 제공하지 않았다면, 실제로도 존재하는 fprintf 함수를 사용할 때 FILE* 인자를 명시적으로 전달해야만 했을 것입니다.

fprintf(stdout, "Hello, World!\n");




지금까지의 설명을 이해했다면, CRT에서 파일을 열 수 있는 제약이 왜 각각 따로인지 알 수 있습니다.

우선, file descriptor를 담고 있는 __pioinfo는 고정 크기로 8192라고 했으니 더 늘릴 수 없고, FILE* 스트림을 담고 있는 __piob는 초기 크기가 512로, 이후 원한다면 _setmaxstdio 함수를 사용해 늘릴 수 있습니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\stdio\setmaxf.cpp

extern "C" int __cdecl _setmaxstdio(int const new_maximum)
{
    // Make sure the request is reasonable:
    _VALIDATE_RETURN(new_maximum >= _IOB_ENTRIES && new_maximum <= _NHANDLE_, EINVAL, -1);

    return __acrt_lock_and_call(__acrt_stdio_index_lock, [&]
    {
        // If the new maximum is the same as our current maximum, no work to do:
        if (new_maximum == _nstream)
            return new_maximum;

        // If the new maximum is smaller than the current maximum, attempt to
        // free up any entries that are beyond the new maximum:
        if (new_maximum < _nstream)
        {
            __crt_stdio_stream_data** const first_to_remove = __piob + new_maximum;
            __crt_stdio_stream_data** const last_to_remove  = __piob + _nstream;
            for (__crt_stdio_stream_data** rit = last_to_remove; rit != first_to_remove; --rit)
            {
                __crt_stdio_stream_data* const entry = *(rit - 1);
                if (entry == nullptr)
                    continue;

                // If the entry is still in use, stop freeing entries and return
                // failure to the caller:
                if (__crt_stdio_stream(entry).is_in_use())
                    return -1;

                _free_crt(entry);
            }
        }

        // Enlarge or shrink the array, as required:
        __crt_stdio_stream_data** const new_piob = _recalloc_crt_t(__crt_stdio_stream_data*, __piob, new_maximum).detach();
        if (new_piob == nullptr)
            return -1;

        _nstream = new_maximum;
        __piob = new_piob;
        return new_maximum;
    });
}

기본 상태라면, FILE* 스트림의 기본값이 512로 더 작기 때문에 이것에서 제약이 먼저 걸릴 테지만, 실질적인 제약은 (상수로 고정된) __pioinfo의 8192개가 제한인 것입니다.

이와 관련해 _setmaxstdio 문서에 나온 설명을 볼까요?

The _setmaxstdio function changes the maximum value for the number of files that may be open simultaneously at the stream I/O level.

C run-time I/O now supports up to 8,192 files open simultaneously at the low I/O level. This level includes files opened and accessed using the _open, _read, and _write family of I/O functions. By default, up to 512 files can be open simultaneously at the stream I/O level. This level includes files opened and accessed using the fopen, fgetc, and fputc family of functions. The limit of 512 open files at the stream I/O level can be increased to a maximum of 8,192 by use of the _setmaxstdio function.

Because stream I/O-level functions, such as fopen, are built on top of the low I/O-level functions, the maximum of 8,192 is a hard upper limit for the number of simultaneously open files accessed through the C run-time library.


정리해 보면, CRT 함수는 크게 file descriptor를 다루는 저수준 함수와 FILE* 스트림을 다루는 고수준 함수로 나뉩니다.

저수준 함수: _open, _read, and _write family of I/O functions
스트림 함수: fopen, fgetc, and fputc family of functions

따라서, 어떤 함수로 파일을 열었냐에 따라 (기본값) 512 또는 8192로 제한이 달라지는 것입니다. 물론, 이것은 순수 CRT 라이브러리에 해당하고 Win32 API인 CreateFile/WriteFile/ReadFile로 직접 파일을 다룬다면 저 제약과는 무관하게 HANDLE의 제약을 받습니다.

Pushing the Limits of Windows: Handles
; https://learn.microsoft.com/en-us/archive/blogs/markrussinovich/pushing-the-limits-of-windows-handles

(검색해 보면, Youtube 영상으로 레지스트리의 "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Configuration Manager"에 "FileHandleLimit"을 설정할 수 있다고 하는데요, 관련해서 마이크로소프트의 공식 자료를 찾을 수는 없었습니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/19/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)
13789정성태10/27/20241439Linux: 97. menuconfig에 CONFIG_DEBUG_INFO_BTF, CONFIG_DEBUG_INFO_BTF_MODULES 옵션이 없는 경우
13788정성태10/26/20241485Linux: 96. eBPF (bpf2go) - fentry, fexit를 이용한 트레이스
13787정성태10/26/20241409개발 환경 구성: 730. github - Linux 커널 repo를 윈도우 환경에서 git clone하는 방법 [1]
13786정성태10/26/20241581Windows: 266. Windows - 대소문자 구분이 가능한 파일 시스템
13785정성태10/23/20241610C/C++: 182. 윈도우가 운영하는 2개의 Code Page파일 다운로드1
13784정성태10/23/20241639Linux: 95. eBPF - kprobe를 이용한 트레이스
13783정성태10/23/20241493Linux: 94. eBPF - vmlinux.h 헤더 포함하는 방법 (bpf2go에서 사용)
13782정성태10/23/20241431Linux: 93. Ubuntu 22.04 - 커널 이미지로부터 커널 함수 역어셈블
13781정성태10/22/20241403오류 유형: 930. WSL + eBPF: modprobe: FATAL: Module kheaders not found in directory
13780정성태10/22/20241511Linux: 92. WSL 2 - 커널 이미지로부터 커널 함수 역어셈블
13779정성태10/22/20241457개발 환경 구성: 729. WSL 2 - Mariner VM 커널 이미지 업데이트 방법
13778정성태10/21/20241675C/C++: 181. C/C++ - 소스코드 파일의 인코딩, 바이너리 모듈 상태의 인코딩
13777정성태10/20/20241577Windows: 265. Win32 API의 W(유니코드) 버전은 UCS-2일까요? UTF-16 인코딩일까요?
13776정성태10/19/20241588C/C++: 180. C++ - 고수준 FILE I/O 함수에서의 Unicode stream 모드(_O_WTEXT, _O_U16TEXT, _O_U8TEXT)파일 다운로드1
13775정성태10/19/20241508개발 환경 구성: 728. 윈도우 환경의 개발자를 위한 UTF-8 환경 설정
13774정성태10/18/20241446Linux: 91. Container 환경에서 출력하는 eBPF bpf_get_current_pid_tgid의 pid가 존재하지 않는 이유
13773정성태10/18/20241796Linux: 90. pid 네임스페이스 구성으로 본 WSL 2 + docker-desktop
13772정성태10/17/20241666Linux: 89. pid 네임스페이스 구성으로 본 WSL 2 배포본의 계층 관계
13771정성태10/17/20241670Linux: 88. WSL 2 리눅스 배포본 내에서의 pid 네임스페이스 구성
13770정성태10/17/20241463Linux: 87. ps + grep 조합에서 grep 명령어를 사용한 프로세스를 출력에서 제거하는 방법
13769정성태10/15/20241975Linux: 86. Golang + bpf2go를 사용한 eBPF 기본 예제파일 다운로드1
13768정성태10/15/20241630C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드파일 다운로드2
13767정성태10/14/20241600오류 유형: 929. bpftrace 수행 시 "ERROR: Could not resolve symbol: /proc/self/exe:BEGIN_trigger"
13766정성태10/14/20241697C/C++: 178. C++ - 파일에 대한 Text 모드의 "translated" 동작파일 다운로드1
13765정성태10/12/20241475오류 유형: 928. go build 시 "package maps is not in GOROOT" 오류
13764정성태10/11/20241389Linux: 85. Ubuntu - 원하는 golang 버전 설치
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...