File I/O
Category: System Programming
Tags: fileio
System Calls
-
System Call : kernel mode로 전환해서 OS에게 필요한 작업을 요청
-> 매우 expensive : 한 번 호출하는데 많은 절차와 시간 요구
-> 하드웨어에 접근하기 위해서 반드시 수행해야 하는 단계
-
Function Call : mode switch 없이 함수를 호출 (일반적인 user libraries, functions)
Steps for System Calls
한 번 시스템 호출하는데 필요한 단계 : 11 steps (= expensive)
- program pushed arguments, calls library
- library sets up trap, calls OS
- OS handles system call
- Control returns to library
- Library returns to user program
-> 순서 : 프로세스가 System call을 호출하면 interrupt가 발생하여 kernel 모드로 진입. 그 후 요청으로 들어온 System call을 수행하고, return-from-trap을 발생시켜서 user 모드로 돌아가 결과를 반환.
Examples of System Calls
시스템 함수는 파일 관리, 프로세스 관리 등의 목적으로 사용된다.
- File Management 관련 시스템 함수
open(2)
: 새로운 파일 열기close(2)
: 열려 있는 파일 닫기read(2)
: 파일 내용 읽어오기 (-> buffer)write(2)
: 파일에 내용 작성하기 (<- buffer)lseek(2)
: 파일 pointer 이동stat(2)
: 파일 정보 가져오기
- Process Management 관련 시스템 함수
fork(2)
: 새로운 child process 생성하기execve(2)
: 새로우 프로그램 실행하기waitpid(2)
: child process가 종료될 때까지 기다리기exit(2)
: 프로세스를 종료하고 status를 반환
System Calls for I/O
- Unix I/O : system-level I/O functions
- other languages : ANSI C (standard I/O library), C++ (< <, > > 연산자)
UNIX I/O Overview
-
Unix file : sequence of m bytes
- Unix/Linux는 “everything as file”로 취급
- I/O device : /dev/tty2
- kernel : /dev/kmem
-> 모두 비슷하게 파일로 취급, input과 output 다루는 방식도 동일함
-
Unix I/O (= unbuffered I/O, System-level I/O)
: 파일과 시스템 장치들을 연결해주는 역할 (kernel)
File Descriptors (= File Handle)
파일을 열 때마다 부여되는 non-negative integer number로, kernel은 파일을 fd로 구분
- 표준 입출력 File Descriptor (unistd.h에 정의)
- STDIN_FILENO : 0
- STDOUT_FILENO : 1
- STDERR_FILENO : 2 -> magic number보다는 macro를 사용하는 것이 좋다
-
새로 열린 파일 File Descriptor : 3부터 시작
ex. fds.c : 파일을 open했을 때 저장되는 file descriptors 확인하는 코드
-
File Sharing : 두 개의 독립적인 프로세스가 하나의 i-node table 가르키는 경우
-> fork를 통해 child 프로세스 생성하면, parent 프로세스의 open file 그대로 상속
open(2) : Opening a file
새로운 또는 기존의 파일을 열 때 사용, fcntl.h
헤더에 포함된 시스템 콜 함수
#include <fcntl.h>
int open(const char &pathname, int oflag /*, mode_t mode */);
int openat(int dirfd, const char &pathname, int oflag /*, mode_t mode */));
// return : file descriptor
// return -1 on error
-
대표적인 oflags : O_RDONLY 읽기전용, O_WRONLY 쓰기전용, O_RDWR 읽기쓰기전용
O_APPEND : 파일의 끝(EOF)에 write하도록 설정
O_CREAT : 파일이 없으면 생성, 있으면 open (dir-w,x권한)
O_EXCL : O_CREAT과 함께 사용, 파일 있으면 error
O_TRUNC : 파일 크기를 0으로 초기화 (file-w권한)
-
대표적인 open errors
EEXIST : O_CREAT와 O_EXCL을 사용할 때, 파일이 있는 경우
EMFILE : 프로세스가 maximum file descriptor에 도달
ENOENT : 파일이 존재하지 않는 경우
EPERM : 특정 permission이 없는 경우
-
파일 open시 kernel에 저장되는 정보
File Descriptor table (process-specific): open에서 사용한 flags와 open file table 포인터를 포함
Open File table (system-wide): 파일의 status flags, access mode, offset, i-node table 포인터를 포함
i-Node/v-Node table (system-wide): i-node 정보(file type)와 파일의 속성(size, permissions, timestamp)을 포함
create(2)
새로운 파일 생성시에 사용, fcntl.h
헤더에 포함된 시스템 콜 함수
#include <fcntl.h>
int create(const char &pathname, mode_t mode);
// return : file descriptor
// return -1 on error
-> same as : open(path, O_CREAT | O_TRUNC | O_WRONLY, mode);
-> write only로만 설정, 다른 모드를 주고 싶으면 close했다가 다시 open해야 함
close(2)
열려있는 파일을 닫을 때 사용, unistd.h
헤더에 포함된 시스템 콜 함수
#include <unistd.h>
int close(int fd);
// return 0
// return -1 on error
-> 사용 가능한 file descriptor 수가 제한적, 사용하지 않는 파일은 close해두는 것이 좋다 !
ex. openex.c : create, open, openerror, createerror, truncate 사용 예제코드
read(2)
열려있는 파일 내용을 읽어들일 때 사용, unistd.h
헤더에 포함된 시스템 콜 함수
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t num);
// return : number of bytes read
// return 0 on EOF, return -1 on error
// 사용 예시
char buf[512]; // 읽어들일 만큼 buffer 크기 지정
int fd;
int nbytes; // ssize_t : signed integer
if ((nbytes = read(fd, buf, sizeof(buf))) < 0)){ // buffer 크기만큼 읽기
perror("read");
exit(1);
}
- current offset부터 읽기 시작, 그리고 읽은 만큼 offset을 증가
- Short counts (
nbytes < sizeof(buf)
) : 요청보다 적게 읽은 경우
write(2)
읽어들인 파일 내용을 다시 출력할 때 사용, unistd.h
헤더에 포함된 시스템 콜 함수
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t num);
// return : number of bytes written
// return -1 on error
// 사용 예시
char buf[512]; // write할 만큼 buffer 크기 지정
int fd;
int nbytes; // number of bytes read
if ((nbytes = write(fd, buf, sizeof(buf))) < 0)){ // buffer 크기만큼 쓰기
perror("write");
exit(1);
}
- current offset부터 작성(O_APPEND는 맨 뒤), write한 만큼 offset을 이동
- Short counts 발생 가능 -> not error !
- 끝까지 읽어들이기 전에 EOF 발생된 경우
- 네트워크 소켓에 reading, writing 하는 경우 (buffer 딜레이)
- signal에 의해 interruption이 발생된 경우
- 터미널에서 명령을 읽어들이는 경우
ex. stdin에서 읽은 값을 stdout으로 1 byte씩 복사
ex. rwex.c : read, write 함수 사용 예제 코드
ex. mycopy.c : write program that copies content of file -> 직접 해보기 !
lseek(2)
파일의 위치를 옮겨주는 함수, random access를 가능하게 함
- Sequential access : 파일을 처음부터 순차적으로 읽어들임, jump 불가능
- Random access : 파일을 어느 순서로도 읽어들일 수 있음, jump 가능
#include <sys/types.h>
#include <fcntl.h>
off_t lseek(int fd, off_t offset, int whence);
// return : new offset
// return -1 on error
-
file offset : 파일의 pointer 개념
파일을 처음 열었을 때 0으로 초기화, read, write 한만큼 증가
-
whence options (offset 설정 기준)
SEEK_SET : 파일의 맨처음을 기준으로
SEEK_CUR : 현채 파일 위치를 기준으로
SEEK_END : 파일의 마지막(EOF)을 기준으로
ex. lseek.c : lseek 함수로 offset이 이동하는 과정
ex. hole.c : 비어있는 경우, 자동으로 중간이 null character로 채워짐
-> 30만큼 lseek로 옮긴 후, write을 한 실행 결과
Atomic Operation
프로세스의 순서가 일정하지 않기 때문에, 두 개의 연동된 프로세스를 실행하는 경우 생각했던 것과 다른 결과를 출력할 수 있음. Atomic Operation은 두 프로세스를 한 뭉텅이로 만들어서 인터럽트가 발생하지 않도록 하는 것
ex. parent 프로세스와 child 프로세스가 같은 일을 하는 경우
-> atomic operation이 필요한 이유 : reliable한 operation !
-> atomic operation 사용 예시
- O_APPEND = lseek + write
- pread, pwrite : lseek + read write
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// returns number of byte read, -1 on error
ssize_t pwrite(int fd, void *buf, size_t nbytes, off_t offset);
// returns number of byte written, -1 on error
File descriptor duplication : dup(2)
file descriptor 를 복사하는 함수, 다른 file descriptor지만 같은 open file table 참조
dup : 같은 물리적 파일을 연 새로운 file descriptor를 반환
dup2 : newfd가 open된 파일이면 닫은 후, oldfd를 복사
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
// return : newfd
// return -1 on error
-> dup을 사용했을 때, 각 테이블 사이의 관계성
I/O Redirection
dup2(fd, 1)
: 표준 출력을 fd 파일로 redirection
-> 출력 결과를 터미널이 아닌 특정 파일에 저장하고 싶을 때 사용
ex. dup2로 복제한 파일들끼리 read
ex. dup으로 복제한 파일들끼리 write
-> dup을 한 파일끼리는 같은 open file table(offset) 공유
File descriptor control : fcntl()
열려 있는 file descriptor에 대해 특정 command를 수행하는 함수
-> 파일에 대한 정보를 가져오거나 file descriptor 속성을 변경할 수 있음
#include <fcntl.h>
int fcntl(int fd, int cmd, ..);
// return : cmd마다 다름
// return -1 on error
-
command options
F_DUPFD : file descriptor를 복제
F_GETFD : file descriptor flags 가져오기
F_SETFD : file descriptor flags 설정하기
F_GETFL : file status flags 가져오기
F_SETFL : file status flags 설정하기
-
dup, dup2 : atomic operation
dup :
fcntl(fd, F_DUPFD, 0);
dup2 :
close(fd2); fcntl(fd, F_DUPFD, fd2);
buffered I/O vs. unbuffered I/O
-
buffered vs. unbuffered 비교
buffered I/O unbuffered I/O functions standard library I/O system-level I/O outputs when buffer flushed every time when called executing time short time long time (= expensive) std streams FILE* type (fd + buffer) int type (fd) interrupt signal handler, can exit well optimized, strong example ANSI C Linux/Unix Systems usages usual programming accessing file meta data, permission
- Buffering Mechanism : buffer를 통해 system call을 최소화 하는 것 !
-> 최적화된 buffer size 중요함, 보통 4096으로 사용- Fully buffered, Block buffered : 가득 채울 때마다 flush
- Line buffered : ‘\n’ 마다 flush
- unbuffered : 아예 사용하지 않는 경우
ex. hello.c : ‘h’ ‘e’ ‘l’ ‘l’ ‘o’ 따로 printf하는 코드
-> 진짜 standard l/O에서 buffer를 사용하는지 확인하기 위함 (sleep)
@Advanced Programming in the UNIX environment, Third edition 내용을 참고함