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