Search
Duplicate
⚙️

[운영체제] File I/O

간단소개
파일 입, 출력의 함수와 개념
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Operating System
Scrap
태그
파일디스크립터
File I/O
9 more properties

File I/O - open, read, write, close ...

File Table

커널영역에 존재하는 open file들에 대한 list이다.
프로세스마다 하나씩 보유한다.
file discriptors 라고 하는 integer으로 접근이 가능하다.
File Table의 각 요소들은 파일에 대한 다음의 정보들을 가지고 있다.
inode
File offset
Access modes

File discriptor

C 언어에서는 integer type을 하고 있다.
0부터 시작해 1씩 증가하며 파일들의 위치를 가르킨다.
생성과 동시에 미리 할당된 인덱스들
일반적인 파일 이외에도 IO 디바이스, pipes, directories, socket들도 관리할 수 있다.

IO함수

파일 열기: open()

int open(const char *name, int flags) int open(const char *name, int flags, mode_t mode) //O_CREAT 전용
C
복사
return: 성공시 fd 반환, 오픈 실패시 -1 반환하고 errno에 적절한 값 세팅
name: 열고자 하는 파일 이름 (절대경로, 상대경로 모두 가능)
flags: 필수적인 요소 1개와 optional 한 요소 들을 | 으로 조합하여 만들 수 있다.

파일 열기(+생성): creat()

int creat(const char *name, mode_t mode) // open(file, O_WRONLY|O_CREAT|O_TRUNC, mode) 와 동일
C
복사
return: 성공시 fd, 실패시 -1반환과 errno 세팅

파일 읽기: read()

ssize_t read( int fd, // 읽을 파일의 fd void *buf, // 읽은 내용을 담아올 버퍼 size_t len) // 최대 얼마나 읽어올지에 대한 내용(상한)
C
복사
offset이 가리키는 곳에서부터 최대 len 만큼의 바이트를 읽어와(eof만나면 거기까지) buf에 담고, fd가 가리키는 파일 테이블에 가서 읽은 길이만큼 offset을 증가시킨다. buf의 마지막에 따로 '\0'를 추가해주진 않는다.
return: 읽으려는 시도가 성공했을 경우 읽은 byte 만큼을 반환한다. offset이 EOF를 가리키는 상태라면 0을 반환한다. 읽는 도중에 다른 시그널의 개입 등으로 읽기가 중단되었을 때 읽기 성공한 바이트만큼을 반환한다. 그리고 errno를 EINTR으로 세팅한다.
만약에 읽으려는 시도는 성공했으나 누군가의 방해로 0바이트를 읽었다면 0을 반환할 것이다. EOF에 도달한 경우와의 차이점은 errno == EINTR 이라는 점이다.
읽기 실패시 -1을 반환하는데 시그널의 interrupt 인 경우일 수 있으니 -1을 받았다고 시스템 에러로 분류하는게 아니라 errno == EINTR인지 확인해 주는 것이 좋다. EINTR일 경우 read를 다시 시도한다. 다른 errno일 경우 손쓰기 어려우니 종료시키자.
예제 코드

파일 쓰기: write()

ssize_t write( int fd, // 쓰기 대상 fd const void *buf, // 쓸 내용을 담은 버퍼 size_t count) // buf에서 최대 얼마나 쓸 것인지
C
복사
최대 count바이트 만큼을 buf에서 offset이 가리키는 위치에 작성한다. 매 바이트 작성하며 offset을 다음 바이트로 옮겨준다.
buf에 NULL문자가 있다면 NULL문자를 작성하는 것이 아니라 그 문자 전까지만 작성된다.
return: 성공시 작성한 바이트 크기만큼 반환하고 실패시 -1을 반환한다. 실패시 -1을 반환하고 errno를 세팅한다. read와 다르게 부분적 성공이 없고, interrupt도 일어나지 않아 -1을 반환받으면 불가능을 인정하고 에러처리하면 된다.
파일에 실재로 쓰는 행위는 delay되는데 쓰는데 비용이 상당하기 때문이다. 이때문에 일정 단위로 모아서 작성하기 위해 메모리 영역의 버퍼에 그 내용들을 임시 보관한다. 즉 write()가 -1을 반환하지 않았다고 해도 실제로 쓰여졌다는 것을 보장하지는 않는다.

page cache에 저장된 delaied들을 실행하라는 독촉: fsync()

int fsync(int fd)
C
복사
현재 page cache에 계류중인 I/O 명령들을 실행하라는 명령이다.
그러나 이 함수 또한 명확히 실행을 보장하지 않고, device driver에 요청을 보낸는 수준까지만 해준다.
이게 할 수 있는 최선이다.
여러번 실행하면 성능하락을 각오해야 한다.
return: 성공시 0 실패시 -1

파일 닫기: close()

int close (int fd)
C
복사
열었던 파일을 닫으라는 명령이다. 파일을 닫은 후 해당 파일을 가리키던 fd는 다시 할당가능상태가 된다. 파일을 닫았다고 page cache의 명령들이 실행되는 것이 아니니 착각하지 말자
return: 성공시 0 실패시 -1

File Offset

개요

read()와 write 함수가 동작할 파일내 위치를 저장한다.
파일의 시작값에서부터 바이트 단위로 위치를 구분한다.
파일이 open 될 시 (append 없을 때)0을 가리킨다.

관련 함수

Offset 위치 재설정: lseek()
Offset 과 독립으로 파일을 읽는 함수: pread()
Offset 과 독립으로 파일에 쓰는 함수: pwrite()
lseek() 사용시 주의점

기타 참고사항(size_t)

size Limits

기타 참고사항 하나를 여러번 open

하나의 파일을 open 할 때마다 Global File Table에 entry가 추가 된다.
즉, 이렇게 여러번 오픈하면 file offset가 중복사용되는 문제를 막을 수 있다.
당연히 프로세스 파일 리스트에 할당된 fd도 다르고 close또한 각각 해주어야 한다.

Multiplexed I/O - select()

개요

File descriptor은 여러 종류가 있다.
일반적인 File descriptor
특별한 File descriptor
어떠한 프로세스도 하나 이상의 file descriptor을 점유하는것은 불가능하다.
read시스템 콜이 발생하면 그것의 실행 결과 (실패든, 성공이든)를 반환받기 전까지 프로세스 자체가 block(sleep)상태로 들어간다. 즉, 하나의 fd에서 결과를 받지 않고 다른 파일에 접근하는 것은 불가능하다.
여러개의 fd를 사용해야 할 때 그 목록들을 넘겨주고, 하나라도 사용 가능하면 그것을 사용하고, 모두 사용가능하지 않을 경우 잠에들지만, 하나라도 사용가능한 상태가 될 경우 깨어나는 기능이 필요

여러 File descriptor의 사용권한 질문: select()

int select(int n, // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달 fd_set *readfds, //read 가능한지 알고싶은 fd fd_set *writefds,// write 가능한지 알고싶은 fd fd_set *exceptfds, // exception발생여부를 알고싶은 fd (소켓에서 사용) struct timeval *timeout) // sleep에 들 시간의 상한 null 주면 무한정 기다림 // 리눅스는 timeout에서 기다린 시간만큼 차감하여 업데이트된 값을 담아줌
C
복사
넘겨받은 set 들 중 사용가능한 것이 있으면 사용 가능한 fd의 개수를 반환하는 함수이며 사용가능한 것이 없을 경우 사용가능한 것이 나타나거나 timeout의 만기에 도달할 때 까지 기다린다.
return: 정상적으로 실행될 경우 사용가능한 fd의 개수를 반환하고, timeout동안 하나도 나타나지 않을 경우0을 반환, 오류가 발생할 경우 -1을 반환하고 errno를 세팅하낟 errno == EINTER일 경우 다른 signal handler에 의해 방해받으 경우이다.
fd_set은 비트단위로 저장되며 어떤 fd에 대한 값을 요청하는지에 대한 정보를 저장한다.
fd_set의 메크로

시그널의 방해를 차단하는 select: pselect()

int pselect(int n, // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달 fd_set *readfds, //read 가능한지 알고싶은 fd fd_set *writefds,// write 가능한지 알고싶은 fd fd_set *exceptfds, // exception발생여부를 알고싶은 fd (소켓에서 사용) const struct timespec *timeout,//sleep에 들 시간의 상한 null 주면 무한정 기다림 const sigset_t *sigmask)// 여기 담긴 시그널의 개입을 함수 실행동안 무시한다.
C
복사

Memory Mapped I/O - mmap()

개요

프로그램의 메모리 영역에 파일의 내용을 그대로 옮겨와서 포인터에 접근하듯이 파일내용에 directly 접근하는 것을 가능하게 해줌

파일을 메모리에 매핑하는 함수: mmap()

void *mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset)
C
복사
메모리 상에 file의 offset부터 len 바이트 크기만큼을 매핑해주는 함수
return: 성공시 실제로 매핑된 주소를 반환 (메모리의 시작 주소) 실패시 MAP_FAILED
매개변수들

매핑된 메모리의 해제: munmap()

int munmap (void *addr, // 해제를 시작할 주소 size_t len) // 얼마만큼 해제할 것인지
C
복사
addr에서부터 len만큼을 메모리에서 해제하라는 명령
return: 성공시 0, 실패시 -1

mmap은 글로벌 테이블의 ref count를 증가시키고 munmap은 ref count를 감소시킨다.

즉, mmap 이후 close가 있더라도 ref가 0이 되지 않아 계속 파일에 접근할 수 있다.

fstat(int fd, struct_stat *sb): 파일 디스크립터에 대한 정보를 sb 에 담아준다.

S_ISREG(sb.st_mode): sb에 담긴 파일이 regular 파일(스토리지에 들어가있는 일반적 파일)인지 검사한다.

mmap의 장점

low overhead: 유저영역에서 커널영역으로 커널에서 유저로 전환되는데 소모되는 오버헤드 감소
easy to share: 여러 프로세스들이 파일 읽고 쓰기를 메모리접근하듯, 빠른 업데이트도 되고 좋음
easy to seek: 포인터를 조작하여 메모리에서 이동하기 때문에 파일내 위치이동이 자유로움

mmap의 단점

wate of space: 몇 바이트 짜리 파일을 매핑하든 4kb(page size)가 소모됨
Limited mapping size: 메모리 공간을 차지하기 때문에 큰용량을 매핑할 수 없다.
High overhead: 외부적으로 적은 오버헤드가 소모되는 것 처럼 보일 수 있으나 내부적으로 그걸 달성하기 위해 매우 분주한다. (그럼에도 장점의 영향이 더크다.)

I/O Redirection - >, >>, 2>, >&, dup

I/O operations

>
좌항의 실행 결과로 stdout에 출력될 내용을 stdout이 아닌 우항의 파일에 덮어씌워준다.
>>
좌항의 실행 결과로 stdout에 출력될 내용을 우항의 파일에 append로 출력
2>
좌항의 실행 결과로 stderr에 출력될 내용을 우항의 파일에 덮어씌워준다.
>&
좌항의 실행 결과로 stderr, stdout에 출력될 내용을 우항의 파일에 덮어씌워준다.

File descriptor을 가능한 가장 낮은 fd에 하나 더 할당: dup()

int dup (int oldfd) // 재할당할 fd
C
복사
이미 할당이 완료된 fd에 담긴 파일을 지금 할당 가능한 새로운 fd에 배정. 기존 fd도 유지 즉 newfd = dup(1);을 실행하면 newfd엔 3이 할당되어 1과 3에 stdout이 배정된다.
return: 성공시 재할당된 fd, 실패시 -1
예제
close(2); newfd = dup(1);
C
복사
stderr에 출력될 내용들이 stdout으로 redirection된다.

복사하여 할당할 fd를 명시할 수 있는 dup: dup2()

int dup2(int oldfd, // 복사할 IO의 fd int newfd) // 복사할 위치
C
복사
oldfd를 newfd에 복사해준다. 만약 newfd가 이미 점유중이라면 기존의 file을 close하는 실행도 해준다.
dup2(1, 2); == close(2); dup(1);
return: 성공시 newfd, 실패시 -1

global file table과 dup의 관계

단순히 dup()나 dup2()로 복사된 fd들은 global file table에서 같은 테이블을 가리킨다.
즉, 같은 offset을 공유하고 ref count만 증가한다는 의미이다.
global file table을 새로 생성하는 명령은 아직까지 배운 범위 내에선 open밖에 없다.

stdio: Standard I/O Library

개요

Performance issues
stdio: User-buffered I/O
내부적으로 File discriptor을 FILE 이라는 타입의 포인터로 매핑하여 사용한다.

I/O 함수들

파일을 열어주는 함수: fopen()
fd를 FILE *로 바꿔주는 함수: fdopen()
FILE 에서 1글자를 읽어주는 함수: fgetc()
FILE 에서 문자열을 읽어주는 함수: fgets()
파일을 특정한 사이즈 단위로 읽어주는 함수: fread()
FILE 에 글자 1개를 쓰는 함수: fputc()
FILE 에 문자열을 입력하는 함수: fputs()
FILE 에 특정한 사이즈 단위로 쓰는 함수: fwrite()
유저레벨에서 버퍼링 중인 것을 실행시키라는 명령: fflush()
FILE 의 offset을 이동시키는 함수: fseek()
FILE 의 offset 위치를 얻어주는 함수: ftell()
fclose(FILE *stream) : 파일 포인터를 반환하는 함수
fcloseall(void): stdin, stdout, stderr을 포함한 모든 파일 포인터를 close