본문 바로가기
【Fundamental Tech】/Linux

[Linux] 블록 장치 I/O 동작 방식 (1) in Linux 3.1-rc4

반응형
*출처: http://studyfoss.egloos.com/5575220

블록 장치는 개별 바이트 단위가 아닌 일정 크기(block) 단위로 접근하는 장치를 말하는 것으로
간단히 말하면 하드 디스크와 같은 대용량 저장 장치를 말한다.
전통적으로 이러한 블록 장치는 다른 (문자) 장치처럼 직접 다루는 대신
파일 시스템이라고 하는 추상화 계층을 통해 간접적으로 접근하게 되며
따라서 프로그래머는 해당 저장 장치가 어떠한 종류의 장치인지와는 무관하게
(또한 VFS에 의해 어떠한 파일 시스템인지와도 무관하게) 일관된 방식으로
(즉, 파일 및 디렉터리의 형태로) 이용하게 된다.

이번에는 이러한 추상화 계층 아래에서 커널이 실제로 블록 장치를 다루기 위해
어떠한 작업들을 수행하는지 알아보기로 한다.

블록 장치, 즉 하드 디스크는 CPU는 물론 메모리에 비해서도 동작 속도가 현저히 느리다.
이는 전통적인 방식의 디스크에서 디스크 헤드가 원하는 위치까지 이동하기 위해 필요한 시간
(seek time)이 매우 길기 때문이다. (SSD와 같은 flash 기반의 디스크 장치는
이러한 제약이 없으므로 접근 속도가 상대적으로 빠르지만 여전히 CPU/메모리에 비해서는 느리다)

따라서 리눅스의 VFS 계층은 디스크 접근을 최소화하기 위해
페이지 캐시를 이용하여 한 번 접근한 디스크의 내용을 저장해 둔다.
하지만 여기서는 이러한 페이지 캐시의 작용은 건너뛰고
실제로 블록 장치와 I/O 연산을 수행하는 경우에 대해서 살펴보게 될 것이다.

블록 I/O의 시작 지점은 submit_bio() 함수이다.
(일반적인 파일 시스템의 경우 buffer_head (bh)라는 구조체를 통해 디스크 버퍼를 관리하는데
이 경우 submit_bh() 함수가 사용되지만 이는 bh의 정보를 통해 bio 구조체를 할당하여
적절히 초기화한 후 다시 submit_bio() 함수를 호출하게 된다.)

이 함수는 I/O 연산의 종류 (간단하게는 READ 혹은 WRITE) 및
해당 연산에 대한 모든 정보를 포함하는 bio 구조체를 인자로 받는다.

bio 구조체는 기본적으로 I/O를 수행할 디스크 영역의 정보와
I/O를 수행할 데이터를 저장하기 위한 메모리 영역의 정보를 포함한다.
여기서 몇가지 용어가 함께 사용되는데 혼동의 여지가 있으므로 간략히 정리하고 넘어가기로 한다.

먼저 섹터(sector)라는 것은 장치에 접근할 수 있는 최소의 단위이며 (H/W적인 특성이다)
대부분의 장치에서 512 바이트에 해당하므로, 리눅스 커널에서는 섹터는 항상
512 바이트로 가정하며 sector_t 타입은 (512 바이트의) 섹터 단위 크기를 나타낸다.
(만약 해당 장치가 더 큰 크기의 섹터를 사용한다면 이는 장치 드라이버에서 적절히 변환해 주어야 한다)

블록(block)은 장치를 S/W적으로 관리하는 (즉, 접근하는) 크기로 섹터의 배수이다.
일반적으로 파일 시스템 생성 (mkfs) 시 해당 파일 시스템이 사용할 블록 크기를 결정하게 되며
현재 관리의 용이성을 위해 블록 크기는 페이지 크기 보다 크게 설정될 수 없다.
즉, 일반적인 환경에서 블록의 크기는 512B(?), 1KB, 2KB, 4KB 중의 하나가 될 것이다.
하나의 블록은 디스크 상에서 연속된 섹터로 이루어진다.

세그먼트(segment)는 장치와의 I/O 연산을 위한 데이터를 저장하는 "메모리" 영역을 나타내는 것으로
일반적으로는 페이지 캐시 내의 일부 영역에 해당 할 것이다.
하나의 블록은 메모리 상에서 동일한 페이지 내에 저장되지만
하나의 I/O 연산은 여러 블록으로 구성될 수도 있으므로
하나의 세그먼트는 (개념적으로) 여러 페이지에 걸칠 수도 있다.

블록 I/O 연산은 기본적으로 디스크에 저장된 데이터를 메모리로 옮기는 것 (READ)
혹은 메모리에 저장된 데이터를 디스크로 옮기는 것 (WRITE)이다.
(장치의 특성에 따라 FLUSH, FUA, DISCARD 등의 추가적인 연산이 발생될 수도 있다.)
I/O 연산이 여러 블록을 포함하는 경우 약간 복잡한 문제가 생길 수 있는데
이러한 블록 데이터가 디스크 혹은 메모리 상에서 연속되지 않은 위치에 존재할 수 있기 때문이다.

예를 들어 파일 시스템을 통해 어떠한 파일을 읽어들이는 경우를 생각해보자.
파일을 연속적으로 읽어들인다고 해도 이는 VFS 상에서 연속된 것으로 보이는 것일 뿐
실제 데이터는 디스크 곳곳에 흩어져있을 수도 있다.
(많은 파일 시스템은 성능 향상을 위해 되도록 연속된 파일 데이터를
디스크 상에서도 연속된 위치에 저장하려고 시도하지만 시간이 지날 수록 단편화가 발생하므로
결국에는 어쩔 수 없이 이러한 현상이 발생하게 될 것이다.)

또한 디스크에서 읽어들인 데이터는 페이지 캐시 상에 저장되는데
페이지 캐시로 할당되는 메모리는 항상 개별 페이지 단위로 할당이 이루어지므로
메모리 상에서도 연속된 위치에 저장된다고 보장할 수 없다.

따라서 bio 구조체는 이러한 상황을 모두 고려하여 I/O 연산에 필요한 정보를 구성한다.
우선 하나의 bio은 디스크 상에서 연속된 영역 만을 나타낼 수 있다.
즉, 접근하려는 연속된 파일 데이터가 디스크 상에서 3부분으로 나뉘어져 있다면
세 개의 bio가 각각 할당되어 submit_bio() 함수를 통해 각각 전달될 것이다.

블록 I/O 연산 시 실제 데이터 복사는 대부분 DMA를 통해 이루어지게 되는데
이 때 (DMA를 수행하는) 장치는 물리 주소를 통해 메모리에 접근하게 되므로
설사 파일 매핑을 통해 파일 데이터를 저장한 페이지들이 (해당 프로세스의) 가상 메모리 상에서
연속된 위치에 존재한다고 하더라도 떨어진 페이지 프레임에 존재한다면 별도의 세그먼트로 인식할 것이다.

구식 장치의 경우 DMA를 수행할 때 디스크는 물론 메모리 상에서도 연속된 하나의 세그먼트 만을 지원했었다.
따라서 디스크 상에서 연속된 위치에 저장된 데이터라고 하더라도 메모리 상에서 연속되지 않았다면
하나의 I/O 연산을 통해 처리할 수 없는 상황이 발생하므로 여러 연산으로 분리해야 했었다.
하지만 장치가 scatter-gather DMA를 지원하거나 IO-MMU를 포함한 머신이라면 얘기가 달라진다.

현재 bio는 세그먼트를 bio_vec 구조체를 통해 저장하는데
세그먼트는 기본적으로 페이지의 형태로 저장되므로 이에 대한 모든 정보가 포함되며
장치가 한 I/O 당 여러 세그먼트를 지원할 수 있으므로 이를 배열(vector) 형태로 저장한다.

혹은 우연히도 디스크 상에 연속된 데이터가 메모리 상에서도 연속된 페이지에 저장되었을 수도 있다.
이 경우 별도의 페이지로 구성되었어도 물리적으로는 하나의 세그먼트로 처리한다.
또는 IO-MMU를 통해 떨어져있는 페이지들을 하나의 세그먼트 (연속된 주소)로 매핑할 수도 있다.

 위 그림은 지금껏 설명한 bio의 구성을 보여준다.
(설명을 간단히하기 위해 블록 크기와 페이지 크기가 동일한 환경을 고려하며
장치는 scatter-gather DMA 등을 통해 여러 세그먼트를 동시에 처리할 수 있다고 가정한다)
연속된 파일 주소 공간에 대한 I/O 요청은 디스크 상의 위치를 기준으로 3개의 bio로 나뉘어졌으며
각 bio는 해당 영역의 데이터를 담는 세그먼트를 여러 개 포함할 수 있다.

반응형