본문 바로가기
시스템 프로그래밍

I/O Systems and Operations

by sepang 2021. 11. 29.

  이번에는 입출력 시스템&입출력 작업에 대해 알아볼 것이다. 다음과 같은 순서로 구성된다.

fig 1

  • An Overview of I/O Subsystem and Operations
  • Stream Model: I/O functions, Standard streams
  • Buffering: Block buffering vs. Line buffering vs. Unbuffered
  • Pipes
  • File I/O: File pointer, File Attributes
  • Device I/O: Device Drivers, Block devices vs. Charater devices vs. Network devices

 

I/O (Input/Output)

  입출력은 컴퓨터의 주요 역할이다. 다른 작업인 컴퓨팅/처리는 부차적인 역할이다. 컴퓨터 I/O에서 OS의 역할은 장치를 제어하고 I/O 작업을 관리하는 것이다.

 

The Role of OS

I/O Device Control

   I/O 장치는 기능과 속도가 매우 다양하다. 이를 제어하기 위해서는 다양한 방법이 필요하다. 이러한 방법은 "OS 커널의 I/O 하위 시스템"을 형성하여 나머지 커널을 I/O 장치 관리의 복잡성과 분리한다.

  포트, 버스, 및 장치 컨트롤러와 같은 기본 I/O 하드웨어 요소는 다양한 I/O 장치를 수용한다.

fig 2.1

  • Port: 장치와 컴퓨터 사이의 연결 지점(ex. serial port)
  • Bus: 장치가 공유하는 공통 회선 세트 및 회선을 통해 보낼 수 있는 메세지 세트를 지정하는 엄격하게 정의된 프로토콜

  다른 장치의 세부 사항과 특이점을 캡슐화하기 위해 OS 커널은 장치 드라이버 모듈을 사용하도록 구성된다. 장치 드라이버는 시스템 콜이 애플리케이션과 OS간의 표준 인터페이스를 제공하는 것처럼 I/O 하위 시스템에 대한 균일한 장치 액세스 인터페이스이다.

fig 2.2

fig 2.2처럼 각 장치 드라이버가 커널에게 인터페이스 function을 제공하고 커널은 이를 통해 여러 입출력 장치를 이용할 수 있는 것이다.

I/O Operation Management

  입출력 작업 또는 처리는 프로그램이 소스(ex. 키보드, 마우스, 파일, 센서, ...)에서 바이트를 수신하거나 대상(ex. 디스플레이, 파일, 프린트, 액추에이터, ...)바이트를 보낼 때 발생한다. Unix/Linux를 포함한 대부분의 최신 OS는 "스트림 모델(stream model)"을 사용하여 입출력을 제어한다.

Stream Model

  모든 입출력 처리는 소스에서 대상으로의 바이트 흐름(=스트림)으로 볼 수 있다. OS는 프로그램이 만든 함수 호출을 기반으로 스트림을 생성하고 관리한다. 예를 들어 fopen() 함수를 사용하여 입출력 스트림 연결을 설정하고 fclose() 함수를 사용하여 끊을 수 있다.

FILE *fpt;
fpt = fopen("output.txt", "w");
fprintf(fpt, "This is a test.");
fclose(fpt);

  다음과 같은 코드에서 스트림의 생성~종료 과정을 표현하면 fig 2.3과 같다.

fig 2.3

스트림이 생성되면 바이트가 전송되는 주소와 바이트를 수신하는 주소가 있는 것으로 생각할 수 있다. 이때 각 주소는 OS에 의해 제어되는 메모리 위치에 있다.

 

Transporting Bytes on Streams

  스트림에서 바이트를 전송하는데 사용할 수 있는 몇 가지 함수가 있다. 예를 들어 ASCII 형식의 텍스트 데이터에서만 작동하도록 특수화된 fprintf(), fscanf()가 있다. 또는 바이트 전송의 일반적인 함수는 fread(), fwrite()이다. fprinft/fscanf 함수는 원하는 바이트 조작이 완료된 후 실제로 바이트를 전송할 때 fread/fwrite를 호출한다. 이외에도 fgetc(), fputc(, gets(), puts() 등의 많은 함수가 있다.

fprintf() & fscanf

fprintf()

fig 3.1

  다음과 같이 fprintf()를 사용하여 프로그램의 스트링을 file에 써줄수가 있다.

fscanf()

fig 3.2

  fscanf()에서는 반대로 파일의 스트링을 프로그램에 읽어올 수 있다. 이때 파일의 스트링을 ' '(공백)이나 '\n'(개행문자)로 구분하는 것을 볼 수 있다. 그리고 문자열의 끝을 알리기 위해 문자열을 읽어온 후 'NULL'을 붙여준다.

fig 3.3

  다음과 같은 경우에는 fscanf를 이용해 파일에서 읽어온 문자열을 정수형으로 변환한다.

  이처럼 fprintf(), fscanf()는 바이트 전송 뿐 아니라 해당 바이트를 아스키 코드로 다룰 수 있게끔 가공하는 역할도 한다. 

fread() & fwrite()

  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

  fread() 함수는 가리키는 스트림에서 데이터의 nmemb 요소(각 크기 바이트 길이)를 읽어 ptr이 지정한 위치에 저장한다. fwrite() 함수는 ptr이 지정한 위치에서 얻어온 데이터의 nmemb 요소를 읽어서 스트림에 쓴다. 해당 함수가 성공적으로 동작하면 fread()/fwrite()가 읽거나 쓴 항목의 수를 반환한다. 이는 실제로 스트림을 따라 이동한 바이트 수를 나타내는 표시이다. 오류가 발생하면 반환 값은 짧은 항목 수(or 0)이다.

fread()

fig 3.4

  다음은 fread()를 이용해 파일의 문자열을 프로그램에서 읽어온 것이다. fread()/fwrite() 함수는 바이트 전송에만 집중하는 함수이기 때문에 text 문자 배열에 내용을 읽어온 후 마지막에 'NULL'을 붙여줬다.

fwrite()

fig 3.5

  fwrite()의 매개변수는 fread()의 매개변수와 동일하다. fig 3.5 코드를 실행하면 "Fortytwo 42 ..."가 포홤된 data2.txt 파일이 생성된다. 이 예제에서는 strlen() 함수를 사용하여 스트림에 보낼 배열의 항목 수를 결정한다. 

System I/O Functions

  I/O에 사용할 수 있는 또 다른 함수들에는 open(), close(), read(), write()가 있다. 앞에서 본 f-버전(fopen, fclose, ...)과 매우 유사해 보인다. f-버전 함수들은 C 라이브러리에서 표준화 되어있기 때문에 어떤 OS인지 상관없이 유사하게 동작한다.

  반면에 non-f I/O 함수들은 시스템 콜이다. 함수는 맞는데 내부에서 해당하는 시스템 콜이 이뤄지기 때문이다. 또다른 차이점은 일반적으로 non-f 버전들은 버퍼링되지 않고, f 버전은 버퍼링 된다.

Standard Streams

  대부분의 프로그램은 키보드에서 사용자 입력을 받고 화면에 텍스트 출력을 표시한다. 따라서 프로그램이 시작될 때마다 OS는 자동으로 3개의 스트림을 생성한다.

  • 표준 입력(stdin): 키보드와 프로그램을 연결한다.
  • 표준 출력(stdout): 프로그램을 디스플레이에 연결한다.
  • 표준 에러(stderr): 오류 표시 전용 보조 디스플레이(일반적으로는 표준 입출력에서의 디스플레이와 동일)에 프로그램을 연결한다. 또한 stderr 스트림은 프로그램의 백업 출력 스트림으로 사용된다.

  표준 스트림은 scanf(), printf() 함수를 호출하여 가장 일반적으로 사용된다. fscanf()는 모든 스트림에서 바이트를 수신할 수 있지만 scanf()는 stdin 스트림에 고정되어 있다. 이는 print(), fprintf()에 관해서도 마찬가지이다.

 

Buffering

  버퍼는 스트림의 바이트의 발신 측과 수신 측 사이(두 장치 사이/장치와 프로그램 사이)임시 저장소이다. source에서 destination까지의 바이트 흐름을 조절하는데 사용되는 추가적인 메모리 조각이라고 할 수도 있다. 그리고 버퍼링은 다음과 같은 이유로 사용된다.

  • 장치 속도 불일치에 대응: 만약 데이터를 보내는 속도가 받아서 처리하는 속도보다 빠르다거나, 프로그램이 계산 중이라서 바이트를 수신할 준비가 되어 있지 않는 경우를 생각해보자.
  • 디바이스 데이터 전송 크기 불일치 대응: 버퍼는 메세지의 조각화와 재조립에 널리 사용되는 컴퓨터 네트워크에서도 자주 사용된다. 컴퓨터 네트워크에 대한 이해가 필요하긴 한데, 간단히 예를 들어 sender는 10bytes 데이터를 한번에 1byte씩 receiver에게 데이터를 보낼 수 있는데 receiver는 데이터를 처리하기 위해 온전한 10bytes 데이터가 필요하다고 할 때 1byte씩 오는 데이터를 버퍼에 저장할 필요가 있다.
  • 애플리케이션 I/O에 대한 '복사의 의미론'지원: 예를 들어 한 어플리케이션의 data block을 보조 기억 장치에 쓰고 싶을 때 write() 시스템 콜을 호출할 것이다. 그렇다면 커널에 data block에 대한 레퍼런스를 넘겨주게 되고 커널은 별도의 버퍼를 마련하여 해당 공간에 data block을 복사한 다음 이것을 보조 기억 장치에 쓴다. 이렇게 되면 어플리케이션의 data block이 이후에 수정되어도 보조 기억 장치에 저장된 내용은 변하지 않기에 '복사의 의미'에 부합하는 것이다.  

Type of Buffering

  버퍼링은 3가지 유형으로 나눠 볼 수 있는데 버퍼의 데이터들이 언제 비워지는지(flush)가 기준이다.

  • Block Buffering: 일정량의 데이터(1KB, 16KB, ...)를 받으면 버퍼가 flush된다. 일반적으로 파일 입출력 같은 대용량 데이터 전송에 사용된다.
  • Line Buffering: 개행 문자('\n')를 받으면 버퍼가 flush된다. 일반적으로 사용자와 상호 작용할 때와 같은 텍스트 기반 I/O에 사용된다.
  • Unbuffered: 버퍼는 버퍼 역할을 하지 않으며 매 바이트마다 flush된다. 응답성이 중요한 경우에는 버퍼링이 사용되지 않을 수 있다.

  기존 스트림의 버퍼링 유형은 setvbuf()setbuf() 함수를 사용하여 변경할 수 있다.

fig 4.1

  stdout은 line buffer를 가짐을 기억하고 첫번째 그림부터 보자. 결과가 어떤 패턴으로 출력될까? printf에서 개행이 이뤄지지 않기 때문에 프로그램 종료후에 한꺼번에 결과가 출력된다. 반면에 두번째 사진에서는 printf에서 개행 문자를 넣어줬다. 때문에 1초에 한줄씩 결과를 출력하게 된다. 3번째 그림에서는 1번과 유사하나 fflush()를 사용해주고 있다. 때문에 한번의 반복마다 강제로 버퍼를 flush한다.

 

Pipe

  파이프는 스트림이 다른 source 또는 desination에 다시 연결되는 경우에 사용된다. 그리고 스트림을 연결하거나 재연결하는 과정을 piping 또는 pipelining이라고 한다. 파이핑의 예는 동일한 바이트를 파일과 디스플레이에 동시에 전송하도록 표준 출력 스트림을 다시 연결하는 것이다. pipe() 및 dup()는 스트림 연결을 조작하는데 사용할 수 있는 I/O 함수(시스템 콜)이다.

Piping Symbols

  파이프라이닝에 사용되는 3가지 기호가 있다. OS는 실행 중인 모든 프로그램에 대해 3개의 스트림(stdin, stdout, stderr)을 자동으로 생성한다는 것을 좀 전에 설명했다. 대부분의 셸은 시작 시 해당 스트림을 리디렉션(redirection)하는 기능을 제공한다.

fig 5.1

  • <: '<' 뒤에 지정된 파일이 표준 입력으로 이동
  • >: 표준 출력이 '>' 뒤에 지정된 파일로 이동
  • |: '|'의 왼쪽의 표준 출력이 '|'의 오른쪽 프로그램의 표준 입력으로 이동

fig 5.2

  예시로 다음 프로그램을 보자. 컴파일 후 실행해보면 키보드로 정수를 입력받아 x에 저장한 뒤 이를 s에 더하는 것을 반복한다. 그럼 '4\n1\n1\n7\n0\n'라는 내용이 담긴 input.txt라는 파일이 있다고 한 뒤 프로그램을 다음과 같이 실행해보자

fig 5.3

'<'를 이용하여 표준 입력 대신 input.txt를 사용하였다. 때문에 키보드로 입력을 주지 않아도 다음과 같은 결과를 출력한다. 반대로 output.txt라는 빈 파일을 만든 뒤 프로그램을 다음과 같이 실행하자.

fig 5.4

'>'를 이용하여 표준 출력을 output.txt로 이동시켰다. 그러므로 output.txt파일을 확인해보면 fig. 5.3과 동일한 내용이 담겨있다.

 

Files

  이번에는 파일에 대해 알아보자. 굉장히 익숙한 단어지만 현재 과목에 적절하게 표현해보면 연속적인 논리적 주소 공간이라고 할 수 있다. 사진, 영상, 데이터베이스 등은 모든 요소를 긴 목록으로 작성하여 1차원의 바이트 배열로 저장할 수 있다.

fig 6.1

다음과 같이 파일은 어느 주소공간을 차지하고 있다. 파일을 이루는 데이터를 해당 공간에 일렬로 쭈르륵 놓은 것이다.

File Pointer

fig 6.2

  파일 포인터는 스트림에서 읽기/쓰기를 위해 위치를 추적하는데 사용되는 마커이다. 파일을 열 때 파일 포인터는 파일의 첫번째 바이트를 가리킨다. 바이트를 읽을 때마다 파일 포인터는 자동으로 다음 바이트로 이동한다. fig 6.2의 파일이 처음 열렸을 때는 파일 포인터가 0번 바이트, 그 다음은 1번 바이트, 만약 두 바이트를 더 읽었다면 3번 바이트를 가리킨다. 그리고 이러한 파일 포인터를 조작하기 위한 함수에는 다음이 있다.

  • fseek(): 스트림의 데이터를 읽거나 쓰지 않고 새 값으로 이동
  • ftell(): 현재 값을 얻음

fseek()

  • int fseek(FILE *stream, long offset, int whence)

  fseek() 함수는 steam이 가리키는 스트림에 대한 파일 위치 표시기를 설정한다. 바이트 단위로 측정된 새 위치는 whence로 지정된 위치에 offset 바이트를 추가하여 얻는다. whence에는 SEEK_SET, SEEK_CUR, SEEK_END가 들어갈 수 있는데 각각 파일의 시작, 현재 위치, 파일 끝을 뜻한다. 다시말해 stream이 가리키는 파일에서 whence로 기준을 정하고 거기서 offset 바이트만큼 떨어진 공간을 가리키는 것이다. 해당 함수가 성공적으로 완료되면 0반환, 오류 발생 시에는 -1을 반환한다.

fig 6.3

  다음 프로그램은 처음에 fig 6.2의 파일을 입력으로 받고 사용자가 숫자를 입력하면 입력 값만큼 바이트 단위로 떨어진 곳의 값을 출력하는 것이다. fread()를 이용해 현재 위치에서 한 바이트를 읽었는데 fseek을 이용하여 다시 원래 위치로 돌아가게 한 것을 확인할 수 있다.

File Attributes

fig 6.4

  파일 속성(file attributes)은 이름, 크기, 최근수정시간, 권한 등의 컴퓨터 파일과 관련된 메타데이터이다. fig 6.4에서는 다음과 같은 파일 속성들을 확인할 수 있다.

 저기서 권환(permission)은 파일에 액세스할 수 있는 사람과 허용되는 액세스 유형을 나타낸다. r은 읽기, w는 쓰기, x는 실행이다. UNIX 시스템에서는 chmod 프로그램(명령어)를 사용하여 이를 변경할 수 있다. fig 6.4에서는 permission 속성에 0~9칸이 있음을 볼 수 있는데 1~3은 owner, 4~6은 group, 6~9는 그 이외에 대한 접근 권한이다.

stat()

  • int stat(const char *pathname, struct stat *buf)

  파일에 대한 속성을 확인할 때 쓰인다. buf가 가리키는 버퍼에 파일에 대한 정보를 반환한다. 확인하려는 파일에 대한 권한은 필요하지 않지만 경로상에 있는 모든 디렉토리에 대해 실행(검색)권한이 필요하다. 성공적으로 실행되면 0 반환, 오류가 발생하면 -1을 반환한다. 그리고 stat 구조체는 fig 6.5 처럼 이루어져 있다.

fig 6.5

Directory

  디렉토리는 파일을 정리하는 도구이다. 폴더와 매우 유사하다. 디렉토리는 각 파일에 대한 이름 및 보조적인 정보들이 담긴 리스트이다. 그리고 OS는 파일을 관리하는 것과 동일한 방식으로 디렉토리를 관리한다. 폴더에 다른 폴더가 들어갈 수 있는 것처럼 말이다. opendir(), readdir(), closedir() 함수를 통해 디렉토리에 액세스할 수 있다.

 

Device

fig 7.1

  UNIX의 정돈된? 측면 중 하나는 컴퓨팅 시스템에 연결된 모든 주변 장치, 장치 또는 HW가 마치 파일인 것처럼 액세스 할 수 있다는 것이다. 이것을 파일 기반 I/O라고 한다. 장치와의 입출력 처리는 파일과의 I/O 처리와 유사하게 처리된다. 둘 다 스트림버퍼를 사용하고 동일한 I/O 기능을 사용한다.

Device Driver

  장치 드라이버는 장치에 접근하는데 사용되는 기능(open(), close(), read(), write(), lseek(), dup(), ...) 집합이다. 특정 장치와 관련된 기능은 장치 파일 이름을 통해 장치에 접근할 때 실행된다.

fig 7.2

  fig 7.2는 커널 I/O 구조 내의 장치 드라이버 계층을 나타낸 것이다. 각 장치 드라이버에서 HW의 디테일을 다루고 있기 때문에 커널 I/O 하위 시스템을 하드웨어와 독립적으로 만들 수 있다.. 때문에 OS 개발자는 커널 I/O 하위 시스템 밑의 계층은 신경 쓸 필요가 없으므로 업무가 간소화된다.

  대부분의 OS에는 애플리케이션에서 장치 드라이버를 제어할 수 있게끔 이스케이프(or 백도어)를 가진다. UNIX에서는 이를 ioctl()이라는 시스템 콜로 제공한다. 이는 애플리케이션이 새로운 시스템 콜을 만들 필요 없이 모든 장치 드라이버가 구현할 수 있는 기능에 액세스할 수 있다.

ioctl()

  • #include <sys/ioctl.h>
  • int ioctl(int fd, unsigned long request, ...);

  ioctl() 시스템 콜은 특수 파일의 기본 장치 매개변수를 조작한다. 특히 문자 특수 파일(ex. 터미널)의 많은 작동 특성은 ioctl() 요청으로 제어 가능하다.

  • 첫 번째 인수 fd는 열린 파일 디스크립터(file descriptor, 애플리케이션을 드라이버에 연결하는 장치 식별자)여야 한다.
  • 두 번째 인수는 장치 종속 요청 코드(=드라이버에서 구현된 명령 중 하나를 선택하는 정수)이다.
  • 세 번째 인수는 애플리케이션과 드라이버가 필요한 제어 정보 또는 데이터를 통신할 수 있도록하는 메모리의 임의 데이터 구조에 대한 포인터이다. 요청 명령에 사용될 argument라고 생각하면 된다.

 

Major/minor number

fig 7.3

   UNIX/Linux의 장치 식별자"major&minor"의 튜플이다. major number는 장치의 유형이며 OS에서 I/O 요청을 적절한 장치 드라이버로 라우팅하는데 사용된다. minor number는 해당 장치의 인스턴스이며 동일한 장치 드라이버 기능을 사용하는 여러 장치 파일 이름에 고유한 ID를 제공하는데 사용된다.

 

The Overall Structure of Device-Driver System

fig 7.4

  Linux에서는 모든 장치를 Block devices, Character devices, Network devices 세 가지 클래스로 나눈다.

  Block device는 디스크 드라이버처럼 바이트 단위의 블록을 전송한다. 때문에 read(), write(), seek()같은 인터페이스를 지원할 수 있어야한다.

  Character-stream device는 키보트, 마우스, 직렬 포트, 프린터 등의 장치가 있는데 데이터를 한 바이트씩 전송한다. 기본적으로 get(), put()을 이용하여 데이터를 읽거나 쓴다. 상단에 계층화된 라이브러리(ex. scanf(), printf(), ...)는 버퍼링 및 편집 서비스를 통해 한 번에 한 줄 씩 읽을 수 있다. 예를 들어 사용자가 백스페이스를 입력하면 입력 스트림에서 문자가 제거되는 것 처럼 말이다.

  Network devices에서 네트워크 I/O의 성능 및 주소 지정 특성은 디스크 I/O와 다르기 때문에 대부분의 OS는 디스크에 사용되는 read()-write()-seek() 인터페이스와 다른 네트워크 I/O 인터페이스를 제공한다. UNIX/Linux 및 Windows를 포함한 많은 OS에서 사용할 수 있는 하나의 인터페이스는 네트워크 소켓 인터페이스이다. 가정집 벽에 있는 전기 콘센트(모든 전기 제품을 꽂을 수 있음)처럼 소켓 인터페이스의 시스템 콜을 통해 애플리케이션은 다음을 수행할 수 있다.

  • 소켓 생성
  • 로컬 소켓을 원격 주소에 연결
  • 로컬 소켓에 연결할 원격 애플리케이션을 수신
  • 연결을 통한 패킷 송수신

 

Summary

  • 스트림: 모든 입력 또는 출력 처리는 바이트의 흐름
  • 버퍼: 발신자와 수신자 사이의 임시 저장소
  • 파이프: 스트림을 연결/재연결 과정
  • 파일: 1차원 바이트 배열로 저장된 파일
  • 장치: 장치는 파일처럼 액세스할 수 있음

자료 출처

  • Abraham Silberschatz, Peter Baer Galvin, and Greg Gagne, “Operating System Concepts (10th Edition) - Wiley 2019

'시스템 프로그래밍' 카테고리의 다른 글

IPC (Inter-Process Communication)  (0) 2021.12.26
Process and Thread Management  (0) 2021.12.25
OS Structures & Linux Overview  (0) 2021.11.12
Operating Systems(OS) Overview  (0) 2021.11.05
Linkers and Loaders  (0) 2021.11.01

댓글