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#과 비교하자면 이런 관계라고 보시면 됩니다.
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의 관리 변수와 이렇게 대응시켜 이해하시면 됩니다.
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"을 설정할 수 있다고 하는데요, 관련해서 마이크로소프트의 공식 자료를 찾을 수는 없었습니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]