개요
Linux의 block layer에는 block device의 성능을 향상시키기 위한 여러 가지 기법이 적용되어 있다. 대표적인 것으로 disk scheduler (I/O scheduler)가 있을 것이다.
이 글에서는, block layer의 성능을 향상시키는 기법 중 하나인, block device plugging에 대해 설명한다. Linux 2.6.39에서 리눅스의 plugging이 대대적으로 개선되었다. 2.6.34의 plug구현을 먼저 살펴보고, 그 문제점을 어떻게 Linux 3.0에서 해결했는지 살펴본다.
작성자: S-Core 김.민.규
작성일: 2011-11-06
Plug란 무엇인가?
그림에서 볼 수 있듯이, Req 1이 들어오자마자 디스크가 Req 1을 처리하기 시작했다. 곧 Req 2가 들어올 예정인데, 디스크는 Req 1을 처리 하고 있기 때문에, 결과적으로 Req 2는 별도로 처리돼서, 시간이 많이 걸린다.
만약 plug를 이용한다면 다음 그림과 같이 Req 1,2를 한꺼번에 처리할 수 있다.

Req 1을 보내기 전에 plug해서, 디스크가 request를 즉석에서 처리하지 않도록 멈춰놓은 다음, Req 1,2를 보내는 것이다. Req 2를 보낸 다음 unplug를 수행하면, 디스크는 한꺼번에 req 1,2를 처리할 수 있다. 결과적으로 더 빨리 끝나게 되는 것이다.
여기서 plug라는 단어의 의미를 짚고 넘어가야 하겠는데… plug라고 하면 전자제품 플러그를 생각하기 쉽다. 따라서 “plug하면 디스크가 동작한다”고 추측하게 되는 것이다. 그런데 Linux에서 말하는 플러그는 “마개”의 의미이다. 디스크 스케쥴러(Request queue)를 막아서 동작을 멈추겠다는 뜻이다. 따라서, 실제 의미는 “Plug하면 디스크가 동작을 멈춘다”가 된다.
Linux 2.6.34에서 plug구현
그렇다면, Linux 2.6에서의 구현을 살펴보자. 필자가 대상으로 한 버전은 2.6.34이다. 앞서 언급했듯이, plug의 구현이 개선된 것은 2.6.39이다.
2.6.34에서는 __make_request함수에서 다음 루틴을 통해 plug한다.
blk_plug_device가 plug를 수행하는 함수인데, 이 함수의 내부를 보면, 크게 두 가지 일을 한다.
1) Request queue에 PLUGGED flag를 set한다. 이 플래그가 set되면, 디바이스 드라이버가 멈추면서 더 이상 request를 처리하지 않는다.
다시말해, Linux 2.6.34에서는 block layer와 device driver가 함께 plugging을 구현하고 있는 것이다. MMC device driver의 코드인 mmc_queue_thread를 살펴보면, plug되지 않았을 때만 request를 처리하는 것을 볼 수 있다.
2) 3ms Unplug timer를 세팅한다. 3ms가 지날때까지 unplug되지 않으면 자동으로 unplug를 해서 request가 처리되도록 하기 위한 것이다.
Unplug되는 경우는 몇 가지 경우가 있지만, 설명의 편의상 간단히 설명하자면 다음과 같이 두 가지 경우라고 볼 수 있다.
1) Request queue에 request가 unplug_thresh(4개) 이상 들어오는 경우. (elv_insert)
2) 앞서 설정한 3ms timeout이 발생하는 경우.
2.6.34의 구현을 종합하자면 다음과 같다.
* request queue가 비어있을 때 request가 들어오면 plug한다.
* plug하고 3ms가 지나면 자동으로 unplug한다.
* 3ms내에 request가 4개 이상 들어와도 unplug한다.
2.6.34 plug의 문제점
2.6.34의 구현에는 다소 문제가 있다. 다음 request가 3ms안에 들어오지 않을 수도 있다는 것이다. Synchronous request하나만 보내놓고 응답을 기다리는 경우, 결국 3ms timer가 expire될 것이다. 그렇다면 괜히 반응성만 3ms나빠지게 되는 것이다.
하지만 block layer에서는 다음 request가 언제 들어올지 정보가 전혀 없다. 따라서 어쩔 수 없이 기다리는 것이다.
Linux 3.0에서 plug 구현
앞서 Linux 2.6.34에서 plug구현의 문제점에 대해 살펴봤다. Linux 3.0에서는 plug의 구현을 수정하고, plug를 파일시스템 레벨에서 이용하게 함으로써 문제를 해결했다. 결국 request를 생성하는 것은 파일시스템인데, 파일시스템에서는 request가 산발적으로 생성되는 것이 아니라, 뭉텅이 지어서(clustered) 생성되기 때문이다.
Linux 3.0에서는 plug를 사용하는 패턴은 다음과 같이 수정됐다.
위 패턴에서 보는 바와 같이, request를 뭉텅이로 보내려고 할 때, 먼저 파일시스템이 명시적으로 plug를 요청한다.(blk_start_plug) 그 다음 submit_bio를 호출하고, 최종적으로 unplug를 하는 것이다. (blk_finish_plug)
blk_start_plug는 current task(current thread)에 plug되었다고 체크를 한다. (이전에는 request queue에 체크를 했었다) 그러면 그때부터 발생하는 request는 스택에 마련된 struct blk_plug에다가 달아놓는다. 그랬다가 blk_finish_plug가 들어오면 한꺼번에 request queue에 밀어 넣음으로써, 먼저 온 request가 홀랑 먼저 처리되는 것을 방지한다.
구현은 __make_request에 있다: 새로운 request가 merge가능한 경우, merge한다. merge가 불가능한데 task가 plug된 경우, current->plug에 request를 달아놓고 나간다. 그리고 blk_finish_plug가 호출되면, task가 가진 request를 모두 request queue에 집어넣는다.
Linux 3.0은 위와 같은 방식으로 구현함으로써, 불필요하게 디스크가 노는 시간이 없도록 하였다. 굳이 필요 없는 timer도 설정하지 않는다.
plug list에 있는 request를 flush하는 경우가 blk_finish_plug말고 한가지 더 있다. 바로 태스크가 sleep되는 경우이다. 이 경우 flush하지 않으면 deadlock이 발생할 수 있다고 한다: 태스크가 plug한 상태에서 memory alloc을 요청했는데, 메모리가 부족하면 memory reclaim이 발생한다. 그런데, memory reclaim을 하기 위해서 우리가 현재 plug list에 달아놓은 page를 reclaim해야 할 상황이 생길 수 있다. 이러한 dead lock을 막기 위해, sleep하기 전에 schedule함수 내에서 plug list를 처리해준다.
결론
정리하자면 이렇다.
Request가 발생하자마자 처리 해 주는 것은, request merge를 방해해서 I/O 성능을 저하할 수 있다. 따라서 이것을 막기 위해 plug이라는 메커니즘이 적용되어 있다. 2.6 커널에서는 최대 3ms까지 디스크를 멈춰놓고 request를 기다리기 때문에, 비효율 적인 경우가 있다. block layer에서 자체적으로 plug를 구현하다 보니 그렇게 되었을 것이다.
이러한 비효율을 해결하기 위해 Linux 3.0은 파일시스템에서 plug/unplug 동작을 수행한다. 파일시스템은 한번에 request를 얼마나 보낼지 알고 있기 때문에, 꼭 필요한 구간만 plug를 이용함으로써, 성능을 개선시킬 수 있다.
참조
http://lwn.net/Articles/438256/
Jens Axboe님이 쓴 글이다. plug의 역사에서부터 시작해서 자세한 설명을 읽을 수 있다. Linux 3.0에서 deadlock상황은 이 글에서 인용했다.
http://studyfoss.egloos.com/5585801
리눅스 커널에 대한 좋은 글이 많은 블로그이다. 위에 링크된 글은 Linux의 block device I/O에 대한 전반적인 내용을 친절하게 설명하고 있다.
Linux의 block layer에는 block device의 성능을 향상시키기 위한 여러 가지 기법이 적용되어 있다. 대표적인 것으로 disk scheduler (I/O scheduler)가 있을 것이다.
이 글에서는, block layer의 성능을 향상시키는 기법 중 하나인, block device plugging에 대해 설명한다. Linux 2.6.39에서 리눅스의 plugging이 대대적으로 개선되었다. 2.6.34의 plug구현을 먼저 살펴보고, 그 문제점을 어떻게 Linux 3.0에서 해결했는지 살펴본다.
작성자: S-Core 김.민.규
작성일: 2011-11-06
Plug란 무엇인가?
먼저 디스크의 특성에 대해 간단한 설명이 필요하다. HDD의 경우 두드러지는 특성이지만, SSD도 특성은 크게 다르지 않다.이런 특성을 염두에 두고, 다음 그림을 살펴보자. 디스크 상에서 연속적인(contiguous) request 두 개를 처리하려고 하는 상황이다. 둘을 한 request로 합쳐서 처리한다면 빨리 처리할 수 있는 상황인 것이다. 하지만, plug가 없는 상황에서, 기본적인 동작은 다음 그림처럼 일어난다.
디스크에서 I/O요청(request)를 처리하는 속도는, request의 크기에 영향을 받지만, request의 개수에도 영향을 받는다. 다시 말해, 8KB를 한번 읽는 것 보다, 4KB를 두 번 읽는 게 훨씬 느리다. 실제로 디스크가 플래터에서 정보를 읽어내는 시간에 비해, 그 외의 준비 하는 데에도 시간이 적잖게 걸리기 때문이다.

만약 plug를 이용한다면 다음 그림과 같이 Req 1,2를 한꺼번에 처리할 수 있다.

Req 1을 보내기 전에 plug해서, 디스크가 request를 즉석에서 처리하지 않도록 멈춰놓은 다음, Req 1,2를 보내는 것이다. Req 2를 보낸 다음 unplug를 수행하면, 디스크는 한꺼번에 req 1,2를 처리할 수 있다. 결과적으로 더 빨리 끝나게 되는 것이다.
여기서 plug라는 단어의 의미를 짚고 넘어가야 하겠는데… plug라고 하면 전자제품 플러그를 생각하기 쉽다. 따라서 “plug하면 디스크가 동작한다”고 추측하게 되는 것이다. 그런데 Linux에서 말하는 플러그는 “마개”의 의미이다. 디스크 스케쥴러(Request queue)를 막아서 동작을 멈추겠다는 뜻이다. 따라서, 실제 의미는 “Plug하면 디스크가 동작을 멈춘다”가 된다.
Linux 2.6.34에서 plug구현
그렇다면, Linux 2.6에서의 구현을 살펴보자. 필자가 대상으로 한 버전은 2.6.34이다. 앞서 언급했듯이, plug의 구현이 개선된 것은 2.6.39이다.
2.6.34에서는 __make_request함수에서 다음 루틴을 통해 plug한다.
if (queue_should_plug(q) && elv_queue_empty(q))
blk_plug_device(q);
(queue_should_plug는 디스크가 SSD(non-rot)이고, command queueing을 지원하지 않는 경우에는 항상 참이다)
쉽게 설명하자면, request queue가 비어있다가 request가 들어오면 device를 plug하는 것이다.
blk_plug_device가 plug를 수행하는 함수인데, 이 함수의 내부를 보면, 크게 두 가지 일을 한다.
1) Request queue에 PLUGGED flag를 set한다. 이 플래그가 set되면, 디바이스 드라이버가 멈추면서 더 이상 request를 처리하지 않는다.
다시말해, Linux 2.6.34에서는 block layer와 device driver가 함께 plugging을 구현하고 있는 것이다. MMC device driver의 코드인 mmc_queue_thread를 살펴보면, plug되지 않았을 때만 request를 처리하는 것을 볼 수 있다.
2) 3ms Unplug timer를 세팅한다. 3ms가 지날때까지 unplug되지 않으면 자동으로 unplug를 해서 request가 처리되도록 하기 위한 것이다.
Unplug되는 경우는 몇 가지 경우가 있지만, 설명의 편의상 간단히 설명하자면 다음과 같이 두 가지 경우라고 볼 수 있다.
1) Request queue에 request가 unplug_thresh(4개) 이상 들어오는 경우. (elv_insert)
2) 앞서 설정한 3ms timeout이 발생하는 경우.
2.6.34의 구현을 종합하자면 다음과 같다.
* request queue가 비어있을 때 request가 들어오면 plug한다.
* plug하고 3ms가 지나면 자동으로 unplug한다.
* 3ms내에 request가 4개 이상 들어와도 unplug한다.
2.6.34 plug의 문제점
2.6.34의 구현에는 다소 문제가 있다. 다음 request가 3ms안에 들어오지 않을 수도 있다는 것이다. Synchronous request하나만 보내놓고 응답을 기다리는 경우, 결국 3ms timer가 expire될 것이다. 그렇다면 괜히 반응성만 3ms나빠지게 되는 것이다.
하지만 block layer에서는 다음 request가 언제 들어올지 정보가 전혀 없다. 따라서 어쩔 수 없이 기다리는 것이다.
Linux 3.0에서 plug 구현
앞서 Linux 2.6.34에서 plug구현의 문제점에 대해 살펴봤다. Linux 3.0에서는 plug의 구현을 수정하고, plug를 파일시스템 레벨에서 이용하게 함으로써 문제를 해결했다. 결국 request를 생성하는 것은 파일시스템인데, 파일시스템에서는 request가 산발적으로 생성되는 것이 아니라, 뭉텅이 지어서(clustered) 생성되기 때문이다.
Linux 3.0에서는 plug를 사용하는 패턴은 다음과 같이 수정됐다.
function_that_generates_requests
{
struct blk_plug plug;
blk_start_plug(&plug);
for pages that need I/O
submit_bio
blk_finish_plug(&plug)
}
위 패턴에서 보는 바와 같이, request를 뭉텅이로 보내려고 할 때, 먼저 파일시스템이 명시적으로 plug를 요청한다.(blk_start_plug) 그 다음 submit_bio를 호출하고, 최종적으로 unplug를 하는 것이다. (blk_finish_plug)
blk_start_plug는 current task(current thread)에 plug되었다고 체크를 한다. (이전에는 request queue에 체크를 했었다) 그러면 그때부터 발생하는 request는 스택에 마련된 struct blk_plug에다가 달아놓는다. 그랬다가 blk_finish_plug가 들어오면 한꺼번에 request queue에 밀어 넣음으로써, 먼저 온 request가 홀랑 먼저 처리되는 것을 방지한다.
구현은 __make_request에 있다: 새로운 request가 merge가능한 경우, merge한다. merge가 불가능한데 task가 plug된 경우, current->plug에 request를 달아놓고 나간다. 그리고 blk_finish_plug가 호출되면, task가 가진 request를 모두 request queue에 집어넣는다.
Linux 3.0은 위와 같은 방식으로 구현함으로써, 불필요하게 디스크가 노는 시간이 없도록 하였다. 굳이 필요 없는 timer도 설정하지 않는다.
plug list에 있는 request를 flush하는 경우가 blk_finish_plug말고 한가지 더 있다. 바로 태스크가 sleep되는 경우이다. 이 경우 flush하지 않으면 deadlock이 발생할 수 있다고 한다: 태스크가 plug한 상태에서 memory alloc을 요청했는데, 메모리가 부족하면 memory reclaim이 발생한다. 그런데, memory reclaim을 하기 위해서 우리가 현재 plug list에 달아놓은 page를 reclaim해야 할 상황이 생길 수 있다. 이러한 dead lock을 막기 위해, sleep하기 전에 schedule함수 내에서 plug list를 처리해준다.
결론
정리하자면 이렇다.
Request가 발생하자마자 처리 해 주는 것은, request merge를 방해해서 I/O 성능을 저하할 수 있다. 따라서 이것을 막기 위해 plug이라는 메커니즘이 적용되어 있다. 2.6 커널에서는 최대 3ms까지 디스크를 멈춰놓고 request를 기다리기 때문에, 비효율 적인 경우가 있다. block layer에서 자체적으로 plug를 구현하다 보니 그렇게 되었을 것이다.
이러한 비효율을 해결하기 위해 Linux 3.0은 파일시스템에서 plug/unplug 동작을 수행한다. 파일시스템은 한번에 request를 얼마나 보낼지 알고 있기 때문에, 꼭 필요한 구간만 plug를 이용함으로써, 성능을 개선시킬 수 있다.
참조
http://lwn.net/Articles/438256/
Jens Axboe님이 쓴 글이다. plug의 역사에서부터 시작해서 자세한 설명을 읽을 수 있다. Linux 3.0에서 deadlock상황은 이 글에서 인용했다.
http://studyfoss.egloos.com/5585801
리눅스 커널에 대한 좋은 글이 많은 블로그이다. 위에 링크된 글은 Linux의 block device I/O에 대한 전반적인 내용을 친절하게 설명하고 있다.




최근 덧글