콘텐츠로 이동

Linux 커널 심층 학습 자료: 이원 구조에서 네트워킹까지

목차

  1. User Space vs Kernel Space: 이원 구조의 이해
  2. 커널 모듈(LKM)의 생애주기
  3. 문자 디바이스 드라이버와 VFS
  4. 네트워크 패킷 가로채기: Netfilter와 sk_buff

챕터 1: User Space vs Kernel Space - 이원 구조의 이해

1.1 CPU 권한 레벨(Ring) 아키텍처

Linux 커널의 가장 기초적인 보안 메커니즘은 CPU 보호 링(Protection Ring) 아키텍처에 기반합니다. x86-64 프로세서는 4개의 권한 레벨(Ring 0~3)을 제공하지만, Linux는 오직 2개만 사용합니다:

  • Ring 0 (Kernel Mode): 커널 코드가 실행되는 최고 권한 영역. 모든 CPU 명령어, 메모리 접근, 하드웨어 제어 가능
  • Ring 3 (User Mode): 사용자 애플리케이션이 실행되는 제한된 권역. 보호된 메모리 영역 접근 불가, 특정 CPU 명령어 실행 불가

이러한 분리는 하드웨어 수준에서 강제되므로, 사용자 프로세스가 임의로 Ring 0으로 전환할 수 없습니다. CPU는 세그먼트 레지스터(CS, DS 등)의 권한 비트를 확인하여 현재 실행 모드를 결정합니다. 사용자 모드 코드에서 권한 명령어(예: lgdt, lidt, 페이지 테이블 수정)를 실행하려 하면, CPU는 즉시 **일반 보호 예외(General Protection Fault)**를 발생시킵니다.

1.2 메모리 주소 공간 분리

각 프로세스는 자신의 **가상 메모리 주소 공간**을 갖습니다(x86-64에서는 64비트 주소 공간). 이 공간은 논리적으로 두 부분으로 나뉩니다:

┌─────────────────────────────────────────┐
│  0xFFFFFFFF FFFFFFFF (2^64 - 1)        │
├─────────────────────────────────────────┤
│                                         │
│     커널 공간 (약 128TB)                  │
│  - 커널 코드, 페이지 테이블, 스택       │
│  - 모든 프로세스가 공유               │
├─────────────────────────────────────────┤
│                                         │
│   사용자 공간 (약 128TB)                 │
│  - 애플리케이션 코드, 데이터, 스택     │
│  - 프로세스별로 격리                   │
│                                         │
├─────────────────────────────────────────┤
│  0x0000000000000000                    │
└─────────────────────────────────────────┘

MMU(Memory Management Unit)**는 가상 주소를 물리 주소로 변환할 때 **권한 비트(Permission Bits)**를 확인합니다. 커널 메모리 페이지는 USER 비트가 0으로 설정되어 있어, Ring 3에서 접근 시도 시 **페이지 폴트(Page Fault) 예외를 발생시킵니다. 이 폴트는 커널의 페이지 폴트 핸들러로 진입하게 하며, 일반적으로 해당 프로세스를 종료합니다.

1.3 System Call 메커니즘: User ↔ Kernel 전환

사용자 프로세스가 하드웨어나 커널 자원에 접근해야 할 때, **시스템 콜(System Call)**을 통해 안전하게 커널 모드로 전환합니다.

x86-64 System Call 흐름:

1. 사용자 애플리케이션에서:
   - 시스템 콜 번호를 rax 레지스터에 저장
   - 인자들을 rdi, rsi, rdx, r10, r8, r9 레지스터에 저장
   - syscall 명령어 실행

2. CPU 하드웨어 (syscall 명령어):
   - CPU 모드를 Ring 3 → Ring 0으로 전환
   - 현재 RIP를 RCX에 저장 (복귀 주소)
   - 현재 RFLAGS를 R11에 저장 (상태 플래그)
   - IA32_LSTAR MSR(Model-Specific Register)에 저장된 
     커널 엔트리 포인트로 점프
   - 페이지 테이블을 커널 페이지 테이블로 전환

3. 커널 진입점 (entry_64.S):
   - 커널 스택으로 전환
   - 사용자 컨텍스트를 커널 스택에 저장
   - 적절한 syscall 핸들러 호출

4. 시스템 콜 처리:
   - 커널 함수(예: sys_open, sys_read)에서 실제 작업 수행
   - 반환값을 rax 레지스터에 저장

5. CPU 복귀 (sysret 명령어):
   - CPU 모드를 Ring 0 → Ring 3으로 전환
   - RCX에 저장된 주소로 점프 (사용자 코드로 복귀)
   - R11의 플래그 값 복원

핵심 특징: - syscall은 순수 레지스터 연산: 메모리 접근이 없어 x86의 sysenter보다 빠름 - 자동 권한 전환: CPU가 하드웨어 수준에서 보장하므로, 중간에 특정 메모리 페이지에만 접근 가능한 상태가 될 수 없음 - 원자적(Atomic) 전환: 전환 중 인터럽트 불가능, 일관성 보장

1.4 Context Switching의 하드웨어 비용

프로세스 컨텍스트 스위칭은 OS의 가장 비싼 연산 중 하나입니다. 다음 작업들이 연달아 발생합니다:

1. CPU 레지스터 상태 저장 및 복원

// arch/x86/entry/entry_64.S의 개념화된 코드
// 프로세스 A → 프로세스 B로 전환

// Step 1: 현재 프로세스 A의 모든 레지스터를 kernel stack에 저장
SAVE_REGS(rax, rbx, rcx, rdx, rsi, rdi, r8-r15)  // 120+ bytes
SAVE_EXTENDED_REGS(xmm0-xmm15)                   // SSE/AVX 상태

// Step 2: task_struct에서 프로세스 B의 레지스터 복원
LOAD_REGS(rax, rbx, rcx, rdx, rsi, rdi, r8-r15)
LOAD_EXTENDED_REGS(xmm0-xmm15)

// Step 3: 다음 명령어로 점프
jmp [rcx]  // RIP 복원

저장/복원되는 항목: - 범용 레지스터 16개: rax, rbx, rcx, rdx, rsi, rdi, r8-r15 (각 8바이트) - FPU/SSE 상태: xmm0-xmm15 (16개 × 16바이트 = 256바이트) - 제어 레지스터: CR0, CR3 (페이지 테이블 기본 주소), CR4 - 세그먼트 레지스터: CS, DS, ES, SS, FS, GS - RFLAGS: CPU 플래그 레지스터

2. 페이지 테이블 전환 (주요 병목)

// 새 프로세스의 페이지 테이블 로드
// mm_struct는 각 프로세스의 메모리 관리 정보를 포함
struct mm_struct *new_mm = next_task->mm;
unsigned long pgd_phys = __pa(new_mm->pgd);

// CR3 레지스터에 새 PGD(Page Global Directory) 물리 주소 로드
// 이 순간 TLB(Translation Lookaside Buffer) 플러시 발생
load_cr3(pgd_phys);

// 이 명령 직후 모든 메모리 접근은 새 주소 공간을 참조

페이지 테이블 전환의 비용: - CR3 로드 직후 TLB 전체 플러시 (ASID 없는 경우): 수 μs 소비 - 새 프로세스의 첫 메모리 접근 시 TLB 미스: 수십 ns → 수백 ns로 증가 - L1/L2/L3 캐시 오염: 이전 프로세스의 캐시 데이터가 쓸모없어짐

3. 커널 스택 전환

// 각 프로세스는 커널 모드에서만 사용하는 별도의 커널 스택을 가짐
// thread_info 구조체는 커널 스택의 맨 위에 배치됨

struct thread_info {
    unsigned long flags;
    __u32 status;
    mm_segment_t addr_limit;
    struct task_struct *task;
    // ... 다른 필드들
};

// 커널은 스택을 전환하기 위해 RSP(스택 포인터) 변경
// 이전: RSP → [프로세스 A의 커널 스택]
// 이후: RSP → [프로세스 B의 커널 스택]

컨텍스트 스위칭의 실제 비용 측정:

Linux의 최신 시스템(Skylake 이상, 마이크로초 단위): - 최소 비용: ~2-3 μs (메모리 캐시에 있을 때) - 전형적인 경우: ~5-10 μs - 최악의 경우: ~20-50 μs (캐시 미스, TLB 재구성)

이는 산술 연산 1000만 회가 약 1 μs이므로, **컨텍스트 스위칭 1회가 산술 연산 1억 회와 동등**합니다.

1.5 System Call 실행 흐름: open() 예제

사용자 코드에서 open("/etc/passwd", O_RDONLY) 호출 시의 상세 동작:

사용자 공간 (User Space):

// C 라이브러리 함수 (glibc)
#include <fcntl.h>

int fd = open("/etc/passwd", O_RDONLY);

glibc의 open() 함수는 내부적으로 다음을 수행:

// glibc 내부 (sysdeps/unix/syscall-template.S)
// 인자 설정
mov $<syscall_number_open>, %eax  // rax = 2 (x86-64 syscall #2는 open)
mov $filename, %rdi               // rdi = "/etc/passwd"
mov $flags, %rsi                  // rsi = O_RDONLY
mov $mode, %rdx                   // rdx = 0 (flags가 O_RDONLY이므로 무시)

// 시스템 콜 진입
syscall                           // CPU가 kernel mode로 전환

커널 공간 (Kernel Space):

// arch/x86/entry/entry_64.S의 syscall entry point
ENTRY(entry_SYSCALL_64)
    // 1. 레지스터 저장 (push를 사용하지 않고 메모리에 직접 저장)
    mov %rsp, %rax                    // 임시로 사용자 RSP 저장
    mov %gs:cpu_current_stack, %rsp   // 커널 스택으로 전환

    // 2. pt_regs 구조체에 사용자 컨텍스트 저장
    SAVE_REGS

    // 3. 시스템 콜 번호로 핸들러 찾기
    cmp $nr_syscalls, %eax            // 유효성 검사
    jae 1f

    // 4. 시스템 콜 테이블(sys_call_table)에서 함수 포인터 조회
    lea sys_call_table(%rip), %r10
    mov (%r10, %rax, 8), %r10         // r10 = sys_open 주소

    // 5. 시스템 콜 함수 호출
    call *%r10                        // sys_open() 실행

    // 6. 반환값을 RAX에 저장 (이미 이 상태)
    // 반환값은 open()의 반환 값 (file descriptor 또는 -errno)

sys_open() 커널 함수 (fs/open.c):

SYSCALL_DEFINE3(open, const char __user *filename, int flags, umode_t mode)
{
    // 1. 사용자 공간 포인터 유효성 확인
    if (unlikely(!filename)) {
        return -EFAULT;  // 잘못된 주소
    }

    // 2. 사용자 공간에서 커널 공간으로 경로 복사
    char *buf = kmalloc(PATH_MAX, GFP_KERNEL);
    // copy_from_user는 매우 중요한 함수
    // 보안 검증, 페이지 폴트 핸들링, MMU 권한 확인 수행
    if (copy_from_user(buf, filename, strlen_user(filename))) {
        return -EFAULT;  // 사용자 공간 메모리 접근 실패
    }

    // 3. 파일 시스템 VFS를 통해 inode 조회
    struct inode *inode = vfs_open_inode(buf);

    // 4. 파일 객체(struct file) 생성
    struct file *filp = alloc_file();
    filp->f_inode = inode;
    filp->f_pos = 0;

    // 5. 프로세스의 파일 디스크립터 테이블에 등록
    int fd = get_unused_fd_flags(flags);
    fd_install(fd, filp);

    // 6. 반환 (RAX = fd)
    return fd;
}

copy_from_user() 함수의 구현:

이 함수는 매우 흥미로운 아키텍처 특성을 보여줍니다:

// include/linux/uaccess.h
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
    // 1. 사용자 공간 주소 범위 확인
    // access_ok는 현재 프로세스의 mm_struct을 확인하여
    // from이 정말 사용자 주소 범위에 있는지 검증
    if (!access_ok(from, n)) {
        return n;  // 모두 실패
    }

    // 2. 아키텍처 특화 복사 루틴 호출
    // x86-64의 경우: __arch_copy_from_user
    return __arch_copy_from_user(to, from, n);
}

// arch/x86/lib/copy_user_64.S
// 매우 저수준 어셈블리로 구현됨
// 이유: 페이지 폴트 시 특수한 처리 필요
__arch_copy_from_user:
    // rep movsb 또는 rep movsq로 바이트/워드 복사
    // 이 명령어 실행 중 페이지 폴트 발생 시:
    // 1. CPU가 예외를 발생시킴
    // 2. 커널의 page fault handler가 호출됨
    // 3. Handler는 필요한 페이지를 메모리에 로드
    // 4. 복사 명령어 재개
    // 5. 성공하면 0 반환, 실패하면 남은 바이트 수 반환
    rep movsb
    ret

복귀 (Return to User Space):

// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64_after_hwframe)
    // 1. 반환값은 이미 RAX에 있음 (fd 번호)

    // 2. 사용자 컨텍스트 복원
    RESTORE_REGS

    // 3. 사용자 모드로 전환하고 사용자 코드로 점프
    sysret  // CPU: Ring 0 → Ring 3, RIP = RCX

사용자 공간 복귀:

// 이제 사용자 프로세스의 다음 명령어 실행
// glibc의 syscall 래퍼는 RAX의 값을 처리
// RAX = fd (양수) 또는 -errno (음수)

int fd = open("/etc/passwd", O_RDONLY);
// fd = 3 (일반적으로 0=stdin, 1=stdout, 2=stderr 다음)

1.6 Docker 컨테이너와 Kernel Space의 관계

Docker 컨테이너가 "경량 가상 머신"처럼 보이지만, 실제로는 모두 동일한 커널을 공유**합니다. 컨테이너 격리는 커널의 **namespace와 cgroup 기능으로 구현됩니다.

Namespace: 시스템 리소스의 논리적 분리

Namespace 격리 대상 설명
PID Namespace 프로세스 ID 컨테이너 내부에서 PID 1부터 시작
Network Namespace 네트워크 스택 독립적인 IP, 라우팅, 방화벽 규칙
Mount Namespace 파일시스템 마운트 컨테이너만의 루트 파일시스템 (/dev/myapp)
IPC Namespace 프로세스 간 통신 공유 메모리, 세마포어, 메시지 큐 격리
User Namespace UID/GID 매핑 컨테이너 내부의 UID 0이 호스트의 일반 사용자
UTS Namespace 호스트명, 도메인명 컨테이너의 독립적인 호스트명

실제 메커니즘:

호스트 OS:
┌─────────────────────────────────────────┐
│      Linux Kernel (Ring 0)              │
│   - 단 1개의 커널 인스턴스             │
│   - 모든 컨테이너가 공유               │
└─────────────────────────────────────────┘
         ↓ Namespace로 분리
    ┌──────┬──────┬──────┐
    │      │      │      │
  ┌─┴──┐┌─┴──┐┌─┴──┐
  │동일 ││동일 ││동일 │  → sys_open()을 호출할 때
  │sys_ ││sys_ ││sys__│    어느 컨테이너의 파일인지
  │open ││read ││write│    namespace에서 판단
  │()  ││()  ││()  │
  └────┘└────┘└────┘

Container A        Container B        Container C
(PID 1 ~ 100)      (PID 101 ~ 200)    (PID 201 ~ 300)
Namespace 격리

호스트 관점: PID 5432  │  호스트 관점: PID 6789  │  호스트 관점: PID 7890
컨테이너 관점: PID 1   │  컨테이너 관점: PID 1   │  컨테이너 관점: PID 1

Cgroup: 자원 제한

// cgroup에서 컨테이너 A의 메모리를 512MB로 제한
// 이는 kernel 수준의 메모리 할당자(kmalloc, page allocator)에서 체크됨

// 시스템 콜 내에서 메모리 할당 시도
void *buf = kmalloc(1024 * 1024, GFP_KERNEL);

// 커널: "이 프로세스가 속한 cgroup의 메모리 한계는?"
// → Container A: 512MB 한계
// → 현재 사용: 510MB
// → 요청: 1MB
// → 결과: 할당 실패 (-ENOMEM)

// 컨테이너 내부 애플리케이션 관점:
// "malloc()이 NULL을 반환했다" = 메모리 부족

Container vs Kernel Space 명확히:

  • Container: Namespace + Cgroup을 통한 사용자 공간의 격리
  • Kernel Space: **모든 컨테이너가 공유**하는 단일 커널
  • /proc/kallsyms (커널 심볼)는 모든 컨테이너에서 동일
  • 시스템 콜 번호 테이블도 동일
  • 드라이버도 공유

따라서 Docker 컨테이너는 가벼우면서도 진정한 격리를 제공하는 아키텍처입니다.

1.7 Key Takeaways

  • CPU Ring 레벨: 하드웨어 수준의 권한 분리, 소프트웨어로 우회 불가
  • Virtual Memory: 각 프로세스의 가상 주소 공간은 독립적, MMU가 권한 검증
  • System Call: 유일한 안전한 커널 진입점, syscall 명령어는 원자적 모드 전환 보장
  • Context Switching: 매우 비싼 연산 (μs 단위), TLB/캐시 재구성 비용 포함
  • Docker Container: Namespace로 UI 격리, 하지만 커널 공유 → 가벼움

챕터 2: 커널 모듈(LKM)의 생애주기

2.1 LKM의 개념과 목적

**Loadable Kernel Module (LKM)**은 실행 중인 커널을 재컴파일/재부팅하지 않고도 런타임에 커널에 코드를 동적으로 로드/언로드할 수 있는 메커니즘입니다.

전통적인 커널 기능 추가 방법: 1. 커널 소스에 코드 추가 2. 전체 커널 재컴파일 (30분 ~ 수시간) 3. 부팅 로더 업데이트 4. 재부팅 5. 새 커널 실행

LKM의 이점: - 빠른 개발 사이클 - 드라이버 격리 (버그가 다른 드라이버를 멈추지 않음) - 선택적 로드/언로드 (필요 시에만 메모리 점유)

2.2 객체 지향 프로그래밍과의 비교

LKM의 구조는 **객체 지향 프로그래밍의 클래스 생명주기**와 매우 유사합니다.

C++ 클래스 생명주기:

class Device {
public:
    // 생성자: 객체 초기화 시점
    Device() {
        this->fd = -1;
        this->buffer = malloc(4096);
        this->state = IDLE;
    }

    // 객체가 사용 중...
    void operate() {
        // 기능 수행
    }

    // 소멸자: 객체 정리 시점
    ~Device() {
        if (this->buffer) free(this->buffer);
        this->fd = -1;
    }
};

// 사용
Device *dev = new Device();  // 생성자 호출
dev->operate();              // 사용
delete dev;                  // 소멸자 호출

LKM 생명주기:

#include <linux/module.h>
#include <linux/kernel.h>

// 초기화: 모듈 로드 시점
static int __init device_init(void) {
    // 메모리 할당
    device_data = kmalloc(4096, GFP_KERNEL);

    // 하드웨어 리소스 등록
    register_chrdev(MAJOR_NUM, "device", &fops);

    // 인터럽트 핸들러 등록
    request_irq(IRQ_NUM, device_irq_handler, 0, "device", NULL);

    printk(KERN_INFO "Device module initialized\n");
    return 0;  // 성공
}

// 정리: 모듈 언로드 시점 (소멸자 역할)
static void __exit device_exit(void) {
    // 역순으로 정리
    free_irq(IRQ_NUM, NULL);
    unregister_chrdev(MAJOR_NUM, "device");
    kfree(device_data);

    printk(KERN_INFO "Device module unloaded\n");
}

// 매크로로 진입점 등록
module_init(device_init);
module_exit(device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Example Character Device Driver");

유사점:

측면 C++ 클래스 LKM
초기화 생성자 Device() module_init(device_init)
메모리 할당 malloc() kmalloc()
리소스 등록 멤버 변수 초기화 드라이버 등록, IRQ 요청
활용 단계 dev->operate() 사용자 공간 애플리케이션이 호출
정리 소멸자 ~Device() module_exit(device_exit)
메모리 해제 free() kfree()
리소스 해제 - 드라이버 언등록, IRQ 해제

중요한 차이점: - C++ 객체는 프로그래머가 명시적으로 new/delete 호출 - LKM은 커널이 자동으로 관리 (insmod/rmmod)

2.3 Kernel Symbol Table과 EXPORT_SYMBOL

LKM이 커널 함수를 호출하려면, 그 함수의 메모리 주소를 알아야 합니다. 이것이 커널 심볼 테이블의 역할입니다.

동적 링킹(Dynamic Linking) 프로세스:

┌─────────────────────────────────────────────┐
│   커널 이미지 (vmlinuz)                      │
│                                             │
│  kmalloc() @ 0xffffffff812a4030             │
│  printk()  @ 0xffffffff812b5020             │
│  ...더 많은 함수들...                        │
│                                             │
│  __ksymtab 섹션 (심볼 테이블)              │
│  ┌─────────────────────────────────────┐   │
│  │ Symbol: "kmalloc"                   │   │
│  │ Address: 0xffffffff812a4030         │   │
│  │ License: GPL                        │   │
│  ├─────────────────────────────────────┤   │
│  │ Symbol: "printk"                    │   │
│  │ Address: 0xffffffff812b5020         │   │
│  │ License: GPL                        │   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

EXPORT_SYMBOL 메커니즘:

// 커널 소스 (kernel/sched/core.c)
void schedule(void) {
    // ... 스케줄링 로직
}

// 이 심볼을 모든 모듈에 공개
EXPORT_SYMBOL(schedule);

// 또는 GPL 라이선스의 모듈만 접근 가능
EXPORT_SYMBOL_GPL(schedule);

컴파일 시 EXPORT_SYMBOL 매크로는 **__ksymtab 섹션에 항목을 추가**합니다:

// include/linux/export.h의 EXPORT_SYMBOL 구현
#define EXPORT_SYMBOL(sym) \
    extern typeof(sym) sym; \
    __CRC_SYMBOL(sym, "_gpl", ""); \
    static const struct kernel_symbol __ksymtab_##sym \
    __attribute__((section("__ksymtab"), used)) = { \
        .value = (unsigned long)&sym, \
        .name = __stringify(sym), \
    }

모듈 로드 시 심볼 해석:

insmod my_module.ko 실행 시:

  1. 커널의 모듈 로더가 my_module.ko 읽음
  2. 모듈의 미해석 심볼(Unresolved Symbols) 목록 추출

    Undefined symbols: kmalloc, printk, schedule
    

  3. 전역 심볼 테이블 조회

    Loop each undefined symbol:
        Look up in /proc/kallsyms or __ksymtab
        If found: record_address(symbol_name, address)
        If not found: ERROR - "Unknown symbol in module"
    

  4. 재배치(Relocation) 수행

    모듈의 call 명령어:
      Before: call 0x0  (주소 미정)
      After:  call 0xffffffff812a4030  (kmalloc의 실제 주소)
    

  5. 모듈을 커널 메모리에 로드하고 초기화 함수 실행

EXPORT_SYMBOL 없을 시의 문제:

// kernel/some_core.c
static void internal_function(void) {  // static = 심볼 비공개
    // ...
}

// my_module.c
extern void internal_function(void);  // 선언

static int __init my_init(void) {
    internal_function();  // 호출 시도
    return 0;
}

module_init(my_init);

insmod my_module.ko 시도:

insmod: ERROR: could not insert module my_module.ko: Unknown symbol in module

GPL Symbol과 Non-GPL의 차이:

// 커널 코드
int sensitive_kernel_function(void) {
    // ...
}

// 논쟁이 되는 심볼: 많은 커널 개발자들이 GPL 준수를 원함
EXPORT_SYMBOL_GPL(sensitive_kernel_function);

// 독점 드라이버 (GPL 미동의)
#include <linux/module.h>

static int __init proprietary_init(void) {
    sensitive_kernel_function();  // 호출 시도
    return 0;
}

// MODULE_LICENSE("Proprietary");
module_init(proprietary_init);
MODULE_LICENSE("Proprietary");

로드 시 실패:

insmod: ERROR: could not insert module proprietary_drv.ko: 
    Bad module signature

2.4 module_init() / module_exit() 매크로 해부

이 매크로들은 **컴파일 시점에 특수한 섹션(Section)에 배치**되어, 커널의 모듈 로더가 자동으로 발견하고 실행할 수 있도록 합니다.

매크로 확장:

// 원본 코드
static int __init device_init(void) {
    printk("Init called\n");
    return 0;
}

module_init(device_init);

// 컴파일러가 보는 실제 확장 코드
// (include/linux/module.h)

static int __init device_init(void) {
    printk("Init called\n");
    return 0;
}

// 특수 섹션에 진입점 등록
static struct kernel_symbol __initcall_device_init \
    __attribute__((section(".initcall.init"))) = {
        .init_fn = device_init,
        .level = INITCALL_ROOTFS,
    };

// module_exit() 매크로
static void __exit device_exit(void) {
    printk("Exit called\n");
}

module_exit(device_exit);

// 확장됨:
static struct kernel_symbol __exitcall_device_exit \
    __attribute__((section(".exitcall.exit"))) = {
        .exit_fn = device_exit,
    };

ELF 섹션과 로더:

컴파일된 모듈 파일 (module.ko)의 구조:

┌──────────────────────────────────────────┐
│         ELF 헤더                         │
├──────────────────────────────────────────┤
│ 섹션 1: .text (코드)                     │
│   device_init()의 바이너리               │
│   device_exit()의 바이너리                │
├──────────────────────────────────────────┤
│ 섹션 2: .data (초기화된 데이터)          │
├──────────────────────────────────────────┤
│ 섹션 3: .init.text (초기화 코드)        │
│   __init로 마킹된 함수들                 │
├──────────────────────────────────────────┤
│ 섹션 4: .init.data (초기화 데이터)      │
│   ┌────────────────────────────────────┐ │
│   │ module_init 진입점 레코드:          │ │
│   │ {                                  │ │
│   │   .init_fn = device_init 주소,     │ │
│   │ }                                  │ │
│   └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ 섹션 5: .exit.data (종료 데이터)        │
│   ┌────────────────────────────────────┐ │
│   │ module_exit 진입점 레코드:          │ │
│   │ {                                  │ │
│   │   .exit_fn = device_exit 주소,     │ │
│   │ }                                  │ │
│   └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ 섹션 6: .symtab (심볼 테이블)           │
│   이 모듈에서 정의한 심볼들             │
├──────────────────────────────────────────┤
│ 섹션 7: .strtab (문자열 테이블)         │
│   심볼명, 섹션명 등의 문자열            │
└──────────────────────────────────────────┘

커널 모듈 로더의 동작:

// kernel/module.c의 개념화된 로드 함수

int load_module(const char *umod, struct load_info *info) {
    // 1. 모듈 검증 및 기본 설정
    struct module *mod = layout_and_allocate(info);

    // 2. 모듈 코드를 커널 메모리에 복사
    memcpy(mod->core_layout.base, load_ptr, info->core_size);

    // 3. 미해석 심볼 해석 (모듈이 사용하는 커널 함수 주소 결정)
    apply_relocations(mod);

    // 4. 초기화 섹션(.init) 실행
    ret = call_init_module(mod);  // module_init() 호출
    if (ret < 0) {
        return ret;  // 초기화 실패 시 모듈 언로드
    }

    // 5. 초기화 섹션 메모리 해제 (.init 섹션은 이제 불필요)
    // __init 함수는 로드 후 버려져도 됨 (부팅 시 사용, 모듈은 런타임 로드)
    free_init_section(mod);

    // 6. 모듈 리스트에 추가
    list_add_rcu(&mod->list, &modules);

    return 0;
}

// 초기화 함수 호출 부분
static int call_init_module(struct module *mod) {
    // .init.data 섹션에서 진입점 레코드 추출
    struct kernel_init_record *init_rec = &mod->init_section;

    // init 함수 포인터 호출
    // 이 함수는 커널 권한(Ring 0)에서 실행됨
    int ret = (*init_rec->init_fn)();

    return ret;
}

2.5 Hello World 모듈: 상세 분석

#include <linux/module.h>    // 모듈 관련 매크로 정의
#include <linux/kernel.h>    // printk() 매크로
#include <linux/init.h>      // __init, __exit 속성

MODULE_LICENSE("GPL");       // 라이선스 선언 (required)
MODULE_AUTHOR("Developer");  // 저자 정보
MODULE_DESCRIPTION("Hello World Module");  // 설명

// 초기화 함수: 모듈 로드 시 호출됨
static int __init hello_init(void) {
    // printk는 커널 로깅 함수
    // 출력은 /var/log/kern.log 또는 dmesg로 확인
    printk(KERN_INFO "Hello World!\n");

    return 0;  // 0 = 성공, 음수 = 실패 (모듈 로드 거부)
}

// 정리 함수: 모듈 언로드 시 호출됨
static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye World!\n");
}

// 컴파일 시점에 확장되는 매크로들
module_init(hello_init);   // 진입점 등록
module_exit(hello_exit);   // 종료점 등록

각 구성 요소의 역할:

  1. #include
  2. module_init, module_exit, EXPORT_SYMBOL 등 매크로 정의
  3. struct module 구조체 정의
  4. 모듈 관련 함수 선언

  5. #include

  6. printk() 매크로 정의
  7. KERN_INFO, KERN_ERR 등 로그 레벨 상수

  8. #include

  9. __init 속성: "이 함수는 초기화 시점에만 필요" → 로드 후 메모리 해제 가능
  10. __exit 속성: "이 함수는 모듈이 로드된 경우에만 필요" → 정적 빌드 시 생략 가능

  11. MODULE_LICENSE("GPL")

  12. 모듈의 라이선스 선언
  13. GPL이 아니면 일부 커널 심볼 접근 불가
  14. 모듈 서명 검증 시에도 사용

  15. static int __init hello_init(void)

  16. static: 심볼을 파일 내에 한정 (다른 모듈에서 접근 불가)
  17. __init: 로드 후 메모리 해제 가능
  18. 반환값: 0 (성공) 또는 음수 에러 코드

  19. module_init(hello_init)

  20. 매크로 확장: __initcall_hello_init 섹션에 hello_init 주소 기록
  21. 커널이 이를 읽어 모듈 로드 시 호출

실행 흐름 (insmod hello.ko):

1. 커널 모듈 로더 (kernel/module.c)
   ├─> hello.ko 파일 읽음 (ELF 형식)
   ├─> 섹션 파싱
   │   ├─ .text: hello_init, hello_exit 바이너리
   │   ├─ .init.data: module_init 레코드 ({.init_fn = hello_init})
   │   └─ .exit.data: module_exit 레코드 ({.exit_fn = hello_exit})
   ├─> 커널 메모리에 모듈 로드
   │   malloc을 사용하지 않음 (Ring 0에서 운영 중)
   │   vmalloc 또는 module_alloc으로 메모리 할당
   ├─> __init 함수 호출: hello_init()
   │   └─> printk(KERN_INFO "Hello World!\n");
   │       [커널 로그 버퍼에 기록]
   ├─> __init 섹션 메모리 해제
   │   [hello_init 함수의 메모리 회수]
   └─> 모듈 로드 완료
       /proc/modules에 나타남
       /proc/kallsyms에 심볼 추가

2. 사용자가 확인
   $ dmesg | tail
   [  234.567890] Hello World!

3. rmmod hello 명령
   ├─> .exit.data 섹션에서 exit 함수 포인터 추출
   ├─> hello_exit() 호출
   │   └─> printk(KERN_INFO "Goodbye World!\n");
   ├─> 모듈 메모리 해제
   └─> /proc/modules에서 제거

4. 사용자가 확인
   $ dmesg | tail
   [  234.890123] Goodbye World!

2.6 insmod, rmmod, lsmod 내부 동작

insmod (Insert Module):

$ sudo insmod my_module.ko param1=value1
// insmod의 내부 동작 (user-space util-linux)

int main(int argc, char *argv[]) {
    // 1. 모듈 파일 읽기
    FILE *fp = fopen("my_module.ko", "rb");
    char *module_data = read_entire_file(fp);

    // 2. 커널에 모듈 로드 요청 (시스템 콜)
    // init_module(module_data, module_size, "param1=value1");
    syscall(SYS_init_module, module_data, module_size, "param1=value1");

    // 3. 커널이 처리
    // → kernel/module.c: load_module() 함수 실행

    return 0;
}

커널 측 sys_init_module():

// kernel/module.c

SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len,
                const char __user *, uargs)
{
    struct load_info info = {};
    struct module *mod;

    // 1. 사용자 공간 데이터를 커널 버퍼로 복사
    char *buf = vmalloc(len);
    copy_from_user(buf, umod, len);  // 매우 중요!

    // 2. 모듈 정보 파싱
    mod = load_module(buf, &info);
    if (IS_ERR(mod)) {
        vfree(buf);
        return PTR_ERR(mod);
    }

    // 3. 모듈의 __init 함수 호출
    // 이 과정에서 printk()가 실행되고 커널 로그 버퍼에 저장됨

    // 4. /proc/modules에 추가 (사용자가 lsmod로 확인 가능)
    list_add_rcu(&mod->list, &modules);

    vfree(buf);
    return 0;
}

dmesg (커널 로그 확인):

$ dmesg | tail -5
[  234.567890] Hello World!

내부 동작: - 커널 로그는 ring buffer(순환 버퍼)에 저장됨 - dmesg는 시스템 콜 syslog()를 사용하여 버퍼 내용 읽음 - 로그가 너무 많으면 오래된 항목이 삭제됨

rmmod (Remove Module):

$ sudo rmmod my_module
// rmmod의 내부 동작

int main(int argc, char *argv[]) {
    // 1. 모듈 이름으로 모듈 제거 요청
    // delete_module("my_module", O_NONBLOCK);
    syscall(SYS_delete_module, "my_module", O_NONBLOCK);

    return 0;
}

커널 측 sys_delete_module():

// kernel/module.c

SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
                unsigned int, flags)
{
    struct module *mod;

    // 1. 모듈 리스트에서 찾기
    mod = find_module(name_user);
    if (!mod) {
        return -ENOENT;  // 없음
    }

    // 2. 모듈 사용 중인지 확인 (refcount 체크)
    if (mod->state == MODULE_STATE_LIVE &&
        !try_stop_module(mod)) {
        return -EWOULDBLOCK;  // 다른 모듈에서 사용 중
    }

    // 3. 모듈의 __exit 함수 호출
    mod->exit();  // my_module의 hello_exit() 실행

    // 4. 모듈 메모리 해제
    free_module(mod);

    // 5. /proc/modules에서 제거
    list_del_rcu(&mod->list);

    return 0;
}

lsmod (List Modules):

$ lsmod
Module                  Size  Used by
my_module              4096  0
nf_nat_ftp            4096  1 xt_nat
xt_nat                 4096  2

내부:

// /proc/modules는 커널이 제공하는 가상 파일

// 읽을 때마다 kernel/module.c의 m_show() 함수가 호출됨
static int m_show(struct seq_file *m, void *p) {
    struct module *mod = list_entry(p, struct module, list);

    seq_printf(m, "%s %u 0\n",
               mod->name,
               mod->core_layout.size);  // 모듈 크기

    // Used by: 이 모듈을 참조하는 다른 모듈들 표시
    print_modules_used_by(m, mod);

    return 0;
}

2.7 모듈 매개변수 (Module Parameters)

#include <linux/module.h>
#include <linux/moduleparam.h>

// 모듈 매개변수 선언
static int debug_level = 0;
module_param(debug_level, int, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

static char *name = "default";
module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(name, "Device name");

static int __init my_init(void) {
    printk(KERN_INFO "Debug: %d, Name: %s\n", debug_level, name);
    return 0;
}

module_init(my_init);

사용:

$ insmod my_module.ko debug_level=3 name=mydev
$ dmesg | tail
[  234.567890] Debug: 3, Name: mydev

$ cat /sys/module/my_module/parameters/debug_level
3

$ echo 2 > /sys/module/my_module/parameters/debug_level  # 런타임 변경

2.8 Key Takeaways

  • LKM: 커널 재컴파일 없이 런타임에 기능 추가/제거
  • 객체 지향과의 유사성: __init = 생성자, __exit = 소멸자
  • EXPORT_SYMBOL: 커널 함수를 모듈에 공개하는 메커니즘
  • module_init/exit: 컴파일 시 특수 섹션(.init.data, .exit.data)에 배치
  • insmod/rmmod: 사용자 공간의 시스템 콜 래퍼
  • 매개변수: /sys/module/을 통해 런타임 조정 가능

챕터 3: 문자 디바이스 드라이버와 VFS (Virtual File System)

3.1 "Everything is a File" 철학

Linux에서는 거의 모든 것을 **파일로 추상화**합니다:

대상 파일 경로 타입 드라이버
터미널 /dev/tty0 문자 디바이스 tty 드라이버
웹캠 /dev/video0 문자 디바이스 V4L2 드라이버
USB 포트 /dev/usb0 문자 디바이스 USB 드라이버
하드 디스크 /dev/sda 블록 디바이스 SATA 드라이버
네트워크 /proc/net/tcp 일반 파일 네트워크 스택
메모리 /dev/mem 문자 디바이스 메모리 드라이버

문자 디바이스(Character Device)의 특징: - 스트림 기반: 데이터가 순차적으로 처리**됨 - **버퍼링 없음: 읽기/쓰기가 직접 하드웨어로 전달됨 - 예: 터미널, 시리얼 포트, 마우스

블록 디바이스(Block Device)의 특징: - 랜덤 접근: 임의의 위치에서 읽기/쓰기 가능 - 버퍼링: 커널이 블록 캐시 관리 - 예: 하드 디스크, SSD

3.2 file_operations 구조체: 인터페이스의 심장

file_operations는 사용자 공간의 system call(open, read, write)과 드라이버 함수를 **매핑하는 인터페이스**입니다.

// include/linux/fs.h

struct file_operations {
    struct module *owner;

    // 파일 위치 이동
    loff_t (*llseek) (struct file *, loff_t, int);

    // 파일 읽기
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

    // 파일 쓰기
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    // 읽기 + 쓰기 동시 (정렬된 I/O)
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

    // 디바이스 열기
    int (*open) (struct inode *, struct file *);

    // 디바이스 닫기
    int (*release) (struct inode *, struct file *);

    // 제어 명령어 (ioctl)
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

    // 메모리 매핑
    int (*mmap) (struct file *, struct vm_area_struct *);

    // 폴링 (select/poll)
    __poll_t (*poll) (struct file *, struct poll_table_struct *);

    // 기타 다양한 함수들...
};

3.3 간단한 문자 디바이스 드라이버

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define DEVICE_NAME "mychdev"
#define CLASS_NAME "myclass"

// 주요 번호 (major number) - 0은 커널이 자동 할당
static int majorNumber;
static struct class *charClass = NULL;
static struct device *charDevice = NULL;

// 디바이스의 메모리 버퍼
#define BUFFER_SIZE 1024
static char deviceBuffer[BUFFER_SIZE] = {};

// open: 사용자가 /dev/mychdev를 열 때 호출
static int device_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device opened\n");
    return 0;  // 성공
}

// release: 사용자가 파일을 닫을 때 호출
static int device_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// read: 사용자가 디바이스로부터 읽을 때 호출
// cat /dev/mychdev 또는 read() 시스템 콜
static ssize_t device_read(struct file *filep, char __user *buffer,
                          size_t len, loff_t *offset) {
    // 버퍼 크기 제한
    int bytes_to_read = (len > BUFFER_SIZE) ? BUFFER_SIZE : len;

    // 커널 메모리(deviceBuffer)에서 사용자 메모리(buffer)로 복사
    // copy_to_user는 매우 중요한 함수
    int error_count = copy_to_user(buffer, deviceBuffer, bytes_to_read);

    if (error_count != 0) {
        // copy_to_user는 실패한 바이트 수 반환
        // 0이 아니면 일부 복사 실패
        return -EFAULT;
    }

    printk(KERN_INFO "Device read %d bytes\n", bytes_to_read);
    return bytes_to_read;
}

// write: 사용자가 디바이스에 쓸 때 호출
// echo "hello" > /dev/mychdev 또는 write() 시스템 콜
static ssize_t device_write(struct file *filep, const char __user *buffer,
                           size_t len, loff_t *offset) {
    int bytes_to_write = (len > BUFFER_SIZE) ? BUFFER_SIZE : len;

    // 사용자 메모리(buffer)에서 커널 메모리(deviceBuffer)로 복사
    int error_count = copy_from_user(deviceBuffer, buffer, bytes_to_write);

    if (error_count != 0) {
        return -EFAULT;
    }

    printk(KERN_INFO "Device wrote %d bytes\n", bytes_to_write);
    return bytes_to_write;
}

// file_operations 구조체 초기화
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

// 모듈 초기화
static int __init chardev_init(void) {
    // 1. 문자 디바이스 등록
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return majorNumber;
    }

    printk(KERN_INFO "Device registered with major number %d\n", majorNumber);

    // 2. 장치 클래스 생성 (udev가 /dev/ 노드 자동 생성)
    charClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(charClass)) {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        return PTR_ERR(charClass);
    }

    // 3. 장치 생성
    charDevice = device_create(charClass, NULL, MKDEV(majorNumber, 0),
                              NULL, DEVICE_NAME);
    if (IS_ERR(charDevice)) {
        class_destroy(charClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        return PTR_ERR(charDevice);
    }

    printk(KERN_INFO "Device created successfully\n");
    return 0;
}

// 모듈 정리
static void __exit chardev_exit(void) {
    device_destroy(charClass, MKDEV(majorNumber, 0));
    class_destroy(charClass);
    unregister_chrdev(majorNumber, DEVICE_NAME);

    printk(KERN_INFO "Device unregistered\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Simple Character Device Driver");

3.4 VFS (Virtual File System) 계층 구조

사용자가 /dev/mychdev에 접근할 때의 VFS 계층:

┌────────────────────────────────────────┐
│      사용자 애플리케이션                 │
│      read("/dev/mychdev", buf, 1024)   │
└────────────────┬───────────────────────┘
                 │ (시스템 콜)
┌────────────────────────────────────────┐
│       VFS - 파일 시스템 인터페이스      │
│ fs/read_write.c: vfs_read()            │
│ - 파일 객체 검증                       │
│ - 권한 확인                            │
│ - 적절한 fops 찾기                     │
└────────────────┬───────────────────────┘
┌────────────────────────────────────────┐
│     devtmpfs - 디바이스 파일 시스템     │
│ - 주요 번호(major)와 부가 번호(minor)  │
│   로부터 드라이버 찾기                 │
└────────────────┬───────────────────────┘
┌────────────────────────────────────────┐
│   문자 디바이스 드라이버                │
│   file_operations.read()               │
│   → device_read() 호출                 │
└────────────────┬───────────────────────┘
        ┌─────────────────────┐
        │   하드웨어 또는       │
        │   메모리 버퍼        │
        └─────────────────────┘

VFS 주요 구조체:

// 파일 시스템의 메타정보
struct inode {
    unsigned long i_ino;           // inode 번호
    struct timespec64 i_atime;     // 마지막 접근 시간
    struct timespec64 i_mtime;     // 마지막 수정 시간
    struct file_operations *i_fop; // 파일 연산 함수 포인터

    // 문자 디바이스의 경우
    struct cdev *i_cdev;           // 문자 디바이스 정보
    union {
        blkcnt_t i_blocks;         // 블록 개수
        struct {
            dev_t i_rdev;          // 주요/부가 번호
        };
    };
};

// 열린 파일의 상태 (프로세스마다 생성)
struct file {
    struct inode *f_inode;         // inode 참조
    struct file_operations *f_op;  // 파일 연산 함수 포인터
    loff_t f_pos;                  // 현재 위치
    unsigned int f_flags;          // O_RDONLY, O_WRONLY 등
    void *private_data;            // 드라이버가 자유롭게 사용
};

// 문자 디바이스 정보
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;  // 우리의 fops!
    struct list_head list;
    dev_t dev;                     // 주요 번호
    unsigned int count;
};

3.5 copy_to_user() 상세 분석

이 함수 없이 직접 메모리를 접근하면 **여러 보안 취약점**이 발생합니다:

취약한 코드 (위험!):

static ssize_t bad_device_read(struct file *filep, char __user *buffer,
                              size_t len, loff_t *offset) {
    // ❌ 위험: 사용자 포인터를 직접 해석
    // 문제 1: buffer가 NULL일 수 있음 → 커널 크래시
    // 문제 2: buffer가 커널 메모리를 가리킬 수 있음 → 데이터 유출
    // 문제 3: buffer가 다른 프로세스의 메모리를 가리킬 수 있음

    memcpy(buffer, deviceBuffer, len);  // 💣 매우 위험

    return len;
}

copy_to_user의 역할:

static ssize_t safe_device_read(struct file *filep, char __user *buffer,
                               size_t len, loff_t *offset) {
    // ✅ 안전: 사용자 포인터 검증
    unsigned long result = copy_to_user(buffer, deviceBuffer, len);

    if (result != 0) {
        // result = 복사 실패한 바이트 수
        // 일부 복사 실패 시 처리
        printk(KERN_ERR "%lu bytes could not be copied\n", result);
        return -EFAULT;
    }

    return len;
}

copy_to_user의 내부 동작:

// arch/x86/lib/usercopy_64.c

unsigned long copy_to_user(void __user *to, const void *from,
                          unsigned long n)
{
    // 1. 주소 범위 검증
    // MMU가 user/kernel 경계를 이미 페이지 테이블로 강제했지만,
    // 추가 소프트웨어 검증도 수행
    if (!access_ok(to, n)) {
        // 이 주소는 사용자 공간이 아님
        return n;  // 모두 실패
    }

    // 2. 잠재적 페이지 폴트 처리
    might_fault();  // 커널 스케줄러에 알림: 이 함수는 휴면할 수 있음

    // 3. 아키텍처 특화 복사
    // x86-64: REP MOVSB 명령어 사용 (바이트 단위 복사)
    // ARM: memcpy의 특수 버전
    return __copy_to_user(to, from, n);
}

// arch/x86/lib/copy_user_64.S - 매우 저수준
__copy_to_user:
    // MOV 또는 REP MOVSB로 바이트 복사
    // 이 중에 페이지 폴트 발생 가능
    copy_bytes:
        mov (%rsi), %al           // 커널 메모리에서 읽음
        mov %al, (%rdi)           // 사용자 메모리에 쓰기 (여기서 faulting!)
        inc %rsi
        inc %rdi
        dec %rcx
        jnz copy_bytes

    ret

페이지 폴트 처리의 예:

사용자 버퍼가 스왑 메모리에 있을 수 있습니다:

1. copy_to_user(user_buffer, kernel_data, 4096) 호출

2. 처음 4KB는 메모리에 있음 → 즉시 복사

3. 다음 4KB는 디스크에 스왑됨 → 페이지 폴트 발생

4. CPU가 page fault exception 발생

5. 커널의 page fault handler 호출
   do_page_fault(regs, error_code)

6. 핸들러: 현재 주소가 copy_to_user/copy_from_user 내부인지 확인
   if (in_exception_table(regs->ip)) {
       // 이것은 예상된 폴트
       // 스왑된 페이지를 메모리로 로드
       do_swap_page(...)
       // 복사 명령어 재개
   }

7. 복사 계속 진행

3.6 copy_from_user() vs Pipe/Socket

커널이 사용자 데이터를 받을 때 몇 가지 방법이 있습니다:

1. copy_from_user() - 직접 복사

// 디바이스 드라이버
static ssize_t device_write(struct file *filep, const char __user *buffer,
                           size_t len, loff_t *offset) {
    char kernel_buf[1024];

    // 사용자 → 커널 메모리로 직접 복사
    if (copy_from_user(kernel_buf, buffer, len)) {
        return -EFAULT;
    }

    // kernel_buf에서 처리
    process_data(kernel_buf, len);

    return len;
}

2. Pipe - 버퍼 중간 저장

// write(pipe_fd, "hello", 5)
ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from) {
    struct pipe_inode_info *pipe = iocb->ki_filp->private_data;

    // 파이프의 내부 버퍼에 데이터 복사
    copy_from_iter(pipe->bufs[pipe->curbuf].page, from);

    // 읽는 쪽이 있으면 깨우기
    wake_up_interruptible(&pipe->rd_wait);

    return len;
}

// read(pipe_fd, buf, 5)
ssize_t pipe_read(struct kiocb *iocb, struct iov_iter *to) {
    struct pipe_inode_info *pipe = iocb->ki_filp->private_data;

    // 파이프 버퍼 → 사용자 메모리
    copy_to_iter(pipe->bufs[pipe->curbuf].page, to);

    return len;
}

3. Socket - 네트워크 버퍼

// send(sock, buf, len, 0)
static int sock_sendmsg(struct socket *sock, struct msghdr *msg) {
    // 소켓의 송신 버퍼에 복사
    // sk->sk_write_queue 큐에 sk_buff 추가

    struct sk_buff *skb = alloc_skb(len, GFP_KERNEL);
    skb_copy_from_user(skb, msg->msg_iov, len);

    // 네트워크 스택으로 전달
    sk->sk_write_queue.push(skb);

    return len;
}

비교:

측면 copy_from_user Pipe Socket
데이터 저장소 드라이버 버퍼 파이프 버퍼 sk_buff (순환 버퍼)
싱크 1:1 1:1 1:많음 (네트워크)
처리 시점 동기적 비동기적 비동기적
오버헤드 낮음 중간 (버퍼 관리) 높음 (네트워크 프로토콜)

3.7 메모리 보안: Ramdisk 예제

실제 하드웨어 없이 메모리를 디바이스처럼 다루는 예제:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>

#define DEVICE_NAME "ramdisk"
#define DEVICE_SIZE (1024 * 1024)  // 1MB 가상 디스크

static int majorNumber;
static void *ramdisk_buffer = NULL;

// 읽기: 메모리의 특정 위치에서 데이터 반환
static ssize_t ramdisk_read(struct file *filep, char __user *buffer,
                           size_t len, loff_t *offset) {
    // offset이 범위 벗어나면 0 반환 (EOF)
    if (*offset >= DEVICE_SIZE) {
        return 0;
    }

    // 읽을 수 있는 최대 크기 계산
    if (*offset + len > DEVICE_SIZE) {
        len = DEVICE_SIZE - *offset;
    }

    // ramdisk_buffer의 offset 위치에서 len만큼 읽기
    if (copy_to_user(buffer, (char *)ramdisk_buffer + *offset, len)) {
        return -EFAULT;
    }

    *offset += len;  // 파일 위치 이동
    return len;
}

// 쓰기: 메모리의 특정 위치에 데이터 저장
static ssize_t ramdisk_write(struct file *filep, const char __user *buffer,
                            size_t len, loff_t *offset) {
    if (*offset >= DEVICE_SIZE) {
        return -ENOSPC;  // 공간 부족
    }

    if (*offset + len > DEVICE_SIZE) {
        len = DEVICE_SIZE - *offset;
    }

    // 사용자 메모리에서 ramdisk_buffer로 복사
    if (copy_from_user((char *)ramdisk_buffer + *offset, buffer, len)) {
        return -EFAULT;
    }

    *offset += len;
    return len;
}

// seek: 파일 위치 변경
static loff_t ramdisk_llseek(struct file *filep, loff_t offset, int origin) {
    loff_t new_pos;

    switch (origin) {
        case SEEK_SET:
            new_pos = offset;
            break;
        case SEEK_CUR:
            new_pos = filep->f_pos + offset;
            break;
        case SEEK_END:
            new_pos = DEVICE_SIZE + offset;
            break;
        default:
            return -EINVAL;
    }

    if (new_pos < 0 || new_pos >= DEVICE_SIZE) {
        return -EINVAL;
    }

    filep->f_pos = new_pos;
    return new_pos;
}

static struct file_operations fops = {
    .read = ramdisk_read,
    .write = ramdisk_write,
    .llseek = ramdisk_llseek,
};

static int __init ramdisk_init(void) {
    // 1MB 메모리 할당
    ramdisk_buffer = vmalloc(DEVICE_SIZE);
    if (!ramdisk_buffer) {
        return -ENOMEM;
    }

    // 버퍼 초기화
    memset(ramdisk_buffer, 0, DEVICE_SIZE);

    // 디바이스 등록
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        vfree(ramdisk_buffer);
        return majorNumber;
    }

    printk(KERN_INFO "Ramdisk initialized\n");
    return 0;
}

static void __exit ramdisk_exit(void) {
    unregister_chrdev(majorNumber, DEVICE_NAME);
    vfree(ramdisk_buffer);
    printk(KERN_INFO "Ramdisk unloaded\n");
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);

MODULE_LICENSE("GPL");

사용 예:

$ sudo mknod /dev/ramdisk c 250 0
$ sudo chmod 666 /dev/ramdisk

# 쓰기
$ echo "Hello Ramdisk!" > /dev/ramdisk

# 읽기
$ cat /dev/ramdisk
Hello Ramdisk!

# seek 테스트
$ dd if=/dev/ramdisk skip=5 bs=1
Ramdisk!

3.8 Key Takeaways

  • VFS: 모든 파일 연산을 통합 인터페이스(file_operations)로 제공
  • file_operations: 사용자 시스템 콜과 드라이버 함수의 매핑 구조
  • copy_to/from_user(): 필수적인 보안 함수, 주소 검증과 페이지 폴트 처리
  • 메모리 격리: MMU + 소프트웨어 검증으로 커널 메모리 보호
  • 추상화: "모든 것은 파일"로 일관된 인터페이스 제공

챕터 4: 네트워크 패킷 가로채기 - Netfilter와 sk_buff

4.1 패킷의 여행 지도: Netfilter Hook

네트워크 패킷이 하드웨어에서 애플리케이션까지 도달하는 과정에서 **5개의 Hook 지점**이 있습니다:

┌──────────────────────────────────────────────────────────────────────┐
│                   NIC (Network Interface Card)                        │
│                    (Ring Buffer에 패킷 도착)                           │
└─────────────────────────────┬──────────────────────────────────────────┘
                              │ (interrupt handler)
        ┌─────────────────────────────────────────────────┐
        │  sk_buff 구조체 생성 및 초기화                   │
        │  (패킷 메타데이터 저장)                         │
        └─────────────────────┬───────────────────────────┘
        ╔═════════════════════════════════════════════════╗
        ║    HOOK #1: NF_INET_PRE_ROUTING                 ║
        ║    (패킷이 라우팅 결정되기 전)                   ║
        ║    - 목적지 주소 변환 (DNAT)                    ║
        ║    - 패킷 필터링                               ║
        ╚═════════════════════╤═════════════════════════════╝
                    라우팅 결정 (Route Lookup)
                    목적지: 로컬인가? 포워드인가?
                ┌─────────────┴────────────┐
                │                          │
                ↓ (로컬)                    ↓ (포워드)
        ┌──────────────────┐         ╔══════════════════════════╗
        │ 로컬 수신 (Input)│          ║ HOOK #2: NF_INET_FORWARD║
        │                 │          ║ (포워딩 결정 전)         ║
        └────────┬────────┘          ║ - 포워딩 방화벽          ║
                 │                   ║ - QoS 설정              ║
        ╔════════════════════╗       ╚══════════════════════════╝
        ║ HOOK #3: INPUT    ║                 │
        ║ (로컬 수신 직전)    ║                 ↓
        ║ - 인바운드 방화벽   ║       ╔══════════════════════════╗
        ╚════════════╤═══════╝       ║ HOOK #4: POST_ROUTING   ║
                   │                ║ (경로 결정 후)            ║
                   ↓                ║ - 출발지 주소 변환 (SNAT) ║
        ┌──────────────────┐        ║ - IP 변조                ║
        │ 애플리케이션 처리 │        ╚══════════════════════════╝
        │ (소켓 수신)       │                 │
        │                 │                 ↓
        └────────┬────────┘        ╔═════════════════════════╗
                 │                 ║ HOOK #5: OUTPUT         ║
                 │                 ║ (로컬 송신 직후)         ║
                 │                 ║ - 로컬 패킷 필터링       ║
                 │                 ║ - DNAT (로컬 출발)      ║
                 │                 ╚════════════╤═════════════╝
                 │                             │
                 │         ┌───────────────────┘
                 │         │
                 └────┬────┘
            ┌──────────────────┐
            │ NIC 송신 (TX)     │
            │ (Ring Buffer)     │
            └──────────────────┘

각 Hook에서 패킷에 대해 취할 수 있는 3가지 결정: - NF_ACCEPT: 계속 진행 - NF_DROP: 패킷 폐기 - NF_QUEUE: 사용자 공간 애플리케이션으로 전달

4.2 sk_buff 구조체: 패킷의 메타데이터

// include/linux/skbuff.h

struct sk_buff {
    // -------- 링크 필드 --------
    struct sk_buff *next;
    struct sk_buff *prev;

    // -------- 메모리 레이아웃 (포인터 기반) --------
    // 패킷 메모리의 구조:
    // [head] ← data ← network_header ← transport_header ← tail ← [end]
    //   ↑                                                           ↑
    //   └─ 할당된 메모리 범위 ─────────────────────────────────────┘

    unsigned char *head;          // 할당된 메모리의 시작
    unsigned char *data;          // 현재 데이터 시작 (이더넷 헤더)
    unsigned char *tail;          // 현재 데이터의 끝
    unsigned char *end;           // 할당된 메모리의 끝

    // -------- 데이터 길이 --------
    unsigned int len;             // data~tail의 바이트 수
    unsigned int data_len;        // 프래그먼트 데이터 길이

    // -------- 프로토콜 스택 추적 --------
    // 각 프로토콜 층의 헤더 위치를 추적 (레지스터처럼 동작)
    union {
        unsigned int transport_header;
        unsigned int th;
    };

    union {
        unsigned int network_header;
        unsigned int nh;
    };

    union {
        unsigned int mac_header;
        unsigned int mac;
    };

    // -------- 제어 정보 --------
    __u16 protocol;               // 이더넷 타입 (0x0800 = IPv4)
    __u16 vlan_tci;               // VLAN 태그

    // -------- 소켓 정보 --------
    struct sock *sk;              // 소속 소켓
    struct net_device *dev;       // 입력/출력 장치

    // -------- 타이밍 정보 --------
    ktime_t tstamp;               // 패킷 타임스탐프

    // -------- 마킹 --------
    __u32 mark;                   // iptables mark (QoS 용)

    // -------- 기타 --------
    void *destructor_arg;         // 메모리 해제 시 호출
    struct sec_path *sp;          // IPsec 정보
};

메모리 레이아웃 시각화:

sk_buff 수신 초기 상태:
┌─────────────────────────────────────────────────────────┐
│ NIC로부터 수신한 프레임 (1500 bytes)                     │
│ [이더넷][IP][TCP][페이로드]                              │
└─────────────────────────────────────────────────────────┘
     ↑                  ↑        ↑          ↑        ↑
     │                  │        │          │        │
   head              data       nh        th        end
                   (=ethernet  (=IP       (=TCP
                    header)    header)    header)
                    offset     offset     offset
                    추적      추적       추적


Zero-Copy 헤더 추가 (예: 터널링):
before:
[IP][TCP][페이로드]
↑           ↑
data       tail

after:
[터널IP][IP][TCP][페이로드]
↑               ↑
data            tail
(data 포인터만 역방향 이동, 실제 메모리 복사 없음!)

4.3 Zero-Copy 효율성

sk_buff의 혁신적인 설계:

전통적인 방식 (데이터 복사):

void old_way_tunnel_packet(struct packet *pkt, char *tunnel_ip) {
    // 1. 터널 IP 헤더를 위한 새 버퍼 할당
    char *new_buffer = malloc(pkt->len + 20);  // 20 = IPv4 헤더

    // 2. 터널 IP 헤더 복사
    memcpy(new_buffer, tunnel_ip, 20);

    // 3. 기존 패킷 복사
    memcpy(new_buffer + 20, pkt->data, pkt->len);  // 💣 비싼 연산!

    // 4. 전송
    send_packet(new_buffer, pkt->len + 20);

    // 5. 메모리 해제
    free(pkt->data);
    free(new_buffer);
}

sk_buff의 Zero-Copy 방식:

void sk_buff_tunnel_packet(struct sk_buff *skb, char *tunnel_ip) {
    // 1. data 포인터를 역방향으로 이동 (IP 헤더를 위한 공간 확보)
    // 실제 메모리 복사 없음!
    if (skb_push(skb, 20) == NULL) {
        return -ENOMEM;  // 사전에 예약된 headroom 부족
    }

    // 2. 터널 IP 헤더만 쓰기 (빠른 memcpy)
    struct iphdr *new_iph = (struct iphdr *)skb->data;
    memcpy(new_iph, tunnel_ip, 20);  // 20바이트만 (원래 패이로드 건들지 않음)

    // 3. 전송 (skb 구조체만 전달)
    send_skb_to_nic(skb);

    // skb_buffer_free() 호출 시에만 메모리 해제
}

성능 차이:

패킷 크기: 1500 bytes (전형적인 이더넷 프레임)

전통적 방식:
  - 메모리 할당: 1520 bytes → ~100 CPU 사이클
  - memcpy(1500 bytes) → ~15,000 CPU 사이클 (L3 캐시에서)
  - 메모리 해제: ~50 사이클
  - 합계: ~15,150 사이클

Zero-Copy 방식:
  - 포인터 감소: ~1 사이클
  - memcpy(20 bytes) → ~200 사이클
  - 합계: ~201 사이클

성능 향상: 약 75배!

4.4 Netfilter Hook 모듈: IP 기반 필터링

특정 IP의 패킷을 DROP하는 방화벽:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/inet.h>  // inet_ntoa 등

// 차단할 IP 주소
static char *blocked_ip = "192.168.1.100";
module_param(blocked_ip, charp, S_IRUGO);

static __be32 blocked_ip_addr = 0;

// Hook 함수: PRE_ROUTING에서 호출됨
static unsigned int hook_func(void *priv,
                             struct sk_buff *skb,
                             const struct nf_hook_state *state) {
    // 1. 패킷 유효성 확인
    if (!skb) {
        return NF_ACCEPT;
    }

    // 2. IP 헤더 추출
    // skb->network_header는 이전에 설정됨 (L3 처리 시)
    struct iphdr *iph = ip_hdr(skb);
    if (!iph) {
        return NF_ACCEPT;  // IP 헤더 없음 (IPv6 또는 기타)
    }

    // 3. 목적지 IP 주소 확인
    if (iph->daddr == blocked_ip_addr) {
        // 차단할 IP에서 수신한 패킷
        printk(KERN_WARNING "Dropping packet from %pI4\n", &iph->saddr);

        // 4. 패킷 폐기
        // 절대 kfree_skb(skb)를 호출하지 말 것!
        // Netfilter가 NF_DROP 후 자동으로 처리
        return NF_DROP;
    }

    // 5. 다른 패킷은 계속 진행
    return NF_ACCEPT;
}

// Hook 등록
static struct nf_hook_ops nfho = {
    .hook = hook_func,
    .hooknum = NF_INET_PRE_ROUTING,  // PRE_ROUTING 지점
    .pf = NFPROTO_IPV4,              // IPv4만
    .priority = NF_IP_PRI_FIRST,     // 가장 먼저 실행
};

static int __init firewall_init(void) {
    // 1. 차단 IP 주소를 네트워크 바이트 순서로 변환
    in4_pton(blocked_ip, -1, (u8 *)&blocked_ip_addr, '\0', NULL);

    printk(KERN_INFO "Firewall: Blocking IP %pI4\n", &blocked_ip_addr);

    // 2. Hook 등록
    nf_register_net_hook(&init_net, &nfho);

    return 0;
}

static void __exit firewall_exit(void) {
    nf_unregister_net_hook(&init_net, &nfho);
    printk(KERN_INFO "Firewall: Unloaded\n");
}

module_init(firewall_init);
module_exit(firewall_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Network Developer");
MODULE_DESCRIPTION("Simple IP-based Firewall");

사용:

$ sudo insmod firewall.ko blocked_ip="10.0.0.1"
$ sudo dmesg | tail
[  1234.567890] Firewall: Blocking IP 10.0.0.1

# 10.0.0.1에서 ping
$ ping 10.0.0.1
# 응답 없음 (차단됨)

$ sudo rmmod firewall
$ sudo dmesg | tail
[  1235.123456] Firewall: Unloaded

4.5 Kernel-Space vs User-Space 처리

User-Space 프록시 (전통적 방식):

// Nginx 같은 리버스 프록시
while (1) {
    // 1. 클라이언트 요청 대기
    accept_connection(server_socket, &client);

    // 2. 요청 데이터 읽기
    recv(client, request_buf, 4096);

    // 3. 요청 분석 및 수정
    if (is_malicious(request_buf)) {
        send_error(client);
    }

    // 4. 백엔드로 전달
    send(backend_socket, request_buf, len);

    // 5. 응답 수신
    recv(backend_socket, response_buf, 4096);

    // 6. 클라이언트로 전송
    send(client, response_buf, len);

    close(client);
}

성능 문제:

클라이언트 → 프록시 → 백엔드 → 프록시 → 클라이언트

1회 왕복 비용:
  - 클라이언트: user-mode recv syscall → ~5 μs
  - 프로세스 스케줄링 (전락 발생 가능) → ~2-10 μs
  - 프록시: 애플리케이션 처리 → ~50-100 μs
  - 백엔드 connect/recv: 네트워크 왕복 → ~100-1000 μs
  - 총: ~150-1100 μs per request

초당 처리: ~1000-6500 req/s (고급 최적화 후)

Kernel-Space 필터링 (Netfilter):

// Linux Netfilter Hook
static unsigned int hook_func(void *priv, struct sk_buff *skb,
                             const struct nf_hook_state *state) {
    struct iphdr *iph = ip_hdr(skb);
    struct tcphdr *tcph = tcp_hdr(skb);

    // 1. TCP 포트 확인 (메모리 접근만 함)
    if (tcph->dest == htons(80)) {
        // 2. Payload 검사 (여전히 커널 메모리에서)
        unsigned char *payload = (unsigned char *)tcph + (tcph->doff * 4);

        // 3. 패턴 매칭 (매우 빠름, 캐시 친화적)
        if (malware_pattern_found(payload, skb->len)) {
            return NF_DROP;  // 즉시 폐기
        }
    }

    return NF_ACCEPT;
}

성능 비교:

Kernel-Space 필터링 비용:
  - IP/TCP 헤더 접근: ~50 ns (L1 캐시)
  - 패턴 매칭: ~200 ns
  - NF_DROP 결정: ~10 ns
  - 합계: ~260 ns per packet

User-Space 프록시:
  - 동일 패킷 처리: ~10,000-100,000 ns

성능 향상: 약 40-400배!

4.6 Context Switch 최소화의 이점

상황: 방화벽이 초당 100,000개 패킷 처리

User-Space 접근 (프로세스마다 처리):

100,000 packets/s
─────────────────────── = 100 프로세스 필요 (각 1000 packets/s 처리)
1,000 packets/s/process

Context Switch 비용:
- 100개 프로세스 간 전환: 99 switches/s (매우 낮음)
- 하지만 패킷당 평균 5-10 syscall 필요
- 500,000-1,000,000 context switches/s!

각 전환: ~5 μs
총 비용: 500,000 * 5 μs = 2.5초!
(1초에 처리할 수 있는 시간이 2.5배 소모)

Kernel-Space 필터링:

100,000 packets/s
- Context Switch 0회! (Ring 0에서 처리)
- 모든 처리가 interrupt handler 또는 softirq context에서
- CPU 사이클만 소비: 100,000 * 260 ns = 26 ms

1초 = 1,000,000,000 ns
네트워크 처리 시간: 26 ms
남은 시간: 974 ms (97.4% CPU 여유)

→ 다른 작업 충분히 처리 가능!

4.7 sk_buff 수정과 포워딩

실제로 패킷을 수정하는 경우:

static unsigned int modify_hook(void *priv, struct sk_buff *skb,
                               const struct nf_hook_state *state) {
    struct iphdr *iph = ip_hdr(skb);
    struct tcphdr *tcph = tcp_hdr(skb);

    // 1. 쓰기 가능한지 확인 (다른 skb에서 공유 중이면 복사)
    if (skb_cloned(skb)) {
        // 데이터 공유 → 복사 필요
        struct sk_buff *new_skb = skb_copy(skb, GFP_ATOMIC);
        if (!new_skb) {
            return NF_DROP;
        }
        consume_skb(skb);
        skb = new_skb;
    }

    // 2. 출발지 IP 변경 (SNAT)
    if (iph->saddr == htonl(0xc0a80101)) {  // 192.168.1.1
        // 새 주소로 변경
        iph->saddr = htonl(0xc0a80102);     // 192.168.1.2

        // 3. 체크섬 재계산 (중요!)
        ip_send_check(iph);

        // TCP 체크섬도 다시 계산 필요
        tcph->check = 0;
        tcph->check = tcp_v4_check(skb->len - (iph->ihl * 4),
                                   iph->saddr, iph->daddr,
                                   csum_partial(tcph, skb->len, 0));
    }

    // 4. 수정된 패킷 전달
    return NF_ACCEPT;
}

4.8 Key Takeaways

  • Netfilter Hooks: 5개 지점에서 패킷을 가로채고 처리
  • sk_buff: 패킷을 메타데이터와 함께 관리, Zero-Copy 헤더 조작 지원
  • Kernel-Space 필터링: User-Space 프로세스 대비 40-400배 빠름 (Context Switch 제거)
  • Zero-Copy: 포인터 조작으로 데이터 복사 최소화
  • 성능 관점: 네트워크 대역폭 다항 처리 시 커널 레벨 처리 필수

전체 요약 및 학습 로드맵

학습 체계화

Level 1: 아키텍처 이해 (이 문서 - 챕터 1) - ✅ CPU Ring과 메모리 격리의 필요성 이해 - ✅ System Call의 하드웨어 메커니즘 파악 - ✅ Context Switching의 성능 영향 인식 - ✅ Docker Container와 커널의 관계 구분

Level 2: 동적 커널 확장 (챕터 2) - ✅ LKM의 생명주기와 module_init/exit 이해 - ✅ EXPORT_SYMBOL을 통한 모듈 간 통신 - ✅ insmod/rmmod의 내부 동작 파악 - ✅ 실제 드라이버 개발의 기초

Level 3: I/O 추상화 (챕터 3) - ✅ VFS를 통한 파일 인터페이스의 힘 이해 - ✅ copy_to/from_user의 필요성과 원리 - ✅ 문자 디바이스 드라이버 개발 능력 - ✅ 메모리 보안과 사용자-커널 데이터 이동

Level 4: 고성능 네트워킹 (챕터 4) - ✅ Netfilter 아키텍처의 5개 Hook 이해 - ✅ sk_buff의 Zero-Copy 설계 원리 - ✅ Kernel vs User-Space 처리의 성능 차이 정량화 - ✅ 실제 방화벽/프록시 구현 기초

실전 응용 방안

  1. 커널 모니터링: eBPF + Netfilter를 조합한 실시간 패킷 감시
  2. 고성능 프록시: User-Space 네트워킹 (DPDK, AF_XDP)과 커널 통합
  3. 보안 강화: LSM (Linux Security Module) 개발
  4. 성능 최적화: 시스템 콜 감소, 페이지 폴트 최소화, 캐시 친화성 향상
  5. 가상화 개선: Namespace/Cgroup 확장, KVM 최적화

부록: 자주 묻는 질문

Q1: Docker Container가 진짜 격리되어 있나요? A: Namespace로는 격리되지만, **커널을 공유**합니다. 따라서: - 커널 취약점은 모든 컨테이너에 영향 - Kernel bypass (seccomp)로 추가 보안 가능 - 완전한 보안을 원하면 VM 사용 필요

Q2: System Call이 왜 그렇게 비싼가요? A: Context Switch 때문: - TLB flush (Translation Lookaside Buffer) - CPU 캐시 재구성 - 레지스터 저장/복원 - 페이지 테이블 전환

Q3: Kernel Module vs built-in code의 차이는? A: 성능/기능상 **동일**하지만, LKM은: - 런타임 로드/언로드 가능 - 커널 재부팅 불필요 - 개발 사이클 단축 - 메모리 절약 (필요할 때만 로드)

Q4: Netfilter가 진짜 빠른가요? A: 측정 기반: - L2 캐시 타중률: ~95% (패킷 헤더는 작음) - 패킷당 ~260 ns (확인됨) - Context Switch 없음 (0 비용) - 초당 ~380만 packets (현대 CPU 기준)


추천 실습

  1. LKM 개발

    cd /path/to/kernel-source
    make menuconfig
    make -j$(nproc)
    sudo insmod hello_world.ko
    dmesg | tail
    

  2. 문자 디바이스 드라이버

    make
    sudo insmod my_chardev.ko
    sudo mknod /dev/mydev c <major_num> 0
    echo "test" > /dev/mydev
    cat /dev/mydev
    

  3. Netfilter 방화벽

    sudo insmod firewall.ko target_ip="10.0.0.1"
    ping 10.0.0.1  # 응답 없음 (차단됨)
    sudo rmmod firewall
    

  4. 성능 벤치마크

    # System Call 오버헤드 측정
    perf stat -e context-switches,cache-misses sleep 1
    
    # 패킷 처리 성능
    tcpdump -i eth0 -w /tmp/packets.pcap
    


문서 작성자 노트: 이 자료는 리눅스 커널의 핵심 개념을 **실무 관점**에서 설명하도록 작성되었습니다. 이론만이 아닌 "왜 이렇게 설계되었고, 실제로 어떻게 작동하며, 어떤 성능 영향이 있는지"에 초점을 맞췄습니다. 각 챕터의 코드 예제는 실제 커널 소스에서 단순화되었으므로, 깊이 있는 학습을 원한다면 다음을 참고하세요:

  • 커널 소스: https://github.com/torvalds/linux
  • Linux Kernel Labs: linux-kernel-labs.github.io
  • LWN.net: 커널 개발 뉴스 및 심층 분석
  • Linux Foundation Documentation: kernel.org/doc

마지막 조언

리눅스 커널 개발은 **시스템의 모든 계층을 이해**하도록 강제합니다. 하드웨어, CPU 아키텍처, 메모리 관리, I/O 처리, 네트워킹까지—모두 연결되어 있습니다. 이 지식은:

  1. 일반 애플리케이션 개발을 더 깊이 있게 합니다
  2. 성능 병목을 직관적으로 파악하게 합니다
  3. **시스템 설계의 trade-off**를 이해하게 합니다
  4. 보안 취약점을 사전에 예방하게 합니다

꾸준한 학습과 실험을 통해, 당신도 **리눅스 시스템의 마스터**가 될 수 있습니다. 화이팅!