본문 바로가기

Programming/Linux_Kernel

per-CPU 변수 (dual-core)

원문 : http://nix102guri.blog.me/90098904482



 

리눅스 2.6에 오면서 생긴 기능 중에 하나가 CPU별 변수 선언(per-CPU variable)이다.

CPU별 변수는 여러가지 면에서 상당히 좋은 기능을 제공한다.

우선, SMP 환경에서 자기 CPU에 해당하는 변수만 접근하게 된다면 동기화에 대해서 고려해야 되는 부분이 줄어들게 된다. 따라서, 전체적인 성능을 높일 수 있게 된다. 각각이 자신의 Processor와 연관된 변수만 사용한다면 캐쉬 효율성이 좋아지게 될 것이다. 이러저러한 이유로 되도록이면 CPU별 변수를 사용하는 것이 좋다.

CPU별 변수를 작성하고 사용하는 방법은 두가지가 있다. 컴파일시에 생성되도록 하는 방법과 런타임시에 생성하는 방법이 있다. CPU별 변수는 어찌되면 일종의 배열과 유사한 형태라고 보면 된다. 하지만, 컴파일러 지시어를 사용해서 공간을 만든다. 내부 구성이야 어찌됐든 겉으로 보기에는 배열과 같다고 보면 된다.

컴파일시 CPU별 변수 선언 방법

#include <linux/percpu.h>

DEFINE_PER_CPU(변 수타입, 변수명);

예를 들어,

DEFINE_PER_CPU(int, my_var);
DEFINE_PER_CPU(struct task_struct, my_struct);

첫번째 것은 int 타입의 변수 my_var를 cpu별로 선언한다. 즉, 내부적으로 배열형태로 공간을 확보한다.
두번째 예는 task_struct 구조체 형태의 cpu별 변수 my_struct 변수를 선언한 얘이다.

이것을 사용하기 위해서는 per_cpu() 매크로를 사용하면 된다.

per_cpu(my_var, smp_processor_id()) = 100;
per_cpu(my_var, smp_processor_id()) += 10;

위의 예는 위에서 선언한 my_var 변수 내용 중 현재 CPU에 해당하는 위치(smp_processor_id()는 현재 Processor의 번호를 반환한다)에 100의 값을 넣고 있다. 그 다음 문장은 그 값에 10을 더하고 있는 코드이다.

CPU별 변수를 사용할 때 자신의 CPU에 대한 공간만을 접근하는 위와 같은 코드의 경우에 CPU간의 동기화에 대해서는 걱정할 필요가 없어진다. 하지만, 하나의 CPU내에서 재진입이 되는 커널 선점의 경우에는 추가적인 작업이 필요하다. 인터럽트 핸들러와의 동기화가 문제라면 인터럽트 금지에 해당하는 처리를 해야 할 것이다. 만약, 2.6에 새로이 추가된 커널 선점 기능이 문제가 된다면 cpu별 변수에 대한 매크로중 get_cpu_var()와 put_cpu_var()를 사용하면 된다. 위의 코드에 대해서 커널 선점 금지 처리를 함께 한다면, 다음과 같이 하면 된다.

get_cpu_var(my_var)++;
put_cpu_var(my_var);

get_cpu_var() 는 현재 코드가 수행중인 processor에 대한 per_cpu() 호출인데, 커널 선점 금지를 함께 해주는 것이다. 따라서, 사용하고 나서는 put_cpu_var()를 통해서 커널 선점 허용을 해주어야 한다.

런타임시  CPU별 변수 선언 방법

void *alloc_percpu(변수타입);
void free_percpu(const void *);

런타임시에 할당되는 것은 해당 type에 대해서 CPU별로 공간을 확보한 후 시작주소를 반환한다. 이것을 사용하기 위해서는 컴파일시 변수 사용 방법과 유사하지만, 다른 이름으로 정의되어 있다.

커 널 선점을 고려하지 않을 경우에는 다음 매크로를 사용하면 된다.

per_cpu_ptr(void *ptr, int cpu);

이것은 주어진 Processor 번호에 해당하는 변수에 대한 포인터를 반환한다.

커널 선점을 고려한다면, get[put]_cpu_var() 매크로에 대응되는 다음 매크로들을 사용하면 된다.

get_cpu_ptr(ptr)
put_cpu_ptr(ptr)

이 를 통해 런타임시에도  CPU별 변수를 선언 및 사용할 수 있다.

컴파일시에 선언된 변수의 경우 커널의 다른 코드(모듈)에서 사용할 수 있도록 하기 위해 심볼을 export 시킬 수 있는데, 변수의 유형이 특이하다 보니, 심볼 export를 위한 매크로도 다른 걸 사용한다. EXPORT_SYMBOL 대신에 다음 것을 사용하여 export 시킨다.

EXPORT_PER_CPU_SYMBOL(변 수명);
EXPORT_PER_CPU_SYMBOL_GPL(변수명);

아래 형태는 GPL 라이센스를 따르는 코드에서만 참조할 수 있는 형태로 export 하겠다는 것이다.
이렇게 export 된 내용을 사용할 경우에도 단순히 extern 하는 것으로는 해결이 안되며 다음 매크로를 사용해서 참조를 선언해야 한다.

DECLARE_PER_CPU(변 수타입, 변수이름);

다음 글(언제써질지는 모르겠지만 =_=;;;)에 커널 쓰레드에 대한 내용을 간단히 살펴본 후 CPU별 커널 쓰레드를 만들면서 간단히 CPU별 변수를 살펴볼 것이다.


구현 부분 살펴보기

SMP 환경일 경우 DEFINE_PER_CPU는 다음과 같이 선언되어 있다.
12
 /* Separate out the type, so (int[3], foo) works. */
13 #define DEFINE_PER_CPU(typename) \
14     __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

컴파일러 옵션에 대해서는 좀더 자세한 조사를 필요할 듯 하지만, 일단 내부적으로는 우리가 호출하는 변수 이름과는 다르게 per_cpu__ 라는 말이 앞에 붙고 .data.percpu 섹션에 공간이 할당되는 것 같다.

SMP 가 아닌 경우에는 단순히 다음과 같이 하나의 변수만 이름을 바꾸어서 선언한다.

33 #define DEFINE_PER_CPU(typename) \
34     __typeof__(type) per_cpu__##name

실 제 내용을 참조하는 함수중 per_cpu()는 다음과 같은 매크로로 구현되어 있다.
16 /* var is in discarded region: offset to particular copy we want */
17 #define per_cpu(var, cpu) (*({                          \
18         extern int simple_identifier_##var(void);       \
19         RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]); }))
20 #define __get_cpu_var(var) per_cpu(var, smp_processor_id())

주 어진 변수의 처음 위치부터 각각 cpu별로 공간이 할당되어 있으며, 그 중 지정한 cpu 번호에 해당하는 정보를 사용할 수 있도록 해준다. __per_cpu_offset[] 변수와 관련된 정보의 초기값은 커널 부팅시 호출되는 init/main.c 에서 확인할 수 있다.

360 static void __init setup_per_cpu_areas(void)
361 {
362         unsigned long sizei;
363         char *ptr;

374         for_each_possible_cpu(i) {
375                 __per_cpu_offset[i] = ptr - __per_cpu_start;
376                 memcpy(ptr, __per_cpu_start__per_cpu_end - __per_cpu_start);
377                 ptr += size;
378         }

CPU별 변수들을 위한 공간은 컴파일 시점에 잡히지 않고 섹션만 할당된 후 런타임시에 각 변수별로 필요한 공간을 확보하고 있다. 즉, 컴파일시 변수 형태로 작성했더라도 실제 공간 확보는 CPU갯수에 해당하는 만큼만 할당된다는 것을 알 수 있다. 이렇게 할당된 공간에 대해서 cpu 번호를 통해 특정 위치의 값을 사용할 수 있게 된다.

동적으로 할당되는 공간의 경우 <linux/percpu.h>에서 확인이 가능하다.

77 static __always_inline void *__percpu_alloc_mask(size_t sizegfp_t gfp, cpumask_t *mask)
78 {
79         return kzalloc(size, gfp);
80 }
81 
82 static inline void percpu_free(void *__pdata)
83 {
84         kfree(__pdata);
85 }
86 
87 #endif /* CONFIG_SMP */
88 
89 #define percpu_populate_mask(__pdata, size, gfp, mask) \
90         __percpu_populate_mask((__pdata), (size), (gfp), &(mask))
91 #define percpu_depopulate_mask(__pdata, mask) \
92         __percpu_depopulate_mask((__pdata), &(mask))
93 #define percpu_alloc_mask(size, gfp, mask) \
94         __percpu_alloc_mask((size), (gfp), &(mask))
95 
96 #define percpu_alloc(size, gfp) percpu_alloc_mask((size), (gfp), cpu_online_map)
97 
98 /* (legacy) interface for use without CPU hotplug handling */
99 
100 #define __alloc_percpu(size)    percpu_alloc_mask((size), GFP_KERNEL, \
101                                                   cpu_possible_map)
102 #define alloc_percpu(type)      (type *)__alloc_percpu(sizeof(type))
103 #define free_percpu(ptr)        percpu_free((ptr))
104 #define per_cpu_ptr(ptr, cpu)   percpu_ptr((ptr), (cpu))
105 
106 #endif /* __LINUX_PERCPU_H */

결 국, alloc_percpu()는 내부적으로 kzalloc() 즉, kmalloc()류의 함수를 사용하여 공간을 확보하는 것을 확인할 수 있다. CPU별 변수는 결국 내부적으로 배열처럼 공간이 확보되는 것이지만, 정확히 CPU갯수만큼만 확보될 수 있어 배열보다 효율적이며 체계적으로 사용할 수 있게 해준다.

마지막으로 get[put]_cpu_var() 매크로는 <linux/percpu.h>에서 확인할 수 있다.

21 #define get_cpu_var(var) (*({                           \
22         extern int simple_identifier_##var(void);       \
23         preempt_disable();                              \
24         &__get_cpu_var(var); }))
25 #define put_cpu_var(var) preempt_enable()

내 부적으로 커널 선점 금지, 커널 선점 허용을 위한 코드가 들어 있는 것을 볼 수 있다. 특히, put_cpu_var()의 경우에는 인수로 전달된 변수는 전혀 사용하지 않는 것을 확인할 수 있다.

-------------------------------------------------------------------------------
간략히 정리하자면 다음과 같음

선언

#include <linux/percpu.h>

DEFINE_PER_CPU(변 수타입, 변수명);

DEFINE_PER_CPU(int[10], 변수명); 요런 형태~

 

사용

per_cpu(변수명, smp_processor_id())++;

per_cpu(변수명, smp_processor_id())+=10;

per_cpu(변수명, smp_processor_id).a = 10;

__get_cpu_var(변수명)
  -> cpu id를 smp_processor_id 호출로 가져오지 않고 미리 설정된 CPU offset을 통해 가져와서 약간 더 빠르며
      x86 등에서는 최적화되어 구현됨
__raw_get_cpu_var(변수명) 
  -> __get_cpu_var 와 같으나 SMP 일반적인 형태로 구현됨

get_cpu_var(my_var)++;  --> preemtion disable
put_cpu_var(my_var);    --> preemtion enable
두 함수가 pair로 사용

 

다이나믹하게 선언하기

void *alloc_percpu(변수타입);
void free_percpu(const void *);

get_cpu_ptr(ptr);
put_cpu_ptr(ptr);

 

export 하기

DECLARE_PER_CPU(변 수타입, 변수이름);

 

커널 모듈에서 심볼 export

 EXPORT_PER_CPU_SYMBOL(변수명);
 EXPORT_PER_CPU_SYMBOL_GPL(변수명);

그외 자주 사용하는 코드 예제

1. 선언

  DEFINE_PER_CPU(uint32_t, ipsec_bridge_packets);

2. 변수 사용

  per_cpu(ipsec_bridge_packets, smp_processor_id())++;
  __get_cpu_var(ipsec_bridge_packets)++;

3. 카운터 값 출력

  uint32_t sum_ipsec_bridge_packets = 0;

  int cpu;

  for_each_online_cpu(cpu) {
    sum_ipsec_bridge_packets += per_cpu(ipsec_bridge_packets, cpu);
  }

  printk("%d\n", sum_ipsec_bridge_packets );

[출처] per-CPU 변수|작성자 열이