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* filenameconst char* mode) // 성공 시 해당 파일의 파일 포인터, 실패시 NULL 반환

filename : c 문자열로 된 파일 이름. 

mode : "r" (읽기 전용), "w" (쓰기 전용), "a"(덧붙이기) 등이 있다. 

 

<그림 2> fopen의 사용 예시 fopen_study.c

컴파일이 된다. 어딘가에 fopen() 함수가 선언 및 정의되어 있다고 생각할 수 있다. 

어셈블리 코드를 살펴보면,  fopen함수를 stdio.h 에서 import 하여 사용하고 있다.

 

<그림 3> fopen_study.asm

 

그렇다면 이 fopen() 함수는 어떻게 선언 및 정의 되어 있을까?

우선 fopen()는 Windows10 MSVC 2019 기준으로 <stdio.h>에 함수가 정의 되어있다. 

 

<그림 4> fopen()의 선언 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

시스템 입출력 : 메모리와 외부장치 (디스크 드라이브, 터미널, 네트워크) 간에 데이터를 복사하는 과정.  

 

리눅스에서 모든것은 파일 ("Everything is a file.")로 간주되고, 디바이스들 또한 파일로 모델링 된다.

 

파일 

- 파일은 연속된 바이트들의 집합이다. 사용자가 디바이스에 접근하고자 하면, 커널은 File Descriptor 값을 반환한다.

- 디바이스, 소켓 등도 파일이다. 

- 운영체제는 다루는 파일이 이진파일인지 텍스트 파일인지 알지 못한다. 

 

디렉토리

 

- 디렉토리는 링크들의 집합이다.

- 점 한개 = (자신의 디렉토리), 점 두개 (부모의 디렉토리)

 

유닉스 운영체제 에서 File Descriptor는 files_struct 구조체의 *fd_array[] 배열의 인덱스 번호이며 inode를 가르키고 있다.  open() 함수를 이용하여 값을 받으며, 실패시 -1를 받는다. 표준입출력과 표준에러를 위해 고정적으로 쓰이는 File Descriptor 값은 아래와 같다. 

 

식별자 의미 Descriptor 값
STDIN_FILENO 표준입력 0
STDOUT_FILENO 표준출력 1
STDERR_FILENO 표준에러 2

 

유닉스 시스템 수준의 open(), read(), write(), close() 등을 사용하여 파일 입출력을 할 수 있다.

read()는 open() 으로 받은 File Descriptor 인자 값을 이용하여 파일 읽기 작업을 수행한다.

write()도 마찬가지로 File Descriptor 값을 사용한다.  close() 할때 인자 값으로 File Descriptor 값을 넣어주어야 한다. 

 

이런 시스템 콜을 사용하여 fopen(), fclose() 등과 같은 C 표준 라이브러리 함수를 구현한 것이다.    

이에 대해서는 다음 포스트에서 서술해 보기로 한다. 

 

참고

 

임성락,  리눅스 시스템 프로그래밍 

Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition

'컴퓨터 시스템 > IO' 카테고리의 다른 글

Unix 시스템 입출력 2 - open() 의 작동원리  (0) 2021.01.22

+ Recent posts