Linux kernel v4.4에서 간단한 블록 장치 드라이버 만들어보기

request-mode를 위한 request-queue 생성

새로운 큐 생성

이전 장에서는 mybrd_alloc()함수에서 mybrd_device 객체를 만들고, blk_alloc_queue_node() 함수를 이용해서 큐를 만들었습니다. blk_alloc_queue_node()함수로 생성한 큐는 드라이버에게 bio 단위로 IO 정보를 전달했지요.

그러니 bio 단위에서 request 단위로 바꾸려면 큐부터 다시 만들어야합니다. 새로운 소스에서 mybrd_alloc()함수를 보면 queue_mode라는게 생겼습니다. bio단위로 IO를 처리하는걸 bio-mode라고 부르고 request단위로 처리하는걸 request-mode라고 부르겠습니다. queue_mode의 값이 MYBRD_Q_RQ이므로 blk_init_queue_node()를 호출해서 큐를 생성합니다. 결과적으로 같은 큐를 만드는데 만드는 함수가 다르네요. blk_init_queue_node()와 blk_alloc_queue_node()의 차이는 잠시 후에 확인하겠습니다.

blk_init_queue_node()는 드라이버에서 제공하는 request 처리함수를 등록합니다. 우리는 mybrd_request_fn() 이라는 함수를 등록하겠습니다. 그리고 큐의 동기화를 위한 spin-lock도 드라이버가 지정합니다.

그리고 blk_queue_prep_rq()함수를 통해 mybrd_prep_rq_fn()을 등록합니다. 이 함수가 무슨 일을 하는지는 잠시 후에 보겠습니다.

그리고 지금은 주석처리를 해놨는데 blk_queue_softirq_done()이라는 함수도 request를 처리하는데 사용될 수 있습니다.

blk_init_queue_node()

이제부터 커널 코드를 조금씩 보겠습니다. 커널 소스를 받고 태그를 생성해놓으셨을거라 생각됩니다. 그럼 blk_init_queue_node()함수를 찾아보시면 blk_alloc_queue_node()와 blk_init_allocated_queue()로 이뤄어져있다는걸 확인할 수 있습니다.

blk_alloc_queue_node()는 이미 우리가 사용했던 함수입니다. 즉 아주 단순하게 bio처리만 할 수 있는 큐를 만드는 함수입니다. 이렇게 만들어진 큐에 blk_init_allocated_queue()를 이용해서 뭔가 추가적인 설정을 하게되고 그래서 결국 request를 처리하는 큐가 만들어지는 것입니다.

blk_init_allocated_queue() 함수를 보면 눈에 띄는 것이 큐의 request_fn 필드에 우리가 전달한 함수 포인터를 설정하는 것입니다. 나중에 커널에서 큐의 request_fn 포인터를 읽어서 드라이버의 함수를 호출할 것입니다. 그리고 우리가 전달한 spin-lock도 큐에 저장됩니다.

그리고 또 익숙한 함수가 있습니다. blk_queue_make_request()함수가 있고, 함수 인자에 blk_queue_bio라는 함수가 전달되고 있습니다. 분명 드라이버가 제공하는 함수가 아닙니다. 커널에 포함된 함수입니다. 나중에 디스크가 커널에 등록되고 큐가 동작을 시작하면 커널이 blk_queue_bio() 함수를 통해 큐에서 bio를 꺼내온다는걸 알 수 있습니다. 그리고 마지막으로 elevator_init()라는 함수를 호출합니다. 이 elevator라는게 뭘까요. 

여기서 커널 분석할때 한가지 팁을 말씀드리겠습니다. 커널 개발자들은 매우 뛰어난 사람들입니다. 전세계에서 얼마나 많은 리눅스 서버들, 임베디드 리눅스 장비들이 짧게는 며칠씩 길게는 몇년씩 돌아가고 있을지 상상해보세요. 그런 안정성을 가진 거대한 소프트웨어를 만들려면 저같은 사람은 비교도 안되게 뛰어난 사람들이 디자인하고 핵심 코드를 만들었을 것입니다. 그런 사람들이 코드에 주석다는걸 좋아할까요? 매우 싫어합니다. 왜냐면 코드만 만들기도 바쁜데 주석을 쓰는 시간도 아깝다는 사람도 있습니다. 그리고 더 큰 이유는 코드만 봐도 이해가 되는데 왜 주석을 달아야되는지 필요성을 모르겠다는 것입니다. 만약 코드가 봐도 뭐하는지 모르겠다면 그건 잘못된 코드라는 것이지요. 얼마나 코드를 잘짜고 잘 읽으면 주석의 필요성을 모르겠다는건지 정말 대단합니다. 어쨌든 커널 코드는 주석을 잘 안답니다. 그런데 보세요. elevator_init()함수에는 주석이 있습니다. /* init elevator */라는 주석을 보는 순간 저는 사실 황당함을 느꼈습니다. 그냥 함수 이름을 그대로 써논거같은데 왜 주석을 달았을까요. 저는 개인적으로 그만큼 elevator라는게 중요하기 때문이라고 생각합니다. 커널 코드는 최대한 주석을 줄이려고하기때문에 주석이 있다는건 그만큼 중요하거나 그 뒷배경이 복잡한 코드라는 것입니다. 따라서 이 elevator라는 것도 블럭 레이어에 있어서 중요한 것일겁니다.

말을 길게 썼는데 사실 elevator는 IO scheduler를 말하는 것입니다. 이전에 우리가 bio 단위로 IO를 처리할 때는 elevator가 없는 큐를 썼었습니다. 그런데 지금 커널이 elevator가 있는 큐를 만들어주고 있습니다. 그래서 결론은 지금 생성되는 큐는 IO scheduler를 가지고 있는 큐라는 것입니다. 그래서 bio단위로 동작하는 큐보다 throughput이 좋을거라고 이전에 설명드렸습니다.

elevator라는 이름이 좀 이상할수도 있습니다. 왜 스케줄러를 elevator라고 부르는건지 이유가 있습니다. 그 이유를 찾는건 조금만 검색해도 알 수 있으므로 여러분께 맡기겠습니다.

커널은 많은 사람들이 의논해서 만들기때문에 이름을 지을때도 논쟁이 될때가 많습니다. IO scheduler같이 커널의 전체적인 성능에 중요한 일을 하는 코드에 갠히 elevator라는 이름을 붙이지는 않을 것입니다. 그 외에 다른 코드를 볼 때도 항상 왜 이런 이름을 붙였을까 고민해보는 것도 좋은 경험이 될것입니다.

blk_queue_bio()

blk_init_queue_node()와 blk_alloc_queue_node()의 차이가 뭘까요. 바로 blk_queue_bio()를 호출하는 것입니다. 드라이버에서 큐를 만들때 blk_alloc_queue_node()로 생성하면 bio처리를 할 때 드라이버가 만든 함수가 호출됩니다. 하지만 blk_init_queue_node()로 큐를 만들면 커널이 제공하는 blk_queue_bio()함수가 bio 처리를 합니다.

내친김에 blk_queue_bio() 함수도 열어볼까요. 조금 복잡하고 저도 다 아는게 아니므로 개론적인 것들만 설명하겠습니다. 우리가 이미 아는 함수들이 사용되고 있습니다. 뭔가 에러가 발생했으면 bio_endio()를 호출하고 끝냅니다. 중간을 보면 elv_merge()라는 함수가 처음으로 request 객체를 사용하고 있습니다. 그리고 바로 밑에 bio_attempt_back_merge()나 bio_attempt_front_merge() 함수를 호출합니다. 감이 오실겁니다. 큐에있는 bio들을 조사해서 현재 전달된 bio와 합치는 것입니다. 

그리고 get_request()함수로 새로운 request를 생성합니다. 하나의 큐가 가질 수 있는 request는 한계가 있습니다. 그 한계를 큐의 depth라고 부릅니다. 만약 큐에 request가 너무 많다면 이 get_request()함수는 프로세스를 잠들게합니다.

그 다음 plug라는게 사용되는데 이건 글이 너무 길어지므로 설명하지 않겠습니다. __blk_run_queue()함수가 최종적으로 큐에 등록된 request_fn 함수를 호출하는 함수입니다.

아주 간략하게만 설명했습니다만 이전에 짧게만 설명했던 큐에서 bio들이 합쳐지는 과정이 어떻게 구현되고 언제 어떻게 드라이버가 등록한 request 처리 함수가 호출되는지 약간은 감이 오셨을거라 생각됩니다. 이제 Understanding the Linux kernel 등 본격적인 커널 책을 보시면 좀더 잘 이해가 되실 겁니다.

 

댓글

댓글 본문
작성자
비밀번호
버전 관리
gurugio
현재 버전
선택 버전
graphittie 자세히 보기