open(), read(), write(), close()과 같은 시스템 콜 함수와 fopen(), fread(), fwrite(), fclose() 같은 c언어 표준 함수와 정확히 어떤 차이가 있는지 잘 몰라서 이에 대해 공부 해보기로 하였다. 특히, 이번 포스트에서는 fopen()과 open()에 대해 다루어 보기로 한다.
open()과 fopen()의 차이는 KLDP 포럼 https://kldp.org/node/1350 에서 그 구체적인 차이와 용도에 대해 시니어 개발자들이 깊게 토론 한 적이 있을 정도로, 겉보기에는 쉬울 것 같지만 깊게 들어가면 심오한 주제라고 생각한다. "Robert Love, 리눅스 커널구조의 심층분석" 을 참고하자면 아래와 같은 구조로 되어있다고 짐작할 수 있다.
| fopen() 호출 | fopen() 함수 래퍼 C 라이브러리의 함수 래퍼 |
open() 시스템 콜 |
sys_open() |
| 사용자 공간 | 커널공간 | ||
<그림 1 > "Robert Love, 리눅스 커널구조의 심층분석"에 따른 open() 함수 작동방식
이에 대해 자세히 살펴보기 위해 우리가 C/C++에서 자주 쓰는 fopen()의 문법과 용도에 대해서 살펴 보았다.
FILE* fopen(const char* filename, const char* mode) // 성공 시 해당 파일의 파일 포인터, 실패시 NULL 반환
filename : c 문자열로 된 파일 이름.
mode : "r" (읽기 전용), "w" (쓰기 전용), "a"(덧붙이기) 등이 있다.

컴파일이 된다. 어딘가에 fopen() 함수가 선언 및 정의되어 있다고 생각할 수 있다.
어셈블리 코드를 살펴보면, fopen함수를 stdio.h 에서 import 하여 사용하고 있다.


그렇다면 이 fopen() 함수는 어떻게 선언 및 정의 되어 있을까?
우선 fopen()는 Windows10 MSVC 2019 기준으로 <stdio.h>에 함수가 정의 되어있다.

그렇다면 구현코드는 어떻게 씌여져 있을까? API마다 조금씩 다르겠지만, Dennis Ritchie & Brian Kernighan의 저서 "The C Programming Language" 에서 설명하는 fopen() 예제에서 크게 벗어나지 않을 것이라고 생각한다. 그들이 책에 소개한 예제 소스 코드를 살펴보자.
FILE *fopen(char *name, char *mode)
{
int fd;
FILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a')
return NULL;
/* Bit operation */
for (fp = _iob; fp < _iob + OPEN_MAX; fp++)
if ( (fp->flag & (_READ | _WRITE)) == 0)
break; /* found free slot */
if (fp >= _iob + OPEN_MAX) /* no free slots */
return NULL;
if (*mode == 'w')
fd = creat(name, PERMS);
else if (*mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1) /* couldn't access name */
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
fp->flag = (*mode == 'r') ? _READ : _WRITE;
return fp;
}
<그림 5> fopen()의 정의
구현 과정에서 open(), creat(), lseek()와 같은 system call 함수를 사용하고 있음을 알 수 있다.
그렇다면, 이런 시스템 콜은 또 어디에 선언 및 정의되어 있을까? 윈도우 커널소스는 공개되어 있지 않으므로 linux 커널 소스를 통해 궁금증을 해결해 보기로 한다.
static __attribute__((unused))
int open(const char *path, int flags, mode_t mode)
{
int ret = sys_open(path, flags, mode);
if (ret < 0) {
SET_ERRNO(-ret);
ret = -1;
}
return ret;
}
<그림 6> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 open() 함수의 구현
<그림 6>에서 보듯, open은 sys_open()이라는 시스템 콜을 수행하고 있었다. 그렇다면 sys_open()은 어떻게 작동하는가? sys_open()의 구현부분을 보자.
static __attribute__((unused))
int sys_open(const char *path, int flags, mode_t mode)
{
#ifdef __NR_openat
return my_syscall4(__NR_openat, AT_FDCWD, path, flags, mode);
#else
return my_syscall3(__NR_open, path, flags, mode);
#endif
}
<그림 7> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 sys_open() 함수의 구현
<그림 7>에서 보듯, 조건에 따라 my_syscall3, 혹은 my_syscall4를 호출하고 있다. 그들이 어떻게 작동하는지 보자.
#define my_syscall3(num, arg1, arg2, arg3) \
({ \
register long _num asm("r7") = (num); \
register long _arg1 asm("r0") = (long)(arg1); \
register long _arg2 asm("r1") = (long)(arg2); \
register long _arg3 asm("r2") = (long)(arg3); \
\
asm volatile ( \
"svc #0\n" \
: "=r"(_arg1) \
: "r"(_arg1), "r"(_arg2), "r"(_arg3), \
"r"(_num) \
: "memory", "cc", "lr" \
); \
_arg1; \
})
#define my_syscall4(num, arg1, arg2, arg3, arg4) \
({ \
register long _num asm("r7") = (num); \
register long _arg1 asm("r0") = (long)(arg1); \
register long _arg2 asm("r1") = (long)(arg2); \
register long _arg3 asm("r2") = (long)(arg3); \
register long _arg4 asm("r3") = (long)(arg4); \
\
asm volatile ( \
"svc #0\n" \
: "=r"(_arg1) \
: "r"(_arg1), "r"(_arg2), "r"(_arg3), "r"(_arg4), \
"r"(_num) \
: "memory", "cc", "lr" \
); \
_arg1; \
})
<그림 8> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 my_syscall3, my_syscall4
확인 결과, my_syscall3, my_syscall4는 인라인 어셈블리로 쓰여져 있다.
쓰다보니 글이 너무 길어졌다. 인라인 어셈블리 부터는 다음 포스트에 연재하기로 한다.
Robert Love, Linux Kernel Development
Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition
Dennis Ritchie & Brian Kernighan <The C Programming Language>
'컴퓨터 시스템 > IO' 카테고리의 다른 글
| Unix 시스템 입출력 1 - 개요 (0) | 2021.01.20 |
|---|