익명 사용자
로그인하지 않음
계정 만들기
로그인
youngwiki
검색
System-Level I/O 문서 원본 보기
youngwiki
이름공간
문서
토론
더 보기
더 보기
문서 행위
읽기
원본 보기
역사
←
System-Level I/O
문서 편집 권한이 없습니다. 다음 이유를 확인해주세요:
요청한 명령은 다음 권한을 가진 사용자에게 제한됩니다:
사용자
.
문서의 원본을 보거나 복사할 수 있습니다.
상위 문서: [[컴퓨터 시스템]] ==개요== 입출력(I/O)은 주기억장치(main memory)와 디스크 드라이브, 터미널, 네트워크와 같은 외부 장치(external devices) 사이에서 데이터를 복사하는 과정이다. 입력 연산(input operation)은 I/O 장치로부터 데이터를 주기억장치로 복사하고, 출력 연산(output operation)은 데이터를 주기억장치에서 장치로 복사한다.<br> 해당 문서에서는 UNIX I/O와 표준 I/O의 일반적인 개념과, C 프로그램에서 이를 안정적으로 사용하는 방법을 설명한다. ==UNIX I/O== 리눅스 파일은 아래와 같은 m 바이트의 시퀸스로 구성된다. B<sub>1</sub>, B<sub>2</sub>, ..., B<sub>k</sub>, ..., B<sub>m-1</sub> 모든 I/O 장치들(예: 네트워크, 디스크, 터미널, 커널! 등)은 아래와 같이 파일로 모델링되며, 모든 I/O는 해당 파일들을 읽고 쓰는 방식으로 수행된다. /dev/sda2 (/usrdiskpartition) /dev/tty2 (terminal) /boot/vmlinuz-3.13.0-55-generic (kernel image) /proc (kernel data structures) I/O 장치들을 모두 파일로 매핑하는 방식 덕분에, 리눅스 커널은 '''UNIX I/O'''로 불리는 단순하고 저수준(low-level)의 인터페이스를 제공할 수 있다. 이를 통해 모든 I/O 작업들을 일관되고 통일된 방식으로 수행할 수 있다. ==Files== 각 리눅스 시스템 내에서의 역할을 나타내는 type을 가진다: * '''일반 파일(regular file)은 임의의 데이터로 구성'''된다. 애플리케이션 들은 종종 일반 파일 들을 ASCII 혹은 유니코드 문자만을 포함하는 텍스트 파일(text file)과, 그 외의 모든 것을 포함하는 바이너리 파일(binary file) 을 구분하지만, 커널은 이를 구분하지 않는다. 따라서 '''UNIX I/O도 텍스트 파일과 바이너리 파일을 구분하지 않는다.''' 리눅스 텍스트 파일은 단순히 '''텍스트 라인들의 시퀸스(sequence of text lines)'''로 구성되며, 각 줄(text line)은 '''문자들의 시퀸스(sequence of characters)로 이루어지고 줄바꿈 문자('\n')<ref>줄 바꿈 문자는 0x0a에 해당하며, 이는 ASCII 값과 동일하다.</ref>로 종료'''된다. * '''디렉토리(directory)'''는 '''링크(link)들의 배열로 구성'''된 파일이며, 각 '''링크는 파일 이름을 파일(혹은 디렉토리)에 매핑'''한다. 각 디렉토리는 적어도 두 개의 항목을 가지고 있다. 먼저 '''<code>.</code>는 디렉토리 자신'''을 가리키는 링크이고, '''<code>..</code>는 디렉토리 계층 구조에서 상위 디렉토리'''를 가리키는 링크이다. 디렉토리는 <code>mkdir</code> 명령어로 만들 수 있고, <code>ls</code> 명령어로 안의 내용을 볼 수 있으며, <code>rmdir</code> 명령어를 통해서 삭제할 수 있다. * [[Application Layer#Process communicating#Socket|소켓(socket)]]은 네트워크를 통해 다른 프로세스와 통신하기 위해 사용되는 파일이다. 그외에도 named pipe, symbolic link, character and block deviced와 같은 여러 type들이 추가로 존재하나, 이에 대해서는 다루지 않느다. ===Directory hierarchy=== [[파일:Portion of the Linux directory hierarchy.png|대체글=Figure 1. Portion of the Linux directory hierarchy|가운데|섬네일|400x400픽셀|Figure 1. Portion of the Linux directory hierarchy]] 리눅스 커널은 모든 파일을 루트(root) 디렉터리 <code>/</code>로 고정된 단일 디렉터리 계층 구조 안에 조직한다. 시스템 내의 각 파일은 루트 디렉터리의 직계 또는 간접 후손(direct or indirect descendant)이다. Figure 1은 리눅스 시스템 내의 디렉토리 계층의 일부를 보여준다.<br> 각 프로세스는 컨텍스트의 일부로 현재의 작업 디렉터리(current working directory)를 가지며, 이는 디렉토리 계층 내에서 현재 위치를 나타낸다. 이때, <code>cd</code> 명령어를 통해 셸(shell)의 현재 작업 디렉터리를 변경할 수 있다. 디렉더리 계층에서의 위치는 경로명(pathname) 으로 지정된다. 경로명은 <code>/</code>로 구분된 일련의 파일 이름들로 구성된 문자열이다.<ref>이때 경로명은 <code>/</code>으로 시작할 수도, 안할 수도 있다.</ref> 경로명은 두 가지 형태가 있다: * '''절대 경로명(absolute pathname)''': '''<code>/</code>로 시작하며, 루트 디렉토리로부터의 경로를 나타낸다.''' ** hello.c의 절대 경로명: <code>/home/droh/hello.c</code> * '''상대 경로명(relative pathname)''': '''파일 이름으로 시작하며, 현재 작업 디렉터리로부터의 경로를 나타낸다.''' ** <code>/home/droh</code>가 현재 작업 디렉터리일 때 hello.c의 상대 경로명: <code>./hello.c</code> ** <code>/home/bryant</code>가 현재 작업 디렉터리일 때 hello.c의 상대 경로명: <code>../home/droh/hello.c</code> ==Opening and Closing Files== ===Opening files=== 애플리케이션은 <code>close()</code> 함수를 통해 커널에게 특정 파일을 열도록 요철하며, 해당 I/O 장치에 접근한다. <syntaxhighlight lang="cpp"> #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 </syntaxhighlight> 커널은 해당 파일 이름을 파일 디스크립터(file descriptor)<ref>0 이상의 정수이다</ref>로 변환하고 그 디스크립터 번호를 반환한다. 반환되는 디스크립터는 항상 프로세스에서 현재 열려 있지 않은 가장 작은 디스크립터 번호이다. 이 디스크립터는 이후 파일에 대한 모든 연산에서 사용된다.<ref>커널은 열려있는 파일에 대한 모든 정보를 추적하고 관리하나, 애플리케이션은 디스크립터만을 추적한다.</ref> 위 <code>close()</code> 함수에서 flag 인자는 프로세스가 파일에 어떻게 접근하려는지를 나타내며, 이는 아래와 같다: * O_RDONLY: 읽기 전용 * O_WRONLY: 쓰기 전용 * O_RDWR: 읽기 및 쓰기 또한, flags 인자는 추가적인 옵션을 제공하는 하나 이상의 비트 마스크(bit mask)와 OR 연산으로 결합될 수 있다. * O_CREAT: 파일이 존재하지 않으면 빈 파일을 새로 만든다. * O_TRUNC: 파일이 이미 존재하면 해당 파일을 빈 파일로 만든다. * O_APPEND: 쓰기 작업(<code>write()</code>)을 할 때마다, 커널이 자동으로 파일의 맨 끝으로 이동해서 쓰게 한다.<ref>기존 내용을 덮어 쓰지 않고 이어 쓰고자 할 때 이용한다.</ref> 예를 들어, 이어 쓰고자 할 때, 파일을 쓰기 전용으로 여는 방법은 다음과 같다. <syntaxhighlight lang="cpp"> fd = Open("foo.txt", O_WRONLY|O_APPEND, 0); </syntaxhighlight> 또한 모든 리눅스 프로세스는 생성되거나 시작하는 즉시 기본으로 열려있는 세개의 파일(디스크립터)를 가지고 있는데, 이는 다음 표와 같다: {| class="wikitable" |+ !이름 !디스크립터 !상수 !역할 |- |standard input |0 |STDIN_FILENO |키보드 등에서 입력 받기 |- |standard output |1 |STDOUT_FILENO |화면(터미널)으로 출력하기 |- |standard error |2 |STDERR_FILENO |에러 메시지 출력하기 |} 아래는 기본 디스크립터를 이용하여 터미널에서 1byte씩 읽고 출력하는 예제 프로그램이다. <syntaxhighlight lang="cpp"> #include "csapp.h" int main(void) { char c; while (Read(STDIN_FILENO, &c, 1) != 0) Write(STDOUT_FILENO, &c, 1); exit(0); } </syntaxhighlight> ===Colsing files=== 애플리케이션이 파일에 대한 접근을 마치면, <code>close()</code> 함수를 호출하여 해당 파일을 닫아달라고 커널에 요청한다: <syntaxhighlight lang="cpp"> #include <unistd.h> int close(int fd); //Returns: 0 if OK, −1 on error </syntaxhighlight> 커널은 위 함수의 호출 결과로 파일을 열 때 생성한 데이터 구조들을 해제하고, 디스크립터를 사용 가능한 디스크립터 풀로 되돌린다. 이때 이미 닫혀있는 디스크립터를 닫는 것은 오류이다. 또한 프로세스가 어떤 이유로든 종료되면, 커널은 열려있는 모든 파일들을 닫고 그 메모리 자원들을 해제한다. ==Reading and Writing Files== ===Reading files=== 애플리케이션은 <code>read()</code> 함수를 호출하여 입력 연산을 수행한다: <syntaxhighlight lang="cpp"> #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 </syntaxhighlight> <code>read()</code> 함수는 디스크립터 fd의 현재 파일 위치(current file position) k로부터 최대 n 바이트를 buf에 복사하고, 그 후에 k를 n만큼 증가시킨다. 이때 파일 크기가 m 바이트일 때, <code>k≥m</code>인 상태에서 읽기 연산을 수행하면 EOF(end of file) 상태가 발생한다.<ref>파일 끝에는 명시적인 EOF 문자는 존재하지 않는다.</ref> 예를 들어 아래의 코드는 100바이트씩 계속 읽다가 더 이상 읽을 게 없으면, <code>read()</code> 함수는 EOF 상태이므로 0을 반환한다. <syntaxhighlight lang="cpp"> int fd = open("myfile.txt", O_RDONLY); char buf[100]; int n; while ((n = read(fd, buf, 100)) > 0) { // 여기서 buf 안의 데이터를 사용함 } if (n == 0) { //n == 0 이면 EOF 도달, n == -1이면 에러 종료 } </syntaxhighlight> ===Writing files=== 애플리케이션은 <code>write()</code> 함수를 호출하여 쓰기 연산을 수행한다: <syntaxhighlight lang="cpp"> #include <unistd.h> ssize_t write(int fd, const void *buf, size_t n); //Returns: number of bytes written if OK, −1 on error </syntaxhighlight> <code>write()</code> 함수는 buf 안에 있는 데이터에서 최대 n 바이트를 복사하여 fd에 해당하는 파일의 현재 파일 위치 k에 적고 k를 갱신한다. 예를 들어, 아래 코드는 "hello"라는 5바이트짜리 문자열을 파일 fd에 쓴다: <syntaxhighlight lang="cpp"> char msg[] = "hello"; write(fd, msg, 5); </syntaxhighlight> ===Changing the current file position=== 애플리케이션은 <code>lseek</code> 함수를 호출하여 현재의 파일 위치를 바꾼다: <syntaxhighlight lang="cpp"> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); //Returns: current file position if OK, -1 on error </syntaxhighlight> <code>lseek</code> 함수는 fd에 해당하는 파일의 현재 파일 위치를 offset byte만큼 옮긴다. 이때 옮긴 위치에 기준을 잡기 위해서 아래 표에 정리되어 있는 whence 옵션이 사용된다: {| class="wikitable" |+ !옵션 !설명 |- |SEEK_SET |Move from the start of the file |- |SEEK_CUR |Move from the current position of the file |- |SEEK_END |Move from the end of the file |} 아래는 <code>lseek</code> 함수가 사용된 코드의 예시이다. <syntaxhighlight lang="cpp"> #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("example.txt", O_RDWR); //파일 열기 if (fd == -1) { //적절한 파일 디스크립터인지 확인 perror("open"); return 1; } off_t pos = lseek(fd, 0, SEEK_CUR); //pos에 현재 파일 위치 저장 printf("Current position: %ld\n", pos); lseek(fd, 5, SEEK_SET); // 파일 시작에서 5바이트 앞으로 이동 write(fd, "HELLO", 5); // 파일 처음에서 5번째 바이트에 HELLO 덮어쓰기 off_t end = lseek(fd, 0, SEEK_END); //현재 파일 위치를 파일의 끝으로 이동 printf("File size: %ld bytes\n", end); close(fd); //파일 닫기 return 0; } </syntaxhighlight> ===Short counts=== 어떤 상황에서는 <code>read()</code>, <code>write()</code> 함수가 '''요청받은 바이트(n)보다 적은 양을 전송'''하기도 한다. 이를 '''short counts'''라고 하며, 이는 오류를 의미하지 않는다. 이는 다음과 같은 상황일때 발생한다: * 읽기 도중 EOF를 만난 경우: 예를 들어, 현재 파일 위치로부터 남은 바이트가 20바이트뿐인 파일을 50바이트씩 읽고 있다고 하자. 이 경우 다음 read 호출은 20이라는 짧은 수치를 반환하고, 그 다음 호출은 0을 반환하여 EOF를 나타낸다. * 터미널에서 텍스트 줄을 읽는 경우: 열린 파일이 터미널(즉, 키보드와 디스플레이)과 연결되어 있다면, 각 <code>read()</code> 함수 호출은 한 줄의 텍스트만 전송하고, 이 줄의 크기만큼의 short counts를 반환한다. * 네트워크 소켓을 읽고 쓰는 경우: 열린 파일이 네트워크 소켓에 해당한다면, 내부 버퍼 제약과 긴 네트워크 지연으로 인해 <code>read()</code>, <code>write()</code> 함수는 짧은 수치를 반환할 수 있다. 또한, 리눅스 파이프(pipe)를 <code>read()</code>, <code>write()</code> 함수로 호출할 때도 발생할 수 있다. ==Robust I/O package== '''RIO(Robust I/O)'''는 효율적이고 굳건한(robust) I/O를 제공하는 래퍼(wrapper) 함수들의 집합으로, 특히 short counts 문제에 취약한 응용 프로그램을 위해 사용된다. RIO는 두 가지 종류의 함수들을 제공한다. * Unbuffered 입출력 함수: 해당 함수들은 메모리와 파일 사이에 데이터를 직접 전송하며, 응용 프로그램 수준의 버퍼링(application-level buffering)은 없다. 주로 네트워크로부터 binary 데이터를 읽고 쓸때에 유용하다. * Buffered 입력 함수: 해당 함수들은 표준 I/O 함수들(printf() 함수 등)이 제공하는 것과 유사하게 응용 프로그램 수준의 버퍼 내에 저장된(cached in an application- level buffer) binary 데이터들과 텍스트 라인들을 읽도록 한다. ** 표준 I/O 함수들과는 달리, RIO buffered 함수들은 '''thread-safe'''하며, 동일한 파일 디스크립터에 대해서 임의로 교차적으로(interleaved) 사용이 될 수 있다. 예를 들어 파일 디스크립터로부터 텍스트 라인 몇 줄을 읽고, 그 다음에 binary 데이터를 읽고, 다시 텍스트 라인을 읽는 것이 가능하다. ===Unbuffered RIO Input and Output=== 응용 프로그램은 <code>rio_readn()</code>과 <code>rio_writen()</code> 함수를 호출하여 메모리와 파일 사이에 데이터를 직접 전송할 수 있다. <syntaxhighlight lang="cpp"> #include "csapp.h" ssize_t rio_readn(int fd, void *usrbuf, size_t n); //Returns: number of bytes transferred if OK, 0 on EOF, −1 on error ssize_t rio_writen(int fd, void *usrbuf, size_t n); //Returns: number of bytes transferred if OK, −1 on error </syntaxhighlight> <code>rio_readn()</code>함수는 디스크립터 fd의 현재 파일 위치로부터 최대 n 바이트를 버퍼 usrbuf로 전송한다. 이때 <code>rio_readn()</code>함수는 EOF에 도달한 경우에만 short count를 반환할 수 있으며, 완전한 EOF인 경우<ref>읽기 작업을 시작하자마자 EOF인 경우에 해당한다.</ref>에는 0을 반환한다. <code>rio_writen()</code>함수는 버퍼 usrbuf에서 디스크립터 fd로 n 바이트를 전송한다. 이때 <code>rio_writen()</code>함수는 절대 short counts를 반환하지 않는다. 이때 두 함수는 같은 디스크립터 내에서 임의로 교차하여 사용할 수 있다. 아래는 <code>rio_readn()</code> 함수를 어떻게 구현하였는지 보여준다: <syntaxhighlight lang="cpp"> ssize_t rio_readn(int fd, void *usrbuf, size_t n) { size_t nleft = n; // 앞으로 읽어야할 byte의 수 ssize_t nread; //지금까지 읽은 byte의 수 char *bufp = usrbuf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { /* read()는 한 번에 n 바이트를 못 읽을 수 있기 때문에 반복해서 읽음 */ if (errno == EINTR) /* 시그널 핸들러로 인해 인터럽트된 경우 */ nread = 0; /* 다시 read 호출 */ else return -1; /* 그 외 오류일 경우 에러 리턴 */ } else if (nread == 0) break; /* read() 함수가 0 반환시 EOF 도달로 간주 */ nleft -= nread; bufp += nread; } return (n - nleft); /* 실제 읽은 바이트 수 리턴 (0 이상) */ } </syntaxhighlight> 위 코드는 파일 디스크립터 fd로부터 정확히 n 바이트를 usrbuf로 읽어오는 작업을 수행한다. 이때 <code>read()</code> 함수는 한번에 데이터를 원하는 만큼 읽어오지 못할 수 있으므로 반복하여 호출된다. 이때 시그널 인터럽트(EINTR)가 발생하면 읽기 작업을 재시도 하며, <code>read()</code> 함수가 0을 반환하면 EOF 도달로 간주하고 종료한다. 이를 통해서 <code>rio_readn()</code>함수는 실제로 읽은 바이트 수를 반환할 수 있다. 이때 <code>read()</code>함수는 다음과 같은 원인으로 인해서 데이터를 한번에 원하는 만큼 읽지 못할 수 있다. # '''커널 버퍼에 데이터가 부족한 경우''' #* <code>read(fd, buf, n)</code>을 요청했는데 커널에 n보다 적은 양만 준비돼 있으면, <code>read()</code>는 그 준비된 양만큼만 읽고 바로 리턴한다. # 시그널로 인한 인터럽트(errno == EINTR) 발생 시, <code>read()</code>는 실패(-1)하고 중단된다. # stream device의 특성으로 인해서 부분적인 읽기가 일상적인 경우도 있다. #* 디스크 파일은 <code>read()</code>함수를 사용시 보통 n바이트 다 읽힌다. #* 하지만 네트워크 소켓, 파이프, 터미널 같은 '''stream 기반 장치는 read가 준비된 만큼만 읽고 종료'''하며, 이는 short counts의 원인이 된다. 위에서 1, 3번은 short counts의 가장 중요한 원인 중 하나이며, 다른 <code>read()</code>함수가 충분하지 못한 양을 읽는 이유도 대부분 short counts와 관련이 있다. 하지만 <code>rio_readn()</code> 함수의 구현 코드를 보면 알 수 있듯이 해당 함수는 short counts가 발생할 시 <code>read()</code>함수를 다시 호출하여, short_counts로부터 자유로워지도록 한다. 아래는 <code>rio_readn()</code>과 <code>rio_writen()</code> 함수를 어떻게 구현하였는지 보여준다: <syntaxhighlight lang="cpp"> ssize_t rio_writen(int fd, void *usrbuf, size_t n) { size_t nleft = n; //버퍼에 저장해야할 바이트 수 ssize_t nwritten; //버퍼에 저장한 바이트 수 char *bufp = usrbuf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { /* write() 함수도 일부만 쓸 수 있으므로 반복하여 호출한다. */ if (errno == EINTR) /* 시그널 핸들러로 인해 인터럽트된 경우 */ nwritten = 0; /* 다시 write 호출 */ else return -1; /* 그 외 오류일 경우 에러 리턴 */ } nleft -= nwritten; bufp += nwritten; } return n; } </syntaxhighlight> 위 코드는 사용자 버퍼 usrbuf의 내용을 fd에 정확히 n 바이트만큼 쓰는 작업을 수행한다. 이때 <code>write()</code>함수가 일부분만 쓸 수 있기 때문에 반복하여 호출된다. 이때 시그널 인터럽트(EINTR)가 발생하면 쓰기 작업을 재시도한다. write는 EOF 개념이 없기 때문에 EOF의 경우는 고려하지 않는다.<br> <code>write()</code>함수가 요청받은 바이트 수 만큼 한번에 쓰지 못하는 경우는 주로 시스템 버퍼가 다 찼을 때 발생한다. OS가 write 요청을 받으면 쓸 데이터를 커널 내부의 출력 버퍼에 저장한다. 하지만 네트워크 소켓이나 파이프의 출력 버퍼는 용량이 제한적이므로, 요청한 쓸 바이트 크기보다 현재 남아있는 출력 버퍼의 크기가 작은 경우, 해당 버퍼의 크기까지만 쓰고 리턴할 수 있다. 하지만 위 코드를 보면 알 수 있듯이, <code>rio_writen()</code>함수는 이러한 문제에서 자유롭다. 즉, 특별한 에러가 발생하지 않는 이상, 요청받은 n 바이트를 모두 쓰는 것을 보장한다. ===Buffered RIO Input Functions=== Buffered RIO 입력 함수들은 내부 메모리 버퍼에 부분적으로 캐시된 파일로부터 텍스트 라인과 binary 데이터를 효율적으로 읽는 함수들이다. 아래는 buffered RIO 입력 함수들이다: <syntaxhighlight lang="cpp"> #include "csapp.h" void rio_readinitb(rio_t *rp, int fd); //Returns: nothing //Returns: number of bytes read if OK, 0 on EOF, −1 on error ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen); ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n); </syntaxhighlight> <code>rio_readinitb(rio_t *rp, int fd)</code> 함수에서 rp는 내부에 입력 버퍼와 상태 정보를 가지는 인자이다. 이 함수를 호출할 경우에는 내부 버퍼를 초기화하고 디스크립터인 fd와 해당 버퍼를 연결시키는 역할을 한다. 이는 <code>rio_readlineb</code>나 <code>rio_readnb</code> 함수를 사용하기 위해서는 어떤 디스크립터로부터 읽을 지와, 버퍼를 어디에 두어야 할지를 지정해야 하기 때문에 사용된다.<br> <code>rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)</code> 함수는 다음 한줄을 읽고, usrbuf에 이를 복사한다. 이때 문자열의 끝에는 항상 '\0' 문자를 붙여 C 문자열 처럼 usrbuf를 다룰 수 있도록 한다. 이때, 복사하는 바이트의 수는 최대 <code>maxlen-1</code> 바이트로 제한되며, 그 이상의 줄은 잘리고, usrbuf의 끝 원소는 '\0'이 차지한다.<br> <code>rio_readnb(rio_t *rp, void *usrbuf, size_t n)</code> 함수는 내부 버퍼에서 n 바이트까지 읽어서 usrbuf에 복사한다. 아래는 <code>rio_readinitb()</code> 함수가 어떻게 구현되는지를 보여준다. <syntaxhighlight lang="cpp"> void rio_readinitb(rio_t *rp, int fd) { rp->rio_fd = fd; rp->rio_cnt = 0; //read()로 rio_buf에 데이터를 채우면 rio_cnt는 그 바이트 수만큼 설정된다.(내부 버퍼 내의 읽을 정보량) rp->rio_bufptr = rp->rio_buf; //내부 버퍼인 rio_buf 안에서, 다음에 읽을 위치를 가리킨다.(내부 버퍼 내의 현재 파일 위치) } </syntaxhighlight> 위 코드에서는 빈 읽기 버퍼를 설정하고, 파일 디스크립터를 해당 버퍼와 연결하는 역할을 한다. 또한 <code>rio_readnb()</code>와 <code>rio_readnb()</code> 함수가 어떻게 구현되는지 알기 위해서는 먼저 <code>rio_read()</code> 함수가 어떻게 구현되는지에 대해서 먼저 알아야 한다. <syntaxhighlight lang="cpp"> static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) { int cnt; while (rp->rio_cnt <= 0) { // "cnt <= 0"는 내부 버퍼에 읽을 데이터가 없다는 것을 의미 rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,sizeof(rp->rio_buf)); //read()로 디스크립터에서 rio_buf에 읽어옴 if (rp->rio_cnt < 0) { //read() 함수가 실패한 경우 if (errno != EINTR) //시그널로 인터럽트된 경우 (EINTR) → 무시하고 다시 시도. return -1; } else if (rp->rio_cnt == 0) //read()가 0을 반환하면 → EOF, 더 이상 읽을 게 없음. return 0; else rp->rio_bufptr = rp->rio_buf; //성공적으로 읽었다면, 내부 포인터를 버퍼의 시작 위치로 초기화 } /* min(n, rp->rio_cnt) 만큼 바이트들을 내부 버퍼에서 usrbuf로 옮긴다. */ cnt = n; if (rp->rio_cnt < n) cnt = rp->rio_cnt; memcpy(usrbuf, rp->rio_bufptr, cnt); rp->rio_bufptr += cnt; //rp의 내부 버퍼의 현재 파일 위치를 복사한 바이트 수 만큼 이동시킨다. rp->rio_cnt -= cnt; //rp의 내부 버퍼의 현재 파일 위치를 복사한 바이트 수 만큼 읽을 수 있는 바이트 수는 줄어든다. return cnt; } </syntaxhighlight> 위 함수를 보면 알 수 있듯이, <code>rio_read()</code> 함수는 <code>read()</code> 함수의 버퍼링된 버전이다. 따라서 해당 함수는 short counts를 반환할 수 있으며, 이는 오류가 아니다. 단지 해당 버퍼 내에 남아있는 바이트의 수(rio_cnt)가 요청받은 바이트 수보다 부족했음을 의미할 뿐이다. 응용 프로그램 입장에서는 <code>rio_read()</code> 함수는 Linux의 <code>read()</code> 함수와 동일한 의미를 가진다: * 에러 시 -1을 반환하고 errno를 설정한다. * EOF 시 0을 반환한다. * 요청한 바이트 수가 버퍼에 남은 바이트 수를 초과할 경우에는 short counts를 반환한다. 두 함수가 유사하므로, <code>read</code>로 대체하여 다양한 종류의 buffered 입력 함수들을 쉽게 구축할 수 있다. 아래는 <code>rio_readnb()</code> 함수를 구현한 코드이다: <syntaxhighlight lang="cpp"> ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) { size_t nleft = n; ssize_t nread; char *bufp = usrbuf; while (nleft > 0) { if ((nread = rio_read(rp, bufp, nleft)) < 0) return -1; /* errno set by read() */ else if (nread == 0) break; /* EOF */ nleft -= nread; bufp += nread; } return (n - nleft); /* Return >= 0 */ } </syntaxhighlight> 위 코드에서 알 수 있듯이, <code>rio_readnb()</code> 함수는 구조상으로 <code>rio_readn()</code> 함수와 동일하다. 다만 내부 버퍼에 파일의 내용을 저장한 뒤, 내부 버퍼에서 해당 내용을 끌어 쓸 뿐이다. 아래는 <code>rio_readlineb()</code> 함수를 구현한 코드이다: <syntaxhighlight lang="cpp"> ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) { int n, rc; char c, *bufp = usrbuf; for (n = 1; n < maxlen; n++) { //최대 maxlen-1 바이트 까지만 읽음 if ((rc = rio_read(rp, &c, 1)) == 1) { *bufp++ = c; if (c == '\n') { //개행 문자를 만나면 반복문 종료 n++; break; } } else if (rc == 0) { /* 예외 케이스 처리 */ if (n == 1) return 0; /* 완전한 EOF이므로 0을 반환함 */ else break; /* 몇몇 바이트를 읽었으나, 더 이상 읽을 데이터가 없음 */ } else return -1; /* Error */ } *bufp = 0; //usrbuf의 마지막 원소는 항상 '\0' 문자 사용 return n - 1; //실제로 읽은 바이트의 수를 반환 } </syntaxhighlight> 마찬가지로 <code>rio_readlineb()</code> 함수 또한 거의 동일한 메커니즘을 사용하는 것을 볼 수 있다. 다만 <code>read_nb()</code> 함수와 마찬가지로, 내부 버퍼에 파일의 내용을 저장한 뒤, 내부 버퍼에서 해당 내용을 끌어 쓸 뿐이다. 이때 궁금증이 들 수 있다. <code>rio_readn()</code>과 <code>rio_readnb()</code>는 사실상 같은 역할을 하는 함수이지만, 왜 같은 일을 하는 함수가 두 개나 존재하는가? 핵심적인 차이는 내부 버퍼를 사용하는지의 여부이다. <code>rio_read()</code> 함수는 내부 버퍼를 사용하지 않고, 데이터를 읽고자 할 때마다 매번 <code>read()</code> 함수를 호출한다. 따라서 해당 함수는 '''stream에서 연속적으로 binary 데이터를 다룰 때 적함한 함수'''이다. 하지만, <code>rio_readbn()</code> 함수는 내부 버퍼를 따로 가지고 있으며, 사용자가 원하는 만큼 꺼내 사용하는 함수이다. 따라서 <code>read()</code> 함수는 오직 내부 버퍼를 채울 때만 수행된다. 따라서, 해당 함수는 '''텍스트 라인과 binary 데이터가 동시에 사용된 파일을 다룰 때 유리하다. 또한 해당 함수는 caching을 사용하므로 더욱 빠르다는 장점이 있다. 이때, '''bufferd 입력 함수는 unbufferd 함수와는 섞어서 사용할 수 없다.''' 그 이유는 buffered 함수가 내부 버퍼를 다룰 때 이미 fd를 이용하여 파일에 접근하므로, unbuffered 함수가 동작할 때는 현재 파일 위치가 예상치 못하게 변해 있을 수 있기 때문이다. ==각주== [[분류:컴퓨터 시스템]]
System-Level I/O
문서로 돌아갑니다.
둘러보기
둘러보기
대문
최근 바뀜
임의의 문서로
미디어위키 도움말
위키 도구
위키 도구
특수 문서 목록
문서 도구
문서 도구
사용자 문서 도구
더 보기
여기를 가리키는 문서
가리키는 글의 최근 바뀜
문서 정보
문서 기록