Input/Ouput (I/O)는 메인 메모리와 디스크 드라이버, 터미널, 네트워크 같은 외부 장치 간에 데이터를 복사하는 프로세스이다. Input은 I/O 디바이스에서 메인 메모리에 데이터를 복사하고 Ouput은 메모리에서 디바이스로 데이터를 복사한다.
모든 language run-time system은 I/O를 수행하기 위해 higher-level의 기능을 제공한다.
예를 들어, ANSI C는 buffer 된 I/O를 실행하는 printf와 scanf 등의 기능을 갖춘 stdin(Standard I/O) 라이브러리를 제공한다. Linux 시스템에서는 커널에서 제공하는 시스템 수준의 Unix I/O 함수를 사용하여 이러한 상위 수준의 I/O 기능이 구현된다. 대부분의 경우 high level의 I/O 기능은 매우 잘 작동하며 Unix I/O를 직접 사용할 필요가 없다.
그럼에도 불구하고 Unix I/O를 배울 필요가 있다.
Unix I/O를 이해하면 다른 시스템 개념을 이해하는 데 도움이 된다. I/O는 시스템 운영에 필수적이며, 이로 인해 I/O와 다른 시스템 간에 순환적인 의존관계가 발생하는 경우가 많다. 예를 들어 I/O는 프로세스 생성 및 실행에 중요한 역할을 한다. 반대로, 프로세스를 생성하는 것은 다른 프로세스와 파일을 공유하는 방법에 중요한 역할을 한다.
따라서 I/O를 제대로 이해하려면 프로세스를 이해해야 하며, 그 반대의 경우도 마찬가지이다.
또한 Unix I/O를 사용할 수밖에 없는 경우가 있다. higher-level I/O 함수를 사용할 수 없거나 부적절한 경우가 있다.
예를 들어 표준 I/O 라이브러리에서는 파일 크기나 파일 생성 시간 등의 메타데이터에 접근할 수 없다. 또, 표준 I/O 라이브러리의 문제도 있어서 네트워크 프로그래밍에 사용하는 것은 위험하다.
Unix I/O
Linux 파일은 m byte의 sequence이다.
- 모든 I/O 디바이스는 파일로 표현된다.
- /dev/sda2 (/usr disk partition) (dev := device)
- /dev/tty2 (teminal)
- Kernel도 파일로 표현된다.
- /boot/vmlinuz-3.13.0-55-generic (kernel image)
- /proc (kenel data structures)
네트워크, 디스크, 터미널과 같은 모든 I/O 디바이스는 파일로 모델링 되며 모든 I/O는 적절한 파일을 읽고 쓰는 방식으로 수행된다. 디바이스와 파일의 매핑을 통해 Linux 커널은 Unix I/O라고 하는 단순하고 낮은 수준의 애플리케이션 인터페이스를 내보낼 수 있다. 이 인터페이스를 통해 모든 입출력을 균일하고 일관된 방법으로 수행할 수 있다.
Opening files
open 동작은 커널에 대응하는 파일을 열도록 요구함으로써 I/O 디바이스에 접근하려는 의도를 알린다.
커널은 파일의 모든 subsequent 동작에서 파일을 식별하는 decriptor라고 불리는 음이 아닌 정수를 반환한다.
커널은 열려 있는 파일에 대한 모든 정보를 추적한다. 프로그램은 decriptor만 추적한다.
Linux 쉘의 프로세스는 표준 입력(0), 표준 출력(1), 표준 오류(2)를 가진채 실행된다.
Changing the current file position
커널은 열려 있는 각 파일에 대해 파일 위치 k(처음에는 0)를 유지한다. 파일 위치는 파일 시작부터 byte offset이다.
프로그램은 seek 동작을 사용하여 현재의 파일 위치 k를 명시적으로 설정할 수 있다.
Reading and writing files.
read 동작은 n > 0 byte를 파일에서 메모리에 복사하고, 현재 파일 위치 k에서 시작하여 k를 n씩 증가시킨다.
크기가 m byte인 파일을 지정하면 k >= m일 때 read 함수를 실행하는 것은 end-of-file(EOF)라는 상태를 발생시킨다.
EOF는 프로그램에 의해 감지된다. 파일 끝에는 명시적인 "EOF 문자"는 없다.
마찬가지로 write 동작은 n > 0 바이트를 메모리에서 파일로 복사하여 현재 파일 위치 k부터 시작하여 업데이트한다.
Closing files
프로그램은 파일에 접근하는 것이 완료되면 커널이 파일을 닫도록 요청한다.
커널은 파일을 열었을 때 작성한 데이터 구조를 해제하고 discriptor를 사용 가능한 discriptor table에 복원한다.
프로세스가 종료되면 커널은 열려 있는 모든 파일을 닫고 할당되어있던 메모리 리소스를 해제한다.
그러면 왜 close를 굳이 해줘야 하는가?
- 프로그램이 종료되기 전에 file discriptor를 재사용하기 위해.
- 비정상적인 종료가 발생했을 때 메모리 누수가 발생하는 것을 방지하기 위해.
-> 비정상적인 종료도 프로그램이 종료되는 것인데, 그때는 메모리 해제가 안되는가..?
Files
파일에는 시스템에서의 역할을 나타내는 타입이 있다.
- Regular file : 일반적인 데이터를 가진 파일.
- Directory : 파일의 집합에 대한 Index. 파일들의 Inode를 가진다.
- Socket : 다른 기기와 통신하는 프로세스.
Regular files
프로그램에서는 ASCII 또는 Unicode 문자만 포함하는 일반 파일인 text 파일과 그외 모든 파일인 bianry 파일을 구분한다. 커널에서는 text 파일과 binary 파일의 차이는 없다.
텍스트 파일은 일련의 text 행으로 구성된다. 각 행은 줄바꿈 문자('\n')로 끝나는 일련의 문자들이다.
End of line (EOL) 은 파일의 끝을 의미한다.
- Linux and Mac Os : '\n' (0xa)
line feed (LF) - Windows and Internet protocols : '\r\n' (0xd 0xa)
Directories
디렉토리는 link 배열로 구성된 파일이며, 각 link는 파일 이름을 파일에 매핑한다. 파일명은 다른 디렉토리일 수 있다.
각 디렉토리에는 적어도 2개의 entry가 있다..(dot)는 현재 작업 중인 디렉토리 자체에 대한 link이며,..(dot-dot)은 디렉토리 계층의 부모 디렉토리에 대한 link이다.
mkdir 명령어를 사용하여 디렉토리를 만들고 ls로 그 내용을 표시하며 rmdir로 삭제할 수 있다.
Socket
socket은 네트워크를 통해 다른 프로세스와 통신하기 위해 사용되는 파일이다.
Directory Hierarchy
Linux 커널은 /(슬래시)라는 root directory에 의해 고정된 단일 directory 계층에 모든 파일을 정리한다.
시스템의 각 파일은 root directory로 부터 직간접적으로 하위 파일이다.
각 프로세스에는 디렉토리 계층 내의 현재 위치를 식별하는 현재 작업 디렉토리(cureent working directory, cwd)가 있다. cd 명령어를 사용하여 쉘의 현재 작업 디렉토리를 변경할 수 있다.
디렉토리 계층의 위치는 경로 이름으로 지정된다. 경로명은 옵션 슬래시 뒤에 슬래시로 구분된 일련의 파일 이름으로 구성된 문자열이다. 경로 이름에는 두 가지 형식이 있다.
absolute pathname (절대 경로)
절대 경로 이름은 /로 시작하여 root 노드로부터의 경로를 나타낸다.
예를 들어 위 그림에서 hello.c의 절대 경로명은 /home/droh/hello.c이다.
relative pathname (상대 경로)
상대 경로 이름은 파일 이름으로 시작하여 현재 작업 디렉토리에서 경로를 나타낸다.
예를 들어 위 그림에서 /home/droh가 현재 작업 디렉토리인 경우 hello.c의 상대 경로는./hello.c이다.
또, /home/bryant가 현재 작업 디렉토리인 경우 상대 경로는../home/droh/hello.c이다.
Opening and Closing Files
Opening Files
프로세스는 기존 파일을 열거나 open 함수를 호출하여 새 파일을 만든다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
Returns: new file descriptor if OK, −1 on error
open 함수는 파일 이름을 file discriptor로 변환하고 descripotr number를 반환한다.
반환되는 discriptor는 항상 현재 프로세스에 열려 있지 않은 가장 작은 dicriptor이다.
즉, 현재 열리지 않은 파일 중 가장 작은 양의 정수, 또는 file dicriptor table을 사용하지 않은 인덱스 중 가장 작은 양의 정수이다.
reserved file discriptor
- 0 : standard input (stdin)
- 1 : standard output (stdout)
- 2 : standard error (stderr)
Closing Files
#include <unistd.h>
int close(int fd);
Returns: 0 if OK, −1 on error
- 파일을 close 하는 것은 커널에게 파일 접근이 끝났음을 알리는 것이다.
- 이미 close 된 파일을 close 하는 것은 threaded 프로그램에 악영향을 미칠 수 있다.
- 항상 반환 값을 확인해야 한다.
int fd; /* file descriptor */
int retval; /* return value */
if ((retval = close(fd)) < 0) {
perror("close");
exit(1);
}
Reading and Writing Files
프로그램은 'read', 'write' 함수를 사용해서 입력과 출력을 수행한다.
Reading Files
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
Returns: number of bytes read if OK, 0 on EOF, −1 on error
read 함수는 file discriptor fd의 현재 파일 위치에서 buf의 메모리 주소로 0바이트부터 최대 n바이트까지 복사하고 파일의 위치를 update 한다. 예를 들어, 4KB 중 512byte를 읽었다고 하면 0에서 512로 offset이 update 된다.
char buf[512];
int fd; /* file descriptor */
int nbytes; /* number of bytes read */
/* Open file fd ... */
/* Then read up to 512 bytes from file fd */
if ((nbytes = read(fd, buf, sizeof(buf))) < 0) {
perror("read");
exit(1);
}
반환 값이 0보다 작으면 에러, 그렇지 않으면 읽은 바이트 수를 나타낸다.
Writing Files
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t n);
Returns: number of bytes written if OK, −1 on error
write 함수는 buf의 메모리 주소에서 fd의 파일로 최대 n바이트를 복사한다.
char buf[512];
int fd; /* file descriptor */
int nbytes; /* number of bytes read */
/* Open the file fd ... */
/* Then write up to 512 bytes from buf to file fd */
if ((nbytes = write(fd, buf, sizeof(buf)) < 0) {
perror("write");
exit(1);
}
read 함수와 비슷하게 write 함수 호출이 성공하면 양의 정수, 실패하면 -1을 반환한다.
Example
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
1 바이트를 읽고 1바이트를 쓰는 예제이다. 1바이트를 읽을 때 사용자 모드에서 커널 모드로 바꿔야 한다. 이렇게 바꾸는 시간이 2~4만 클럭 사이클이 소요된다. 이렇게 하면 성능이 매우 떨어진다.
On Short Counts
occur
- EOF까지 다 읽을 때 정확히 size만 read 한 게 아닐 경우.
- 터미널이 text를 읽을 때
- 네트워크 소켓에서 읽고 쓸 때.
never occur
- 디스크는 보통 블락 단위로 쓰기 때문에 발생하지 않는다.
- 디스크에 있는 파일을 읽어 들일 때. (EOF 제외)