Linux Kernel Compilation and Module Programming
1️⃣ Kernel Compilation
2️⃣ Module Program
1️⃣ Kernel Compilation
Kernel Compilation and Module Programming
이번 시간에는 리눅스 커널(Linux Kernel) 과 컴파일(Compilation) 에 대해 학습한다. 리눅스를 공부하다 보면 커널이라는 단어를 자주 접하게 되는데, 커널은 운영체제의 핵심 부분으로 하드웨어와 소프트웨어 사이에서 자원을 관리하고 시스템이 동작할 수 있도록 도와주는 역할을 한다.
이번 강의에서는 리눅스 커널이 무엇인지뿐만 아니라, 공개되어 있는 소스 코드(Source Code) 를 어떻게 수정하고, 수정한 내용을 실제 시스템에 적용하는 방법까지 살펴본다. 이를 위해 먼저 커널 컴파일(Kernel Compilation) 의 기본 절차를 이해하고, 이후에는 커널 기능을 보다 유연하게 확장할 수 있는 모듈 프로그램(Module Program) 에 대해서도 다룬다.
커널 컴파일(Kernel Compilation) 은 말 그대로 커널의 소스 코드를 컴퓨터가 실행할 수 있는 형태로 변환하는 작업이다. 컴파일이라는 행위 자체는 단순해 보일 수 있지만, 실제 커널 컴파일에는 여러 준비 과정과 설정 단계가 필요하다. 특히 리눅스 커널은 매우 큰 규모의 프로그램이기 때문에 전체 커널을 컴파일하는 데 시간이 오래 걸릴 수 있다.
과거에도 커널 컴파일은 당시 컴퓨터 성능에 비해 상당히 무거운 작업이었다. 현재는 CPU 성능과 코어 수가 많이 향상되었지만, 리눅스 커널 자체도 계속 확장되고 복잡해졌기 때문에 여전히 컴파일 시간이 적지 않게 걸릴 수 있다. 그래서 모든 기능을 커널에 직접 포함시키는 방식만 사용하는 것이 아니라, 필요한 기능을 별도로 추가하거나 제거할 수 있는 모듈(Module) 방식을 함께 사용한다.
모듈(Module) 은 커널의 기능을 확장하기 위한 프로그램 형태로 이해할 수 있다. 커널 전체를 다시 컴파일하지 않고도 특정 기능을 추가하거나 사용할 수 있게 해 주기 때문에, 커널을 보다 효율적으로 관리하는 데 도움이 된다. 예를 들어 특정 장치 드라이버(Device Driver)나 파일 시스템(File System) 기능 등을 모듈 형태로 사용할 수 있다.
이번 수업의 핵심은 먼저 리눅스 커널 컴파일(Linux Kernel Compilation) 의 기본 절차를 이해하는 것이다. 그다음에는 모듈 프로그램(Module Program) 을 간단히 작성하고 컴파일하여 사용할 수 있다는 수준까지 익히는 것이 목표이다. 모듈 프로그램은 다양한 분야에서 활용될 수 있지만, 이번 시간에는 복잡한 응용보다는 “커널 기능을 별도의 모듈로 만들고 사용할 수 있다”는 기본 개념을 중심으로 학습한다.
정리하면, 이번 강의는 리눅스 운영체제의 핵심인 커널(Kernel) 을 이해하고, 공개된 커널 소스를 수정하여 적용하는 방법, 그리고 커널 전체를 다시 컴파일하지 않고 기능을 확장할 수 있는 모듈(Module) 개념을 배우는 시간이다. 이를 통해 리눅스가 단순히 사용하는 운영체제가 아니라, 사용자가 직접 수정하고 확장할 수 있는 구조를 가진 운영체제라는 점을 이해할 수 있다.
Kernel Release Types and Maintainers
이번 강의에서는 커널 컴파일(Kernel Compilation) 을 본격적으로 다루기 전에, 리눅스 커널 소스에서 자주 보게 되는 릴리스 관련 용어들을 먼저 정리한다. 리눅스 커널은 단순히 한 사람이 만든 코드를 공개해 두고 아무나 사용하는 구조가 아니라, 오랜 시간 동안 많은 개발자와 메인테이너(Maintainer) 들이 체계적으로 관리하는 프로젝트이다.
리눅스 커널은 처음에 리누스 토발즈(Linus Torvalds) 가 학창 시절 취미 프로젝트로 시작했다. 그는 자신의 운영체제 커널 아이디어를 인터넷 뉴스그룹과 메일을 통해 공유했고, 이에 관심을 가진 사람들이 참여하면서 점차 커다란 오픈소스 프로젝트로 발전했다. 당시의 개발 방식은 지금처럼 실시간 채팅으로 즉각 반응하는 방식이라기보다는, 메일링 리스트(Mailing List) 를 통해 패치를 보내고 의견을 주고받는 방식에 가까웠다.
리눅스 커널은 무료로 사용할 수 있고 소스 코드도 공개되어 있지만, 아무 체계 없이 관리되는 것은 아니다. 커널 소스에는 저작권 표시와 라이선스 조건이 존재하며, 대표적으로 GPL(General Public License) 에 따라 배포된다. 따라서 누구나 소스 코드를 확인하고 수정할 수 있지만, 수정한 코드를 배포할 때는 라이선스 조건을 따라야 한다. 즉, “완전히 아무 조건 없는 공짜 코드”라기보다는, 오픈소스 라이선스 안에서 자유롭게 사용하고 수정할 수 있는 구조라고 이해하는 것이 정확하다.
커널 개발이 계속 진행되면서 여러 기능이 추가되고, 각 기능이나 하위 시스템을 관리하는 사람들이 생겼다. 이들을 메인테이너(Maintainer) 라고 한다. 메인테이너는 특정 드라이버, 파일 시스템, 네트워크, 메모리 관리 등 커널의 특정 영역을 관리하며, 외부 개발자들이 보낸 패치(Patch) 를 검토한다. 패치는 버그를 수정하거나, 더 나은 알고리즘을 적용하거나, 새로운 기능을 추가하기 위해 기존 코드를 변경한 내용이다.
개발자가 패치를 보내면 메인테이너는 해당 코드가 적절한지 검토하고, 필요한 경우 테스트용 코드 흐름에 포함시킨다. 이후 일정 기간 동안 테스트를 거치며 문제가 없는지 확인한 뒤, 최종적으로 공식 커널 릴리스에 반영될 수 있다. 리눅스 커널 개발에는 많은 메인테이너가 참여하지만, 핵심적인 최종 흐름을 관리하는 사람은 많지 않다. 특히 메인라인 커널(Mainline Kernel) 의 최종 통합과 릴리스에는 리누스 토발즈의 역할이 크다.
커널 릴리스에서 먼저 볼 수 있는 용어는 프리패치(Prepatch) 또는 RC(Release Candidate) 이다. 이는 정식 안정 버전으로 공개되기 전 단계의 커널이다. 새로운 기능이나 수정된 코드가 들어간 상태이며, 개발자와 관심 있는 사용자들이 직접 컴파일하고 테스트해 볼 수 있다. 아직 안정 버전으로 확정된 것은 아니기 때문에, 실제 운영 환경보다는 테스트 목적으로 사용하는 것이 적절하다. 공식 kernel.org 설명에서도 Prepatch 또는 RC 커널은 mainline 커널의 사전 릴리스이며, 보통 커널 개발자와 리눅스 애호가를 대상으로 한다고 설명한다.
다음은 메인라인(Mainline) 이다. 메인라인 커널은 새로운 기능이 본격적으로 반영되는 중심 개발 흐름이다. 새로운 드라이버, 기능 개선, 내부 구조 변경 등이 이 흐름을 통해 들어온다. 공식 문서에 따르면 리눅스 커널은 느슨한 시간 기반 릴리스 방식을 사용하며, 보통 새로운 주요 커널 릴리스가 2~3개월 간격으로 나온다.
스테이블(Stable) 은 메인라인 커널이 공개된 이후, 버그 수정과 안정성 보완이 적용되는 릴리스이다. 새로운 기능을 계속 추가하기보다는 이미 공개된 커널에서 발견된 문제를 고치고, 필요한 수정 사항을 반영하는 데 초점이 있다. 안정 커널의 패치는 검토 후 RC 형태로 다시 테스트될 수 있으며, 문제가 없을 때 안정 릴리스에 포함된다.
마지막으로 롱텀(Longterm) 또는 LTS(Long Term Support) 는 특정 안정 커널 버전을 오랫동안 유지 관리하는 릴리스이다. 어떤 커널 버전은 오래 사용해도 좋다고 판단되면 장기 지원 대상으로 선정되고, 이후 몇 년 동안 필요한 버그 수정과 보안 수정이 계속 제공된다. 서버, 임베디드 시스템, 기업 환경처럼 커널을 자주 바꾸기 어려운 곳에서는 이런 Longterm 커널이 중요하다. kernel.org의 릴리스 페이지에서도 mainline, stable, longterm과 같은 분류를 통해 현재 관리 중인 커널 버전을 구분해 제공한다.
정리하면, Prepatch/RC 는 정식 공개 전 테스트용 후보 버전이고, Mainline 은 새로운 기능이 들어가는 중심 개발 버전이다. Stable 은 메인라인 공개 이후 버그 수정 중심으로 관리되는 버전이며, Longterm/LTS 는 특정 안정 버전을 장기간 유지보수하는 버전이다. 따라서 커널을 직접 컴파일하거나 소스를 내려받을 때는 자신이 테스트 목적의 최신 기능을 원하는지, 일반적인 안정성을 원하는지, 장기간 유지보수되는 버전을 원하는지에 따라 적절한 커널 종류를 선택해야 한다.
커널 소스 다운로드와 커널 버전 번호
Kernel Source Download and Kernel Version Numbering
리눅스 커널 소스(Kernel Source) 는 공식적으로 kernel.org에서 다운로드할 수 있다. 강의에서 언급한 주소는 https://www.kernel.org/pub/이며, 이곳에 접속하면 웹 브라우저를 통해 디렉터리 구조를 직접 확인할 수 있다. 커널 소스는 보통 그 안의 linux/kernel 디렉터리 아래에서 버전별로 정리되어 있으며, v4.x, v5.x, v6.x와 같은 형태로 세대별 디렉터리가 나뉘어 있다. kernel.org의 공식 디렉터리에도 v4.x, v5.x, v6.x와 같은 항목이 실제로 구분되어 있다.
커널 소스를 다운로드하는 방법은 여러 가지가 있다. 웹에서 파일을 직접 내려받는 HTTP Protocol, 개발 이력을 함께 관리할 수 있는 Git, 그리고 서버 간 파일 동기화에 사용되는 Rsync Protocol 방식이 있다. 같은 커널 소스라도 어떤 프로토콜을 사용하느냐에 따라 접근 위치가 다를 수 있다. 일반적으로 단순히 커널 소스 압축 파일을 내려받아 컴파일해 보는 목적이라면 HTTP 방식으로도 충분하고, 커널 개발 흐름을 따라가거나 패치를 적용하고 관리하려면 Git 을 사용하는 것이 적절하다.
커널 릴리스 페이지에서는 앞에서 배운 Mainline, Stable, Longterm 과 같은 정보도 함께 확인할 수 있다. Mainline Kernel 은 새로운 기능이 들어가는 중심 개발 흐름이고, Stable Kernel 은 mainline이 공개된 뒤 버그 수정이 반영되는 안정 버전이다. Longterm Kernel 은 오래된 커널 계열을 장기간 유지보수하는 버전으로, 중요한 버그 수정이 필요한 경우 이전 커널 트리에 수정 사항을 반영하는 방식으로 관리된다. kernel.org에서도 mainline은 리누스 토발즈가 관리하고, stable과 longterm은 지정된 stable 커널 메인테이너가 관리한다고 설명한다.
강의에서 예로 든 5.10.252와 같은 숫자는 커널의 구체적인 버전 번호이다. 여기서 5.10은 커널 계열을 의미하고, 마지막의 .252는 해당 계열에서 안정화 패치가 누적되어 올라간 패치 버전으로 볼 수 있다. 즉, 5.10.252는 5.10 계열 커널이 장기간 유지보수되면서 여러 번의 수정 사항이 반영된 상태라고 이해하면 된다.
이때 Longterm 목록에 5.10 계열까지만 보인다면, 4.x 계열은 현재 kernel.org의 공식 장기 지원 대상에서는 빠졌다고 볼 수 있다. 다만 이것은 4.x 커널 소스 자체가 사라졌다는 뜻은 아니다. 실제 kernel.org의 커널 디렉터리에는 v4.x 디렉터리가 남아 있다. 따라서 구형 시스템이나 임베디드 시스템에서 4.x 계열 커널을 사용하고 있다면, kernel.org에서 제공하는 공식 longterm 유지보수 범위에서는 벗어났을 가능성이 크다고 이해하면 된다.
하지만 사용자가 설치한 리눅스 배포판이나 특정 회사가 4.x 기반 커널을 별도로 관리할 가능성은 있다. 예를 들어 기업용 배포판, 임베디드 장비 제조사, 특정 하드웨어 벤더는 자신들의 제품 안정성을 위해 오래된 커널을 자체적으로 유지보수할 수 있다. kernel.org에서도 배포판들이 자체적인 장기 유지보수 커널을 제공할 수 있으며, 이런 커널은 kernel.org에 호스팅되지 않고 kernel.org 개발자들이 직접 지원하지 않는다고 설명한다. 이 경우 유지보수는 해당 회사나 커뮤니티의 자본력, 기술력, 지원 정책에 따라 달라질 수 있고, 기업용 지원이라면 비용이 발생할 수도 있다.
커널 버전 번호의 의미도 함께 알아둘 필요가 있다. 예전 리눅스 커널에서는 버전 번호의 홀수와 짝수에 의미가 있었다. 예를 들어 첫 번째 점 뒤의 숫자가 홀수이면 개발 버전, 짝수이면 안정 버전처럼 구분하던 시기가 있었다. 하지만 이 방식은 오래전에 사라졌다. kernel.org에서도 이 홀수·짝수 구분 방식은 2.6 릴리스 이후 폐기되었고, 현재는 사전 릴리스 커널을 -rc로 표시한다고 설명한다.
최근의 커널 버전 번호는 과거처럼 엄격한 의미를 갖는다기보다는 순차적으로 올라가는 번호에 가깝게 이해하면 된다. 4.x, 5.x, 6.x처럼 큰 숫자가 바뀐다고 해서 항상 커널 내부 구조가 완전히 달라졌다는 뜻은 아니다. kernel.org의 설명에 따르면 큰 버전 번호는 점 뒤의 숫자가 너무 커 보일 때 증가하며, 그 외에 특별한 이유가 있는 것은 아니라고 한다. 따라서 최근에는 버전 번호 자체의 상징적 의미보다는, 해당 버전이 Mainline, Stable, Longterm 중 어디에 속하는지, 그리고 지원 기간이 어떻게 되는지를 확인하는 것이 더 중요하다.
커널 소스를 받을 수 있는 대표적인 경로로는 Git 과 Rsync 도 있다. Git 저장소는 git.kernel.org에서 확인할 수 있고, Rsync 경로는 rsync://rsync.kernel.org/pub/ 형태로 제공된다. Rsync 는 파일과 디렉터리를 효율적으로 동기화하는 데 사용되는 도구이자 프로토콜이다. 과거에는 백업이나 서버 간 미러링 작업에 많이 사용되었고, 현재도 저장소나 백업 시스템 내부에서는 활용될 수 있다. 다만 리눅스를 처음 사용하는 입장에서는 직접 사용할 일이 많지 않을 수 있으므로, 커널 소스를 다운로드할 수 있는 여러 방식 중 하나로 알아두면 된다.
웹에서 커널 소스 읽기와 메일링 리스트
Reading Kernel Source Online and Mailing Lists
커널 소스가 처음이라면, 반드시 처음부터 소스 파일을 직접 다운로드해서 압축을 풀고 하나하나 열어볼 필요는 없다. 물론 전문적으로 커널을 수정하거나 컴파일을 직접 해 보려면 컴퓨터 안에 커널 소스를 내려받아 확인하는 것이 좋다. 하지만 단순히 커널 소스가 어떻게 구성되어 있는지 보고 싶거나, 특정 함수와 파일을 가볍게 살펴보고 싶은 단계라면 웹에서 커널 소스를 볼 수 있는 사이트를 이용하는 방법도 있다.
대표적인 예로 Bootlin Elixir Cross Referencer 가 있다. 강의에서 언급한 https://elixir.bootlin.com/linux/v6.19.8/source 와 같은 주소에 들어가면 리눅스 커널 소스가 웹에서 보기 쉽게 정리되어 있다. Elixir 는 리눅스 커널 같은 C/C++ 프로젝트의 여러 릴리스를 색인화하고, 소스 코드 안의 함수나 매크로, 구조체 등을 서로 연결해서 볼 수 있도록 만든 Source Code Cross-Referencer 이다. Bootlin의 Elixir GitHub 설명에서도 Elixir는 LXR에서 영감을 받은 소스 코드 교차 참조 도구이며, Linux kernel 같은 C/C++ 프로젝트의 모든 릴리스를 색인화하는 것이 주요 목적이라고 설명한다.
실제로 커널 소스를 컴퓨터에 다운로드해서 압축을 풀면, 웹에서 보는 것과 비슷한 디렉터리 구조가 나타난다. 커널 소스 루트에는 매우 많은 폴더와 파일이 있으며, 각각의 디렉터리는 커널의 특정 기능이나 하위 시스템과 관련되어 있다. 예를 들어 네트워크와 관련된 부분, 장치 드라이버와 관련된 부분, 파일 시스템과 관련된 부분처럼 커널 내부 기능이 디렉터리별로 나뉘어 있다. 웹 브라우저에서 이런 디렉터리를 클릭하면 파일 목록을 확인할 수 있고, 개별 파일을 클릭하면 소스 코드를 바로 읽을 수 있다.
이런 웹 기반 소스 브라우저의 장점은 단순히 파일 내용을 보여주는 데서 끝나지 않는다는 점이다. 코드 안에 있는 함수 이름이나 자료구조, 매크로 등이 링크처럼 연결되어 있어, 클릭하면 관련 정의나 사용 위치를 쉽게 따라갈 수 있다. 처음 커널 소스를 보는 입장에서는 어디서부터 봐야 할지 막막할 수 있는데, 이런 방식은 소스 코드의 연결 관계를 따라가며 이해하는 데 도움이 된다. 즉, 커널 소스가 궁금하지만 아직 직접 다운로드하거나 빌드 환경을 구성하기 어렵다면, 먼저 Elixir 같은 웹 도구를 이용해 구조를 익히는 것도 좋은 방법이다.
반대로 커널을 전문적으로 수정하고 싶거나 실제로 패치를 작성하고 싶다면, 웹에서 보는 것만으로는 부족하다. 이 경우에는 커널 소스를 로컬 컴퓨터에 직접 내려받고, Git으로 버전 관리를 하면서 파일을 수정하고 컴파일하는 방식으로 작업해야 한다. 웹 브라우저는 소스를 읽고 탐색하는 데는 편리하지만, 실제 개발과 테스트는 로컬 환경에서 이루어지는 경우가 많다.
커널 개발과 관련해서 또 알아둘 것이 메일링 리스트(Mailing List) 이다. 강의에서 언급된 https://kernelnewbies.org/ML 은 커널 개발 입문자에게 도움이 되는 메일링 리스트 정보를 정리한 페이지이다. 메일링 리스트는 쉽게 말해 단체 이메일 주소라고 볼 수 있다. 어떤 사람이 메일링 리스트 주소로 메일을 보내면, 그 리스트에 가입된 사람들이 같은 메일을 받아볼 수 있다. KernelNewbies 페이지에서도 여러 유용한 메일링 리스트를 소개하고 있으며, kernelnewbies 메일링 리스트에 가입하려면 별도의 구독 페이지를 이용하라고 안내하고 있다.
리눅스 커널 개발은 오래전부터 메일 중심으로 이루어져 왔다. 앞에서 리누스 토발즈가 자신의 프로젝트를 메일과 뉴스그룹을 통해 알렸던 것처럼, 커널 개발 문화에서는 지금도 메일을 통해 패치를 보내고, 의견을 주고받고, 코드 리뷰를 진행하는 방식이 중요하다. KernelNewbies의 메일링 리스트 설명에 따르면, 이 리스트는 리눅스 커널을 함께 배우기 위한 이메일 리스트이며, 모든 리스트 구성원에게 글을 보내려면 kernelnewbies@kernelnewbies.org 로 메일을 보내면 된다.
특정 분야에 관심이 있다면 관련 메일링 리스트를 찾아보는 것도 도움이 된다. 예를 들어 메모리 관리와 관련된 내용을 보고 싶다면 메모리 관련 커널 메일링 리스트나 해당 하위 시스템의 논의 내용을 찾아볼 수 있다. 메일링 리스트에 가입하면 해당 주제와 관련된 개발자들의 논의, 패치 검토, 질문과 답변을 받아볼 수 있다. 다만 커널 관련 메일링 리스트는 메일 양이 많을 수 있으므로, 처음에는 KernelNewbies처럼 입문자용 자료를 먼저 참고하거나, 필요한 주제의 아카이브를 검색하는 방식으로 접근하는 것이 부담이 적다.
리눅스/유닉스 강의노트: 커널 패치 제출을 위한 준비 과정
Preparing to Submit Kernel Patches
이번에는 리눅스 커널 패치(Kernel Patch) 를 제출하기 위해 어떤 준비 과정이 필요한지 살펴본다. 사용자가 커널을 직접 수정하고, 그 수정 사항을 개인적으로만 사용하는 것이 아니라 실제 리눅스 커널 개발 흐름에 반영하고 싶다면 단순히 코드를 고치는 것만으로는 부족하다. 커널 코드를 수정하는 도구, 코드 작성 형식, 이메일 설정, Git 사용법, 커널 소스 다운로드와 컴파일 과정까지 함께 준비해야 한다.
먼저 커널 코드를 수정하려면 편집기(Editor) 가 필요하다. 강의에서는 그 예로 Vim 설정을 언급한다. Vim은 오래전부터 리눅스와 유닉스 환경에서 널리 사용되어 온 텍스트 편집기이며, 리눅스 커널 개발에 참여해 온 많은 개발자들이 익숙하게 사용해 온 도구이기도 하다. 리눅스 커널은 20세기 후반에 시작된 오래된 프로젝트이기 때문에, 초기부터 개발에 참여했던 사람들 중에는 터미널 기반 편집기와 메일 기반 개발 환경에 익숙한 사람이 많았다. 그래서 커널 개발 문서나 예제에서도 vi 또는 vim 같은 편집기를 자연스럽게 볼 수 있다.
하지만 중요한 것은 반드시 Vim만 사용해야 한다는 뜻은 아니다. 핵심은 어떤 편집기를 쓰더라도 리눅스 커널의 코딩 스타일(Coding Style) 에 맞게 코드를 작성해야 한다는 점이다. 개인적으로 혼자 수정해서 사용하는 코드라면 자기에게 편한 방식으로 작성해도 큰 문제가 없을 수 있다. 그러나 수정한 코드를 전체 리눅스 커널에 반영하고 싶다면, 중간에서 코드를 검토하는 메인테이너(Maintainer) 들이 보기 편한 형식과 커널 프로젝트에서 정한 규칙을 따라야 한다.
예를 들어 들여쓰기 인덴테이션(Indentation) 방식이 중요하다. 일반적인 프로젝트에서는 2칸 또는 4칸 들여쓰기를 사용하는 경우가 많지만, 리눅스 커널의 C 코드 스타일에서는 탭을 사용하며, 탭은 8글자 너비로 간주된다. 공식 리눅스 커널 코딩 스타일 문서에서도 “탭은 8글자이며, 따라서 들여쓰기도 8글자”라고 설명한다. 이런 규칙을 지키지 않으면 코드 내용이 아무리 좋아도 리뷰 과정에서 지적을 받을 수 있고, 형식이 맞지 않아 패치가 받아들여지지 않을 수도 있다.
그래서 커널 개발을 준비할 때는 편집기 설정을 커널 스타일에 맞춰 두는 것이 좋다. Vim을 사용한다면 탭 너비, 자동 들여쓰기, 공백 처리 등을 커널 스타일에 맞게 설정할 수 있다. 또한 커널 소스에는 스타일 문제를 확인할 수 있는 도구도 제공된다. 예를 들어 checkpatch 는 패치가 커널 코딩 스타일을 잘 따르는지 검사하는 데 사용된다. 공식 문서에서도 코드 들여쓰기는 스페이스가 아니라 탭을 사용해야 하며, 문서나 일부 설정 파일을 제외하면 일반 코드의 들여쓰기에 스페이스를 사용하지 않는다고 설명한다.
두 번째 준비는 이메일 설정(Email Setup) 이다. 리눅스 커널 개발에서는 패치를 이메일로 보내는 문화가 여전히 중요하다. 커널 패치는 보통 메일 본문에 인라인 텍스트 형태로 보내며, 첨부 파일 방식은 리뷰 과정에서 인용하고 토론하기 불편하기 때문에 일반적으로 선호되지 않는다. 공식 문서에서도 리눅스 커널 패치는 이메일로 제출되며, 가능하면 이메일 본문에 인라인 텍스트로 넣는 것이 좋다고 설명한다. 따라서 커널 패치를 제출하려면 일반적인 메일 작성뿐 아니라, 패치 형식이 깨지지 않도록 보내는 설정도 중요하다.
세 번째는 Git 설정(Git Setup) 이다. 현재 리눅스 커널은 Git으로 관리된다. Git은 리누스 토발즈가 커널 개발을 위해 만든 분산 버전 관리 시스템이며, 커널 소스를 내려받고, 변경 이력을 관리하고, 패치를 생성하는 데 핵심적으로 사용된다. 커널 개발을 제대로 하려면 단순히 압축 파일을 다운로드하는 것보다 Git 저장소를 이용하는 것이 좋다. 공식 패치 제출 문서에서도 현재 커널 소스 트리가 없다면 Git으로 소스를 받는 것을 안내하며, tarball로 커널 릴리스를 다운로드하는 방법도 가능하지만 커널 개발을 하기에는 어려운 방식이라고 설명한다.
그다음은 커널 소스 다운로드(Downloading Kernel Source) 이다. 커널을 수정하려면 먼저 수정할 대상이 되는 커널 소스가 필요하다. 단순 학습이라면 특정 버전의 커널 소스를 다운로드해서 살펴볼 수 있지만, 실제 패치를 제출하려면 어느 트리를 기준으로 작업할지 확인해야 한다. 모든 패치가 무조건 리누스 토발즈의 메인라인 트리만을 기준으로 만들어지는 것은 아니며, 특정 하위 시스템의 메인테이너가 관리하는 별도 트리를 기준으로 패치를 요구할 수도 있다. 공식 문서에서도 대부분의 하위 시스템 메인테이너는 자신들이 관리하는 트리를 기준으로 패치를 준비하길 원할 수 있다고 설명한다.
다섯 번째는 커널 설정(Kernel Configuration) 이다. 커널 소스를 받은 뒤에는 어떤 기능을 포함하고 제외할지 설정해야 한다. 커널은 매우 큰 프로그램이기 때문에 모든 기능을 무조건 하나의 형태로 빌드하는 것이 아니라, 시스템에 필요한 기능을 선택하고, 어떤 기능은 커널에 직접 포함하고, 어떤 기능은 모듈로 만들 수 있다. 이 설정 과정이 끝나야 실제 컴파일을 진행할 수 있다.
여섯 번째는 커널 컴파일(Kernel Compilation) 이다. 설정이 끝나면 커널 소스를 컴파일하여 실행 가능한 커널 이미지와 필요한 모듈들을 만든다. 커널 컴파일은 일반 프로그램보다 시간이 오래 걸릴 수 있으며, 시스템 성능과 설정 내용에 따라 소요 시간이 달라진다. 따라서 처음부터 전체 커널을 완벽하게 수정하려고 하기보다는, 작은 수정부터 시작해 컴파일과 테스트 과정을 반복하면서 익숙해지는 것이 좋다.
일곱 번째는 코드 수정과 재컴파일(Code Modification and Recompilation) 이다. 커널 소스를 수정한 뒤에는 다시 컴파일하여 오류가 없는지 확인해야 한다. 단순히 문법 오류가 없는지만 보는 것이 아니라, 수정한 내용이 실제 커널 동작에 영향을 주는 만큼 빌드와 테스트 과정이 중요하다. 커널 코드는 시스템 전체와 연결되어 있기 때문에 작은 수정도 예상하지 못한 문제를 만들 수 있다.
마지막은 수정한 코드 제출(Submitting Modified Code) 이다. 수정한 코드가 준비되면 Git을 이용해 패치를 만들고, 정해진 형식에 맞춰 메일로 제출한다. 패치 메일 제목에는 보통 [PATCH] 를 붙여 다른 논의 메일과 구분한다. 공식 문서에서도 리눅스 커널 메일 트래픽이 많기 때문에 제목에 [PATCH] 를 붙이는 것이 일반적인 관례이며, git send-email을 사용하면 이를 자동으로 처리해 준다고 설명한다.
정리하면, 커널 패치를 제출하기 위한 준비 과정은 단순히 “코드를 고친다”에서 끝나지 않는다. 편집기 설정, 커널 코딩 스타일 준수, 이메일 설정, Git 설정, 커널 소스 다운로드, 커널 설정, 컴파일, 수정, 재컴파일, 패치 제출까지 이어지는 흐름을 이해해야 한다. 특히 리눅스 커널은 오랜 시간 동안 메일 기반 협업 문화와 엄격한 코드 형식을 유지해 온 프로젝트이기 때문에, 전체 커뮤니티에 반영하고 싶은 수정이라면 그 문화와 규칙에 맞춰 작업하는 것이 중요하다.
커널 컴파일 준비와 소스 코드 압축 풀기 Preparing for Kernel Compilation and Extracting Source Code
이번에는 실제로 커널 컴파일(Kernel Compilation) 을 하기 위한 준비 과정을 살펴본다. 커널을 컴파일하려면 먼저 컴파일에 필요한 도구와 라이브러리를 설치해야 하고, 그다음 리눅스 커널 소스 코드를 다운로드한 뒤 압축을 풀고 소스 코드 디렉터리로 이동해야 한다.
먼저 필요한 도구 패키지를 설치한다. 강의에서 제시된 명령어는 다음과 같다.
sudo apt-get install libncurses5-dev gcc make git exuberant-ctags bc libssl-dev flex bison
여기서 apt-get 은 Debian 계열 리눅스 배포판에서 패키지를 설치하거나 관리할 때 사용하는 명령어이다. Ubuntu나 Debian 기반 환경에서는 apt-get install을 사용해 필요한 패키지를 설치할 수 있다. 최근에는 사용자 입장에서 더 간단하게 apt install을 사용하는 경우도 많다. 따라서 아래처럼 작성해도 된다.
sudo apt install libncurses5-dev gcc make git exuberant-ctags bc libssl-dev flex bison
다만 apt-get이 완전히 사라진 것은 아니다. Ubuntu 공식 문서에서는 apt는 대화형 사용에 적합하고, apt-get은 스크립트에서 사용하는 것이 좋다고 설명한다. 기본적인 설치 명령에서는 두 명령어의 문법이 거의 같기 때문에, 수업이나 실습에서는 apt install로 이해해도 무리가 없다. (Ubuntu Documentation)
설치하는 패키지들을 보면 각각 커널 컴파일 과정에서 필요한 역할이 있다. gcc 는 C 언어 컴파일러이고, make 는 Makefile에 따라 빌드 과정을 실행하는 도구이다. git 은 커널 소스 코드를 내려받거나 버전 관리를 할 때 사용한다. libncurses5-dev 는 커널 설정 메뉴를 터미널에서 보기 좋게 표시하는 menuconfig 같은 기능에 필요할 수 있다. bc 는 커널 빌드 중 계산 작업에 사용될 수 있고, libssl-dev 는 인증서나 암호화 관련 빌드 과정에서 필요할 수 있다. flex 와 bison 은 일부 설정 파일이나 문법 분석 관련 도구를 생성하는 데 사용된다. exuberant-ctags 는 소스 코드 안의 함수나 변수 위치를 추적하기 쉽게 해 주는 태그 파일 생성 도구로, 커널 소스 탐색에 도움이 된다.
강의 자료에서는 두 번째 단계로 리눅스 소스 코드 다운로드(Downloading Linux Source Code) 를 언급한다. 녹취록에는 두 번째 단계에도 패키지 설치 명령어가 한 번 더 적혀 있는데, 문맥상 이 부분은 커널 소스 코드를 다운로드하는 단계로 보는 것이 자연스럽다. 예를 들어 kernel.org에서 원하는 커널 버전의 압축 파일을 다운로드하거나, Git을 이용해 소스 코드를 받을 수 있다. 강의에서는 이후 linux-6.1.166.tar.xz 파일을 예로 들고 있으므로, 이 파일을 다운로드했다고 가정하고 설명이 이어진다.
그다음 단계는 압축 풀기(Extracting Archive) 이다. 예시 명령어는 다음과 같다.
tar xJvf linux-6.1.166.tar.xz
여기서 tar는 여러 파일과 디렉터리를 하나의 아카이브 파일로 묶거나 풀 때 사용하는 명령어이다. 옵션의 의미를 보면 x는 압축을 푼다는 뜻이고, v는 진행 과정을 화면에 자세히 보여준다는 뜻이며, f는 뒤에 파일 이름이 온다는 뜻이다. 가운데의 대문자 J는 xz 압축(XZ Compression) 형식의 파일을 처리할 때 사용하는 옵션이다. GNU tar 문서에서도 -J 옵션은 --xz와 같은 의미로, 아카이브를 xz 방식으로 처리한다고 설명한다. (GNU tar Manual)
따라서 파일 이름이 .tar.xz로 끝난다면 위와 같이 xJvf 옵션을 사용할 수 있다. 만약 파일이 .tar.gz 또는 .tgz로 끝난다면 gzip 형식이므로 보통 xzvf를 사용한다.
tar xzvf linux-6.1.166.tar.gz
파일이 .tar.bz2로 끝난다면 bzip2 형식이므로 보통 xjvf를 사용한다.
tar xjvf linux-6.1.166.tar.bz2
즉, 다운로드한 파일의 확장자가 무엇인지에 따라 tar 옵션이 달라질 수 있다. 강의에서 말한 것처럼 .xz로 끝나는 파일은 J 옵션을 사용하는 예로 이해하면 된다.
압축을 풀고 나면 커널 소스 코드 디렉터리가 생성된다. 예를 들어 linux-6.1.166.tar.xz 파일을 풀면 보통 linux-6.1.166이라는 디렉터리가 만들어진다. 그다음에는 해당 디렉터리로 이동한다.
cd linux-6.1.166
이제 이 디렉터리 안에서 커널 설정과 컴파일 작업을 진행할 수 있다. 즉, 이번 단계의 흐름은 필요한 빌드 도구를 설치하고, 커널 소스 코드를 다운로드한 뒤, 압축을 풀고, 생성된 소스 코드 디렉터리로 이동하는 과정이라고 정리할 수 있다.
커널 설정 파일과 .config
Kernel Configuration File and .config
커널 컴파일(Kernel Compilation) 을 하려면 먼저 커널 설정 파일인 .config 파일을 준비해야 한다. 커널 소스 코드를 다운로드하고 압축을 풀었다고 해서 바로 컴파일할 수 있는 것은 아니다. 리눅스 커널은 매우 많은 기능과 드라이버, 파일 시스템, 네트워크 기능, 보안 기능 등을 포함하고 있기 때문에, 그중 어떤 기능을 커널에 포함할지 먼저 설정해야 한다. 이 설정 내용을 저장하는 파일이 바로 .config 이다.
여기서 파일 이름 앞에 붙은 점 .은 리눅스에서 숨김 파일을 의미한다. 따라서 .config 파일은 일반적인 ls 명령으로는 보이지 않을 수 있고, ls -a처럼 숨김 파일까지 표시하는 옵션을 사용해야 확인할 수 있다. 이 .config 파일은 커널 소스 코드의 최상위 디렉터리, 즉 Kernel Source Tree 의 루트 위치에 있어야 한다. 그래야 make 명령으로 커널을 설정하거나 컴파일할 때 해당 설정을 기준으로 빌드가 진행된다.
가장 간단한 방법은 현재 사용 중인 커널의 설정을 복사해서 사용하는 것이다. 여기서 “현재 사용 중인 커널”이란, 지금 시스템을 부팅할 때 실제로 사용된 커널을 의미한다. 리눅스 시스템에는 보통 현재 부팅된 커널의 설정 정보가 /boot 디렉터리 아래에 저장되어 있다. 예를 들어 현재 실행 중인 커널 버전은 uname -r 명령으로 확인할 수 있고, 그 버전에 해당하는 설정 파일을 커널 소스 디렉터리의 .config로 복사해서 사용할 수 있다.
cp /boot/config-$(uname -r) .config
이렇게 하면 현재 사용 중인 환경의 커널 설정을 새로 컴파일할 커널 소스에 가져와서 사용할 수 있다. 이 방식이 처음 커널을 컴파일하는 사람에게 가장 안전한 이유는, 현재 시스템에서 정상적으로 동작하고 있는 드라이버와 기능 설정을 최대한 그대로 가져오기 때문이다. 예를 들어 지금 사용하는 네트워크 카드, 저장장치, 파일 시스템, 그래픽 장치 등에 필요한 설정이 포함되어 있을 가능성이 높다.
물론 사용자가 커널 설정을 직접 하나하나 선택할 수도 있다. 기본 설정을 적용하고 싶다면 다음과 같은 명령을 사용할 수 있다.
make defconfig
make defconfig 는 현재 아키텍처에 맞는 기본 커널 설정을 생성하는 명령이다. 이 명령을 사용하면 커널 소스에 정의된 기본값을 바탕으로 .config 파일이 만들어진다. 다만 이 기본 설정이 현재 내 컴퓨터의 하드웨어와 완전히 일치한다고 보장할 수는 없다. 따라서 처음 커널 컴파일을 실습할 때는 현재 사용 중인 커널 설정을 복사해 오는 방식이 더 안전할 수 있다.
설정을 직접 수정하고 싶다면 make menuconfig 또는 make nconfig 를 사용할 수 있다.
make menuconfig
make nconfig
make menuconfig 는 터미널 안에서 메뉴 형태로 커널 옵션을 선택할 수 있게 해 주는 설정 도구이다. make nconfig 도 비슷하게 텍스트 기반 메뉴 화면을 제공하지만, menuconfig보다 개선된 형태의 인터페이스를 제공한다. 공식 커널 문서에서도 make menuconfig는 텍스트 기반 컬러 메뉴, 라디오 리스트, 대화상자를 제공하고, make nconfig는 향상된 텍스트 기반 컬러 메뉴라고 설명한다.
이런 설정 화면에 들어가면 커널 소스 트리 안의 수많은 기능에 대해 선택할 수 있다. 어떤 기능은 커널 안에 직접 포함할 수 있고, 어떤 기능은 모듈(Module) 로 빌드할 수 있으며, 어떤 기능은 아예 제외할 수 있다. 일반적으로 설정 화면에서는 기능을 선택할 때 Y, M, N과 같은 형태로 표시한다. Y는 해당 기능을 커널에 직접 포함한다는 뜻이고, M은 모듈로 빌드한다는 뜻이며, N은 포함하지 않는다는 뜻으로 이해할 수 있다.
이 과정에서 의존성(Dependency) 도 중요하다. 커널의 어떤 기능은 다른 기능이 함께 켜져 있어야 제대로 동작한다. 만약 필요한 옵션이 빠지거나 의존성이 맞지 않으면, 컴파일 자체가 실패할 수도 있고, 컴파일은 성공하더라도 새 커널로 부팅했을 때 원하는 기능이 동작하지 않을 수 있다. 공식 커널 문서에서도 새 커널 버전에서는 새로운 설정 옵션이 추가될 수 있으므로, 설정 단계를 건너뛰면 예상하지 못한 문제가 생길 수 있다고 설명한다.
가장 대표적인 문제가 드라이버(Driver) 를 빼먹는 경우이다. 예를 들어 기존 커널로 부팅했을 때는 네트워크가 정상적으로 동작했는데, 새 커널을 컴파일하면서 네트워크 카드 드라이버를 제외해 버렸다면 새 커널로 부팅한 뒤 네트워크가 연결되지 않을 수 있다. 사용자는 컴파일 환경에서는 분명히 네트워크가 됐기 때문에 당황할 수 있지만, 실제로 새로 부팅한 커널 안에는 해당 네트워크 드라이버가 없기 때문에 문제가 발생하는 것이다.
그래서 처음 커널을 컴파일할 때는 현재 사용 중인 커널 설정을 복사해 오는 방식이 실용적이다. 현재 커널 설정을 가져오면 지금 시스템에서 사용 중인 드라이버와 기능들이 설정 파일에 포함되어 있을 가능성이 높다. 물론 커널 버전이 달라지면 새로운 설정 항목이 생길 수 있으므로, 기존 .config를 복사한 뒤에는 make oldconfig 또는 make olddefconfig 같은 명령으로 새 버전에 맞게 설정을 정리하는 것이 일반적이다. 공식 문서에서도 기존 .config 파일을 새 버전으로 가져갈 때 make oldconfig를 사용하면 기존 설정을 바탕으로 하고, 새로 추가된 설정 항목에 대해서만 질문한다고 설명한다.
결국 .config 파일은 커널 컴파일에서 매우 중요한 역할을 한다. 커널 소스 전체가 재료라면, .config는 그 재료 중 무엇을 사용할지 정하는 설계도와 같다. 현재 시스템과 비슷한 커널을 만들고 싶다면 기존 설정을 복사하는 것이 좋고, 특정 기능을 직접 켜거나 끄고 싶다면 make menuconfig나 make nconfig를 통해 세부 옵션을 조정하면 된다.
커널 컴파일과 병렬 빌드 Kernel Compilation and Parallel Build
커널 설정 파일인 .config 까지 준비되었다면, 실제 커널 컴파일(Kernel Compilation) 과정은 명령어 자체만 보면 간단하다. 커널 소스 디렉터리에서 다음과 같이 make 명령을 실행하면 된다.
make
다만 커널은 규모가 큰 프로그램이기 때문에 단순히 make만 실행하면 시간이 오래 걸릴 수 있다. 그래서 보통은 병렬 컴파일(Parallel Compilation) 을 사용한다. 병렬 컴파일은 여러 작업을 동시에 나누어 실행하는 방식이다. 이때 사용하는 옵션이 -j이다.
make -j4
여기서 -j4는 make가 동시에 최대 4개의 작업을 실행하도록 한다는 뜻이다. 숫자를 크게 하면 더 많은 작업을 동시에 처리하려고 하기 때문에 컴파일 시간이 줄어들 수 있다. 하지만 숫자가 무조건 크다고 좋은 것은 아니다. 실제로 일을 처리할 수 있는 CPU Core 수와 시스템 자원에 영향을 받기 때문이다.
예를 들어 CPU 코어가 1개뿐인데 make -j100을 입력한다고 해서 실제로 100명이 동시에 일하는 것처럼 빨라지는 것은 아니다. 작업을 많이 만들어도 실제로 처리할 CPU 자원이 부족하면 오히려 문맥 전환이나 메모리 사용량이 늘어나 비효율적일 수 있다. 반대로 CPU 코어가 여러 개 있다면 -j 옵션을 사용해서 여러 컴파일 작업을 동시에 실행할 수 있고, 그만큼 결과를 더 빨리 볼 수 있다. GNU Make 문서에서도 make -j 옵션은 한 번에 여러 레시피를 동시에 실행할 수 있게 하는 병렬 실행 옵션이라고 설명한다.
실습에서는 컴파일 시간이 얼마나 걸리는지 확인하기 위해 앞에 time 명령을 붙일 수 있다.
time make -j4
또는 CPU 코어 수에 맞춰 자동으로 값을 넣고 싶다면 다음과 같이 사용할 수도 있다.
time make -j$(nproc)
여기서 nproc은 현재 시스템에서 사용할 수 있는 처리 장치 수를 출력하는 명령이다. 따라서 make -j$(nproc)은 현재 시스템의 CPU 코어 수에 맞춰 병렬 컴파일을 실행하는 방식으로 이해할 수 있다.
time 명령을 붙여 실행하면 컴파일이 끝난 뒤 아래와 비슷한 시간이 출력된다.
real 7m10.000s
user 35m20.000s
sys 3m40.000s
여기서 real time 은 실제로 사용자가 체감한 시간이다. 쉽게 말해 시계로 잰 시간이다. 예를 들어 오전 10시에 컴파일을 시작해서 오전 10시 7분에 끝났다면 real 시간은 약 7분으로 표시된다. Red Hat 문서에서도 real은 명령이 시작해서 끝날 때까지 실제 경과한 시간이라고 설명한다.
user time 은 프로그램이 User Space 에서 CPU를 사용한 시간이다. 커널 컴파일에서는 컴파일러가 소스 코드를 분석하고 오브젝트 파일을 만드는 작업 대부분이 사용자 영역에서 실행되므로, user 시간이 크게 나올 수 있다. 특히 make -j로 여러 작업을 동시에 실행하면 여러 CPU 코어가 각각 시간을 소비하기 때문에, user 시간이 real 시간보다 훨씬 크게 나올 수 있다.
sys time 은 프로그램이 Kernel Space 에서 CPU를 사용한 시간이다. 예를 들어 파일을 읽고 쓰거나, 프로세스를 생성하거나, 시스템 호출을 처리하는 데 사용된 시간이 여기에 포함된다. Red Hat 문서도 user는 user mode에서 소비된 CPU 시간이고, sys는 kernel mode에서 소비된 CPU 시간이라고 설명한다.
여기서 처음 헷갈릴 수 있는 부분은 real보다 user 시간이 더 크게 나오는 경우이다. 이것은 오류가 아니다. 병렬 컴파일에서는 여러 작업이 동시에 실행되기 때문에 CPU 사용 시간이 합산되어 표시된다. 예를 들어 4개의 컴파일 작업이 각각 10분씩 CPU를 사용했다면, 실제 시계 시간은 10분에 가까울 수 있지만 user 시간은 합산되어 40분처럼 표시될 수 있다. 강의에서 말한 것처럼 “10분, 10분, 10분, 10분 해서 총 40분을 소비했다고 계산하는 방식”으로 이해하면 된다.
반대로 real 시간은 긴데 user와 sys 시간이 작다면, CPU가 계속 바쁘게 계산한 것이 아니라 중간에 기다리는 시간이 많았다는 뜻일 수 있다. 예를 들어 디스크 입출력을 기다렸거나, 다른 프로세스 때문에 대기했거나, 병렬로 처리할 작업이 충분하지 않았을 수 있다. 따라서 커널 컴파일 시간을 볼 때는 단순히 real만 보는 것이 아니라, user와 sys 시간이 어떻게 나오는지도 함께 보면 시스템이 CPU를 얼마나 적극적으로 사용했는지 어느 정도 파악할 수 있다.
결국 커널 컴파일은 .config 파일을 준비한 뒤 make를 실행하는 구조이지만, 실제 시간을 줄이기 위해서는 make -j 옵션을 적절히 사용하는 것이 중요하다. -j 뒤의 숫자는 클수록 무조건 좋은 것이 아니라, 시스템의 CPU 코어 수와 메모리, 디스크 성능에 맞춰 정해야 한다. 그리고 time 명령을 함께 사용하면 실제 경과 시간과 CPU 사용 시간을 나누어 확인할 수 있어, 컴파일이 얼마나 효율적으로 진행되었는지 이해하는 데 도움이 된다.
2️⃣ Module Program
모듈 프로그램의 개념과 장점 Concept and Advantages of Module Programming
이번에는 모듈 프로그램(Module Program) 에 대해 간략하게 살펴본다. 앞에서 다룬 커널(Kernel) 은 운영체제의 핵심 전체에 가까운 큰 개념이다. 반면 모듈(Module) 은 그보다 작은 단위이기 때문에 처음 공부할 때 커널 전체를 다루는 것보다 조금 더 쉽게 느껴질 수 있다.
모듈의 사전적 의미는 표준화된 독립적인 단위라고 볼 수 있다. 컴퓨터에서도 비슷한 의미로 사용된다. 어떤 기능이 하나의 독립된 단위로 만들어져 있고, 필요할 때 끼워 넣거나 필요 없을 때 빼는 방식으로 사용할 수 있다. 즉, 모듈은 자체적으로 하나의 기능을 담당하는 비교적 독립적인 구성 요소라고 이해할 수 있다.
리눅스에서 말하는 커널 모듈(Kernel Module) 은 실행 중인 커널에 동적으로 추가하거나 제거할 수 있는 코드이다. 예를 들어 특정 장치 드라이버(Device Driver), 파일 시스템 기능, 네트워크 관련 기능 등이 모듈 형태로 제공될 수 있다. 커널 설정에서 어떤 기능을 커널 안에 직접 포함할 수도 있고, 모듈로 따로 빌드할 수도 있다. 모듈로 만들어진 기능은 필요할 때 커널에 올려 사용하고, 필요하지 않으면 제거할 수 있다.
커널 모듈을 컴파일해서 사용할 때 가장 큰 장점은 재부팅(Reboot) 이 필요하지 않다는 점이다. 일반적으로 커널 자체를 수정하면 수정된 커널을 다시 컴파일하고, 그 커널 이미지로 부팅해야 한다. 예를 들어 현재 1.0.0 커널로 부팅해 사용하고 있는데 커널의 일부 기능을 수정했다면, 수정한 커널을 다시 빌드한 뒤 새 커널로 부팅해야 한다. 결국 시스템을 껐다 켜야 한다는 뜻이다.
물론 커널의 매우 중요한 부분이나 시스템 안정성과 직접 관련된 부분을 수정했다면, 새 커널로 안전하게 부팅하는 과정이 필요할 수 있다. 하지만 단순히 특정 기능이나 특정 드라이버만 바꾸고 싶은 상황이라면 이야기가 달라진다. 예를 들어 특정 장치 드라이버만 수정해야 하는 경우, 전체 커널을 다시 컴파일하고 시스템을 재부팅하는 것은 부담이 크다. 이때 해당 드라이버를 모듈 형태로 컴파일하고, 실행 중인 커널에 적용할 수 있다면 훨씬 효율적이다.
이 점은 실제 서비스를 운영하는 환경에서 매우 중요하다. 서버나 금융 시스템처럼 계속 동작해야 하는 시스템에서는 잠깐의 중단도 사용자에게 큰 불편을 줄 수 있다. 예를 들어 금융기관에서 카드 결제가 중단되는 시간이 공지되었는데, 사용자가 하필 그 시간에 결제를 해야 한다면 큰 불편을 겪게 된다. 컴퓨터 시스템 입장에서도 마찬가지로, 서비스가 내려가는 다운타임(Downtime) 이 발생하면 그 시간 동안 사용자는 해당 서비스를 이용할 수 없다.
따라서 리눅스 커널 모듈이 재부팅 없이 동적으로 적용될 수 있다는 점은 큰 장점이 된다. 전체 커널을 새로 빌드하고 부팅하는 것보다, 필요한 기능만 모듈로 올리거나 내릴 수 있다면 시스템 운영 측면에서 훨씬 유연하다. 리눅스 커널 문서에서도 외부 모듈을 빌드하려면 미리 빌드된 커널과 그 빌드에 사용된 설정 및 헤더 파일이 필요하다고 설명한다. 또한 배포판 커널을 사용하는 경우, 현재 실행 중인 커널에 맞는 패키지가 배포판에서 제공된다고 설명한다.
커널 모듈은 주로 C Language 로 작성된다. 리눅스 커널 자체가 대부분 C 언어로 작성되어 있기 때문에, 커널 모듈도 일반적으로 C 언어를 사용한다. 다만 일반 사용자 프로그램을 작성하는 C 코드와는 다르다. 커널 모듈은 커널 내부에서 동작하기 때문에 일반적인 표준 C 라이브러리를 자유롭게 사용할 수 없고, 커널이 제공하는 함수와 자료구조를 사용해야 한다.
또 하나 중요한 점은 커널 모듈을 컴파일할 때 현재 사용 중인 커널의 커널 헤더(Kernel Header) 와 설정 정보가 맞아야 한다는 것이다. 모듈은 실행 중인 커널에 붙어서 동작하기 때문에, 다른 커널 버전을 기준으로 컴파일하면 로드되지 않거나 동작 중 문제가 생길 수 있다. 공식 커널 문서에서도 외부 모듈을 빌드하려면 빌드에 사용된 설정과 헤더 파일을 포함한 미리 빌드된 커널이 필요하다고 설명한다.
결국 모듈 프로그램은 커널 전체를 다시 수정하고 재부팅하는 부담을 줄여 주는 방식이다. 커널 전체가 큰 덩어리라면, 모듈은 그 안에 필요할 때 끼워 넣을 수 있는 독립적인 기능 단위라고 볼 수 있다. 그래서 커널 모듈은 드라이버처럼 특정 기능만 교체하거나 추가해야 할 때 유용하고, 서비스 중단을 최소화해야 하는 환경에서 특히 큰 장점을 가진다.
Hello World 커널 모듈 프로그램 Hello World Kernel Module Program
이번 예시는 Hello World 커널 모듈(Hello World Kernel Module) 프로그램이다. 겉으로 보면 일반 C 프로그램(C Program) 과 크게 다르지 않아 보이지만, 중요한 차이가 있다. 일반 C 프로그램에서는 출력할 때 printf()를 사용하지만, 커널 모듈에서는 printk() 를 사용한다.
printk() 는 커널 내부에서 사용하는 출력 함수이다. 일반 프로그램의 printf()처럼 문자열을 출력하는 역할을 하지만, 화면에 바로 출력하기보다는 커널 로그 버퍼(Kernel Log Buffer) 에 메시지를 남긴다. 이 메시지는 보통 dmesg 명령으로 확인할 수 있다. 예를 들어 다음과 같은 코드가 사용된다.
printk(KERN_ALERT "Goodbye, cruel world\n");
여기서 KERN_ALERT 는 메시지의 중요도를 나타내는 로그 레벨(Log Level) 이다. 커널 메시지는 단순 출력이 아니라, 긴급도나 중요도에 따라 구분되어 기록된다.
또 다른 차이는 일반 C 프로그램에서 보이는 main() 함수가 없다는 점이다. 일반 C 프로그램은 main()에서 시작하지만, 커널 모듈은 독립적으로 실행되는 프로그램이 아니라 실행 중인 커널에 삽입되는 코드이다. 그래서 main() 대신 다음과 같은 매크로를 사용한다.
module_init(hello_init); module_exit(hello_exit);
module_init() 은 모듈이 커널에 로드될 때 실행할 함수를 지정하고, module_exit() 은 모듈이 제거될 때 실행할 함수를 지정한다. 즉, hello_init은 모듈 시작 시점의 함수이고, hello_exit은 모듈 종료 시점의 함수라고 볼 수 있다. 공식 문서에서도 module_init()은 모듈 삽입 시 실행될 함수를 지정하고, module_exit()은 모듈 제거 시 실행될 함수를 지정한다고 설명한다.
따라서 커널 모듈 프로그램은 일반 C 프로그램처럼 main()을 기준으로 시작하지 않는다. 이미 실행 중인 커널 안에 들어가서 동작하기 때문에, 모듈이 삽입될 때 어떤 함수를 실행할지, 제거될 때 어떤 함수를 실행할지를 따로 등록하는 방식으로 동작한다.
이 지점에서 커널 전체의 시작점도 함께 생각해 볼 수 있다. 커널 소스는 매우 크고 많은 파일로 구성되어 있지만, 코드가 많다고 해서 시작점이 여러 개라고 단순히 말할 수는 없다. 커널은 일반 C 프로그램처럼 단순히 main() 하나에서 시작하는 구조가 아니라, 부트로더, 아키텍처별 초기화 코드, 일부 어셈블리 코드(Assembly Code), 그리고 C 코드 초기화 과정이 이어지면서 실행된다. 그래서 이 부분은 단순 암기보다 “왜 커널 모듈에는 main()이 없는가”를 이해하는 것이 중요하다.
Hello World 커널 모듈 설명
Explanation of Hello World Kernel Module
Hello World 커널 모듈(Hello World Kernel Module) 에서는 모듈이 시작될 때와 끝날 때 실행되는 함수가 따로 지정된다. 모듈이 커널에 적재되어 활동을 시작할 때는 module_init(hello_init)에 등록된 hello_init 함수가 실행되고, 모듈이 제거되어 활동이 끝날 때는 module_exit(hello_exit)에 등록된 hello_exit 함수가 실행된다.
모듈에는 라이선스 정보(Module License Information) 도 적어야 한다. 예제에서는 보통 다음과 같이 작성한다.
MODULE_LICENSE("Dual BSD/GPL");
이것은 해당 모듈이 BSD/GPL 이중 라이선스(Dual BSD/GPL License) 를 따른다는 의미이다. 모듈 라이선스는 단순한 설명처럼 보이지만 실제로 영향을 줄 수 있다. 예를 들어 GPL 호환 라이선스가 아닌 모듈은 일부 커널 심볼을 사용할 수 없거나, 커널이 오염된 상태인 Tainted Kernel 로 표시될 수 있다.
출력 함수로는 일반 C 프로그램의 printf()가 아니라 printk() 를 사용한다. printk()는 커널에서 사용하는 출력 함수이며, 메시지는 화면에 바로 출력되는 것이 아니라 커널 로그 버퍼(Kernel Log Buffer) 에 기록된다. 이 로그는 보통 dmesg 명령이나 /dev/kmsg를 통해 확인할 수 있다.
즉, printf()는 일반 사용자 프로그램이 User Space 에서 출력할 때 사용하는 함수이고, printk()는 Kernel Space 에서 동작하는 커널용 출력 함수이다. 사용자 공간과 커널 공간은 분리되어 있기 때문에, 커널 내부에서 일반적인 printf()를 사용하는 것이 아니라 커널이 제공하는 printk()를 사용한다.
Hello World 모듈 컴파일 준비 Preparing to Compile the Hello World Module
Hello World 커널 모듈(Hello World Kernel Module) 을 컴파일하려면 현재 실행 중인 커널에 맞는 커널 헤더(Kernel Headers) 가 필요하다. 모듈은 실행 중인 커널에 적재되어 동작하므로, 컴파일할 때 사용하는 헤더 파일도 현재 커널 버전과 맞아야 한다.
먼저 패키지 목록을 최신 상태로 갱신한다.
sudo apt update
그다음 현재 사용 중인 커널 버전에 맞는 헤더 패키지를 검색할 수 있다.
sudo apt-cache search linux-headers-uname -r
여기서 uname -r 은 현재 실행 중인 커널 버전을 출력하는 명령이다. 예를 들어 따로 실행하면 다음처럼 커널 버전을 확인할 수 있다.
uname -r
명령어 안의 백틱은 명령 치환(Command Substitution) 을 의미한다. 즉, uname -r 부분이 먼저 실행되고, 그 결과값이 명령어 안에 들어간다. Bash 문서에서도 명령 치환은 명령의 출력을 다른 명령의 일부로 사용할 수 있게 해 준다고 설명한다.
그래서 아래 두 방식은 같은 의미로 볼 수 있다.
sudo apt install linux-headers-uname -r sudo apt install linux-headers-$(uname -r)
최근에는 백틱보다 $(uname -r) 형태를 더 읽기 쉬운 방식으로 많이 사용한다. 따라서 현재 커널에 맞는 헤더를 설치할 때는 다음 명령을 쓰면 된다.
sudo apt install linux-headers-$(uname -r)
명령어 사이의 세미콜론 ; 은 앞 명령을 실행한 뒤 다음 명령을 이어서 실행하라는 의미이다. 예를 들어 다음 명령은 sudo apt update를 먼저 실행하고, 이어서 헤더 패키지를 검색한다.
sudo apt update; sudo apt-cache search linux-headers-$(uname -r)
Hello World 컴파일 2: Makefile 만들기
Hello World Compilation 2: Creating a Makefile
Hello World 커널 모듈(Hello World Kernel Module) 을 컴파일하려면 Makefile 이 필요하다. 커널 모듈은 일반 C 프로그램처럼 gcc hello.c처럼 단순히 컴파일하는 방식이 아니라, 현재 실행 중인 커널의 빌드 시스템을 이용해서 컴파일해야 한다. 그래서 컴파일 명령을 매번 길게 입력하지 않도록 Makefile에 정리해 둔다.
예시 Makefile은 다음과 같은 구조이다.
obj-m += hello.o
PWD := $(CURDIR)
all:
\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) modules
clean:
\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) clean
여기서 obj-m += hello.o 는 hello.c 파일을 커널 모듈로 빌드하겠다는 의미이다. 빌드가 성공하면 최종적으로 hello.ko라는 커널 오브젝트(Kernel Object) 모듈 파일이 만들어진다. 리눅스 커널 문서에서도 외부 모듈을 빌드할 때 obj-m := <module_name>.o 형태를 사용한다고 설명한다.
PWD := \((CURDIR) 는 현재 Makefile이 있는 디렉터리 경로를 PWD 변수에 저장하는 부분이다. 이후 M=\)(PWD)로 이 경로를 커널 빌드 시스템에 넘긴다. 즉, “현재 디렉터리에 있는 모듈 소스를 빌드하라”는 뜻이다.
all은 기본 빌드 대상이다. 사용자가 그냥 make를 입력하면 보통 all 항목이 실행된다. 여기서 중요한 부분은 다음 명령이다.
\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) modules
/lib/modules/\((shell uname -r)/build는 현재 실행 중인 커널 버전에 맞는 빌드 디렉터리를 가리킨다. 이곳에는 커널 모듈을 컴파일하는 데 필요한 헤더와 빌드 정보가 연결되어 있다. \)(shell uname -r)은 현재 커널 버전을 명령어 결과로 가져오는 부분이다. 커널 문서에서도 외부 모듈을 빌드하려면 현재 커널에 맞는 설정과 헤더가 포함된 빌드된 커널이 필요하다고 설명한다.
마지막으로 clean은 컴파일하면서 생성된 중간 파일들을 정리할 때 사용한다.
make clean
즉, 이 Makefile은 “현재 디렉터리의 hello.c를 현재 실행 중인 커널 버전에 맞춰 모듈로 컴파일하고, 필요하면 생성 파일을 정리하는 역할”을 한다고 이해하면 된다.
Hello World 컴파일 3: 모듈 컴파일 결과 확인
Hello World Compilation 3: Checking the Module Build Result
Makefile 을 만든 뒤 make 명령을 실행하면, Makefile에 작성된 내용이 실행되면서 화면에 컴파일 과정이 출력된다. 예시 화면에서는 현재 커널 버전에 맞는 빌드 디렉터리인 /lib/modules/$(uname -r)/build로 들어가서, 현재 디렉터리에 있는 hello.c 파일을 커널 모듈로 컴파일하는 과정이 표시된다.
출력 내용을 보면 CC [M] hello.o처럼 C 파일을 오브젝트 파일로 컴파일하는 단계가 나오고, 이후 MODPOST, hello.mod.o, hello.ko 생성 과정이 이어진다. 여기서 최종적으로 중요한 결과물은 hello.ko 이다. .ko는 Kernel Object 를 의미하며, 리눅스 커널에 적재할 수 있는 모듈 파일이다.
화면에 나오는 warning: the compiler differs from the one used to build the kernel 메시지는 현재 모듈을 컴파일하는 데 사용한 컴파일러와 커널을 빌드할 때 사용된 컴파일러가 다르다는 뜻이다. 예시에서는 커널은 aarch64-linux-gnu-gcc-13으로 빌드되었고, 현재 모듈은 gcc-13으로 빌드되고 있다. 단순 경고일 수 있지만, 커널 모듈은 실행 중인 커널과 밀접하게 연결되므로 가능하면 커널과 호환되는 환경에서 빌드하는 것이 좋다.
또한 Skipping BTF generation ... due to unavailability of vmlinux라는 메시지도 보인다. 이는 BTF(BPF Type Format) 정보를 만들려고 했지만 vmlinux 파일이 없어 생략했다는 의미이다. 일반적인 Hello World 모듈 실습에서는 이 메시지가 나와도 hello.ko가 정상적으로 생성되었다면 모듈 컴파일 자체는 성공한 것으로 볼 수 있다.
즉, 이 단계에서는 make를 실행했을 때 여러 컴파일 과정이 출력되고, 마지막에 에러 없이 끝나며 hello.ko 파일이 생성되었는지를 확인하면 된다. hello.ko가 만들어졌다면 Hello World 커널 모듈 컴파일은 완료된 것이다.
Hello World 컴파일 결과와 모듈 사용 명령어
Hello World Compilation Result and Module Commands
Hello World 커널 모듈(Hello World Kernel Module) 을 컴파일하고 나면 여러 개의 중간 파일이 생성된다. 그중 실제로 우리가 사용할 최종 결과물은 hello.ko 파일이다. 여기서 .ko는 Kernel Object 를 의미하며, 커널에 적재할 수 있는 모듈 파일이다.
컴파일 과정에서 hello.o, hello.mod.o, Module.symvers, modules.order 같은 파일도 함께 생성될 수 있다. 하지만 이것들은 빌드 과정에서 필요한 중간 결과물이거나 관리용 파일이고, 실제로 커널에 올려 사용할 파일은 최종적으로 생성된 hello.ko 이다.
이 모듈을 사용하려면 대표적으로 lsmod, insmod, rmmod 명령을 사용한다. lsmod 는 현재 커널에 적재되어 사용 중인 모듈 목록을 보여준다. insmod 는 커널 모듈을 적재해서 사용할 수 있게 해 주는 명령이고, rmmod 는 적재된 모듈을 제거하는 명령이다.
예를 들어 hello.ko 모듈을 커널에 올리려면 다음과 같이 실행한다.
sudo insmod hello.ko
현재 모듈이 올라갔는지 확인하려면 다음 명령을 사용할 수 있다.
lsmod
사용을 끝낸 뒤 모듈을 제거하려면 다음과 같이 실행한다.
sudo rmmod hello
여기서 주의할 점은 모듈이 사용 중일 때는 함부로 제거할 수 없다는 것이다. USB 메모리를 사용 중일 때 강제로 뽑으면 문제가 생길 수 있어서 “꺼내기(Eject)”를 먼저 하는 것처럼, 커널 모듈도 누군가 사용 중이라면 rmmod가 실패할 수 있다. 이 경우에는 어떤 프로세스나 기능이 해당 모듈을 사용 중인지 확인하고, 사용을 끝낸 뒤 제거해야 한다.
즉, 컴파일의 최종 결과물은 hello.ko 이고, 이 파일을 커널에 올릴 때는 insmod, 현재 적재된 모듈을 확인할 때는 lsmod, 모듈을 제거할 때는 rmmod 를 사용한다고 정리할 수 있다.
모듈 사용하기: insmod와 dmesg
Using a Module: insmod and dmesg
컴파일이 끝나서 hello.ko 파일이 생성되었다면, 이제 이 모듈을 커널에 적재해서 사용할 수 있다. 모듈을 적재할 때는 insmod 명령을 사용한다.
sudo insmod hello.ko
이 명령을 실행해도 터미널 화면에는 특별한 출력이 보이지 않을 수 있다. 이유는 Hello World 모듈 안에서 사용한 출력 함수가 일반 C 프로그램의 printf() 가 아니라 커널용 출력 함수인 printk() 이기 때문이다. printk()는 표준 출력(Standard Output)으로 바로 보여주는 것이 아니라 커널 로그(Kernel Log) 에 메시지를 남긴다.
따라서 모듈에서 출력한 내용을 확인하려면 dmesg 명령을 사용한다.
sudo dmesg
dmesg를 실행하면 커널 로그에 기록된 내용들이 출력된다. 여기에는 시스템이 부팅될 때 출력된 여러 커널 메시지도 포함되어 있고, 방금 hello.ko 모듈을 적재하면서 printk()로 남긴 hello world 메시지도 확인할 수 있다.
부팅 과정에서 화면에 여러 메시지가 지나가는 것도 같은 맥락으로 볼 수 있다. 부팅이 완전히 끝나기 전에는 아직 일반적인 User Mode 환경으로 제어권이 완전히 넘어온 상태가 아니기 때문에, 커널이 직접 출력하거나 기록하는 메시지들이 나타난다. 반면 이미 부팅이 끝난 뒤 모듈을 적재하면 printk() 메시지는 터미널 표준 출력이 아니라 커널 로그에 남기 때문에 dmesg로 확인해야 한다.
이미지에 보이는 것처럼 hello: loading out-of-tree module taints kernel, module verification failed 같은 메시지도 함께 나올 수 있다. 이는 직접 만든 외부 모듈을 적재했기 때문에 커널이 이를 기록하는 것이다. 중요한 것은 마지막에 hello, world와 같은 메시지가 보인다면 모듈이 정상적으로 적재되고 printk()가 실행되었다고 볼 수 있다.
사용 중인 모듈 확인하기 Checking Loaded Modules
모듈을 커널에 적재한 뒤에는 lsmod 명령으로 현재 사용 중인 모듈 목록을 확인할 수 있다. 앞에서 만든 모듈 이름이 hello였기 때문에, lsmod 결과에서도 hello라는 이름의 모듈이 표시된다.
lsmod를 실행하면 보통 다음과 같은 항목들이 나온다.
Module Size Used by
hello 12288 0
여기서 Module 은 모듈 이름이고, Size 는 해당 모듈이 차지하는 크기이다. Used by 는 이 모듈을 현재 몇 개의 다른 모듈이나 기능이 사용하고 있는지를 나타낸다.
예를 들어 hello 모듈의 Used by 값이 0이라면, 현재 이 모듈을 다른 모듈이나 기능이 사용하고 있지 않다는 뜻이다. 반대로 어떤 모듈의 Used by 값이 2라면, 두 곳에서 해당 모듈을 사용 중이라는 의미이고, 오른쪽에 어떤 모듈들이 사용하는지도 함께 표시될 수 있다.
이 정보는 모듈을 제거할 때 중요하다. 어떤 모듈이 다른 모듈이나 장치에서 사용 중이라면 rmmod 로 제거가 되지 않을 수 있다. 따라서 모듈을 제거하기 전에는 lsmod로 현재 사용 여부를 확인하는 것이 좋다.
모듈 사용 끝내기 Removing a Kernel Module
사용 중인 커널 모듈(Kernel Module) 을 더 이상 사용하지 않을 때는 rmmod 명령을 사용한다. 앞에서 hello.ko 모듈을 insmod로 적재했다면, 제거할 때는 다음과 같이 입력한다.
sudo rmmod hello
여기서 주의할 점은 rmmod를 사용할 때는 파일 이름인 hello.ko가 아니라, 모듈 이름인 hello를 사용한다는 것이다. 이 명령을 실행하면 모듈이 커널에서 제거되고, 모듈 안에 등록된 module_exit(hello_exit) 함수가 실행된다.
예제에서 제거 후 메시지가 보이는 이유는 rmmod 명령 자체가 메시지를 출력한 것이 아니라, 모듈의 종료 함수 안에 작성된 printk() 가 실행되었기 때문이다. 즉, rmmod hello를 실행하면 hello_exit 함수가 호출되고, 그 안의 printk() 문장이 커널 로그에 메시지를 남긴다.
커널 로그를 확인하려면 다시 dmesg 명령을 사용한다.
sudo dmesg
dmesg 결과를 보면 메시지 앞에 대괄호 [] 안의 숫자가 붙어 있다. 이 숫자는 사람이 읽기 쉬운 날짜와 시간이 아니라, 시스템이 부팅된 이후 경과한 시간을 나타내는 값이다. 예를 들어 [30624.71020]처럼 표시된다면, 부팅 후 약 30624초가 지난 시점에 해당 메시지가 기록되었다고 볼 수 있다.
사람에게는 “몇 월 며칠 몇 시 몇 분”처럼 달력 기반 시간이 편하지만, 컴퓨터 입장에서는 부팅 시점을 기준으로 시간이 얼마나 지났는지를 숫자로 기록하는 방식이 더 단순하다. 그래서 커널 로그에서는 부팅 이후 흐른 시간을 기준으로 메시지 발생 시점을 표시한다.



