[WEEK 07] 컴퓨터 시스템 - 10. 시스템 수준 입출력

신호정 벨로그·2021년 9월 22일
0

Today I Learned

목록 보기
33/89

입출력(I/O)은 메인 메모리와 디스크 드라이브, 터미널, 네트워크 같은 외부 장치들 간에 데이터를 복사하는 작업이다.

입력 연산은 데이터를 입출력 장치에서 메인 메모리로 데이터를 복사하고, 출력 연산은 데이터를 메모리에서 디바이스로 복사한다.

리눅스는 Unix I/O 모델에 기초해서 응용이가 파일을 open, close, read, write할 수 있도록 하는 적은 수의 시스템 수준 함수를 제공한다. 그리고 I/O 재지정을 수행할 수 있게 해준다.

리눅스의 읽기와 쓰기 연산은 응용이가 예상하고 정확히 처리해야 하는 짧은 카운트가 발생할 수 있다.

Unix I/O 함수들을 직접 호출하는 대신, 응용이는 RIO 패키지를 사용해야 한다.

RIO 패키지는 모든 요청한 데이터가 전송되었을 때까지 읽기와 쓰기 연산을 반복 수행해서 자동으로 짧은 카운트를 처리한다.

10.1 Unix I/O

리눅스에서 파일은 연속된 m개의 바이트다. (B0, B1, ..., Bk, ..., Bm-1)

네트워크, 디스크, 터미널 같은 모든 I/O 디바이스들은 파일로 모델링되며, 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수행된다.

  1. 파일 열기: 응용이는 I/O 디바이스에 접근하겠다는 의도를 해당 파일을 열겠다고 커널에 요청하는 방법으로 알린다. 커널은 식별자(descriptor)라고 하는 작은 비음수를 리턴하며, 이것을 이후의 파일에 관한 모든 연산에서 이 파일을 나타낸다.

커널은 열린 파일에 관한 모든 정보를 추적한다. 응용이는 식별자만을 추적한다.

리눅스 쉘이 만든 각 프로세스는 세 개의 열린 파일을 가지고 동작한다: 표준 입력(식별자 0), 표준 출력(식별자 1), 표준 에러(식별자 2). 헤더 파일 <unistd.h>는 상수 STDIN_FILENO, STDOUT_FILENO 및 STDERR_FILENO를 정의하고 있으며, 이들은 명시적인 식별자 값들 대신에 사용될 수 있다.

  1. 현재 파일의 위치 변경: 커널은 파일을 열 때마다 파일 위치 k를 관리하며, 이것은 처음에는 0이다. 파일 위치는 파일의 시작 부분에서부터의 바이트 오프셋이다. 응용프로그램은 seek 연산을 수행해서 현재의 파일 위치를 명시적으로 설정할 수 있다.

  2. 파일 읽기와 쓰기: 읽기 연산은 현재 파일 위치 k에서 시작해서 n > 0 바이트를 파일에서 메모리로 복사하고, k를 n 증가시킨다. 크기가 m바이트인 파일이 주어졌을 때, k >= m인 읽기 연산을 수행하면 end-of-file EOF라고 알려진 조건이 발생한다.

마찬가지로, 쓰기 연산은 현재 파일 위치 k에서 시작해서 n > 0 바이트를 메모리에서 파일로 복사하고, k를 갱신한다.

  1. 파일 닫기: 응용이가 파일 접근을 끝마치면, 커널에 파일을 닫아줄 것을 요청한다. 커널은 파일을 열었을 때 만든 자료구조들을 반환하는 것으로 대응하며, 식별자를 가용 식별자 풀로 복원한다. 프로세스가 어떤 이유에서든 종료할 때, 커널은 모든 열려 있는 파일들을 닫고, 이들의 메모리 자원을 반환한다.

10.2 파일

각각의 리눅스 파일은 시스템에서의 역할을 나타내는 타입을 가진다.

  1. 일반 파일: 일반 파일임의의 데이터를 포함한다.

리눅스 텍스트 파일텍스트 라인들로 구성되며, 각각의 라인은 새 줄 문자('\n')로 종료된다. 새 줄 문자는 아스키의 줄 채우기 문자(LF)와 같으며, 숫자 값 0x0a를 갖는다.

  1. 디렉토리: 디렉토리링크들의 배열로 구성되며, 각각의 링크는 파일 이름을 파일로 대응시키며, 이 이름은 또 다른 디렉토리일 수도 있다. 각각의 디렉토리는 최소 두 개의 항목을 포함한다. .은 자신의 디렉토리로의 링크이고, ..은 디렉토리 계층구조에서 부모 디렉토리로의 링크다.

mkdir 명령으로 디렉토리를 만들 수 있고, ls 명령으로 디렉토리의 내용을 볼 수 있으며, rmdir 명령으로 디렉토리를 삭제할 수 있다.

  1. 소켓: 소켓네트워크 상의 다른 프로세스와 통신하기 위해 사용되는 파일이다.

10.3 파일 열기와 닫기

/* 파일 열기와 닫기 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);

open 함수는 filename을 파일 식별자로 변환하고 식별자 번호를 리턴한다. 리턴된 식별자는 항상 프로세스 내에서 현재 열려 있지 않은 가장 작은 식별자이다.

flags 인자는 어떻게 프로세스가 파일에 접근하는지를 나타낸다.

  1. O_RDONLY: Reading only
  2. O_WRONLY: Writing only
  3. O_RDWR: Reading and writing

mode 인자는 새 파일들의 접근 권한 비트들을 명시한다.

10.4 파일 읽기와 쓰기

응용이는 read와 write 함수를 호출해서 읽기와 쓰기를 수행한다.

/* 파일 읽기와 쓰기 */
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t n);

ssize_t write(int fd, const void *buf, size_t n);

read 함수는 식별자 fd의 현재 파일 위치에서 최대 n바이트를 메모리 위치 buf로 복사한다. 리턴 값 -1은 에러를, 0은 EOF를 나타낸다. 그렇지 않으면, 리턴 값은 실제로 전송한 바이트 수를 나타낸다.

write 함수는 매모리 위치 buf에서 식별자 fd의 현재 파일 위치로 최대 n바이트를 복사한다.

size_t와 ssize_t의 차이는 무엇인가?

read 함수는 size_t 입력 인자를 가지며, ssize_t를 리턴 값으로 가진다.

x86-64에서 size_t는 unsigned long으로 정의되고, ssize_t(signed size)는 long으로 정의된다.

read 함수는 비부호형 크기 대신에 부호형 크기를 리턴하는데, 그 이유는 에러 발생 시에 -1을 리턴해야 하기 때문이다.

10.5 RIO 패키지를 이용한 안정적인 읽기와 쓰기

RIO(Robust I/O) I/O 패키지는 짧은 카운트를 자동으로 처리한다. RIO 패키지는 짧은 카운트가 발생할 수 있는 네트워크 프로그램 같은 응용에서 편리하고, 안정적이고 효율적인 I/O를 제공한다.

  1. 버퍼 없는 입력 및 출력 함수: 이 함수들은 메모리와 파일 사이에 응용 수준의 버퍼링 없이 직접 데이터를 전송한다. 이들은 특히 네트워크에서 바이너리 데이터를 읽고 쓸 때 유용한다.

  2. 버퍼를 사용하는 입력 함수: 이 함수들은 텍스트 라인들과 내용이 응용 수준 버퍼에 캐시되어 있는 파일의 바이너리 데이터를 효율적으로 읽도록 해준다.

10.5.1 RIO 버퍼 없는 입력 및 출력 함수

응용이는 rio_readn과 rio_writen 함수를 호출해서 메모리와 파일 간에 직접 데이터를 전송할 수 있다.

/* RIO 버퍼 없는 입력 및 출력 함수 */
#include "csapp.h"

ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);

rio_readn 함수는 식별자 fd의 현재 파일 위치에서 메모리 위치 usrbuf최대 n바이트를 전송한다. rio_readn 함수는 EOF를 만나면 짧은 카운트만을 리턴한다.

rio_writen 함수는 usrbuf에서 식별자 fd로 n바이트를 전송한다. rio_writen 함수는 절대로 짧은 카운트를 리턴하지 않는다.

10.5.2 RIO 버퍼를 통한 입력 함수

어떤 텍스트 파일 내 텍스트 라인의 수를 세는 프로그램을 작성하는 경우 텍스트 라인 전체를 내부 읽기 버퍼에서 복사하는 래퍼 함수(rio_readlineb)를 호출하는 것으로, 이것은 버퍼가 비워지게 될 때마다 버퍼를 다시 채우기 위해 자동으로 read를 호출한다.

/* RIO 버퍼를 통한 입력 함수 */
#include "csapp.h"

void rio_readinitb(rio_t *rp, int fd);

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);

rio_readinitb 함수는 식별자 fd주소 rp에 위치rio_t 타입의 읽기 버퍼와 연결한다.

rio_readinitb 함수는 한 개의 빈 버퍼를 설정하고, 이 버퍼와 한 개의 오픈한 파일 식별자를 연결한다.

rio_readlineb 함수는 다음 텍스트 줄을 파일 rp(종료 새 줄 문자를 포함해서)에서 읽고, 이것을 메모리 위치 usrbuf로 복사하고, 텍스트 라인을 NULL 문자로 종료시킨다.

readline은 버퍼를 제공하지만, readn은 그렇지 않기 때문에 이 두 함수는 같은 식별자에서 함께 사용될 수 없다.

rio_t 구조체

/* rio_t 구조체 */
#define RIO_BUFSIZE 8192

typedef struct {
    /* Descriptor for this internal buf */
    int rio_fd;
    /* Unread bytes in internal buf */
    int rio_cnt;
    /* Next unread byte in internal buf */
    char *rio_bufptr;
    /* Internal buffer */
    char *rio_buf[RIO_BUFSIZE];
} rio_t;

10.6 파일 메타데이터 읽기

응용이는 파일에 관한 정보를 stat과 fstat 함수를 호출해서 가져올 수 있다.

  1. S_ISREG(m): Is this a regular file?

  2. S_ISDIR(m): Is this a directory file?

  3. S_ISSOCK(m): Is this a network socket?

10.9 I/O 재지정

리눅스 쉘은 표준 입력 및 출력을 디스크 파일과 연결할 수 있도록 해주는 I/O 재지정 연산자를 제공한다.

/* dup2 함수 */
#include <unistd.h>

int dup2(int oldfd, int newfd);

dup2 함수는 식별자 테이블 엔트리의 이전 내용을 덮어써서 식별자 테이블 엔트리 oldfd식별자 테이블 엔트리 newfd로 복사한다.

10.10 표준 I/O

/* 표준 I/O */
#include <stdio.h>

/* Standard input (descriptor 0) */
extern FILE *stdin;
/* Standard output (descriptor 1) */
extern FILE *stdout;
/* Standard error (descriptor 2) */
extern FILE *stderr;

0개의 댓글