Skip to content

필사 모드: 커널 C는 응용 C가 아니다 — 메모리 할당과 동시성의 규칙

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 문법은 같은데 규칙이 다릅니다

5편에서 모듈을 만들었습니다. 열 줄짜리였을 때는 문제가 없었지만, 거기에 코드를 조금만 더 넣기 시작하면 바로 이런 일이 벌어집니다. 컴파일은 아무 경고 없이 통과하는데, 로드하면 시스템이 멈춥니다.

이유는 커널 C에 문법으로 표현되지 않는 규칙이 여럿 있기 때문입니다. 어떤 함수는 특정 상황에서 부르면 안 되는데, 컴파일러는 그걸 모릅니다. 어떤 락은 특정 문맥에서 다른 변종을 써야 하는데, 타입은 같습니다.

이 글은 그 규칙들을 정리합니다. 이 시리즈에서 밀도가 가장 높은 편이고, 한 번에 다 외우려 하지 않는 편이 좋습니다. 대신 어떤 종류의 함정이 있는지를 알아 두고, 실제로 부딪혔을 때 돌아와 확인하는 용도로 쓰시길 권합니다.

이 블로그의 운영체제 개념 시리즈가 뮤텍스와 세마포어 같은 동기화 도구의 이론을 다룹니다. 이 글은 그 이론을 다시 설명하지 않습니다. 리눅스 커널이 그 도구들을 어떤 이름으로 제공하며, 언제 어느 것을 골라야 하는가만 다룹니다.

1. 스택부터 다릅니다

가장 먼저 부딪히는 차이입니다. 커널 스택은 아주 작습니다.

x86_64에서 스택 크기는 arch/x86/include/asm/page_64_types.h에 이렇게 정의돼 있습니다.

#define THREAD_SIZE_ORDER	(2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

KASAN을 쓰지 않는 일반적인 설정에서 4KB 페이지 기준으로 계산하면 16KB입니다. 사용자 공간 스레드가 흔히 8MB를 갖는 것과 비교하면 500분의 1입니다.

그래서 커널 코드에는 이런 제약이 생깁니다.

  • 큰 지역 배열을 잡으면 안 됩니다. 1KB짜리 버퍼도 스택에서는 부담입니다.
  • 재귀를 쓰면 안 됩니다. 깊이를 예측할 수 없으면 위험합니다.
  • 호출 깊이가 깊어지는 구조를 피해야 합니다.

스택을 넘치면 어떻게 되는가. 조용히 옆 메모리를 덮어씁니다. 그리고 한참 뒤에 전혀 관계없어 보이는 곳에서 이상한 일이 벌어집니다. 커널 디버깅에서 가장 추적하기 어려운 종류의 버그입니다.

커널 문서 Documentation/process/submit-checklist.rst가 이 검사를 요구합니다. make checkstack으로 스택을 많이 쓰는 함수를 찾아내고, 512바이트를 넘는 함수는 문제로 봅니다.

make checkstack | head -20

큰 버퍼가 필요하면 스택이 아니라 힙에서 할당해야 합니다. 그게 다음 절의 주제입니다.

2. 할당기 고르기 — kmalloc, vmalloc, kvmalloc

malloc은 없습니다. 대신 여러 개가 있고, 무엇을 고를지가 의미를 갖습니다.

커널 문서 Documentation/core-api/memory-allocation.rst의 안내를 기준으로 정리하겠습니다.

kmalloc

문서는 kmalloc으로 할당할 수 있는 덩어리의 최대 크기가 제한되어 있고 실제 한계는 하드웨어와 커널 설정에 따라 다르다고 하면서, 페이지 크기보다 작은 객체에 kmalloc을 쓰는 것이 좋은 관행이라고 명시합니다.

struct my_data *d = kmalloc(sizeof(*d), GFP_KERNEL);
if (!d)
	return -ENOMEM;
/* ... */
kfree(d);

sizeof(*d)라고 쓰는 것이 커널 관용구입니다. 타입 이름을 반복하지 않으므로 나중에 타입이 바뀌어도 따라 고칠 필요가 없습니다.

0으로 초기화된 메모리가 필요하면 kzalloc을 씁니다. 배열이라면 kmalloc_arraykcalloc이 있고, 문서는 크기 계산 시 오버플로를 피하기 위해 struct_size(), array_size(), array3_size() 헬퍼를 쓰라고 안내합니다.

kmalloc이 돌려주는 메모리는 물리적으로 연속입니다. 이게 중요한 이유는 DMA 때문입니다. 하드웨어가 직접 접근하는 버퍼는 물리 연속이어야 합니다.

vmalloc

문서는 큰 할당에는 vmalloc()이나 vzalloc()을 쓰거나 페이지 할당기에 직접 요청할 수 있다고 하면서, vmalloc 계열이 돌려주는 메모리는 물리적으로 연속이 아니라고 명시합니다.

가상 주소만 연속이므로 큰 덩어리를 얻기 쉽지만, 대가가 있습니다. 페이지 테이블 항목을 새로 만들어야 하므로 할당 자체가 느리고, TLB 압박도 커집니다. 그리고 DMA 버퍼로는 쓸 수 없습니다.

kvmalloc

문서는 할당 크기가 kmalloc에 너무 큰지 확신할 수 없을 때 kvmalloc() 계열을 쓸 수 있다고 안내합니다. 먼저 kmalloc으로 시도하고 실패하면 vmalloc으로 재시도합니다. 다만 쓸 수 있는 GFP 플래그에 제약이 있고, 반환된 메모리가 물리적으로 연속이 아닐 수 있습니다.

해제 함수를 짝 맞추는 것이 중요합니다. 문서가 정리한 대응 관계입니다.

할당해제
kmalloc, kzallockfree 또는 kvfree
vmalloc, vzallocvfree 또는 kvfree
kvmallockvfree
kmem_cache_allockmem_cache_free, kfree, kvfree

슬랩 캐시

같은 구조체를 아주 많이 할당한다면 전용 캐시를 만드는 편이 낫습니다. 문서는 kmem_cache_create()로 캐시를 만든 뒤 kmem_cache_alloc()으로 할당하라고 안내하고, 캐시의 일부가 사용자 공간으로 복사될 가능성이 있으면 kmem_cache_create_usercopy()를 쓰라고 덧붙입니다. /proc/slabinfo에서 이 캐시들의 상태를 볼 수 있습니다.

3. GFP 플래그 — 할당기에게 허락하는 것

두 번째 인자가 이 글에서 가장 중요한 개념 중 하나입니다. 이 플래그는 "얼마나 급한가"가 아니라 "할당기가 무엇을 해도 되는가"를 지정합니다.

문서의 설명을 기준으로 자주 쓰는 것들을 정리하면 이렇습니다.

플래그언제 쓰는가
GFP_KERNEL대부분의 경우. 문서는 "대부분의 경우 필요한 것은 GFP_KERNEL"이라고 씁니다
GFP_NOWAIT원자적 문맥. 인터럽트 처리기가 대표적입니다
GFP_ATOMIC메모리 예비 영역까지 써야 하고 실패하면 커널이 곤란해지는 경우
GFP_KERNEL_ACCOUNT신뢰할 수 없는 사용자 공간의 요청으로 할당하는 경우
GFP_USER 계열사용자 공간용 할당
GFP_NOIO, GFP_NOFS재귀 교착 회피를 위한 오래된 플래그
GFP_DMA, GFP_DMA32주소 지정 능력이 제한된 하드웨어용
__GFP_NOFAIL성공할 때까지 무한히 재시도

핵심만 뽑겠습니다.

GFP_KERNEL은 잠들 수 있습니다. 문서는 이 플래그가 회수를 허용하며, 메모리 압박 상황에서 직접 회수가 발동할 수 있다고 설명합니다. 직접 회수는 페이지를 디스크에 쓰고 캐시를 비우는 일이고, 그동안 호출한 쪽은 기다립니다. 즉 이 호출은 잠들 수 있습니다.

GFP_NOWAIT은 잠들지 않습니다. 문서는 인터럽트 처리기 같은 원자적 문맥에서 할당한다면 이 플래그를 쓰라고 안내하고, 이것이 직접 회수와 입출력 및 파일시스템 연산을 막는다고 설명합니다.

GFP_ATOMIC은 예비 영역에 손을 댑니다. 문서는 메모리 예비 영역 접근이 정당화되고 할당이 성공하지 않으면 커널이 곤란해지는 경우에 쓰라고 합니다. 즉 남용하면 안 되는 플래그입니다. 인터럽트 문맥에서 무조건 이걸 쓰는 습관은 잘못된 것이고, 대개는 GFP_NOWAIT이 맞습니다.

__GFP_NOFAIL은 성공할 때까지 무한 반복합니다. 문서의 표현대로 끝없이 루프를 돕니다. 정말 실패를 감당할 수 없는 자리에만 씁니다.

그래서 실무적 규칙은 이렇게 요약됩니다.

  • 프로세스 문맥에서 잠들어도 되면 GFP_KERNEL.
  • 잠들면 안 되는 문맥이면 GFP_NOWAIT.
  • 그런데 잠들면 안 되는 문맥에서 큰 할당을 하고 있다면, 애초에 설계를 의심해 보세요.

4. "잠든다"가 무슨 뜻인가

이 절이 이 글의 중심입니다.

커널에서 잠든다는 것은 schedule()을 불러 CPU를 다른 태스크에 넘긴다는 뜻입니다. 나중에 조건이 충족되면 다시 깨어나 이어서 실행됩니다. 사용자 공간에서 뮤텍스를 기다리거나 파일을 읽을 때 일어나는 것과 같은 일입니다.

문제는 잠들면 안 되는 구간이 커널에 존재한다는 것입니다.

원자적 문맥

커널 문서 Documentation/kernel-hacking/locking.rst가 규칙을 분명하게 씁니다. 커널의 많은 함수가 직접 또는 간접적으로 잠들며, 스핀락을 잡고 있거나 선점이 꺼져 있는 상태에서는 절대로 그런 함수를 부를 수 없다고 명시합니다.

원자적 문맥에 해당하는 상황은 대략 이렇습니다.

  • 인터럽트 처리기 안
  • 소프트IRQ와 타스클릿 안
  • 스핀락을 잡고 있는 동안
  • 선점을 명시적으로 끈 구간

잠들 수 있는 함수들

같은 문서가 대표적인 것들을 열거합니다. 여기서 놀라는 사람이 많습니다.

함수왜 잠드는가
copy_from_user, copy_to_user사용자 페이지가 스왑되어 있으면 디스크에서 가져와야 합니다
kmalloc(GFP_KERNEL)메모리가 부족하면 회수를 기다립니다
mutex_lock락을 다른 쪽이 잡고 있으면 기다립니다

copy_from_user가 잠들 수 있다는 사실이 특히 중요합니다. 4편 6절에서 이 함수 안에 might_fault()가 있는 것을 봤습니다. 페이지 폴트가 날 수 있다는 것은 곧 그 페이지를 가져오는 동안 잠들 수 있다는 뜻입니다.

그래서 인터럽트 처리기 안에서는 사용자 메모리에 접근할 수 없습니다. 애초에 그 시점에는 "어느 프로세스의 사용자 공간인가"라는 질문 자체가 성립하지 않습니다.

어기면 무슨 일이 생기는가

운이 좋으면 커널이 경고를 찍고 넘어갑니다. 운이 나쁘면 시스템이 그대로 멈춥니다. 잠들려면 스케줄러가 다른 태스크를 골라야 하는데, 인터럽트 문맥에는 돌아올 태스크 문맥이 없기 때문입니다.

컴파일러는 이걸 잡아 주지 못합니다. 대신 커널이 실행 중에 잡아 주는 기능이 있습니다.

설정잡아 주는 것
CONFIG_DEBUG_ATOMIC_SLEEP원자적 문맥에서 잠들 수 있는 함수를 부르면 경고
CONFIG_DEBUG_SPINLOCK스핀락 오용
CONFIG_DEBUG_MUTEXES뮤텍스 오용
CONFIG_PROVE_LOCKING락 순서 문제
CONFIG_PROVE_RCURCU 사용 규칙 위반
CONFIG_KASAN메모리 오류 전반
CONFIG_SLUB_DEBUG슬랩 할당기 오류

실습용 커널에는 이걸 전부 켜 두시길 강력히 권합니다. 성능은 크게 나빠지지만, 실습에서 성능은 아무 의미가 없고 대신 실수를 즉시 알려 줍니다. 커널 문서 Documentation/process/submit-checklist.rst도 패치를 제출하기 전에 이 계열의 옵션들을 동시에 켠 상태로 실행 테스트하라고 요구합니다.

might_sleep()이라는 표시도 알아 두면 좋습니다. include/linux/kernel.h에 정의돼 있고, 함수 안에 이게 있으면 "이 함수는 잠들 수 있다"는 선언입니다. 위의 디버그 옵션이 켜져 있으면 이 표시를 근거로 경고가 나옵니다. 자기가 만든 함수가 잠들 수 있다면 이 표시를 붙여 주는 것이 예의입니다.

5. 스핀락과 뮤텍스

커널 문서 Documentation/kernel-hacking/locking.rst의 설명이 명료합니다. 근본적인 형태는 스핀락이고, 이는 아주 단순한 단일 보유자 락으로 얻지 못하면 얻을 때까지 계속 시도하며 회전합니다. 두 번째 형태는 뮤텍스이고, 스핀락과 비슷하되 잡은 채로 블록될 수 있습니다.

여기서 선택 기준이 나옵니다.

기준스핀락뮤텍스
잡은 채로 잠들 수 있는가안 됩니다됩니다
인터럽트 문맥에서 쓸 수 있는가됩니다안 됩니다
대기 방식CPU를 돌며 기다립니다잠들었다가 깨어납니다
적합한 보호 구간아주 짧은 구간길어질 수 있는 구간

스핀락을 잡는 순간 그 CPU에서는 선점이 꺼집니다. 즉 스핀락 구간 안은 원자적 문맥이고, 4절의 모든 제약이 적용됩니다. kmalloc(GFP_KERNEL)도, copy_from_user도, mutex_lock도 부를 수 없습니다.

인터럽트를 고려해야 하는 경우

문제는 데이터가 인터럽트 처리기에서도 만져질 때입니다. 프로세스 문맥에서 스핀락을 잡고 있는데 같은 CPU에서 인터럽트가 걸리고, 그 처리기가 같은 락을 잡으려 하면 영원히 회전합니다. 자기 자신을 기다리는 교착입니다.

그래서 인터럽트를 함께 막는 변종이 있습니다. 같은 문서가 기본 규칙을 이렇게 정리합니다. 프로세스 문맥에서 다른 프로세스를 배제하려면 뮤텍스를 쓰고, 데이터가 인터럽트에서도 만져진다면 spin_lock_irqsave()spin_unlock_irqrestore()를 쓰라는 것입니다. 소프트IRQ와 사용자 문맥이 함께 접근한다면 spin_lock_bh()spin_unlock_bh()입니다.

같은 문서의 최소 잠금 요구 표에서 자주 만나는 조합을 뽑으면 이렇습니다.

어느 쪽과 어느 쪽필요한 것
인터럽트 처리기끼리spin_lock_irqsave
소프트IRQ끼리spin_lock
사용자 문맥과 소프트IRQspin_lock_bh
사용자 문맥과 사용자 문맥mutex_lock_interruptible

규칙 하나로 압축하면: 어느 문맥에서 이 데이터가 만져지는지 목록을 만들고, 그중 가장 제약이 강한 문맥에 맞춰 락을 고르면 됩니다.

락 순서와 lockdep

락을 두 개 이상 잡기 시작하면 새로운 문제가 생깁니다. A를 잡고 B를 잡는 코드와, B를 잡고 A를 잡는 코드가 동시에 돌면 교착입니다. 이론적인 배경은 운영체제 개념 시리즈의 교착 상태 편에 정리돼 있습니다.

커널의 대응책은 락 순서를 정해 두고 항상 그 순서를 지키는 것입니다. 그리고 그 규칙이 지켜지는지 실행 중에 검사해 주는 기능이 CONFIG_PROVE_LOCKING입니다.

이 기능이 훌륭한 이유는 실제로 교착이 일어나지 않아도 경고한다는 점입니다. 위험한 순서를 한 번이라도 관찰하면 그 자리에서 보고합니다. 실습 커널에 반드시 켜 두세요.

6. RCU — 무슨 문제를 푸는가

커널을 읽다 보면 rcu_read_lock()이 반드시 나옵니다. 이름에 lock이 들어 있지만 락이 아닙니다.

어떤 문제인가

읽기가 압도적으로 많고 쓰기가 아주 드문 자료구조가 있습니다. 라우팅 테이블이나 프로세스 목록 같은 것들입니다. 여기에 보통의 락을 걸면, 읽는 쪽이 매번 락 연산을 해야 합니다. CPU가 수십 개면 그 락 자체가 병목이 됩니다.

커널 문서 Documentation/RCU/whatisRCU.rst는 RCU를 2.5 개발 시기에 추가된 동기화 메커니즘으로 소개하며, 읽기가 대부분인 상황에 최적화되어 있다고 설명합니다. 갱신이 읽는 쪽에 무거운 동기화를 강요하지 않고 진행되도록 하며, 동시에 실행 중인 RCU 읽기 쪽은 이전 버전에 계속 접근하면서 원자적 연산과 메모리 배리어와 캐시 미스를 면제받습니다.

어떻게 하는가

핵심 아이디어는 이렇습니다. 갱신할 때 기존 데이터를 고치지 않고 새 사본을 만들어 포인터만 바꿉니다. 그 순간 이후에 읽는 쪽은 새 것을 보고, 이미 읽고 있던 쪽은 옛것을 계속 봅니다. 그리고 옛것을 읽고 있던 쪽이 모두 끝나면 그때 옛것을 해제합니다.

그 "모두 끝나기를 기다리는" 구간이 유예 기간입니다. 문서는 synchronize_rcu()가 이후에 시작되는 읽기 구간까지 기다리지는 않는다고 명시합니다. 시작 시점에 이미 진행 중이던 것만 기다립니다.

핵심 API 다섯 개입니다.

함수역할
rcu_read_lock()읽기 구간 시작
rcu_read_unlock()읽기 구간 끝
rcu_dereference()보호되는 포인터를 안전하게 읽기
rcu_assign_pointer()보호되는 포인터를 안전하게 쓰기
synchronize_rcu() / call_rcu()유예 기간을 기다리거나 콜백 등록

실제로 어디서 만나는가

추상적으로 들리지만, 4편에서 본 것과 같은 수준의 아주 단순한 코드에도 나옵니다. kernel/sys.cgetppid 구현입니다.

SYSCALL_DEFINE0(getppid)
{
	int pid;

	rcu_read_lock();
	pid = task_tgid_vnr(rcu_dereference(current->real_parent));
	rcu_read_unlock();

	return pid;
}

여덟 줄짜리 시스템 콜입니다. 하는 일은 부모 프로세스의 PID를 돌려주는 것뿐인데 RCU가 들어 있습니다.

왜 필요한가. 이 값을 읽는 순간 부모 프로세스가 종료될 수 있기 때문입니다. 그러면 real_parent가 가리키던 구조체가 해제될 수 있고, 읽는 쪽은 해제된 메모리를 건드리게 됩니다. RCU 읽기 구간 안에 있으면 그 해제가 유예되므로 안전합니다.

읽는 쪽의 비용이 거의 0이라는 것이 여기서 드러납니다. 이 시스템 콜은 아주 자주 호출되는데, 그때마다 락 연산을 했다면 낭비였을 것입니다.

규칙

  • RCU 읽기 구간 안에서는 잠들면 안 됩니다. 기본 형태에서는 그렇습니다.
  • 보호되는 포인터는 rcu_dereference()로 읽어야 합니다. 그냥 읽으면 컴파일러나 CPU가 순서를 바꿀 수 있습니다.
  • 갱신 쪽에는 별도의 락이 여전히 필요합니다. RCU는 쓰는 쪽끼리의 경쟁을 해결해 주지 않습니다.
  • synchronize_rcu()는 잠깁니다. 원자적 문맥에서 부르면 안 됩니다.

이 규칙들을 실행 중에 검사해 주는 것이 CONFIG_PROVE_RCU입니다.

7. 커널 C에서 자주 보게 되는 다른 것들

앞의 규칙들만큼 위험하지는 않지만 코드를 읽을 때 자주 마주치는 것들을 모아 두겠습니다.

likely()unlikely(). 분기 예측 힌트입니다. 4편에서 본 vfs_read에도 있었습니다. 성능에 민감한 경로에서만 씁니다.

container_of(). 구조체 멤버의 주소에서 구조체 자체의 주소를 계산합니다. 커널의 연결 리스트 구현 전체가 이 매크로 위에 서 있습니다. 커널 코드를 읽다가 처음 만나면 반드시 멈춰서 이해하고 넘어가야 하는 것 중 하나입니다.

IS_ERR()PTR_ERR(). 포인터를 반환하는 함수가 에러를 표현하는 방법입니다. 주소 공간의 특정 영역을 에러 코드용으로 예약해 두고 거기에 인코딩합니다.

__init, __exit, __user, __iomem 같은 표시들. 5편에서 앞의 둘을, 4편에서 __user를 다뤘습니다. __iomem은 장치 메모리 주소라는 표시이고, 이것도 직접 역참조하면 안 됩니다.

READ_ONCE()WRITE_ONCE(). 락 없이 공유 변수를 읽고 쓸 때, 컴파일러가 그 접근을 쪼개거나 합치거나 없애 버리는 것을 막습니다.

부동소수점 금지. 5편 6절에서 다룬 그대로입니다.

흔한 함정

스핀락을 잡고 kmalloc(GFP_KERNEL)을 부릅니다. 이 글에서 다룬 규칙 위반의 대표 사례입니다. CONFIG_DEBUG_ATOMIC_SLEEP이 잡아 줍니다.

인터럽트 처리기에서 copy_to_user를 부릅니다. 잠들 수 있는 함수이기도 하고, 애초에 그 문맥에는 대상 사용자 공간이 없습니다.

인터럽트 문맥이라는 이유로 무조건 GFP_ATOMIC을 씁니다. 3절에서 다룬 대로 대개는 GFP_NOWAIT이 맞습니다. GFP_ATOMIC은 예비 영역을 갉아먹습니다.

할당 실패를 검사하지 않습니다. 커널에서 할당은 실패할 수 있고, 그 결과를 그냥 역참조하면 널 포인터 접근입니다. 사용자 공간에서 굳어진 습관이 여기서 위험해집니다.

해제 함수를 짝 맞추지 않습니다. 2절의 표를 참고하세요.

뮤텍스를 인터럽트 처리기에서 씁니다. 잠들 수 있으므로 불가능합니다.

락 순서를 문서화하지 않습니다. 락을 두 개 이상 쓰는 코드에는 주석으로 순서를 적어 두세요. CONFIG_PROVE_LOCKING이 잡아 주지만, 그 경로를 실행해 봐야 잡힙니다.

디버그 옵션을 끄고 실습합니다. 4절에서 나열한 옵션들이 없으면, 규칙을 어겨도 한동안 아무 일도 일어나지 않다가 나중에 엉뚱한 곳에서 터집니다.

마치며

이 글의 규칙들을 한 장으로 압축하면 이렇습니다.

스택은 작습니다. 큰 것은 힙에서 잡습니다.

할당기는 여럿입니다. 페이지보다 작으면 kmalloc, 크면 vmalloc, 모르겠으면 kvmalloc, 짝 맞춰 해제합니다.

GFP 플래그는 허가서입니다. 잠들어도 되면 GFP_KERNEL, 아니면 GFP_NOWAIT.

모든 것은 "지금 잠들 수 있는 문맥인가"로 귀결됩니다. 인터럽트 문맥이거나 스핀락을 잡고 있으면 잠들 수 없고, 그러면 쓸 수 있는 함수가 줄어듭니다.

락은 문맥이 고릅니다. 잠들어야 하면 뮤텍스, 인터럽트가 얽히면 spin_lock_irqsave, 소프트IRQ가 얽히면 spin_lock_bh.

읽기가 압도적으로 많으면 RCU가 있습니다. 읽는 쪽이 거의 공짜인 대신 해제가 유예됩니다.

그리고 마지막으로, 이 규칙들을 다 외우려 하지 마세요. 대신 디버그 옵션을 켜 두세요. 커널은 이 규칙들을 실행 중에 검사해 주는 기능을 스스로 갖고 있고, 그게 사람의 기억보다 훨씬 정확합니다.

다음 편은 마지막입니다. 지금까지 배운 것을 실제 커널 커뮤니티에 내보내는 절차, 즉 첫 패치를 보내는 이야기를 합니다.

이 글의 문서 인용과 코드는 2026년 8월 19일에 공식 문서와 메인라인 트리에서 확인했습니다.

직접 해보기

이전 편과 다음 편

참고 자료

모든 링크는 2026년 8월 19일에 확인했습니다.

현재 단락 (1/177)

[5편](/blog/linux/2026-08-19-kernel-modules)에서 모듈을 만들었습니다. 열 줄짜리였을 때는 문제가 없었지만, 거기에 코드를 조금만 더 넣기 시작하면...

작성 글자: 0원문 글자: 10,422작성 단락: 0/177