-
System-Level I/O시스템 프로그래밍 2022. 4. 5. 14:56728x90
Unix I/O Overview
리눅스 file은 일련의 byte의 연속입니다. 한마디로 byte덩어리의 배열과 비슷하다고 보면 됩니다.
ex) B0, B1, ....,Bk (size는 모두 똑같습니다)
모든 I/O 장치들은 file로 표현되어있습니다. (disk, 터미널, 커널 등등)
우리가 보통 input이라고 하면 외부 장치(HDD, SDD, 터미널, 네트워크 등등)에서
main memory까지 데이터가 오는것을 의미하고
output은 main memory의 데이터를 외부 장치로 가는것을 말합니다.
우리는 printf나 scanf처럼 ANSIC에서 제공하는 standard I/O 라이브러리에 내재되있는 함수를 사용합니다.
Unix는 시스템 레벨인 커널에 있는 Unix I/O함수 를 이용해 High-level로 구현합니다.
Unix I/O의 가장 특징은 "simple interface", "입출력 형식의 균일" 이있습니다.
Unix I/O의 기본적인 I/O 연산(system call)은 다음과 같습니다.
- file 열기 / 닫기 : open(), close()
- file 읽기 / 쓰기 : read(), write()
- 현재 읽고있는 file 위치 변경 : lseek()
File Types
- Regular file : 일반적인 파일로 binary, text 등 데이터를 담고있습니다. OS는 이 파일을 보면 그저 "일련의 byte"라고 생각합니다.
- Directory file : 폴더 입니다. 파일의 이름과 주소를 담고 있습니다.
- Socket : 네트워크 통신의 용도로 사용됩니다.
- Named pipes (FIFOs) : process간 통신 용도로 사용됩니다.
- Symbolic links : 우리 바탕화면에 바로가기 icon
- Character and block devices : disk (키보드로 입력된 입려값과, 모니터에 나타낼 블록 단위의 값)
Regular Files
regular file에는 text, binary file이 존재합니다.
Text file은 ASCII, Unicode
Binary file은 거의 모든것입니다.(object file, JPEG 이미지 등등)
커널을 text file인지 binary file인지 신경안씁니다.
text file는 일련의 text줄입니다. text file의 각줄은 \n으로 되어있습니다.
End of line(EOL)
리눅스 와 Mac OS : '\n'으로 표현합니다. LF(line feed)
윈도우 : '\r\n'으로 표현합니다. CRLF(Carriage return line feed)
Directories
디렉토리는 파일들의 link들에 대한 배열을 구성합니다.
각 디렉토리는 자기자신과 부모 디렉토리를 포함합니다.
.(dot) : 현재 디렉토리
..(dot dot) : 부모 디렉토리
mkdir : 새로운 디렉토리 만들기
ls : 현재 디렉토리에 있는 내용물 보기
rmdir : 디렉토리 삭제
각 디렉토리는 '/'라는 root 디렉토리 부터 이어져 있습니다.
커널은 각 프로세스마다 현재 디렉토리가 무엇인지 가지고 있습니다.
절대경로는 위와 같이 root에서 시작한 경로를 말합니다.
ex) /home/droh/hello.c
상대경로는 현재 위치를 기준을 말합니다.
ex) /droh/hello,c
Opening Files
file에 access할거라고 kernel에 알려주는것입니다.
기본 형식 : open("parh", mode)
return 값 : file descriptor (만약 에러 발생하면 -1을 return)
각 process가 리눅스 shell에서 생성되면 기본적으로
0 : standard input(stdin)
1: standard output(stdout)
2 : standard error(stderr)
위의 파일을 열 수 있습니다.
Closing Files
file에 access하는 것을 끝냈다는 것을 kernel에게 알려주는 것입니다.
기본 형식 : close(file descripter)
return값 : 음수이면 에러발생, 그외엔 이상없음
파일이 이미 닫혔는데 또 닫으면 엄청난 문제가 생길 수 있습니다.
Reading Files
현재 file 위치에서 메모리로 byte들을 복사하고 file 위치를 업데이트합니다.
기본 형식 : read(fd, buf, sizeof(buf))
return 값 : 읽은 byte의 크기(부호 있는 정수), 만약 에러 발생 시 음수 return
읽고자 하는 데이터보다 작은 경우는 short count가 반환됩니다.
short counts : 3번째 인자인 sizeof(buf) (예상 파일 크기) 보다 작게 return된 경우
이러한 경우 여러가지가 있는데 다음에 설명
Writing Files
메모리에서 현재 file 위치로 byte들을 복사하고 현재 file 위치를 update합니다.
기본형식 : write(fd, buf, sizeof(buf))
return 값 : write 한 byte수, 만약 에러 발생시 음수 return
short counts 발생가능 합니다. 하지만 에러는 아닙니다.
Simple Unix I/O example
system call로 한 byte씩 데이터를 읽고 쓰게 됩니다.
비효율적입니다. 엄청난 overhead이기 때문에 효율에 문제가 생깁니다.
Short Counts
보통 short counts는 다음과 같은 상황일 때 발생합니다.
- EOF (end-of-file)을 읽는 도중 만난경우
- text lines을 터미널로 부터 읽어올때 (예측이 힘듬)
- 네트워크 socket을 읽고 쓸때
반면 절대 발생하지 않는 경우도 있습니다.
- disk 파일을 읽고 쓸때 (크기가 정해져 있는 상황)
Robust I/O(RIO)라는 패키지를 이용해 short count를 다루는 방법을 설명합니다.
RIO Package
Robust I/O
이름에 robust가 들어간 이유는 입출력 장치를 견고하게 만들어 준다는 의미를 지니고 있습니다.
network program할때 좋습니다.
RIO는 두가지 형태를 지닙니다.
- 2진 데이터로 이루어진 Unbuffered input / output
rio_readen, rio_write 함수로 이를 처리합니다.
- 2진 데이터와 text line으로 이루어진 Buffered input
rio_readilneb, rio_readnb 함수로 설명합니다.
unix나 standard에서 하지 못하는 thread(스레드)관리를 해줍니다.
Unbuffered RIO Input and Output
Unix의 read(), write()와 똑같은 interface입니다.
특히 network socket 데이터를 전달 할때 유용합니다.
사용하기 위해 "csapp.h"를 불러옵니다.
함수는 읽기(readn), 쓰기(writen) 2가지가 있습니다.
rio_readen은 eof 파일을 만나게 되면 short count를 return합니다.우리가 얼마나 데이터를 읽을지 알때 사용합니다.
rio_writen은 short count를 return하지 않습니다.
rio_readn, rio_writen은 똑같은 file descriptor 중간 간섭(interleaved)을 허용합니다.
readn은 n byte만큼 데이터를 읽습니다. 그리고 while을 돌면서 데이터를 읽기 시작합니다.
Buffered RIO Input Functions
내부 메모리 buffer에 부분적으로 캐시된 파일에서 text 및 binary 데이터를 효율적으로 읽습니다.
예를들어 hello를 출력하려면, 'h', 'e', 'l', 'l', 'o'를 각각 따로 출력해줘야 하기 때문에 매우 비효율적입니다.
이는 buffer를 사용하는 읽기 방식으로 해결가능합니다.
rio_readinitb : 초기화 함수입니다.
rio_readlineb :
한 줄 단위로 입력을 받습니다. fd로 부터 maxlen byte만큼 읽고 그것을 usrbuf에 저장합니다.
이때 argument(maxlen)로 입력의 최대 byte의 크기를 넣어주어야합니다.
maxlen byte를 읽었거나 EOF를 마주쳤거나 '\n'(개행문자)와 마주쳤을 때 멈춥니다.
rio_readnb : 파일 fd에서 n byte 크기를 읽어옵니다.
maxlen byte를 읽었거나 EOF를 마주치면 멈춥니다.
rio_readlineb, rio_readnb은 똑같은 file descriptor 중간 간섭(interleaved)을 허용합니다.
하지만 rio_readn과 중간간섭(interleaved)하면 안됩니다.
초록색은 이미 읽은 부분, 빨간색은 아직 읽지 않은 부분을 뜻합니다.
rio_buf는 버퍼가 시작된 지점, rio_bufptr은 현재 읽는 부분을 나타내는 포인터, rio_cnt는 읽을 부분의 수를
나타냅니다.
Unix file관점에서 보면 일단 먼저 문자 하나하나 읽어가는게 아니라
미리 system call로 일단 많이 읽어 놓고 user code에서 한 byte씩 천천히 읽어들일때 도움이 됩니다.
이에 대한 정보들은 rio_t라는 구조체에 저장됩니다.
text file의 한 line을 standard input으로 부터 standard output으로 복사
File Metadata
메타 데이터는 실제 파일의 내용은 아니지만, 실제 관리하다 보면 파일의 작성시작, 수정시간 등
여러가지 다른 정보들을 메타데이터라 부르고, "데이터의 데이터"라고도 합니다.
kernel에서 유지 관리하는 파일별 메타데이터입니다.
stat과 fstat 함수에 의해 user로 부터 access되어집니다.
Example of Accessing File Metadata
How the Unix Kernel Represents Open Files
Kernel이 파일을 open한 것을 어떻게 표현하는 봅니다.
Descriptor table :
fd(file descriptor)안의 정보를 담는 테이블 입니다. 먼저 0, 1, 2는 미리 정해진 값으로 고정이며, 수정할 수 없습니다.
따라서 처음으로 파일을 연다면 그 파일의 descriptor는 fd 3에 저장될 것입니다.
Open file table :
fd가 가리키는 파일 테이블로서, 파일 포인터를 담고 있습니다.
v-node table : 파일 포인터가 가리키는 테이블로, 파일에 대한 metadata(크기, 타입 등)을 담고 있고,
파일을 최종적으로 읽는 부분입니다.
File Sharing
open함수로 두개의 동일한 file을 호출할때 나타납니다.
int foo1, foo2;
foo1 = open("foo"); = fd3
foo2 = open("foo"); = fd4
서로 다른 descriptor가 동일한 disk file을 접근할 수 있습니다. 동일한 파일에 두번 접근하면
open 파일은 다른 두개가 나오지만 파일에 대한 metadata는 하나만 존재하게 됩니다.
file descriptor table은 각 process 마다 가지고 있지만 file table과 v-node는 공유합니다.
How Processes Share Files : fork
부모 process가 fork를 통해 child process를 생성하였다면 어떻게 될까?
fork 전
위에서 봤던 것처럼 정상적으로 파일에 접근합니다.
fork 후
fork를 하게 되면 부모 process의 descriptor table을 그대로 복제합니다.
refcnt는 +1이 더해지게 됩니다.
I/O Redirection
리눅스 shell의 명령어로
linux >ls >foo.txt를 입력하면
ls의 output이 foo.txt의 input으로 사용할 수 있게 됩니다.
dup2 라는 system call을 사용합니다. dup2(oldfd, newfd)
예를 들어 dup2(4, 1)을 실행하면
EX)
먼저 foo.txt의 파일을 open한 과정입니다.
dup2(4, 1) 을 통해 newfd인 fd1을 fd4가 가리키는 곳으로 연결해주었습니다.
따라서 해당 output은 terminal이 아닌 foo.txt에 나오게 됩니다.
Standard I/O Functions
C에서 우리가 흔히 사용하는 #include<stdio.h> 를 코드 위에 사용하여 I/O Function을 사용합니다.
ex) printf, scanf, fprintf, fscanf 등
Standard I/O Streams
standard I/O는 file을 stream으로 엽니다.
c 프로그램은 3개의 open stream(stdio.h)과 함께 시작됩니다.
1. stdin (standard input)
2. stdout (standard output)
3. stderr (standard error)
Buffered I/O : Motivation
getc, putc, ungetc, gets, fgets 등 얘넬들은 기본적으로 buffered I/O를 사용해서 overhead가 큽니다.
즉, 한 문자씩 하나하나 읽거나 씁니다.
이를 해결해주느게 Buffered read입니다.
한번에 여러개의 byte를 block으로 묶어서 가지고 옵니다. 그 buffer로 부터 요청되는 byte를 가지고 오는것입니다.
Unix read를 사용하여 byte block을 가져옵니다.
Buffering in Standard I/O
Buffered I/O를 Standard I/O에 적용하는 예제입니다.
hello를 한 문자씩 적으면 내부적인 buf pointer에다가 하나씩 찍게 됩니다.
이를 좀더 내부적으로 자세히 보고싶으면
리눅스 shell에 strace 명령어를 입력하면 현재 실행하는 파일의 system call을 모두 보여줍니다.
따라서 보면 execve를 호출하고 write 시스템 콜은 한번만 호출됩니다. 그래서 printf는 결국 한번만 호출을 해
성능을 최적화 한것입니다.
Unix I/O vs Standard I/O vs RIO
맨 밑에 Unix I/O가 있고 그 위에 Standard I/O와 RIO가 올라가있습니다.
즉, Standard I/O는 system call을 기반으로 구현한 라이브러리입니다.
보통 RIO를 사용하는 이유는 네트워크 communication을 위해서 사용합니다. Standard I/O는 local 데이터를 위해서
사용됩니다.
반드시 이 둘을 섞어 쓰는것은 안됩니다. 쓰는 buffer가 다르기 때문입니다.
Unix I/O
장점
1) 가장 일반적이고 효율적이며 저렴합니다.
2) 메타데이터를 제공합니다.
3) async-signal-safe (stack을 사용하여 signal handler안에서도 안전합니다. =reentrant)
단점
1) short count가 발생할 수 있습니다.
2) 버그가 많고 buffer를 직접 써야합니다.
Standard I/O
장점
1) Buffering을 효율적으로 사용할 수 있습니다.
2) short count 핸들링이 가능합니다.
(우리가 직접적으로 read, write를 사용하지 않아서 효율적입니다.)
단점
1) 메타데이터를 제공하지 않습니다.
2) not async-signal-safe (signal handling에 부적합 합니다.)
3) network socket을 사용할 때 불편한점이 많습니다.
Standard I/O를 쓸 경우 disk나 터미널 파일에 사용합니다.
Unix I/O 를 쓸 경우 효율이 좋기 때문에 signal handling 코드를 만들 때 사용합니다.
RIO 를 쓸 경우 네트워크 socket 통신에 사용합니다.
'시스템 프로그래밍' 카테고리의 다른 글
Concurrent Programming (0) 2022.04.16 Network Programming : Part 2 (0) 2022.04.14 Network Programming : Part 1 (0) 2022.04.12 Exceptional Control Flow : Signals and Nonlocal Jumps (0) 2022.03.29 Exceptional Control Flow : Exceptions and Processes (0) 2022.03.10