오늘은 FSOP(File Stream Oriented Programming)에 대해서 공부하면서 모든 데이터는 파일이라는 사실을 처음 알게 되면서 새롭게 느낀 감정과 내용들을 설명하려고 한다.
우선 여기서 말하는 stream은 일종의 다리라고 생각하면 이해하기 편하다. hdd에 저장되어있는 파일을 가져오기 위해서는 다리가 필요하고 그걸 logical하게 만든 stream이라는 다리로 그 파일의 데이터를 가져오는 것이다. 그렇다면 이 과정에서는 어떤 일들이 일어날까라는 궁금증이 생기기 마련이다. 그래서 fopen, fread, fwrite, fclose와 같이 파일을 열고 일고 쓰고 닫고 관련 함수에 대해서 이해보고 어떤 함수들이 필요하고 어떤 struct를 사용하는지 알아보려고 한다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include "local.h"
#include <linux/stat.h>
FILE *
fopen(const char *file, const char *mode)
{
FILE *fp;
int f;
int flags, oflags;
if ((flags = __sflags(mode, &oflags)) == 0)
return (NULL);
if ((fp = __sfp()) == NULL)
return (NULL);
if ((f = open(file, oflags, DEFFILEMODE)) < 0) {
fp->_flags = 0; /* release */
return (NULL);
}
fp->_file = f;
fp->_flags = flags;
fp->_cookie = fp;
fp->_read = __sread;
fp->_write = __swrite;
fp->_seek = __sseek;
fp->_close = __sclose;
/*
* When opening in append mode, even though we use O_APPEND,
* we need to seek to the end so that ftell() gets the right
* answer. If the user then alters the seek pointer, or
* the file extends, this will fail, but there is not much
* we can do about this. (We could set __SAPP and check in
* fseek and ftell.)
*/
if (oflags & O_APPEND)
(void) __sseek((void *)fp, (fpos_t)0, SEEK_END);
return (fp);
}
이 코드를 보면 flags 변수가 존재하는데 여기서는 FILE struct에 있는 flag를 의미한다. 그러니 _IO_FILE struct에 대해서 알지 못한다면 이 코드를 이해하지 못한다. 그래서 아래에는 _IO_FILE에 대해서 먼저 보려고 한다.
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable; /* 파일 관련 작업을 수행하는 가상 함수 테이블입니다. */
};
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* 파일 읽기 버퍼에 대한 포인터입니다. */
char *_IO_read_end; /* 파일 읽기 버퍼 주소의 끝을 가리키는 포인터입니다. */
char *_IO_read_base; /* 파일 읽기 버퍼 주소의 시작을 가리키는 포인터입니다. */
char *_IO_write_base; /* 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터입니다. */
char *_IO_write_ptr; /* 쓰기 버퍼에 대한 포인터입니다. */
char *_IO_write_end; /* 파일 쓰기 버퍼 주소의 끝을 가리키는 포인터입니다. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain; /* 프로세스의 _IO_FILE 구조체는 _chain 필드를 통해 링크드 리스트를 만듭니다. 링크드 리스트의 헤더는 라이브러리의 전역 변수인 _IO_list_all 에 저장됩니다. */
int _fileno; /* 파일 디스크립터의 값입니다. */
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
이렇게 FILE *fp를 하면 저 struct를 통해서 값을 설정할 수 있다 예를 들면 fp->flag = x 이런 느낌으로 할 수 있다. 그렇다면 구조체에 있는 변수들은 어떤 의미를 담고 있는지 알아보려고 한다.
flags는 파일에서 어떤걸 허용하는지에 대해서 알려주는 변수이다.
/* Magic number and bits for the _flags field. The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
위와 같은 options들이 존재를 한다. 이를 통해서 스트림의 동작을 제어하는데 사용하는 변수가 바로 flags인 것이다.
_IO_MAGIC - IO 파일인지 check하는 option
_IO_USER_BUF - 파일 스트림이 사용자가 제공한 버퍼를 사용하고 있을 때인지 확인하는 option
_IO_NO_READS - 읽기 작업이 허용되지 않는 option
_IO_NO_WRITES - 쓰기 작업이 허용되지 않는 option
_IO_EOF_SEEN - 파일의 끝에 도달했다는 것을 알리는 option
_IO_ERR_SEEN - 파일 I/O 작업 중에 오류가 발생했다는 것을 알리는 option
_IO_DELETE_DONT_CLOSE - 파일을 닫을 때 _fileno에 대한 close 함수를 호출하지 않도록 지정하는 option
_IO_LINKED - 현재 열려 있는 파일 목록에 포함되어 있다는 것을 알려주는 option
_IO_IN_BACKUP - 백업 작업 중에 있는지 알려주는 option
_IO_LINE_BUF - 개행문자를 check하여 작업하고 개행문자가 있으면 입력혹은 출력하라는 option
_IO_TIED_PUT_GET - put과 get 포인터가 같은지 확인하는 option
_IO_CURRENTLY_PUTTING - 현재 파일에 데이터를 쓰는 중임을 나타내는 option
위 option들을 보면 버퍼를 처리할 때 조건들이 많다. 그런 조건들을 stdio.h에서 setvbuf에서 어떻게 buffer를 처리 할 것인지에 대해서 설정을 한다. 버퍼링을 제어하는 함수라고 설명이 되어있는데 여기서 말하는 버퍼링은 버퍼에 데이터를 저장하고 처리하는 그런 과정을 이야기한다.
위와 같은 option들을 사용하여 flag의 조건들을 세팅하여 stream들을 제어하는 것이다.
이제 다시 fopen함수를 보면 fp struct에 값들을 다 입력을 한 후 fp를 return시키는 함수인 것을 알 수 있다. 파일을 열 때에는 system call을 이용해서 파일을 연다. 그리고 이렇게 파일을 fopen으로 열 때 heap 영역에 _IO_FILE 구조체가 저장이 된다.
fopen 함수 내부에서 실행되는 함수인 _IO_new_file_fopen이다. 이 함수를 통해서 fopen에서 인자로 받은 mode를 check하고 권한을 부여한다.
_IO_new_file_fopen (FILE *fp, const char *filename, const char *mode,
int is32not64)
{
int oflags = 0, omode;
int read_write;
int oprot = 0666;
int i;
FILE *result;
const char *cs;
const char *last_recognized;
if (_IO_file_is_open (fp))
return 0;
switch (*mode)
{
case 'r':
omode = O_RDONLY;
read_write = _IO_NO_WRITES;
break;
case 'w':
omode = O_WRONLY;
oflags = O_CREAT|O_TRUNC;
read_write = _IO_NO_READS;
break;
case 'a':
omode = O_WRONLY;
oflags = O_CREAT|O_APPEND;
read_write = _IO_NO_READS|_IO_IS_APPENDING;
break;
default:
__set_errno (EINVAL);
return NULL;
}
last_recognized = mode;
for (i = 1; i < 7; ++i)
{
switch (*++mode)
{
case '\0':
break;
case '+':
omode = O_RDWR;
read_write &= _IO_IS_APPENDING;
last_recognized = mode;
continue;
case 'x':
oflags |= O_EXCL;
last_recognized = mode;
continue;
case 'b':
last_recognized = mode;
continue;
case 'm':
fp->_flags2 |= _IO_FLAGS2_MMAP;
continue;
case 'c':
fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
continue;
case 'e':
oflags |= O_CLOEXEC;
fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
continue;
default:
/* Ignore. */
continue;
}
break;
}
result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
is32not64);
if (result != NULL)
{
/* Test whether the mode string specifies the conversion. */
cs = strstr (last_recognized + 1, ",ccs=");
if (cs != NULL)
{
/* Yep. Load the appropriate conversions and set the orientation
to wide. */
struct gconv_fcts fcts;
struct _IO_codecvt *cc;
char *endp = __strchrnul (cs + 5, ',');
char *ccs = malloc (endp - (cs + 5) + 3);
if (ccs == NULL)
{
int malloc_err = errno; /* Whatever malloc failed with. */
(void) _IO_file_close_it (fp);
__set_errno (malloc_err);
return NULL;
}
*((char *) __mempcpy (ccs, cs + 5, endp - (cs + 5))) = '\0';
strip (ccs, ccs);
if (__wcsmbs_named_conv (&fcts, ccs[2] == '\0'
? upstr (ccs, cs + 5) : ccs) != 0)
{
/* Something went wrong, we cannot load the conversion modules.
This means we cannot proceed since the user explicitly asked
for these. */
(void) _IO_file_close_it (fp);
free (ccs);
__set_errno (EINVAL);
return NULL;
}
free (ccs);
assert (fcts.towc_nsteps == 1);
assert (fcts.tomb_nsteps == 1);
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_base;
/* Clear the state. We start all over again. */
memset (&fp->_wide_data->_IO_state, '\0', sizeof (__mbstate_t));
memset (&fp->_wide_data->_IO_last_state, '\0', sizeof (__mbstate_t));
cc = fp->_codecvt = &fp->_wide_data->_codecvt;
cc->__cd_in.step = fcts.towc;
cc->__cd_in.step_data.__invocation_counter = 0;
cc->__cd_in.step_data.__internal_use = 1;
cc->__cd_in.step_data.__flags = __GCONV_IS_LAST;
cc->__cd_in.step_data.__statep = &result->_wide_data->_IO_state;
cc->__cd_out.step = fcts.tomb;
cc->__cd_out.step_data.__invocation_counter = 0;
cc->__cd_out.step_data.__internal_use = 1;
cc->__cd_out.step_data.__flags = __GCONV_IS_LAST | __GCONV_TRANSLIT;
cc->__cd_out.step_data.__statep = &result->_wide_data->_IO_state;
/* From now on use the wide character callback functions. */
_IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;
/* Set the mode now. */
result->_mode = 1;
}
}
return result;
}
omode를 통해 해당하는 비트를 할당해준다.