모든 언어의 런타임 시스템은 입출력을 수행하기 위한 고급 기능들을 제공한다. C는 버퍼를 통해 I/O를 수행하는 printf, scanf 같은 함수들을 표준 I/O 라이브러리에서 제공한다. 리눅스에서는 이러한 고급 I/O 함수들은 커널이 제공하는 시스템 수준 Unix I/O 함수들을 사용해서 구현되어 있다. 결국 이러한 I/O도 파일을 통해 구현이 되어 있다.
리눅스에서 파일은 연속된 m개의 바이트이다.
네트워크, 디스크, 터미널 같은 모든 I/O 디바이스들은 파일로 모델링되며, 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수행된다.
파일 열기
응용은 I/O 디바이스에 접근하겠다는 의도를 해당 파일을 열겠다고 커널에 요청하는 방법으로 알린다.
커널은 식별자라고 식별자라고 하는 작은 비음수를 리턴하여, 이후의 파일에 관한 모든 연산에서 파일을 나타낸다. 이후에 열린 파일에 관한 모든 정보를 추적한다.
현재 파일 위치 변경
커널은 파일을 열 때 마다 파일 위치 k를 관리하며, 이것은 처음에는 0이다. 파일 위치는 파일의 시작 부분부터 바이트 오프셋이다.
파일 읽기 / 쓰기
읽기 연산은 현재 파일 위치 k에서 시작해서 n > 0 바이트를 파일에서 메모리로 복사하고, k를 n 증가한다. m바이트 파일에서 k가 m 바이트를 오버해서 읽으면 end-of-file EOF가 발생한다.
파일 닫기
응용이 파일 접근을 마치면, 커널에 파일을 닫아줄 것을 요청한다.프로세스가 종료시, 커널이 모든 열려있는 파일을 닫고, 메모리 자원을 반환한다.
각각의 리눅스 파일은 시스템에서의 역할을 나타내는 타입을 가진다.
다른 파일 유형들에는 이름 있는 파이프, 심볼형 링크, 문자 및 블록 장치 등이 있지만, 다루지 않는다.
디렉토리 계층 구조에서의 위치는 경로 이름으로 명시한다. 경로 이름은 사선으로 구분된 일련의 파일 이름들이 사선 다음에 따라 올 수 있는 스트링이다. 경로 이름은 두가지 형태가 있다.
절대 경로
사선으로 시작하며 루트로부터 경로를 나타낸다. (/home/droh/hello.c)
상대 경로
파일이름으로 시작하며, 현재 작업 디렉토리로부터의 경로를 표시한다. (/home/bryant 기준 ../home/droh/hello.c)
프로세스는 다음과 같은 open 함수를 호출해서 기존의 파일을 열거나 새 파일을 생성한다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
open 함수는 filwname을 파일 식별자로 변환하고 식별자 번호를 리턴한다. 리턴된 식별자는 항상 프로세스 내에서 현재 열려 있지 않은 가장 작은 식별자이다. flags 인자는 어떻게 프로세스가 파일에 접근하는지를 나타낸다.
flags 인자
다음 과 같은 코드는 기존 파일을 읽기 위해 어떻게 오픈하는지 보여준다.
fd = Open("foo.txt", O_RDONLY, 0);
flags 인자는 쓰기 작업을 위한 추가적인 명령을 제공하도록 한 개 이상의 비트 마스크들을 OR 형태 작성 가능.
위 명령을 사용해 일부 데이터를 추가하려고 기존의 파일을 오픈하는 코드는 다음과 같다.
fd = Open("foo.txt", O_WRONLY|O_APPEND, 0);
mode인자는 새 파일들의 접근 권한 비트들을 명시한다. 비트들의 심볼 이름은 다음과 같다.

자신의 컨텍스트의 일부로, 각 프로세스는 umask를 가지고, 이건 umask함수를 호출해서 설정한다. 프로세스가 어떤 mode인자를 사용해서 open함수를 호출하고, 이를 통해서 새 파일을 생성하면, 파일의 접근 권한 비트들은 mode & ~umask로 설정된다.
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
다음과 같이 값들이 주어지면, 파일의 소유자가 읽기와 쓰기 권한을 가지고 다른 사람들은 읽기 허가를 가지는 새 파일을 만든다.
umask(DEF_UMASK);
fd = Open("foo.txt", O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);
마지막으로, 프로세스는 오픈한 파일을 close 함수를 호출해서 닫는다.
#include <unistd.h>
int close(int fd);
이미 닫은 파일 식별자 닫으면 에러다.
응욘은 read와 write 함수를 호출해서 읽기와 쓰기를 수행한다.
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
read함수
식별자 fd의 현재 파일 위치에서 최대 n 바이트를 메모리 위치 buf로 복사한다. 리턴 값 -1은 에러, 0은 EOF를 나타낸다. 그렇지 않으면, 리턴 값은 실제로 전송한 바이트 수를 나타낸다.
write함수
메모리 위치 buf에서 식별자 fd의 현재 파일 위치로 최대 n 바이트를 복사한다.
++ 응용은 lseek 함수를 사용해서 현재 파일 위치를 명시적으로 수정할 수 있다.
크게 3가지의 유형이 있다.
EOF를 읽기 중에 만났을 때
20 바이트만 가지고 있는 파일의 현재 파일위치에서 읽을 준비가 되었으며, 이 파일에서 50바이트를 한 번에 읽으려 한다고 하면, 다음 번의 read는 짧은 카운트 20을 리턴하고, 이후 read는 짧은 카운트 0을 리턴해서 EOF를 알려줄 것이다.
터미널에서 텍스트 줄을 읽을 때
만일 열린 파일이 터미널과 연결되어 있으면, 각 read 함수는 한 번에 한 개의 텍스트 줄을 전송할 것이다. 텍스트줄의 길이와 동일한 짧은 카운트를 리턴한다.
네트워크 소켓을 읽거나 쓸 때
만일 열린 파일이 네트워크 소켓에 대응된다면, 내부 버퍼링 제약과 긴 네트워크 지연시간 때문에 read와 write는 짧은 카운트를 리턴할 수 있다.
RIO 패키지는 CSAPP에서 소개하는 패키지이다. 공식적인 리눅스에서 지원하는 패키지가 아님을 유념하자.
해당 패키지는 다음과 같은 함수를 제공한다.
버퍼 없는 입력 및 출력 함수
이 함수들은 메모리와 파일 사이에 응용 수준의 버퍼링 없이 직접 데이터를 전송한다. 이들은 특히 네트워크에서 바이너리 데이터를 읽고 쓸 때 유용하다.
버퍼를 사용하는 입력 함수
텍스트 라인들과 내용이 응용 수준 버퍼에 캐시되어 있는 파일의 바이너리 데이터를 효율적으로 읽도록 해주며, 이것은 printf와 같이 표준 I/O 함수를 위해 제공되는 것과 유사하다.
10.5 RIO 패키지는 앞에 서술한 것처럼 CSAPP가 만든 것으로 스킵하겠다.
응용은 파일에 관한 정보를 stat와 fstat 함수를 호출해서 가져올 수 있다.
#include <unistd.h>
#include <sys/stat.h>
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
stat 함수는 파일 이름을 입력으로 받아서 stat 구조체의 멤버들을 채워준다. fstat 함수도 비슷하게 파일 이름 대신 파일 식별자를 사용한다.
응용 프로그램은 readdir 계열의 함수를 이용해서 디렉토리의 내용을 읽을 수 있다.
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
opendir함수는 경로 이름을 받아서 디렉토리 스크림을 가리키는 포인터를 리턴한다. 스트림은 항목들의 정렬된 리스트에 대한 추상화, 이 경우에는 디렉토리 항목들의 리스트가 된다.
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
readdir를 호출하면 dirp 스트림에서 다음 디렉토리 항목으로의 포인터를 리턴하거나 만일 더 이상의 항목이 없는 경우에는 NULL을 리턴한다. 각 디렉토리 항목은 다음과 같다.
struct dirent {
ino_t d_ino; /* inode number */
char d_name[256]; /* Filename */
};
일부 리눅스 버전에서는 다른 구조체 멤버를 포함할 수 있지만, 이들은 모든 시스템에서 표준인 단 두 개의 멤버를 가진다.
에러 발생시 readdir은 NULL을 리턴하고 errno를 설정한다.
#include <dirent.h>
int closedir(DIR *dirp);
closedir 함수는 스트림을 닫고 자신의 자원들을 반환한다.
리눅스 파일은 여러 가지 방법으로 공유 가능하다. 커널은 세 개의 관련 자료구조를 사용해서 오픈한 파일들을 표현한다.
식별자 테이블
각 프로세스는 자신만의 별도의 식별자 테이블을 가지고 있고, 이들의 엔트리는 프로세스의 오픈된 파일 식별자로 인덱스 된다. 각 오픈 식별자 엔트리는 파일 테이블 내의 한 개의 엔트리를 가리킨다.
파일 테이블
오픈 파일들은 모든 프로세스들이 공유하는 모든 프로세스들이 공유하는 한 개의 파일 테이블로 표시된다. 각 파일 테이블 엔트리는 현재 파일 위치, 현재 가리키는 식별자 엔트리들의 참조 횟수, v-노드 테이블의 엔트리로의 포인터로 구성된다.
| 구조 | 역할 | 누가 소유? |
|---|---|---|
| 파일 디스크립터 테이블 | 프로세스가 열린 파일을 가리키는 포인터 목록 | 각 프로세스마다 따로 존재 |
| 오픈 파일 테이블 | 실제 파일의 상태, 위치, 접근 모드 등 관리 | 커널 내부에서 공유 |
v-노드 테이블
파일 테이블처럼 v-노드 테이블은 모든 프로세스들이 공유한다. 각 엔틀히는 st_mode, st_size 멤버를 포함해서 stat 구조 내의 대부분의 정보를 가지고 있다.
파일 디스크립터(File Descriptor, FD)
숫자로 된 핸들(파일 번호)입니다. (예: 0 = stdin, 1 = stdout, 2 = stderr)
open() 함수로 파일을 열면 FD가 리턴됨
1, 4를 가지고 있음.요점: 파일마다 고유한 파일 디스크립터와 오픈 파일 테이블이 따로 있음.

open("a.txt", ...)을 두 번 호출하면 FD가 2개 생김 (예: fd1, fd2)요점: 같은 파일을 여러 번 열면 각각 독립적으로 다룰 수 있음!

fork() 호출 후fork()를 호출하면 자식 프로세스가 생성됨
→ 중요 결과
| 상황 | 디스크립터 | 오픈 파일 테이블 | 파일 위치 공유 여부 |
|---|---|---|---|
| 파일을 두 번 open | 서로 다름 | 서로 다름 | ❌ 독립적 위치 |
dup() 사용 | 서로 다름 | 같음 | ✅ 위치 공유 |
fork() 후 자식 | 같음 | 같음 | ✅ 위치 공유 |
비유로 설명하기
open() 두 번 → 책 2권 따로 빌림 (독립적)fork() → 독서대를 통째로 복사 (책은 같이 공유, 페이지도 같이 넘김)리눅스 쉘 표준 입력 및 출력을 디스크 파일과 연결할 수 있도록 해주는 I/O 재지정 연산자를 제공한다.
ls > foo.txt
해당 명령어는 다음과 같은 의미를 가진다. ls 프로그램의 표준 출력(stdout) 이 화면(터미널) 이 아니라 파일 foo.txt로 리디렉션(전환) 되게 한다는 것이다.
실제 시스템 동작 과정
일반적인 상황에서 stdout은 기본적으로 디스크립터 1번이다. 이 디스크립터는 보통 터미널을 가리킨다.
그런데, ls > foo.txt를 실행하면, 셀은 dup2() 같은 함수를 사용해서 표준 출력을 foo.txt로 바꾸는 것이다.
int dup2(int oldfd, int newfd);
oldfd → 이미 열린 파일 디스크립터 (예: foo.txt)newfd → 바꾸고 싶은 대상 (예: 1번 = stdout)의미: stdout(1번)이 가리키는 대상(터미널)을 닫고, oldfd(예: foo.txt)와 같은 것을 가리키도록 설정
dup2 호출 전
1번 디스크립터(stdout) → 파일 A (터미널)4번 디스크립터 → 파일 B (foo.txt)dup2(4, 1) 호출 후
1번 디스크립터(stdout) → 파일 B (foo.txt)printf()나 write(1, ...) 하면 화면이 아니라 파일로 출력됨file B의 참조 수(ref count)는 2 → 3으로 증가 (공유 중이니까)| 개념 | 설명 |
|---|---|
dup2(oldfd, newfd) | newfd를 oldfd와 같은 파일을 가리키게 만든다 |
| 기존 newfd가 열려 있으면 | 먼저 자동으로 닫은 후 복사 |
| 사용 목적 | 표준 입력/출력/에러를 다른 파일로 리디렉션 할 때 |
| 예시 | dup2(fd_foo, 1) → stdout을 foo.txt로 리디렉션 |
실생활 비유
stdout(1번) = 수도꼭지dup2()로 연결을 바꾸면 호스를 다른 통(foo.txt) 으로 바꿔 꽂는 것표준 입출력 라이브러리란?
C 언어는 Unix 시스템 호출(read, write 등) 보다 조금 더 편한 방식으로 파일 입출력을 할 수 있도록
표준 I/O 함수들을 제공합니다.
대표적인 함수들:
fopen() / fclose() → 파일 열기/닫기fread() / fwrite() → 바이트 단위 읽기/쓰기fgets() / fputs() → 문자열 읽기/쓰기scanf() / printf() → 포맷 지정 입출력"파일 스트림(FILE *)" 이란?
C에서는 파일을 FILE *이라는 스트림(stream)으로 다룹니다.
FILE *fp = fopen("hello.txt", "r");
이렇게 하면 fp는 실제 파일을 추상화한 포인터로,
파일 디스크립터 + 버퍼를 합쳐 놓은 구조체를 가리킵니다.
기본으로 열려 있는 스트림 3개
ANSI C 프로그램을 실행하면, 자동으로 아래 세 가지 스트림이 열려 있습니다:
| 이름 | 설명 | 번호 (디스크립터) |
|---|---|---|
stdin | 표준 입력 | 0 (keyboard) |
stdout | 표준 출력 | 1 (screen) |
stderr | 표준 에러 출력 | 2 (screen) |
이것들 역시 FILE * 타입으로, 우리가 사용하는 printf, scanf 등이 내부적으로 사용합니다.
스트림 버퍼란?
문제점
만약 getc() 같은 함수를 호출할 때마다 read()를 직접 호출한다면,
→ 매번 비싼 시스템 호출이 발생하게 됩니다. (속도 느림)
해결책
"스트림 버퍼" 라는 메모리 공간을 만들어
한 번 read()를 호출할 때 여러 바이트를 한꺼번에 읽어와 버퍼에 저장합니다.
그 다음부터는…
getc()나 fgets()를 여러 번 호출하더라도흐름을 도식으로 표현
프로그램 ─→ getc() ─→ [스트림 버퍼] ─→ read() ─→ 파일
read()가 파일에서 데이터를 버퍼에 채움getc()가 버퍼에서 하나씩 꺼내감read()로 다시 채움요약표
| 개념 | 설명 |
|---|---|
FILE * | 파일을 나타내는 스트림 포인터 |
| 스트림 | 파일 디스크립터 + 버퍼로 구성된 고수준 I/O |
stdin, stdout, stderr | 기본 제공되는 3가지 스트림 |
| 스트림 버퍼 | 시스템 호출 수를 줄여 I/O 성능을 높이는 메모리 공간 |
getc(), fgets() | 버퍼에 있는 데이터를 효율적으로 읽는 고수준 함수 |
세 가지 입출력 방식의 계층 구조
우리가 사용하는 입출력 함수는 3가지 층으로 나눌 수 있습니다.
open, read, write, close 등 (저수준)rio_readn, rio_writen, rio_readlineb (버퍼 있음, 견고함)fopen, fread, fwrite, printf, scanf 등 (가장 편함)어떤 함수를 언제 써야 하나?
이 부분은 가이드라인 3가지(G1, G2, G3)로 설명되어 있다.
G1: 가능하면 Standard I/O 를 사용하기
fopen, fclose, fread, fgets, printf 등이 가장 쉽고 편리합니다.stat()은 유닉스 전용이므로 표준 I/O에 없습니다.요약: 텍스트 파일이나 터미널 입출력에는 표준 I/O 함수를 쓰는 게 최고입니다.
G2: scanf / rio_readlineb는 바이너리 파일에 쓰지 마세요
scanf나 rio_readlineb는 텍스트 파일 전용입니다.0x0a 등)가 많아, 이상한 동작이 나올 수 있습니다.요약: 바이너리 파일 → read, fread, rio_readn 같은 바이트 기반 함수 써야 안전합니다.
G3: 네트워크 소켓에서는 Rio 함수를 사용하세요
lseek 같은 건 사용 불가능합니다.표준 I/O의 문제점 (소켓에서)
제약 1: 출력 후 입력 → fflush 또는 fseek 필수이다.
→ 하지만 fseek은 소켓에서 불가능하므로 fflush만 가능
제약 2: 입력 후 출력 → fseek 등 없으면 안 됨
→ 소켓에서 해결 불가능.
→ 그래서 fdopen()을 두 번 써서 읽기/쓰기를 따로 처리하려고 해도 fclose() 두 번 호출하게 되면 동일 디스크립터를 두 번 닫아 위험하다.
이런 이유로 멀티스레드 환경에서는 특히 치명적일 수 있습니다.
해결책은?
rio_readlineb()rio_writen()sprintf()로 문자열 만든 다음 rio_writen()으로 전송rio_readlineb()로 문자열 읽고 sscanf()로 분석요약표
| 상황 | 추천 함수 |
|---|---|
| 일반 텍스트 파일 입출력 | fopen, fgets, fprintf 등 (표준 I/O) |
| 바이너리 파일 읽기/쓰기 | read, write, fread, fwrite 등 |
| 네트워크 소켓 입출력 | rio_readlineb, rio_writen, sprintf, sscanf |
| 파일 정보 조회 (크기 등) | stat() (Unix I/O 전용) |