# I/O 장치
## 시스템 구조
![시스템 구조 모형](./assets/시스템%20구조%20진호.png)
- 메모리 버스: cpu와 주 메모리, 범용 I/O 버스의 데이터 통신을 위한 통로
- 범용 I/O 버스: 그래픽 카드와 디스크, 키보드와 같은 입출력 장치와 연결
## 표준 장치
![표준장치](./assets/표준장치%20진호.png)
표준 장치는 인터페이스와 내부 구조로 구성, 인터페이스는 상호작용에 사용되고 내부 구조는 칩들과 내부동작을 정의하는 펌웨어(firmware)로 구성
## 표준 방식
표준 장치의 인터페이스는 상태(status), 명령(command), 데이터(data) 레지스터로 구성되어있다.
- 상태(status): 장치의 현재 상태를 저장
- 명령(command): 장치가 특정 동작을 수행하도록 요청
- 데이터(data): 장치에 데이터를 송수신할 때 사용
3개의 레지스터를 사용해서 운영체제는 장치의 동작을 제어할 수 있다. 운영체제는 표준 장치의 인터페이스에만 접근하면 내부 기능을 사용할 수 있다.
```c
while(STATUS == BUSY) // 장치가 바쁜 상태가 아닐 때 까지 대기
    ;  
DATA = data; // 데이터를 DATA 레지스터에 쓰기
COMMAND = command;  // 명령어를 COMMAND 레지스터에 쓰기
while(STATUS == BUSY)   // 요청을 처리하여 완료될 때 까지 대기
    ;
```
### I/O 요청 방식
1. i/o 장치의 status 레지스터를 폴링하여 사용가능한 상태인지 확인
2. Programmed I/O: 운영체제가 데이터 레지스터에 데이터 전달
3. 운영체제가 명령 레지스터에 명령어 기록
4. 디바이스가 요청을 처리했는지 폴링하여 확인

표준 방식은 제대로 작동하지만 매우 비효율적
- 폴링 방식의 비효율성
- 대기 시간에 아무것도 하지 않기 때문에 cpu 시간 낭비

## 인터럽트를 이용한 cpu 오버헤드 개선
디바이스를 폴링하는 대신 인터럽트를 사용하여 성능 개선
- 인터럽트 서비스 루틴(Interrupt Service Routing, ISR): 디바이스가 작업을 끝내면 하드웨어 인터럽트를 발생시킴 이때 수행할 명령어 실행, 인터럽트 핸들러라고도 한다. 운영체제 코드의 일부
![interrupt](./assets/디스크%20인터럽트%20진호.png)
위 그링메서 처럼 디스크 i/o가 실행중일 때 cpu는 다른 작업을 처리할 수 있다.

위 그림을 보면 인터럽트의 방식이 최적의 해법처럼 느껴진다. 하지만 인터럽트 방식이 항상 좋은 방식은 아니다. 한번의 폴링만으로 끝나는 매우 빠른 장치가 있다면 인터럽트를하는게 오히려 문맥교환으로 느려지게 된다.

책에서는 장치의 속도를 알 수 없거나 처리 속도의 차이가 크다면 짧은 시간동안에는 폴링을 하고 그 뒤에는 인터럽트르 사용하는 하이브리드 방식을 추천한다.

네트워크 환경에서는 인터럽트 방식을 사용하지 않는 것이 좋다. 패킷이 분할되어 전송 될 때 각 패킷마다 인터럽트가 발생하기 때문에 운영체가 인터럽트만 처리하게 되어 **무한반복(livelock)** 에 빠질 가능성이 있다.

### 병합(coalescing)
cpu에 인터럽트를 전달하기 전에 잠시 기다렸다가 인터럽트를 발생시킨다. 기다리는 동안 다른 요청이 끝나기 때문에 여러번 인터럽트를 발생시기는 대신 한번의 인터럽트만 cpu에 전달하면 된다. 이를 통해서 오버헤드를 줄일 수 있다.

## DMA를 이용한 효율적인 데이터 이동
![disk pio](./assets/disk%20copy%20jinho.png)
많은 양의 데이터를 디스크로 전달하기 위해서 Programmed I/O (PIO)를 사용하면 cpu가 단순 작업 처리에 소모된다. 
위 그림에서 보면 프로세스 1이 디스크에 데이터를 기록하기 위해서 c(copy) 작업을 싫애하는 동안 다른 프로세스를 처리하지 못하는 현상을 볼 수 있다. 이 문제를 해결하기 위해서 직접 메모리 접근 방식(Direct Memory Access, DMA)를 사용할 수 있다. DMA 엔진은 시스템 내에 있는 특수 장치로 cpu의 간섭없이 메모리와 장치간에 전송을 담당한다.

![dma](assets/DMA%20jinho.png)
DMA를 사용하면 운영체제는 DMA 엔진에 메모리 상의 데이터 위치와 전송할 데이터의 크기, 대상 장치를 전달한다. 주소를 바로 전달해주었기 때문에 cpu는 바로 다른 프로세스의 작업을 수행할 수 있다.

## 디바이스와 상호작용하는 방법
1. I/O 명령을 명시적으로 사용
운영체제가 특정 장치 레지스터에 데이터를 전송할 수 있는 방법을 제공한다. 명령어는 대부분 **특권** 명령어로 운영체제가 장치를 제어하는 역할을 한다. 그래서 운영체제만이 장치들과 직접 통신할 수 있다. 
2. Memory Mapped I/O 사용
   하드웨어는 장치의 레지스터들이 메모리 상에 존재하는 것처럼 취급된다. 특정 레지스터에 접근하기 위해서 운영체제는 해당 주소에 읽기, 쓰기를 하면된다. 
## 디바이스 드라이버
디바이스 드라이버는 디바이스를 추상화하여 사용자에게 제공해주는 역할을 한다. 
![file systemm stack](assets/파일%20시스템%20스택%20진호.png)
각 계층에서의 상위 사용자는 하의 레이어의 인터페이스만 사용하여 원하는 기능을 수행할 수있다. 각각의 레이어는 캡슐화 되어있기 때문에 어떻게 동작하는지는 알수없다. 

이러한 캡슐화는 단점도 가지고 있는데 특수 기능을 가지고 잇는 디바이스가 있을 때 커널이 범용적인 인터페이스만을 제공하면 특수 기능을 사용할 수 없게 된다. 

## 간단한 IDE 디스크 드라이버
실제로 동작하는 IDE 드라이버인 xv6의 예시를 통해서 디스크 드라이버를 이해해보자.ide 디스크는 시스템에 control, command block, status, error 이렇게 4가지의 단순한 인터페이스를 제공한다. 
- 장치가 준비될 때까지 대기: 드리이브가 사용중이지 않고 ready 상태가 도리 때가지 status를 읽는다.
- command 레지스터에 인자 값 쓰기: 섹터의 수와 접근해야할 섹터들의 논리 블럭 주소, 드라이브 번호를 command 레지스터에 기록한다.
- I/O 시작: command 레지스터에 읽기/쓰기를 전달한다. read-write 명령어를 command 레지스터에 기록한다.
- (쓰기의 경우) 데이터 전송: 드라이브의 상태가 ready이고 DRQ(drive request for data, 데이터를 위한 드라이브 요청)일 때까지 기다린다. 데이터 포트에 데이터를 기록한다.
- 인터럽트 처리: 가장 간단하게는 각 섹터가 전송되었을 때마다 인터럽트를 처리하게 하고 좀 더 복잡한 방법은 일괄처리가 가능하도록 만들어서 모든 전송이 완료되었을 때 최종적으로 한번만 인터럽트를 발생시키도록 한다.
- 에러 처리: 각 동작 이후에 status 레지스터를 읽는다. 만약 error 비트가 설정되어 있다면 error 레지스터를 읽어서 상세 정보를 확인한다.

### 기본 함수
- ide_rw(): 대기중인 다른 요청이 있다면 요청을 큐에 삽입하거나 디스크에 직접 명령
- ide_start_request(): 요청을 디스크로 내려보낸다.
- ide_wait_ready(): 요청을 명령하기 전에 드라이브가 준비가 되었는지 확인한다.
- ide_intr(): 인터럽트가 발생했을 때 호출

# 파일과 디렉터리
- 파일: 읽거나 쓸 수 있는 바이트의 배열, 각 파일은 저수준의 이름(아이노드 넘버)을 갖고 있음
- 디렉터리: 저수준의 이름을 가짐, 파일과 달리 디렉터리의 내용은 구체적으로 정해져 있음. 디렉터리 내에 다른 디렉터리를 포함하여 디렉터리 트리를 구성할 수 있다.
## 파일 시스템 인터페이스
open 시스템 콜을 사용하여 파일을 생성할 수 있다. 
```c
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
```
O_CREAT는 파일을 생성하는 플레그이고, O_WRONLY는 파일이 열렸을 때 쓰기만 가능하도록 하는 플래그이다. O_TRUNC를 사용해 파일이 이미 존재할 때는 파일의 크기를 0 바이트로 줄여서 기존 내용을 모두 삭제하게 한다.

open()은 파일 디스크립터를 리턴한다.

## 파일의 읽기와 쓰기
![trace cat](assets/trace%20cat%20jinho.png)
1. foo 파일을 읽기전용, 64비트 오프셋으로 연다.
2. read를 이용하여 몇 바이트식 반복적으로 읽는다.
3. write는 1번 파일 디스크립터에 읽어온 데이터를 쓴다. 
4. read는 추가적으로 더 읽어오지만 데이터가 없어 0을 리턴한다.
5. close()를 사용해서 종료한다.

## 비 순차적 읽기와 쓰기
문서의 인덱스를 만들고 특정 단어를 찾을 때 문서 내의 임의의 오프셋에서 읽기를 수행해야한다. lseek()라를 시스템 콜을 사용한다.

```c
off_t lseek(int fildes, off_t offset, int whence);
```
- fildes: 파일 디스크립터
- offset: 파일의 특정 위치
- whence: 탐색방식 결정

## fsync()를 이용한 즉시 기록
fsync()를 호출하게 되면 파일 시스템은 지정된 파일의 모든 더티 데이터를 디스크로 강제로 내려 보낸다. 
fsync()가 리턴하면 응용 프로그램은 데이터가 영속성을 갖게 되었다는 것을 보장한다.

## 파일 이름 변경
mv를 사용해서 파일의 이름을 변경하면 ```rename(char *old, char* new)```를 호출하는 것을 확인 할 수 있다. 해당 명령어는 시스템 그래시에 대해 원자적으로 구현되어있다.

## 파일 정보 추출
메타데이터: 파일에 대한 정보

파일 시스템은 각 파일에 대한 정보를 따로 저장하고 있으며 stat()이나 fstat()을 사용해서 확인할 수 있다.

## 파일 삭제
rm을 사용해서 파일을 삭제하면 unlink()라는 시스템콜을 호출하는 것을 확인할 수 있다. 

## 디렉터리 생성
디렉터리는 파일 시스템의 메타데이터로 분류되며 간접적으로만 변경된다. 
## 디렉터리 읽기
디렉터리는 ls 를 사용해서 읽을 수 있다. 
![ls](assets/ls%20jinho.png)
opendir(), readdir(), closedir()을 사용해서 단순히 디렉터리 항목을 하나씩 읽은 후에 파일 이름과 아이노드 번호를 출력한다.

## 디렉터리 삭제하기
rmdir()을 사용하면 디렉터리를 삭제할 수 있다.  파일 삭제와 다름 점은 디렉터리 삭제는 하나의 명령으로 많은 양이 데이터를 지울 수 있기 때문에 위험하다는 것이다. 

## 하드 링크
파일 삭제시 unlink()를 하는 이유는 link 때문에다. 월래 파일에 새로운 이름을 링크하면 동일한 파일에 접근할 수 있다. 그래서 파일을 삭제할때 unlink()를 해주어야한다. 

## 심볼릭 링크(소프트 링크)
심볼릭 링크는 하드 링크와 달리 다른 형식의 독립된 파일이다. 심볼릭 링크는 파일과 디렉터리 처럼 파일 시스템에 있는 유형이다. 심볼릭 링크는 포인터와 같이 작동하며 dangling reference가 발생하지 ㅏㅇㄴㅎ게 주의해야한다.

## 파일 시스템 생성과 마운트
여러 개의 파일 시스템 파이션들이 모여서 하나의 큰 디렉터리를 구성한다. 각각 파일 시스템을 생성하고, 이들을 "마운트" 함으로써 단일 디렉터리 트리를 구성한다. 

파일 시스템을 생성하는 mkfs를 사용하면된다. 

# 파일 시스템 구현
VSFS(Very Simple File System)

파일 시스템에 대해 학습할 때 두가지 측면에서 접근할 것을 권장한다.
1. 자료구조

파일 시스템이 자신의 데이터와 메타데이터를 관리하기 위해 디스크 상에 어떤 종류의 자료구조가 있어야할까?

2. 접근 방법

프로세스가 호출하는 명령어들은 파일 시스템의 자료구조와 어떤 관련이 있는지, 특정 시스템 콜을 실행할 때 어떤 자료구조를 읽히는지 고려해야안다.

## 전체 구성
vsfs는 디스크를 블럭 단위로 나누다. 여기서는 일반적으로 사용하느 4kb 단위로 나누었다. 사용자 데이터가 있는 디스크 공간을 데이터 영역이라고 한다. 메타데이터가 저장된 부분을 아이노드라고 하고 아이노드 테이블에 디스크에 저장된다.
## 디렉터리 구조
디렉터리는 (항목의 이름, 아이노드 번호) 쌍의 배열로 구성되어있다. 디렉터리의 데이터 블럭에는 문자열과 숫자가 쌍으로 존재하고 문자열 길이에 대한 정보도 가지고 있다. 
## 빈 공간의 관리
파일 시스템은 아이노드와 데이터 블럭 사용 여부를 관리해아한다. 빈공간 관리를 위해서 vsfs는 두개의 비트맵을 사용한다.
파일 생성시 아이노드를 할당해야 하기 때문에 파일 시스템은 아이노드 비트맵을 탐색하여 비어있는 아이노드를 찾아 파일에 할당한다. 

데이터 블럭 할당시에 기존에 할당된 공간에 이어서 블럭을 할당해야한다. 이를 위해서 연속적으로 여러개의 블럭들이 비어있는 공간을 할당하여 해당 파일에 대한 입출력 성능을 개선한다. 이를 선할달이라고한다.

## 읽기와 쓰기
### 디스크에서 파일 읽기
파일을 일긱 위해서 파일 시스템은 아이노드를 찾아야한다. 경로명을 따라가는 것은 항상 파일 시스템의 루트에서 시작한다. 
1. 파일 시스템은 경로명을 따라서 원하는 아이노드를 찾는다. 
2. 읽어들인 아이노드에서 데이터 블럭의 포인터를 추출한다. 
3. 포인터를 사용해서 디렉터리 정보를 읽어 원하는 파일을 찾는다.
4. 경로명을 따라가서 원하는 아이노드를 찾는다.
5. open()을 사용해서 아이노드를 메모리로 읽어들인다.
6. 파일에 대한 접근 권한을 확인하고 open file-table에서 파일 디스크립터를 할당받아 사용자에게 리턴한다.
### 디스크에 쓰기
1. 파일을 연다.
2. write()를 호출하여 새로운 내용으로 파일을 갱신한다.
3. 파일을 닫는다.
## 캐싱과 버퍼링
파일 시스템은 자주 사용되는 블럭들을 메모리(DRAM)에 캐싱한다. 캐싱을 하지 않으면 파일을 여는 동작은 디렉터리 레벨마다 최소 두번의 읽기가 필요하다(디렉터리 아이노드 일기, 디렉터리 데이터 읽기)

초기의 고정 크기 캐시는 전체 메모리의 약 10%를 차지하기 때문에 낭비가 크다.

현대의 시스템은 동적 파티션을 사용한다. 일원화된 페이지 캐시(unified page cache)를 사용한다. 필요에 따라 파일 시스템과 가상 메모리에 융통성있게 메모리를 할당할 수 있다.