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

per-cpu 변수를 정적으로 만들기

DEFINE_PER_CPU와 per-cpu 섹션

가장 기본적으로 알아야될게 per-cpu 변수를 어떻게 저장하는지겠지요. 일단 DEFINE_PER_CPU(type, name) 매크로는 type 타입으로 name라는 이름의 per-cpu변수를 정적으로 만드는 매크로입니다. 정적으로 만든다는게 중요합니다. 즉 컴파일타임에 변수가 만들어진다는 것입니다. 소스를 한번 보겠습니다.

#define DEFINE_PER_CPU(type, name) \
    __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

C코드가 아닌 것들이 나옵니다. __로 시작해서 __로 끝나는 지시어들은 대부분 gcc가 제공하는 기능들입니다. 물론 구글로 검색하면 gcc 메뉴얼에서 해당 페이지를 찾아줍니다만 간단하게 설명해보면 이렇습니다.

  • __attribute__: gcc에게 전달할 명령이 있다는걸 알려줍니다.
  • __section__: 다음에 나타날 변수를 어떤 섹션에 저장하라고 알려줍니다.
  • __typeof__: 다른 변수의 타입을 가져옵니다.

우선 섹션이라는게 뭘까요? 실행파일이나 object 파일이 저장될 때 ELF라는 포맷이 있습니다. 파일의 어디부터 어디까지 코드를 저장할지, 어디에 데이터를 저장할지, 어디에 파일의 헤더나 구조에 대한 정보를 저장할지 등등 미리 정해진 포맷이 있는거지요. 이때 파일이 각 구역을 섹션이라고 부릅니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[], char *envp[])
{
    int dd[3];
	__typeof__(dd) k;
	printf("%d %d\n", sizeof(k), sizeof(dd));
	return 0;
}

이렇게 __typeof__를 쓰는 예제를 한번 빌드해서 a.out을 만들어보겠습니다. 그리고 readelf -S 명령으로 어떤 섹션들이 있는지 한번 뽑아볼까요.

$ readelf -S a.out
There are 30 section headers, starting at offset 0x1a08:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000078  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400330  00000330
       000000000000005a  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040038a  0000038a
       000000000000000a  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400398  00000398
       0000000000000030  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             00000000004003c8  000003c8
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004003e0  000003e0
       0000000000000060  0000000000000018  AI       5    12     8
  [11] .init             PROGBITS         0000000000400440  00000440
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400460  00000460
       0000000000000050  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         00000000004004b0  000004b0
       00000000000001c2  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000400674  00000674
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000400680  00000680
       000000000000000b  0000000000000000   A       0     0     4
  [16] .eh_frame_hdr     PROGBITS         000000000040068c  0000068c
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         00000000004006c0  000006c0
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000038  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601038  00001038
       0000000000000010  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000601048  00001048
       0000000000000008  0000000000000000  WA       0     0     1
  [26] .comment          PROGBITS         0000000000000000  00001048
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  00001075
       0000000000000108  0000000000000000           0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00001180
       0000000000000630  0000000000000018          29    45     8
  [29] .strtab           STRTAB           0000000000000000  000017b0
       0000000000000251  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

파일안에 어떤 섹션이 어느 위치에 얼마 크기로 있는지가 출력됩니다. C 프로그래밍을 좀 오래해본 분들은 .data나 .bss, .text 섹션은 들어보셨을겁니다. 각 섹션마다 역할이 있겠지만 __attribute__ ((__section__ ("섹션이름"))) 을 쓰면 내가 만든 변수를 특정한 섹션에 강제로 저장시킬 수 있습니다.

그럼 커널에는 어떤 섹션들이 있나 볼까요. 커널에서 섹션을 만드는 파일은 arch/x86_64/kernel/vmlinux.lds.S 에 있습니다. 파일을 열어보면 아래와같이 .data.percpu 섹션을 만든게 있습니다.

125   __per_cpu_start = .;
126   .data.percpu  : { *(.data.percpu) }
127   __per_cpu_end = .;
128   . = ALIGN(4096);

lds파일은 링커에게 어떤 섹션을 어떻게 만들지 등을 알려주는 파일입니다. 그리고 .data.percpu 섹션 이름 위아래에 __per_cpu_start와 __per_cpu_end가 있는데 각각 섹션의 시작 위치와 끝 위치를 저장한 변수입니다. 나중에 커널 코드에서 변수처럼 사용할 것입니다.

결국 링커를 이용해서 .data.percpu라는 섹션을 커널 이미지의 특정한 부분에 만들어놓고, 컴파일러는 DEFINE_PER_CPU 매크로로 정의한 변수들을 섹션에 저장한다는 것입니다.

그럼 __typeof__가 뭔지는 알아보기위해 a.c 파일을 한번 빌드해서 실행해보겠습니다.

$ ./a.out
12 12

dd 변수는 int[3] 타입인데 k도 int[3] 타입이 된것 입니다. 타입을 복사하되 단순히 int 타입이라는것만 복사하는게 아니라 배열의 크기까지도 복사하기 때문에 per-cpu 변수로 배열을 쓰건 포인터를 쓰건 상관없이 똑같은 변수를 프로세서 갯수만큼 여러개 만들 수 있는 것입니다.

일단 DEFINE_PER_CPU는 .data.percpu 섹션에 변수를 하나 저장하는 일을 합니다. 그러면 프로세서 갯수만큼 변수를 여러개 만드는건 어디서 하는 걸까요?

그건 setup_per_cpu_areas() 함수를 보면 알 수 있습니다.

.data.percpu섹션의 크기는 위에 링커 스크립트에서 본대로 __per_cpu_end - __per_cpu_start 값이 됩니다. alloc_bootmem 함수로 섹션 크기만큼 메모리를 할당하고 .data.percpu 섹션을 복사합니다. cpu_pda[].data_offset에는 .data.percpu 섹션과 새로 할당된 메모리의 offset을 저장하는데, 나중에 cpu 번호를 알면 cpu_pda[cpu번호].data_offset 값과 변수의 포인터를 가지고 per-cpu 변수의 위치를 계산하게 됩니다.

예를 들어 .data.percpu 섹션의 시작 위치가 0x1000 이고, 크기가 0x1000이라고 가정해봅시다. 각 cpu별 per-cpu 변수가 저장될 메모리가 alloc_bootmeme으로 다음과 같이 할당되었다고하면,

cpu0 -> 0x2000

cpu1 -> 0x3000

cpu2 -> 0x4000

cpu3 -> 0x5000

각 프로세서의 data_offset은 다음고 같이 됩니다.

cpu[0].data_offset = 0x1000

cpu[1].data_offset = 0x2000

cpu[2].data_offset = 0x3000

cpu[3].data_offset = 0x4000

그러면 만약 foo라는 per-cpu변수의 원래 위치가 0x1010 이었다면, cpu0이 사용할 per_cpu_foo 변수의 위치는 0x1010 + 0x1000 = 0x2010 이 되고, cpu1은 0x3010 등이 될 것입니다.

이렇게 DEFINE_PER_CPU는 링커와 컴파일러의 기능을 사용해서 간단하게 프로세서별 메모리 영역을 만들어놓습니다.

댓글

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