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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1755정성태9/22/201434268오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424590VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420601오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441060Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438924.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423827.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423730.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425379개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428400오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426088.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423032개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431042.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420983오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426954개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421330.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432499.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426499.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422041.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419750VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425566VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418155.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419808오류 유형: 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/201426337.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434460Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427063개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
1730정성태8/11/201422154개발 환경 구성: 234. Royal TS의 터미널(Terminal) 연결에서 한글이 깨지는 현상 해결 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...