'Linux/API'에 해당되는 글 4건

  1. 2008/01/26 I/O Port 다루기 on x86
  2. 2008/01/25 인터럽트
  3. 2008/01/25 커널 시간 관리
  4. 2007/10/08 CPU별 변수 사용

앞에 GPIO에서도 잠깐 언급했듯이 하드웨어를 다루는 방법은 크게 두가지가 있다. 바로, 독립된 I/O 주소를 가지는 형태와 메모리로 매핑된 형태가 있다.

아키텍쳐마다 달라질 수 있는 부분인데, 앞에 GPIO에서 매모리 매핑된 형태를 살펴봤기 때문에 여기에서는 IO 포트를 사용하는 방법을 살펴 보도록 하겠다. 인텔 머신(x86)은 두가지 형태를 다 가지고 있는데, 비디오 메모리의 경우에는 메모리 매핑된 형태이고 시리얼, 프린터 등등은 IO 포트를 사용하는 형태이다.

IO 포트에 대한 연산을 하는 경우에는 다음 함수들을 사용해서 처리할 수 있다.

void outb(u8 v, u16 port);
u8 inb(u16 port);
void outw(u16 v, u16 port);
u16 inw(u16 port);
void outl(u32 v, u16 port);
u32 inl(u32 port);



port 인수는 접근하려는 하드웨어 장치에 대한 포트번호이다. in과 out은 각각 주고 받는 자료형의 종류에 따라서 b, w, l 등으로 이름이 붙어 있으므로 원하는 형태를 사용하면 된다.

해당 포트에 한 개 이상의 데이타를 보내야 하는 경우를 위해서 s 가 붙은 형태들도 제공한다.

void insb(u16 port, void *addr, unsigned long count);
void insw(u16 port, void *addr, unsigned long count);
void insl(u16 port, void *addr, unsigned long count);

void outsb(u16 port, void *addr, unsigned long count);
void outsw(u16 port, void *addr, unsigned long count);
void outsl(u16 port, void *addr, unsigned long count);



ins?() 함수의 경우에는 port 주소에서 값을 꺼내와 addr 위치에 저장하는데, 이것을 count 횟수만큼 반복한다. outs?() 함수는 addr위치부터의 내용을 count 횟수만큼 port 주소에 쓰기를 하는 함수이다.

리눅스에서 I/O 포트를 접근할 때에 무작정 접근하지 않고 대부분은 해당 포트를 점유한 후에 사용을 하게 된다. 물론, 이 규칙을 따르지 않아도 동작할 수 있지만, 좋은 접근 방법이 아니다. 반드시 사용하고자 하는 포트를 잡고 사용하고 다 사용한 후에는 포트를 반환하는 절차를 지켜주는 것이 좋다. 이와 관련한 함수는 다음과 같다.

#include <linux/ioport.h>

struct resource *request_region(unsigned long from, unsigned long extent,
                                              const char *name);
void release_region(unsigned long from, unsigned long extent);



이 함수를 사용해서 해당 포트를 점유했다고 해서 반드시 이것을 나 혼자 쓸수 있다고 장담해서는 안된다. 다른 드라이버가 위의 명령을 사용하지 않고 접근할 수 도 있기 때문이다. 하지만, 규칙을 따라야 다른 드라이버와 충돌을 줄일 수 있는 것은 사실이기 때문에, 꼭 이 명령들을 사용하도록 하자.

현재 등록되어 있는 IO 포트 영역은 /proc/ports 파일을 통해서 확인할 수 있다.

[root@localhost ~]# cat /proc/ioports
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-006f : keyboard
0070-0077 : rtc
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : 0000:00:07.1
  0170-0177 : libata
01f0-01f7 : 0000:00:07.1
  01f0-01f7 : libata
02f8-02ff : serial
0376-0376 : 0000:00:07.1
....




위 컴퓨터의 경우에는 0x60부터 0x6f까지를 키보드 관련한 드라이버에서 점유하고 있음을 나타낸다. 이런식으로 점유를 하고 있는 경우 다른 드라이버에서 request_region 했을 경우 NULL을 반환하게 된다.

스피커 제어 예제

여기에서는 간단히 스피커를 제어하는 예제를 살펴 보도록 하겠다. 내장 스피커를 켜거나 끄는 것은 0x61번 포트의 0, 1번 비트를 통해서 결정된다. 0, 1번 비트가 1이면 스피커가 켜지며, 0일 경우 스피커가 꺼진다. 스피커를 켜는것과 함께 출력될 소리의 높이도 결정해야 한다. 이것은 주파수를 통해서 결정되면 PIT(Programmable Interval Timer)에서 설정하게 된다.

Intel PIT(8253/8254 칩)는 3개의 채널로 구성되어 있으며, 0번 채널은 시스템 클럭 업데이트에 사용되고 채널 1은 DMA 컨트롤러 refresh용으로 사용된다. 주파수를 설정하는 것은 2번 채널을 사용한다. 각각은 다음과 같은 IO 포트를 할당받아 사용한다.

0x40 : 0번 채널, 시스템 클럭 업데이트
0x41 : 1번 채널, DMA 컨트럴 refresh
0x42 : 2번 채널, 주파수
0x43 : 컨트럴 포트

주파수를 설정하기 위해서는 2번 채널(0x42번 포트)을 사용해야 하며 이것은 0x43번 포트를 통해서 관련 설정을 먼저 해주어야 한다.

0x43 포트의 비트는 다음과 같은 구성을 가지고 있다.
7, 6 :  채널 선택
5, 4 :  자료 전달 순서(하위 바이트부터 보낼 것인지...)
3, 2, 1 : mode (011)
0 : 포맷 (BCD나 바이너리)

여기에서는 하위 바이트부터 보내는 사운드 제어를 위해서 0xB6을 사용하겠다.
데이터를 0x42번 포트에 넣을 때는 두 바이트를 한 바이트씩 집어 넣어야 한다. 또한, 주파수 값은 다음과 같은 공식으로 계산된 값을 넣어야 한다.

저장할 값(2바이트) = 1193180 / 주파수;

소리를 출력하는 코드와 제거하는 코드는 다음과 같은 형태가 될 것이다.

#define CLK_FREQ (1193180L)
#define PIO  (0x61)
#define PIT_CMD  (0x43)
#define PIT_DATA (0x42)
#define SETUP  (0xB6)
#define TONE_ON  (0x03)
#define TONE_OFF (0xFC)

void sound(int freq) {
 unsigned int value = inb(PIO);
 freq = CLK_FREQ / freq;
 if ((value & TONE_ON) == 0) {
  outb(value | TONE_ON, PIO);
  outb(SETUP, PIT_CMD);
 }
 outb(freq & 0xff, PIT_DATA);
 outb((freq >> 8) & 0xff, PIT_DATA);
}
void nosound(void) {
 unsigned int value = inb(PIO);
 value &= TONE_OFF;
 outb(value, PIO);
}



여기에서는 이 예제를 약간 응용해서 모르스 신호를 출력하는 예제를 만들어 보려고 한다. 모르스 코드 테이블은 구글에서 검색하면 쉽게 찾을 수 있다.

사용자 삽입 이미지

다음은 모르스 신호를 이용하는 간단한 드라이버이다. 이것은 x86에서만 수행된다는 점을 명심하고 보드에 올리는 실수를 하지 않기를 바란다. :)


코드를 테스트 하기 위해서 어플리케이션을 개발할 필요는 없다. 디바이스 노드를 만들고 나서(mknod 명령을 사용해서..), 다음처럼 테스트 하면 된다.

mknod /dev/my_morse c <디바이스 주번호> 0
echo SOS > /dev/my_morse

이 드라이버는 영어밖에 처리를 못한다. 한글 처리에 관심이 있다면 다음 테이블을 보고 직접 수정해 보는 것도 재미 있을 것이다.

사용자 삽입 이미지




크리에이티브 커먼즈 라이선스
Creative Commons License
이올린에 북마크하기(0) 이올린에 추천하기(0)

'Linux > API' 카테고리의 다른 글

I/O Port 다루기 on x86  (0) 2008/01/26
인터럽트  (0) 2008/01/25
커널 시간 관리  (0) 2008/01/25
CPU별 변수 사용  (0) 2007/10/08
Posted by Daniel Kwon
TAG IO

인터럽트

Linux/API 2008/01/25 17:04

디바이스 드라이버를 개발할 때 하드웨어에서 발생하는 정보를 얻기 위해서는 주기적으로 하드웨어를 검사(폴링)하거나 인터럽트를 사용해야 한다.

폴링은 불필요한 CPU자원을 계속 낭비할 수 있기 때문에 하드웨어에서 언제 정보가 발생하는지 알 수 없을 경우에는 인터럽트를 사용하는 것이 훨씬 효율적이다.

여기에서는 인터럽트 관련된 내용을 살펴 보도록 하겠다.

인터럽트는 CPU 아키텍쳐에 따라서 처리하는 방식이 많이 다르기 때문에 여기에서는 커널에서 인터럽트를 접근하는 측면에서 살펴보도록 하겠다.

일단, 현재 커널에 등록되어 있는 인터럽트 핸들러들의 리스트를 보기 위해서는 /proc/interrupts 파일을 살펴보면 된다.


인터럽트의 활성화/비활성화

경우에 따라서 인터럽트로부터의 방해를 받고 싶지 않을 수가 있다. 이러한 경우에는 인터럽트 금지 함수를 사용해서 인터럽트가 CPU로 전달되지 않도록 하면 된다. 관련 함수들은 다음과 같다.

#include <linux/interrupt.h>
#include <asm/irq.h>

void local_irq_disable(void);
void local_irq_enable(void);

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enabled_irq(int irq);


local_irq_disable(), local_irq_enable() 함수는 모든 IRQ 인터럽트를 막는 것이며, 아래 함수들은 특정 IRQ만 금지하는 명령이다. 이 함수들은 CPU에게 명령을 내려서 CPU가 처리하지 않도록 하는 것이기 때문에 다른 CPU에 인터럽트가 전달되는 것은 막을 수 없다.

인터럽트 핸들러 작성

리눅스에서는 인터럽트 디스크립터 테이블(IDP)를 직접 건드리지 않는다. IDP는 부팅시 초기화 된 이후에는 변경되지 않으며, 디바이스 드라이버 등에서는 리눅스에서 잡아놓은 환경위에서 동작하게 된다. 인터럽트 핸들러를 다루기 위해서는 다음 함수들을 사용한다.

#include <linux/interrupt.h>

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags,
                     const char *devname, void *dev_id);
void free_irq(unsigned int irq, void *dev_id);

인터럽트 핸들러를 특정 번호에 등록하기 위해서는 request_irq 함수를 사용하면 된다. 이때, irq 번호에 원하는 인터럽트 번호를 지정하고, handler에는 나중에 수행될 인터럽트 핸들러를 지정하면 된다. irqflags는 핸들러의 특성을 지정할 수 있다. devname은 /proc/interrupts에서 확인할 수 있는 핸들러의 이름이다.

인터럽트 핸들러 함수의 프로토타입은 2.4와 2.6에서 약간 차이가 있다. 다음은 2.4에서의 인터럽트 핸들러 함수 프로토타입니다.

void my_interrupt(int irq, void *dev_id, struct pt_regs *regs);

2.6에서는 먼저 인터럽트 핸들러에 리턴값을 반환하도록 변형되었는데, 2.6.19부터는 마지막 인수도 제거되었다. 따라서, 2.6.19 이후의 버전에서는 다음과 같은 형태가 된다.

irqreturn_t my_interrupt(int irq, void *dev_id);

irqreturn_t는 인터럽트 처리를 했는지 안했는지를 나타내는데, IRQ_NONE이나 IRQ_HANDLED를 사용하면 된다.




 

크리에이티브 커먼즈 라이선스
Creative Commons License
이올린에 북마크하기(0) 이올린에 추천하기(0)

'Linux > API' 카테고리의 다른 글

I/O Port 다루기 on x86  (0) 2008/01/26
인터럽트  (0) 2008/01/25
커널 시간 관리  (0) 2008/01/25
CPU별 변수 사용  (0) 2007/10/08
Posted by Daniel Kwon
TAG interrupt

커널 시간 관리

Linux/API 2008/01/25 14:32

커널에서 프로그래밍을 하다보면 시간 간격을 두고 작업을 처리해야 하는 경우가 발생한다.

시간은 절대시간과 상대시간 형태로 표현할 수 있는데, 커널에서는 주로 상대시간을 사용하여 작업 흐름을 제어한다. 여기에서는 상대적인 시간을 다루기 위해서 사용하는 몇가지 방법을 살펴보도록 하겠다.

jiffies

jiffies는 리눅스 커널에서 가장 기본이 되는 시간값으로서 특정 클럭 단위로 값이 1씩 증가하는 변수이다. jiffies는 <linux/jiffies.h>에 다음과 같이 선언되어 있다.

extern unsigned long volatile __jiffy_data jiffies

;

jiffies 값을 증가시키는 클럭의 횟수는 HZ 매크로로 정의되어 있다. HZ의 값은 아키텍쳐 또는 버전에 따라서 다르다. x86의 경우에 2.4버전에서는 100이었으며, 2.6 오면서 1000으로 증가되었다. 하지만, 현재는 1000보다는 작은 값을 사용하고 있는데, 커널 컴파일시 설정을 바꿀 수 도 있다.  HZ의 값은 1초에 발생하는 클럭틱의 수를 나타낸다. 따라서, x86용 2.4 커널의 경우 10밀리초에 한 번씩 클럭틱이 발생하며 jiffies값도 10밀리초마다 증가한다.

jiffies값은 증가형태로만 사용되기 때문에 일정 시간이 지나고 나면 오버플로우가 되고 다시 0부터 증가하게 된다.  HZ가 1000일 경우에 50일정도면 오버플로우가 발생한다. 따라서, jiffies를 사용할 때에는 오버플로우에 대한 고려를 해야 한다. 보통 jiffies를 사용하는 이유는 특정 시간 후 작업을 계속하기 위해서인데, 간단한 경우는 다음과 같이 작성할 수 도 있다.

my_times = jiffies + 5 * HZ;
while (my_times > jiffies)
    ;



이 경우 5초동안 잠시 멈춰 있도록 하는 것이다. 하지만, 5초동안 CPU를 점유하고 있기 때문에 비선점커널로 컴파일 한 경우 전체 시스템이 멈춰있게 되는 문제가 있다. 실제로 위와 같이 작성하는 경우는 거의 없으며, 일정 시간을 딜레이를 해야 한다면 커널 타이머를 사용하게 된다.

위 예제처럼 jiffies는 특정 시점에서 시간값을 비교하기 위해서 사용하게 되는데, 이때 단순히 크기를 비교할 경우 오버플로우 문제에 대처하지 못하는 문제가 있다. 이러한 문제를 해결하기 위해서는 커널에서 제공해주는 다음과 같은 함수들을 사용해야 한다.

time_after(a,b); /* a가 b보다 나중 시간값이면 true가 된다 */
time_before(a,b)
time_after_eq(a,b); /* a 가 b보다 나중이거나 같은 시간이면 true가 된다 */
time_before_eq(a,b)



2.6 버전부터는 jiffies값이 증가하게 됨에 따라 오버플로우가 좀 더 자주 발생하게 되었다. 따라서, 32비트 jiffies로는 감당하기 힘들어서 jiffies_64를 같이 제공하고 있다. jiffies와 jiffies_64는 같은 메모리 번지에 맵핑되어 있기 때문에 jiffies를 증가시킬 경우 jiffies_64도 증가가 된다. 정확히는 jiffies_64를 증가시켜서 jiffies 값이 증가되도록 한다. 둘은 선택적으로 사용할 수 있으며, 32비트 환경에서 jiffies는 32비트를 가져오고 64비트 환경에서는 64비트 값을 가져온다. 따라서, 긴 시간 간격에 대한 비교가 필요할 경우에는 jiffies_64를 사용한다.

jiffies_64 변수는 직접 건드리지 않고 다음 함수를 사용하도록 권장하고 있다.

u64 get_jiffies_64(void);

HZ보다 작은 단위의 시간 지연

jiffies를 통해서 대부분의 상대적인 시간 처리를 하지만 경우에 따라서는 HZ보다 더 세밀한 시간 간격(HZ가 1000인 경우, 1밀리세컨드)을 필요로 하는 경우가 있다. 즉, 마이크로초나 나노초 단위의 지연을 하고 싶은 경우에는 다음 매크로를 사용하면 된다.

#include <linux/delay.h>

void mdelay(unsigned long milliseconds);
void udelay(unsigned long microseconds);
void ndelay(unsigned long nanoseconds);



mdelay()는 밀리초단위, udelay()는 마이크로초 단위, ndelay()는 나노초 단위의 지연을 하게 된다. 이 연산도 내부적으로는 반복문을 사용하기 때문에 장시간 지연을 주는 것은 삼가할 필요가 있다. 만약 클럭틱보다 큰 값동안의 딜레이를 원한다면 커널 타이머를 사용하는 것이 바람직하다.

커널 타이머

커널 타이머는 미래의 특정 시간에 지정한 함수를 수행하도록 하는 방법이다. 이것은 커널이 수행을 해주기 때문에 그 때까지 불필요한 코드를 수행하면서 기다릴 필요가 없다.

커널 타이머를 사용하기 위해서 필요한 함수들은 다음과 같다.

#include <linux/timer.h>

struct timer_list {
        struct list_head entry;
        unsigned long expires;

        void (*function)(unsigned long);
        unsigned long data;

        struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
        void *start_site;
        char start_comm[16];
        int start_pid;
#endif
};

void init_timer(struct timer_list *timer);
void add_timer(struct timer_list *timer);
void add_timer_on(struct timer_list *timer, int cpu);
void mod_timer(struct timer_list *timer, unsigned long expires);
int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);


이 함수들을 사용하는 유형은 대충 다음과 같다.

struct timer_list my_timer;

void check_hw(unsigned long arg)
{
  /* code for checking the hardware */
 ....
}

void some_func() {
    init_timer(&my_timer);
    my_timer.function = check_hw();
    my_timer.data = &my_arg;
    my_timer.expires = jiffies + 5 * HZ;

    add_timer(&my_timer);
...
}



timer_list 구조체를 통해 수행할 함수와 수행할 시간을 명시하게 된다. 하지만, 세팅을 하기 전에 반드시 init_timer()를 통해 초기화를 시킨 후 사용해야 한다.

구조체의 data 필드는 function 함수를 호출할 때 인수로 넘길 내용이며, unsigned long 형태로 되어 있다. expires 필드는 수행할 시간을 나타내는데, 상대 시간이 아닌 절대 시간값을 사용한다. 즉, 5초 후에 수행하겠다면 5 * HZ로 표현하는 것이 아니라, jiffies + 5 * HZ로 표현해야 한다. expires는 클럭틱 발생 주기보다 세밀한 단위로 시간값을 줄 수는 없다.

커널에 등록하기 위해서는 add_timer() 함수를 사용해야 한다. 2.6 커널에는 추가로 add_timer_on() 이라는 함수가 생겼는데, 이것은 등록해 놓은 함수가 특정 CPU에서 수행되게 할 때 이용한다. add_timer()로 사용했을 경우에는 현재 CPU에서 나중에 수행된다.

만약, 등록해 놓은 타이머 함수가 아직 호출되지 않은 상태에서 시간값을 바꾸고 싶은 경우에는 mod_timer() 함수를 사용하면 된다.

호출되지 않은 상태의 타이머 함수를 제거하기 위해서는 del_timer() 또는 del_timer_sync() 함수를 사용하면 된다. 이 함수들을 호출했을 때, 해당 타이머 함수가 아직 수행되지 않았다면 제거되고, 이미 수행되었다면 그냥 리턴된다. del_timer_sync()는 해당 타이머 함수가 수행되고 있을 경우, 작업이 끝나길 기다린 후 타이머를 제거하고 빠져나온다. del_timer()는 수행중일 경우에 대한 고려를 하지 않는다.


크리에이티브 커먼즈 라이선스
Creative Commons License
이올린에 북마크하기(0) 이올린에 추천하기(0)

'Linux > API' 카테고리의 다른 글

I/O Port 다루기 on x86  (0) 2008/01/26
인터럽트  (0) 2008/01/25
커널 시간 관리  (0) 2008/01/25
CPU별 변수 사용  (0) 2007/10/08
Posted by Daniel Kwon
TAG timer

CPU별 변수 사용

Linux/API 2007/10/08 11:54
리눅스 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(type, name) \
14     __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

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

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

33 #define DEFINE_PER_CPU(type, name) \
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 size, i;
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 size, gfp_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()의 경우에는 인수로 전달된 변수는 전혀 사용하지 않는 것을 확인할 수 있다.
크리에이티브 커먼즈 라이선스
Creative Commons License
이올린에 북마크하기(0) 이올린에 추천하기(0)

'Linux > API' 카테고리의 다른 글

I/O Port 다루기 on x86  (0) 2008/01/26
인터럽트  (0) 2008/01/25
커널 시간 관리  (0) 2008/01/25
CPU별 변수 사용  (0) 2007/10/08
Posted by Daniel Kwon