Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.
이번 장에서는 하드 디스크 드라이브에 대해 더 자세히 알아보자. 하드 디스크 드라이브는 수십년 동안 가장 대표적인 영구적인 데이터 저장 장치로 쓰여 왔고, 파일 시스템 기술의 대부분도 이 드라이브의 동작에 기반하고 있다. 따라서 이런 디스크를 관리하는 파일 시스템 소프트웨어를 만들기 전에 디스크의 동작에 대한 세부적인 내용을 이해해두는 것이 좋다.
현대 하드 디스크 드라이브는 어떻게 데이터를 저장할까? 인터페이스는 어떨까? 데이터는 실제로 어떻게 저장되고 접근될까? 디스크 스케줄링은 어떻게 성능을 개선할까?
현대 디스크 드라이브의 인터페이스에 대한 이해부터 시작하자. 모든 현대 드라이브의 기본적인 인터페이스는 간단하다. 드라이브는 많은 수의 섹터(512B 블럭)들을 포함하고 있는데, 그 섹터 각각은 읽거나 쓰일 수 있다. 개의 섹터를 가지는 디스크의 섹터는 각각 에서 번으로 번호 매겨진다. 따라서 디스크는 섹터의 배열로 이해할 수 있고, 에서 은 드라이브의 주소 공간으로 생각할 수 있다.
다중 섹터 작업도 가능하다. 실제로 많은 파일 시스템들은 한 번에 4KB 이상의 데이터를 읽거나 쓸 수 있다. 하지만 디스크를 업데이트할 때, 드라이브 제작자들이 보장하는 것은 한 512 바이트의 쓰기가 원자적으로 이루어진다는 것 뿐이다. 그러므로 갑작스런 파워 손실이 발생한다면 대용량 쓰기의 일부분만 완료된다. 이를 가리켜 찢긴 쓰기(torn write)라 부른다.
대부분의 디스크 드라이브 사용자들이 가정하고 있는 것들이 있지만, 그것들은 인터페이스에 직접 명시되어 있지는 않다. Schlosser와 Ganger는 이를 디스크 드라이브의 "불문 계약"이라 불렀다. 구체적으로, 드라이브의 주소 공간에서 인접한 두 블럭에 접근하는 것은 떨어진 두 블럭에 접근하는 것보다 빠를 것이라 가정할 수 있다. 또한 연속된 청크의 블럭들에 접근하는 것이 가장 빠른 접근 모드이고, 다른 랜덤 접근 패턴보다 보통 더 빠를 것이라 가정할 수도 있다
현대 디스크의 몇 가지 구성 요소들에 대해 이해해보자. 시작은 플래터(platter)다. 이는 자기적 변화를 통해 데이터가 영구적으로 저장되는 단단한 원형 표면을 말한다. 디스크는 하나 이상의 플래터를 가지며, 각 플래터는 두 면의 표면(surface)을 가진다. 이 플래터는 보통 알루미늄과 같은 단단한 물질로 만들어져있고, 드라이브의 전원이 나가도 비트들을 영구적으로 저장할 수 있게 얇은 자성층으로 코팅되어 있다.
플래터는 스핀들(spindle)에 고정되어 있는데, 이들은 플래터를 일정한 고정 비율로 회전시키는 모터에 연결되어있다. 회전율은 보통 분당 회전 수(RPM, rotations per minute)로 계산되며, 현대 시스템에서는 7200~15000RPM이 전형적이다. RPM이 주어졌을 때, 한 번 회전하는 데에 걸리는 시간이 관심 대상이 되곤 한다는 것에 유의하자. 예를 들어 드라이브가 10,000 RPM으로 회전한다는 것은 한 번 회전하는 데 약 6ms의 시간이 걸린다는 것이다.
데이터는 섹터 동심원의 각 표면에 인코딩된다. 이러한 동심원을 가리켜 트랙(track)이라 부른다. 이런 트랙들을 수백 개는 모아야 사람의 머리털 두께가 되는데, 하나의 표면은 빽빽하게 붙어있는 수천 개의 트랙들로 이루어져 있다.
표면을 읽거나 쓰기 위해서는 그렇게 하기 위한 메커니즘이 필요하다. 이러한 읽고 쓰기는 디스크 헤드(disk head)에 의해 처리되는데, 드라이브에는 표면마다 하나의 헤드가 있다. 디스크 헤드는 하나의 디스크 암(disk arm)에 붙어있고, 이는 헤드를 원하는 트랙에 위치시키기 위해 표면을 이동한다.
이제 디스크가 어떻게 동작하는지를 이해하기 위해 한 번에 한 트랙씩 모델을 만들어보자. 하나의 트랙을 가지고 있는 간단한 디스크를 가정한다. 이 트랙은 각 512 바이트의 크기를 가지는 12개의 섹터들로 이루어져 있고, 이 섹터들은 0에서 11번으로 번호 매겨져 있다. 이 플래터는 모터가 달려있는 스핀들을 중심으로 회전한다.
물론 트랙은 그 자체로는 흥미롭지 않다. 읽기 쓰기 작업을 위해서는 디스크 암에 달린 디스크 헤드가 있어야 한다. 이 간단한 모델은 다음 그림과 같이 생겼다.
이런 간단한 모델에서 어떻게 요청이 처리되는지 이해하기 위해, 0번 블럭을 읽는 요청이 들어왔다고 가정해보자. 디스크는 이 요청을 어떻게 서비스해야할까?
간단한 모델에서 디스크는 사실 할 일이 많이 없다. 단지 원하던 섹터가 디스크 헤드 밑에 올 때까지 기다리면된다. 이러한 대기는 현대 드라이브에서 자주 일어나는 일로, 회전 지연(rotational delay)이라 부른다. 예제에서 한 바퀴를 다 도는 데에 걸리는 회전 지연을 이라 하면, 0번에 쓰거나 읽기 위해 필요한 회전 지연은 다. 최악의 경우는 5번 섹터에 써야하는 경운데, 해당 섹터를 디스크 헤드에 위치 시키기 위해서는 거의 한 바퀴를 돌아야 하기 때문이다.
지금까지는 디스크가 하나의 트랙만을 가지고 있었지만, 이는 현실적이지는 않다. 현대 디스크들은 수백만 개의 트랙을 가지고 있다. 따라서 이제 좀 더 현실적인 디스크 표면을 살펴보자. 이번에는 세 개의 트랙을 가지고 있다.
여기에서 헤드는 가장 안 쪽 트랙에 위치해있고, 다음 트랙은 다음 섹터 집합을 가지고 있다.
드라이브가 어떻게 주어진 섹터에 접근하는지를 이해하려면, 멀리 떨어진 섹터에의 요청이 일어났을 때 어떤 일이 일어나는지를 추적해봐야 한다. 예를 들어 11번 섹터에 접근해야 한다고 해보자. 이 섹터에 접근하려면 드라이브는 우선 디스크 암을 올바른 트랙으로 이동시켜야 한다. 이 과정을 가리켜 탐색(seek)이라 부른다. 탐색은 회전과 함께 디스크 작업에서 가장 비싼 작업 중 하나다.
탐색은 여러 단계로 이루어져 있다.
드라이브는 반드시 정확히 올바른 트랙에 위치해야하므로 안정화 시간(settling time)은 상당히 오래 걸릴 수 있다.
탐색 후 디스크 암은 올바른 트랙에 헤드를 위치시키게 된다. 위 그림의 오른쪽과 같다.
그림에서 보면 탐색 중 암은 원하던 트랙으로 이동했고, 플래터도 조금(3 섹터) 회전했다. 9번 섹터가 막 헤드의 아래를 지나가고 있으므로 전송을 마치려면 짧은 회전 지연만 견디면 된다.
11번 섹터가 디스크 헤드의 아래를 지나갈 때, 전송(transfer)이라 불리는 I/O의 마지막 단계가 일어난다. 이 단계에서는 표면을 읽거나 쓴다. 이제 I/O 시간에 대한 완전한 그림을 그릴 수 있게 됐다. 우선은 탐색하고, 회전 지연 동안 대기한 후, 전송한다.
많은 시간을 쏟지는 않을 것이지만, 어떻게 하드 드라이브가 동작하는지에 대한 다른 흥미로운 세부 사항들이 있다. 많은 드라이브들은 트랙 스큐(track skew)라 부르는 것을 이용해, 트랙 경계를 지나는 경우에도 순차적 읽기가 가능하게 한다.
섹터는 종종 이렇게 비틀려 있는데, 디스크는 한 트랙에서 다른 트랙으로 전환할 때 헤드를 재위치시키기 위한 시간을 필요로 하기 때문이다. 이러한 비틀림이 없으면 헤드는 다음 트랙으로 옮겨질 수 있겠지만 원하던 다음 블럭이 이미 헤드 밑을 지나갈 수도 있고, 이 경우 드라이브는 다음 블럭에 접근하기 위해 거의 전체 회전 지연을 대기해야할 수도 있게 된다.
바깥 쪽 트랙은 안쪽 트랙보다 더 많은 섹터를 가진다는 사실도 염두에 두자. 이 트랙들을 멀티 구역(multi-zoned) 디스크 드라이브라 부른다. 여기서 디스크는 여러 개의 구역으로 만들어져있고, 각 구역은 표면 위에 있는 트랙들의 연속적인 집합이다. 각 구역은 트랙 별로 같은 수의 섹터를 가지고 있고, 외부 구역은 안쪽 구역보다 더 많은 섹터들을 가지고 있다.
마지막으로 현대 디스크 드라이브에서 중요한 부분은 캐시로, 이는 역사적인 이유로 종종 트랙 버퍼(track buffer)라 불린다. 이 캐시는 읽히거나 쓰인 데이터를 담기 위해 드라이브가 사용할 수 있는 작은 양의 메모리다. 예를 들어 디스크로부터 섹터를 읽을 때, 드라이브는 해당 트랙에 있는 모든 섹터들을 읽고 메모리에 캐시할지의 여부를 결정할 수 있다. 이렇게 한다면 드라이브는 같은 트랙에 연속적인 요청이 들어왔을 때 더 빠르게 응답할 수 있게 된다.
쓰기의 경우, 드라이브에는 여러 선택지가 있다. 쓰기 작업이 완료되었다는 사실은 언제 알아야 할까? 메모리에 데이터를 썼을 때일까? 아니면 실제로 데이터가 디스크에 쓰였을 때일까? 전자의 경우를 write-back 캐싱이라 부르고 후자의 경우를 write-through라 부른다. write-back 캐싱의 경우, 종종 드라이브가 더 빠른 것처럼 보이게 하지만, 위험하다. 만약 파일 시스템이나 애플리케이션이 정확성을 위해 데이터가 특정 순서로 디스크에 쓰이기를 원하는 경우, write-back 캐싱은 문제를 일으킬 수 있다.
이제 추상적인 디스크 모델을 가지고 있으니, 디스크 성능에 대해 더 잘 이해하기 위해 조금의 분석을 곁들여보자. 구체적으로, 이제는 I/O 시간을 다음과 같이 세 주요 구성 요소들의 합으로 나타낼 수 있다.
두 드라이브를 비교하기 위해 쓰이는 I/O의 속도()는 시간을 통해서 쉽게 계산된다는 것에 유의하자. 간단하게 전송되는 양을 시간으로 나누면 된다.
I/O 시간에 대해 더 익숙해지기 위해, 다음과 같이 직접 계산을 해보자. 관심이 있는 워크로드에는 두 가지가 있다고 가정한다. 첫 번째는 랜덤 워크로드로, 디스크의 임의의 위치에서 작은 양(예를 들면 4KB)의 읽기를 수행한다. 랜덤 워크로드는 DBMS를 포함한 많은 주요 애플리케이션들에서 흔히 쓰인다. 두 번째는 순차 워크로드로, 간단히 디스크에 있는 많은 양의 섹터들을 순차적으로 읽는 것이다. 이 패턴 또한 흔하고 중요하다.
랜덤 워크로드와 순차 워크로드의 성능 상 차이를 이해하려면, 일단은 디스크 드라이브에 대해 몇 가지 가정을 해야한다. Seagate의 디스크 두 개를 살펴보자. 첫 번째는 Cheetah 15K.5로 고성능 SCSI 드라이브이고, 나머지는 Barracuda라 하는 대용량 드라이브다. 세부 스펙은 다음과 같다.
이 스펙을 바탕으로 이 드라이브들이 위에서 그린 두 워크로드에서 얼마나 잘 동작하는지를 계산해보자. 우선은 랜덤 워크로드 부터다. 각 4KB 읽기가 디스크의 랜덤 위치에서 일어난다고 가정한다면, 각 읽기가 얼마나 걸릴지를 계산할 수 있다.
평균 탐색 시간 은 제조사에서 보고한 평균 시간을 가져온 것이다. 전체 탐색 시간의 경우에는 보통 이보다 2-3배 더 오래 걸린다는 것에 유의하자. 평균 회전 지연은 RPM을 통해 바로 계산할 수 있다. 15000 RPM은 250RPS이고, 따라서 각 회전은 의 시간이 걸린다. 평균적으로 디스크는 반 바퀴 회전을 하므로 가 평균 시간이 된다. 마지막으로 전송 시간은 전송량을 최고 전송 속도로 나눈 값이다. 여기에서는 로 아주 작다.
에 대한 위의 등식에 따르면 Cheetah의 는 대충 다. 의 속도를 계산하려면 전송량을 평균 시간으로 나누면 되고, 따라서 랜덤 워크로드에서의 Cheetah의 는 이다. 같은 방식의 계산을 Barracuda에 한다면, 는 약 , 속도는 임을 볼 수 있다.
다음으로는 순차 워크로드를 보자. 여기서는 아주 긴 전송 이전에 한 번의 탐색 및 회전이 있다고 가정한다. 단순화하기 위해, 전송량은 100MB라 하자. 이때 Cheetah와 Barracuda의 는 각각 와 가 되고 는 각각 가 된다.
여기서 중요한 점을 알 수 있다. 우선 가장 중요한 것은, 랜덤 워크로드와 순차 워크로드 사이에는 커다란 드라이브 성능의 갭이 있다는 것이다. Cheetah의 경우에는 거의 200배가 차이나고 Barracuda에서는 300 이상이 차이난다.
다음은 더 미묘한 포인트다. 고사양 고성능 드라이브와 저사양 고용량 드라이브 사이에도 성능 차이가 크다는 것이다. 이러한 이유로 사람들은 전자에는 많은 돈을 쏟아부으려 하면서, 후자는 최대한 싸게 구하려 한다.
I/O의 높은 비용으로 인해 역사적으로 OS는 디스크에 요청되는 I/O의 순서를 결정하는 역할을 맡아왔다. 더 구체적으로, I/O 요청 집합이 주어졌을 때 디스크 스케줄러(disk scheduler)는 요청들을 조사해 어떤 것을 다음으로 스케줄링할지를 결정한다.
각 작업의 길이가 보통 알려지지 않는 작업 스케줄링과 달리, 디스크 스케줄링에서는 해당 요청이 얼마나 오래 걸릴지를 잘 예측할 수 있다. 탐색과 요청의 가능한 회전 지연을 가늠함으로써 디스크 스케줄러는 각 요청이 얼마나 오래 걸릴지를 알 수 있고, 따라서 서비스하기에 가장 적은 시간이 걸리는 것을 고를 수 있다. 따라서 디스크 스케줄러는 작업에서 SJF의 원칙을 따르려 할 것이다.
초기 디스크 스케줄링 방식 중 하나는 최단 탐색 시간 우선(shortest-seek-time-first, SSTF, SSF)이라 불린다. SSTF는 트랙을 기준으로 I/O 요청 큐를 정렬하고, 가장 가까운 트랙의 요청을 골라 우선 완료한다. 예제로는 Figure 37.7을 보자. 현재 헤드의 위치가 안쪽 트랙에 있고, 21번과 2번 섹터에 요청이 발생했다고 하자. 그렇다면 우선 21번의 요청을 먼저 보내고, 해당 요청이 완료되기를 기다리고, 그 다음에 2번 섹터에의 요청을 보낼 것이다.
SSTF는 이 예제에서, 중간 트랙을 먼저 탐색하고, 이후 바깥 트랙을 탐색하는 방식으로 잘 작동한다. 하지만 SSTF은 다음과 같은 이유로 만병통치약이 아니다. 우선 드라이브 기하는 호스트 OS에게는 가능하지 않다. OS는 이를 단순히 블럭의 배열로 본다. 다행스럽게도 이 문제는 쉽게 해결될 수 있다. SSTF를 사용하는 대신 OS는 가장 가까운 블럭 주소를 다음으로 스케줄링하는 최근접 블럭 우선(nearest-block-first, NBF)을 쉽게 구현할 수 있다.
다음 문제가 더 근본적이다. 바로 기아(starvation) 문제다. 위 예시에서 현재 헤드가 위치해 있는 안쪽 트랙에서 요청이 계속 발생한다고 해보자. 순수 SSTF 방식을 사용하는 경우 다른 트랙으로의 요청은 완전히 무시될 것이다.
어떻게 SSTF 같은 스케줄링을 구현하면서도 기아 문제를 피할 수 있을까?
이 물음에 대한 답은 꽤 오래전에 개발되었고, 비교적 직관적이다. 원래 SCAN이라 불렸던 이 알고리즘은 트랙을 가로지르는 요청들을 서비스하기 위해 단순히 디스크를 앞뒤로 가로질러 이동한다. 디스크를 한 쪽에서 다른 한 쪽으로, 즉 안에서 밖으로, 혹은 밖에 안으로 가로지르는 것을 스윕(sweep)이라 부르자. 만약 이미 한 번의 디스크 스윕 중에 서비스된 적이 있는 트랙의 블럭에 대한 요청이 발생한다면, 이는 즉시 처리되지 못하고 다음 스윕까지 큐에 대기하게 된다.
SCAN은 많은 변주를 가지고 있는데, 이들은 모두 거의 같은 일을 한다. 예를 들어 Coffman 등은 F-SCAN을 도입했는데, 이는 스윕을 하는 중에는 서비스될 큐를 동결시킨다. 이 행동은 스윕 중에 들어오는 요청들을 큐에 넣고 다음에 서비스되게 한다. 이렇게 하는 것은, 현재 요청 근처에 있지만 나중에 도착한 요청들에 대한 서비스를 지연시킴으로써, 멀리 떨어진 요청들에 대한 기아 문제를 해결한다.
C-SCAN은 또 다른 흔한 변주로, Circular SCAN의 준말이다. 이는 디스크를 양방향으로 스윕하지 않고, 밖에서 안으로 스윕한 후, 다시 밖으로 초기화되어 스윕한다. 이렇게 하면 중간 트랙을 선호하는 원래 SCAN과 달리, 안쪽과 바깥쪽 트랙에 대한 요청을 좀 더 공정하게 처리할 수 있게 된다.
SCAN은 엘리베이터 때때로 알고리즘이라고도 불리는데, 엘리베이터의 동작과 마찬가지로 단순히 가까이에 있는 요청을 우선 처리하지 않고 한 쪽에서 다른 쪽으로 이동하면서 요청들을 처리하기 때문이다.
안타깝게도 SCAN과 그 변주들이 최고의 스케줄링 기술인 것은 아니다. 특히 SCAN은, 심지어 SSTF도 SJF 원칙을 최대한 따르려 하지는 않는다. 구체적으로, 이들은 회전을 무시한다.
어떻게 탐색과 회전 모두에서 SJF에 근사한 알고리즘을 만들 수 있을까?
최단 위치 시간 우선(shortest-positioning-time-first, SPTF), 혹은 최단 접근 시간 우선(shortest-access-time-first, SATF)에 대해 논의하기 전에, 무엇이 지금 문제인지에 대해 더 자세히 알아보자.
위 예제에서, 헤드는 현재 안쪽 트랙의 30번 섹터에 위치해있다. 스케줄러는 따라서 다음을 결정해야 한다. "중간 트랙의 16번 섹터를 먼저 처리해야할까, 바깥 트랙의 8번 섹터를 처리해야할까?" 어떤 걸 먼저 처리해야할까?
정답은 "케바케"다. 엔지니어링에서 케바케는 거의 항상 정답이다. 항상 트레이드 오프에 대해서 생각해야 한다. 어떤 경우에는 어때야할까? 생각해보자.
여기에서 고려해야 할 것은 회전과 탐색의 상대적 시간이다. 위 예제에서, 만약 탐색 시간이 회전 지연보다 상당히 오래 걸리면, SSTF와 그 변주들을 사용하는 것도 좋다. 하지만 만약 탐색이 회전보다 좀 더 빠른 경우를 생각해보자. 이 경우에는 탐색을 좀 더 진행해 바깥 트랙의 8번 섹터에의 요청을 처리하는 것이 16번 섹터가 있는 중간 트랙으로의 탐색을 수행하는 것보다 나을 수 있다. 후자의 경우 16번 섹터에 헤드가 위치하기 위해서는 거의 한 바퀴를 돌아야할 수도 있기 때문이다.
현대의 드라이브에서는 위에서 본 것과 같이, 탐색과 회전이 거의 동등하기 때문에 SPTF가 유용하고, 성능도 향상시킨다. 하지만 이는 OS에서 구현하기 훨씬 더 어렵다. OS는 트랙의 경계가 어디에 있고, 현재 디스크 헤드가 어디에 있는지를 알 수 없기 때문이다. 그렇기 때문에 SPTF는 아래에서 설명할 것과 같이, 보통 디스크 내부에서 수행된다.
여기에서의 간단한 기본 디스크 동작, 스케줄링, 그리고 관련 토픽에 관한 설명에서는 다루지 않은 다른 많은 이슈들이 있다. 그 중 하나는 "현대 시스템에서 디스크 스케줄링은 어디에서 수행되는가?"하는 것이다. 좀 더 오래된 시스템에서 OS는 모든 스케줄링을 도맡아 했다. 처리 대기 중인 요청 집합을 살펴보고 OS는 최선의 것을 하나 고르고 해당 요청을 디스크로 보낸다. 요청 처리가 완료되면 다음 것이 정해지고, ... 계속된다. 그때는 디스크도 좀 더 간단했다.
현대 시스템에서 디스크는 다수의 대기 중인 요청들을 수용할 수 있고, 그 자체의 세련된 내부 스케줄러를 가지고 있다. 따라서 OS 스케줄러는 보통 최적일 것 같은 몇 개의 요청들을 고르고 이들을 모두 디스크로 보낸다. 그러면 디스크는 헤드 위치나 세부적인 트랙 레이아웃 정보 등의 내부 정보를 이용해 그런 요청들을 가능한 최선의 순서로 서비스한다.
디스크 스케줄러에 의해 수행되는 중요 관련 작업에는 I/O 병합(I/O merging)도 있다. 예를 들어, 블럭 33, 8, 34를 읽는 요청 시리즈가 있다고 생각해보자. 이 경우 스케줄러는 블럭 33, 34에 대한 요청을 하나의 2-블럭 요청으로 병합한다. 이후 스케줄러가 하는 재정렬 작업은 이 병합된 요청에 대해서만 수행된다. 병합은 OS 수준에서는 특히 중요한데, 이것이 디스크로 보내는 요청의 수를 줄이고 오버헤드를 낮추기 때문이다.
현대 스케줄러가 다뤄야 할 마지막 문제는, 디스크에 I/O 요청을 보내기 전에 시스템이 얼마나 대기해야 하느냐는 것이다. 순진하게는 하나의 I/O만 발생해도 즉시 드라이브에 요청을 보내야한다고 생각할 수도 있다. 이러한 방식을 work-conserving이라 부르는데, 디스크는 처리해야할 요청이 있는 경우 절대 쉬지 않기 때문이다. 하지만 예측 디스크 스케줄링(anticipatory disk scheduling)에 대한 연구는, 때때로는 잠시 동안은 대기하는 것이 좋다는 것을 보였다. 이를 가리켜 non-work-conserving 방법이라 부른다. 대기를 함으로써, 새롭고 더 나은 요청이 디스크에 도착하게 될 수 있고, 이로써 전체적인 효율성이 올라갈 수 있기 때문이다. 물론 언제, 얼마나 기다릴지를 결정하는 일은 까다로운 일이다.
디스크가 어떻게 동작하는지에 대한 개요를 보였다. 이는 기능적 모델에 대한 것이지, 실제 드라이브 설계에서 일어나는 물리, 전자, 재료과학 등의 내용은 아니다. 그러한 내용들에 관심이 있는 사람은? 전공을 바꿔 보는 게 좋을 것 같다. 아니면 부전공도 좋고. 이제는 모델을 통해 장치들 위에서 좀 더 흥미로운 시스템들을 만들 수 있게 될 것이다.