linux kernel

들어가며

kernel 환경 구축은 처음이라서 일단 드림핵으로 basics 강의를 수강하였다.

[주버전][부버전][개정판 번호]
ex) 6.6.30 

커널 버전은 위와 같이 구분한다. 사실 이러한 구분보다는 LTS/Stable 구분이 더 중요하다.

LTS는 장기간의 지원을 제공하는 커널 버전이다. 이보다는 더 빠르게 기능이 추가되는 커널이 Stable이다. 대신 지원 기간은 더 짧다.

또한, 리눅스 커널은 시스템 아키텍처마다 동작 방식이 약간씩 다르다. 소스 안 arch 디렉토리를 들어가면 각 아키텍처의 코드가 포함되어 있다. x86부터 arm, MIPS 등 hexagon 같은 이외 아키텍처도 많다.

Background

Kernel Space & User Space

리눅스에서 메모리는 위 두 가지 스페이스로 구분하여 관리한다. 시스템의 안전성과 보안을 위함이다. 각 영역은 접근 권한이 다르며 user가 kernel space에 있는 리소스를 사용하기 위해서는 kernel에 요청을 해야하는데, 이때 이 요청을 system cal 이라고 한다.

x86 기준, syscall 어셈블리 명령어로 요청을 수행한다.

  1. 시스템 콜 호출
  2. CPU는 커널 모드로 전환
  3. 시스템 콜의 진입점, entry_SYSCALL_64() 함수 진입
  4. 시스템 콜 테이블인 sys_call_table 로부터 유저가 요청한 시스템 콜 호출

task

리눅스의 응용 프로그램은 fork()를 호출해서 프로세스를 생성하고, pthread_create()를 호출해서 스레드를 생성한다.

  1. 프로세스는 독립된 메모리 영역(코드, 데이터, 힙, 스택)을 가짐
  2. 스레드는 스택 제외 메모리 영역 공유

다만, 커널 입장에서는 이 프로세스, 스레드를 구분하지 않고 모두 태스크로 정의한다. 응용 프로그램에서는 fork, pthread_create로 구분되지만, 커널 단으로 들어오면 모두 copy_process() 함수를 호출한다.

그리고 이 태스크가 생성되면 task_struct 구조체가 할당된다.

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
        /*
         * For reasons of header soup (see current_thread_info()), this
         * must be the first element of task_struct.
         */
        struct thread_info              thread_info;
#endif
        unsigned int                    __state;
        /* saved state for "spinlock sleepers" */
        unsigned int                    saved_state;
        /*
         * This begins the randomizable portion of task_struct. Only
         * scheduling-critical items should be added above here.
         */
        randomized_struct_fields_start
        void                            *stack;
        refcount_t                      usage;
        /* Per task flags (PF_*), defined further below: */
        unsigned int                    flags;
        unsigned int                    ptrace;
#ifdef CONFIG_SMP
        int                             on_cpu;
        struct __call_single_node       wake_entry;
        unsigned int                    wakee_flips;
        unsigned long                   wakee_flip_decay_ts;
        struct task_struct              *last_wakee;
        ...

구조체 구성 요소

Memory Architecture

task_struct의 멤버 mm은 메모리 아키텍처의 주소들을 담고 있다.

struct mm_struct {
        struct {
                /*
                 * Fields which are often written to are placed in a separate
                 * cache line.
                 */
                ...
                unsigned long start_code, end_code, start_data, end_data;
                unsigned long start_brk, brk, start_stack;
                unsigned long arg_start, arg_end, env_start, env_end;
                ...
        } __randomize_layout;
        /*
         * The mm_cpumask needs to be at the end of mm_struct, because it
         * is dynamically sized based on nr_cpu_ids.
         */
        unsigned long cpu_bitmap[];
};

위 코드를 보면 struct에 start, end로 코드 세그먼트, 데이터 세그먼트, 힙 세그먼트, 스택 세그먼트 등의 시작 주소와 끝 주소를 담고 있다.

user 영역의 메모리 구조는 프로세스마다 독립적이지만, kernel 영역은 모든 프로세스가 공유하는 영역이다.

주소 영역 설명
0x000000000000000000 ~ 0x00007ffffffffffff 유저 영역이 위치함, 코드 세그먼트, 스택 세그먼트 등이 존재
0xffff88800000000000 ~ 0xffffc87ffffffffff 물리적인 RAM을 나타내는 영역
0xffffc9000000000000 ~ 0x0000e8fffffffffff vmalloc 영역
0xffffffff80000000 ~ 0xffffffff9ffffffff 커널 코드가 위치한 .text 영역, 커널 이미지
0xffffffffa0000000 ~ 0xffffffffefffffff 커널 모듈이 위치한 영역

위 메모리 맵은 x86-64 아키텍처의 4-level page table에 해당한다. 커널 CONFIG_X86_5LEVEL 옵션에 따라 구조가 달라질 수는 있다.

마지막으로 Addr에 대해 짚고 넘어가자. 알다시피 시스템에서 주소란, 가상 주소(VA)와 물리 주소(PA)로 나뉜다.

가상 주소는 커널이 프로세스에게 제공하는 주소 공간이다. 각 프로세스의 메모리 독립성을 보장하기 위해 해당 주소 개념을 사용한다.

물리 주소의 경우, 실제 메모리 하드웨어의 주소를 나타낸다. 물리 주소 공간은 RAM의 실제 메모리에 직접 매핑이 된다.

  1. 커널과 하드웨어는 물리 주소로 메모리에 접근
  2. 그리고 데이터 읽고 쓰기 수행

때문에 앞서 설명했던 page table중에 RAM 영역에 위치한다.

가상 주소의 경우 MMU에 의해 물리 주소로 변환된다.

Slab Allocator

슬랩 할당자는 커널의 동적 메모리 할당 관리를 위해 고안된 개념이다. kmalloc()을 호출하면 슬랩 할당자로부터 메모리를 할당 받을 수 있고, kfree()를 호출하여 할당받은 메모리를 해제할 수 있다.

과거에는 SLAB, SLUB, SLOB으로 나뉘었으나 지금은 특수한 경우를 제외하곤 SLUB만 사용한다.

슬랩 할당자 주요 구성 요소

슬랩 캐시 : 슬랩 페이지의 집합 슬랩 페이지 : 동일한 크기의 슬랩 객체로 구성

# cat /proc/slabinfo | grep "kmalloc"
...
kmalloc-8k           450    502  16384    2    8 : tunables    0    0    0 : slabdata    251    251      0
kmalloc-4k          2597   3068   8192    4    8 : tunables    0    0    0 : slabdata    767    767      0
kmalloc-2k          2224   3968   4096    8    8 : tunables    0    0    0 : slabdata    496    496      0
kmalloc-1k          4921   8608   2048   16    8 : tunables    0    0    0 : slabdata    538    538      0
kmalloc-512        28209  44128   1024   32    8 : tunables    0    0    0 : slabdata   1379   1379      0
kmalloc-256        37684  87040    512   32    4 : tunables    0    0    0 : slabdata   2720   2720      0
kmalloc-192       150168 209824    256   32    2 : tunables    0    0    0 : slabdata   6557   6557      0
kmalloc-128        21693  23968    256   32    2 : tunables    0    0    0 : slabdata    749    749      0
kmalloc-96         60864  60864    128   32    1 : tunables    0    0    0 : slabdata   1902   1902      0
kmalloc-64        199168 199168    128   32    1 : tunables    0    0    0 : slabdata   6224   6224      0
kmalloc-32         17173  28544     64   64    1 : tunables    0    0    0 : slabdata    446    446      0
kmalloc-16         27226  46464     32  128    1 : tunables    0    0    0 : slabdata    363    363      0
kmalloc-8          31277  47022     40  102    1 : tunables    0    0    0 : slabdata    461    461      0

리눅스의 /proc/slabinfo를 확인해보면, 현재 시스템에서 사용 중인 슬랩 캐시 목록을 확인할 수 있다.