· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Unreliable Guide To Hacking The Linux Kernel

Unreliable Guide To Hacking The Linux Kernel


저자 : Paul Rusty Russell <rusty@rustcorp.com.au>

번역 : 김남형 <pastime@ece.uos.ac.kr>


이 문서는 자유 소프트웨어이다; 당신은 Free Software Foundation 에서 발표한 GNU General Public License 하에 이 문서를 수정하거나 재배포할 수 있다; License 버전 2 혹은 (당신이 원한다면) 그 이후의 버전을 사용할 수 있다.

유용하게 이용되기를 바라는 마음에서 이 문서를 배포하지만, 여기에서 대한 어떠한 책임도 지지 않음을 밝혀둔다; 상업적이나 특정한 목적에 따라 이용하는 경우에도 이 문서에 의해 발생하는 문제에 대해 어떠한 책임도 물을수 없다. 더 자세한 내용은 GNU General Public License 문서를 살펴보기 바란다.

GNU General Public License 문서를 받아보기를 원한다면 Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 03111-1307 USA 로 신청하기 바란다.

더 자세한 사항은 Linux 소스 배포판의 COPYING 파일에 적혀있다.




Contents

1. 소개
2. 동작 모드 (the Players)
2.1. User Context
2.2. 하드웨어 인터럽트 (Hard IRQs)
2.3. 소프트웨어 인터럽트 (Bottom Halves, Tasklets, softirqs)
3. 기본적인 개념들
4. ioctls: 새로운 시스템 콜을 추가하지 않는 방법
5. Deadlock 처리법
6. 공통 루틴
6.1. printk() <include/linux/kernel.h>
6.2. copy_to/from_user()/get/put_user() <include/asm/uaccess.h>
6.3. kmalloc()/kfree() <include/linux/slab.h>
6.4. current <include/asm/current.h>
6.5. udelay()/mdelay() <include/asm/delay.h> / <include/linux/delay.h>
6.6. cpu_to_be/le32()/be/le32_to_cpu() <include/linux/byteorder/*.h>
6.7. local_irq_save/restore() <include/asm/system.h>
6.8. local_bh_disable/enable() <include/asm/softirq.h>
6.9. smp_processor_id()/cpu_number/logical_map() <include/asm/smp.h>
6.10. init/exit/__initdata <include/linux/init.h>
6.11. __initcall()/modult_init() <include/linux/init.h>
6.12. module_exit() <include/linux/init.h>
6.13. MOD_INC_USE_COUNT/MOD_DEC_USE_COUNT <include/linux/module.h>
7. 대기 큐 (Wait Queues) <include/linux/wait.h>
7.1. 선언하기
7.2. 큐에 넣기
7.3. 큐에 들어있는 태스크 깨우기
8. 원자적 연산
9. 심볼
9.1. EXPORT_SYMBOL() <include/linux/module.h>
9.2. EXPORT_NO_SYMBOLS <include/linux/module.h>
9.3. EXPORT_SYMBOL_GPL() <include/linux/module.h>
10. Routines and Conventions
10.1. Doubly-Linked Lists <include/linux/list.h>
10.2. 리턴값에 대한 전통
10.3. Breaking Compilation
10.4. 구조체 변수 초기화
10.5. GNU 확장
10.6. C++
10.7. #if
11. 당신의 코드를 커널내에 넣는 방법
12. Kernel Cantrips
13. 감사의 글


1. 소개


Rusty 의 Unreliable Guide to Linux Kernel Hacking 문서를 읽고 있는 여러분을 환영한다. 이 문서는 커널 코드에서 사용되는 공통적인 루틴들과 일반적인 요구사항들을 설명하고 있다: 이 문서의 목적은 숙련된 C 프로그래머들에게 리눅스 커널 개발에 대한 입문서로서 사용되고자 하는 것이다. 이 문서에서는 구체적인 구현에 관한 부분은 다루지 않는다

이 문서를 읽기 전에, 한가지 사실을 이해해 주기 바란다. 사실 개인적으로 나는 전체적으로 만족스럽지 못한 수준의 이 문서를 작성하고 싶지 않았었다. 하지만 항상 이러한 문서를 읽고는 싶었기 때문에 어쩔수 없이? 이렇게 작성하게 되었다. 이 문서가 훌륭한 요약서 혹은 커널에 대한 일반적인 시작점이나 임의의 정보를 제공할 수 있는 좋은 문서로 발전해 가기를 바란다.


2. 동작 모드 (the Players)


특정한 순간에 시스템 상의 CPU 는 다음과 같은 상태 중의 하나가 된다:

  • 특정 프로세스에 연관되지 않은, 하드웨어 인터럽트 처리
  • 특정 프로세스에 연관되지 않은, 소프트웨어 인터럽트 (softirq, tasklet, bh) 처리
  • 특정 프로세스에 연관되어 커널 모드에서 동작
  • 사용자 모드에서 특정 프로세스 수행

위의 리스트는 각각이 엄격한 우선순위를 가진다: 마지막의 상태 (사용자 모드) 를 제외한 다른 상태들은 오직 자신보다 상위에 있는 상태에 의해 선점될 수 있다. 예를 들어, softirq 가 CPU 에서 수행되고 있는 동안 다른 softirq 는 이를 선점할 수 없지만 하드웨어 인터럽트가 발생하면 선점된다. 하지만 시스템내의 다른 CPU 들은 독립적으로 수행된다.

앞으로 user context [1] 에서 실제적인 비선점성을 가지기 위해 인터럽트를 막아두는 방법들을 살펴볼 것이다.


2.1. User Context


user context 는 시스템 콜이나 다른 트랩 등에 의해서 진입한 코드를 말한다: 여기에서는 sleep 을 할 수 있고 (인터럽트를 제외하면) schedule() 함수를 호출하기 전까지 CPU 를 소유하게 된다. 다시 말하면, user context 는 (사용자 모드와 달리) 비선점성을 가진다.

(!) 참고: 모듈을 로딩하거나 언로딩할 때, 그리고 블록 디바이스 드라이버에 대한 연산을 수행하는 경우에는 항상 user context 에 있는 것이다.

user context 상에서는 (현재 수행중인 태스크를 가리키는) current 포인터를 이용할 수 있고, in_interrupt() 매크로 (include/asm/hardirq.h) 는 거짓을 리턴한다.

/!\ 주의: 인터럽트 혹은 하반부 핸들러가 이미 비활성화 되어있는 경우에도, in_interrupt() 매크로가 무조건 거짓을 리턴한다는 것을 염두에 두자.


2.2. 하드웨어 인터럽트 (Hard IRQs)


타이머 틱, 네트워크 카드, 키보드와 같은 것들은 어떤 순간에도 인터럽트를 발생시킬 수 있는 실제 하드웨어 들이다. 커널에서는 인터럽트 핸들러를 수행해서 이러한 하드웨어들에 대한 처리를 한다. 커널에서는 이러한 인터럽트 핸들러가 절대 재진입되지 않도록 해 준다: 만약 (처리 중에) 또다른 인터럽트가 발생되었다면, 그것은 큐에 들어가게 된다 (큐가 가득 차있는 경우 버려진다). 이렇게 인터럽트를 비활성화 해 두기 때문에, 인터럽트 핸들러는 매우 빨리 수행되어야 한다: 주로 인터럽트 핸들러에서는 인터럽트를 받았다는 응답을 보내주고, 실행은 소프트웨어 인터럽트 를 이용해 처리하도록 표시한 후 종료한다.

하드웨어 인터럽트 부분을 처리하는 부분에서는 in_irq() 매크로가 참을 리턴한다.

/!\ 주의: 인터럽트가 비활성화 되어있는 경우에는 in_irq() 매크로가 무조건 거짓을 리턴한다는 것을 염두에 두자.


2.3. 소프트웨어 인터럽트 (Bottom Halves, Tasklets, softirqs)


시스템 콜을 호출한 뒤 사용자 모드로 돌아가기 전이나 하드웨어 인터럽트 핸들러가 종료한 후에는 (보통 하드웨어 인터럽트 핸들러에서 처리한) 표시된 소프트웨어 인터럽트 가 수행된다.

많은 실제적인 인터럽트 처리가 이 부분에서 이루어진다. SMP 초창기에는 오직 bottom halves (하반부, BHs) 라는 개념만이 존재하였는데, 이것은 다수의 CPU 에 의한 장점을 살리지 못했다. 얼마 후 (고성능의 컴퓨터로 전환한 후에?) 이러한 제한사항들은 사라졌다.

include/linux/interrupt.h 파일에 여러가지 하반부들의 리스트가 있다. 얼마나 많은 CPU 를 가지고 있는가에 상관없이, 하반부는 동시에 두 개이상 수행될 수 없다. 이러한 방식은 SMP 에 적용하기는 쉽지만, 성능을 개선시키기는 어렵다. 하반부에서 중요한 것은 타이머 하반부이다. (include/linux/timer.h): 여기서는 주어진 길이만큼의 시간이 지난 후에 특정 함수를 호출하도록 등록할 수 있다.

커널 버전 2.3.43 에서 softirq 가 소개되었고, 그 아래에서 하반부가 동작하도록 수정되었다. (지금은 하반부의 사용을 권하지 않고 있다... deprecated) softirq 는 SMP 의 장점을 완전히 살릴 수 있도록 한 하반부라고 할 수 있다: 동시에 수행될 수 있는 CPU 의 수만큼 한번에 수행된다. 이것은 경쟁 조건에서 각각의 락을 이용해서 공유된 데이타에 접근하도록 하는 처리가 필요하다는 것을 의미한다. 어떤 softirq 가 활성화 되었는지를 표시하기 위해 bitmask 를 사용하므로, 32 개 이상의 softirq 를 처리할 수 없다.

tasklet (include/linux/interrupt.h) 은 동적으로 등록할 수 있다는 점 (즉, 원하는 만큼 많이 등록할 수 있다) 을 제외하고 softirq 와 동일하다. 그리고 (하반부와 달리) 각각의 tasklet 은 동시에 수행될 수 있지만, 특정 tasklet 은 한 순간에 오직 하나의 CPU 에서만 수행되도록 보장한다. [2]

/!\ 주의: tasklet 이라는 이름은 잘못된 것이다: 이것은 task 와 아무런 연관이 없다. (아마 이때 Alexey Kuznetsov 가 보드카를 많이 마신 것 같다..)

현재 softirq (혹은 하반부나 tasklet) 가 실행 중인지를 알아보기 위해 in_softirq() 매크로 (include/linux/softirq.h)를 사용할 수 있다.

/!\ 주의: 하반부에 대한 락이 걸려있는 경우에는 in_softirq() 매크로가 항상 거짓을 리턴함을 염두에 두자


3. 기본적인 개념들


* 메모리 보호 기능을 지원하지 않음
만약 user context 나 인터럽트가 걸린 상태에서 메모리 영역을 잘못 사용하게 된다면 시스템 전체가 망가질 수 있다. 당신이 지금 하려는 작업이 사용자 모드에서 해결할 수 있는 작업인지를 먼저 확인해 보자

* 부동소수점 이나 MMX 연산을 지원하지 않음
FPU 상태정보는 저장되지 않는다. user context 에서도 FPU 의 상태는 현재 프로세스에 대응되지 않을 것이다: 사용자 모드의 프로세스가 FPU 의 상태정보를 가지고 있다는 사실은 잊어버리는 것이 좋다. 만약 정말로 이런 작업이 필요하다면 명시적으로 모든 FPU 의 상태정보를 저장하고 복구하는 (그리고 인터럽트를 금지시켜야 한다) 작업을 해야한다. 이러한 작업은 일반적으로 좋은 아이디어가 아니다: 먼저 고정소수점[3] 연산을 이용해서 해결하도록 하자.

* 엄격한 스택 제한
커널 버전 2.2 에서 커널 스택의 크기는 대략 6K 정도이고 (대부분의 아키텍쳐에서 그렇다는 것이다: Alpha 의 경우는 약 14K 이다) 인터럽트 처리 루틴과 스택을 공유하기 때문에 이 영역 모두를 쓸 수가 없다. 많은 재귀호출이나 스택 변수로 큰 배열을 잡아두는 것을 피하도록 하자. (대신 동적으로 할당받도록 한다)

* 리눅스 커널은 이식성이 있다
이 원칙을 가슴속에 새겨두자. 당신이 작성하는 코드는 64-비트 에서도 호환되도록 하고 endian 에 종속적이지 않아야 한다. 또한 CPU 에 종속적인 부분을 줄여야 한다. 즉 포팅하기 쉽도록 인라인 어셈블리는 깔끔하게 캡슐화되어야 하고 최소화 해야 한다. 일반적으로 이것은 커널 트리 내의 아키텍쳐 종속적인 부분에 해당되는 이야기이다.


4. ioctls: 새로운 시스템 콜을 추가하지 않는 방법


시스템 콜은 다음과 같은 형태가 된다.

asmlinkage int sys_mycall(int arg) 
{
        return 0; 
}

우선 대부분의 경우에 있어서 당신은 새로운 시스템 콜을 추가하고 싶지는 않을 것이다. 캐릭터 디바이스를 만들고 거기에 대한 적절한 ioctl 을 구현하는 방법을 사용할 수 있다. 이것이 시스템 콜보다 훨씬 유연한 방법이다. 모든 아키텍쳐에 대해 include/asm/unistd.h 파일과 arch/kernel/entry.S 파일을 수정할 필요가 없고 또 Linus 가 잘 받아들이는 방식이기도 하다.

만약 당신이 작성한 모든 루틴이 몇가지 파라미터를 읽고 쓰는 연산을 수행한다면 대신 sysctl 인터페이스를 구현하는 방법을 고려해 볼 수 있다.

ioctl 내에서는 process 의 user context 에 속한 것이다. 에러가 발생했다면 음수값인 errno (include/linux/errno.h 참고) 를 리턴하고, 그렇지 않다면 0 을 리턴하도록 한다.

sleep 에서 깨어난 후라면 signal 을 받았는지 검사해야 한다. Unix/Linux 에서 signal 을 처리하는 방법은 시스템 콜에서 -ERESTARTSYS 라는 에러코드를 리턴하는 것이다. 시스템 콜 진입코드는 user context 로 돌아와서 signal handler 를 실행하고 (사용자가 금지하지 않았다면) 시스템 콜을 다시 시작할 것이다..? 그러므로 어떤 자료구조를 다루고 있는 경우에 있어서 프로세스가 다시 시작될 수 있도록 준비를 해 두어야 한다.

if (signal_pending()) 
        return -ERESTARTSYS;

만약 오랜 시간동안 계산하는 일이 필요하다면: 먼저 사용자 모드에서 수행할 것을 고려해 본다. 만약 정말로 이러한 연산을 커널 내에서 수행해야 한다면 CPU 를 양도해야 하는지를 정기적으로 검사해야 한다 (CPU 마다 협동적으로 멀티태스킹을 수행한다는 것으로 기억하라). 다음과 같은 관용적인 표현이 쓰인다:

if (current->need_resched)
        schedule(); /* Will sleep */ 

인터페이스 디자인에 대해서 한마디 하자면: UNIX 시스템 콜의 모토는 "메카니즘을 제공하고 정책을 제공하지는 않는다" (Provide mechanism not policy) 이다.


5. Deadlock 처리법


만약 다음과 같은 상황이 아니라면, sleep 에 들어갈 수 있는 어떤 루틴이라도 실행해서는 안된다:

  • user context 에 있는 경우
  • 어떤 spinlock 도 소유하지 않는 경우
  • 인터럽트를 활성화 하고 있는 경우 (실제로, Andi Kleen 은 스케줄링 관련 코드에서도 인터럽트를 활성화 시킬수 있다고 했다. 하지만 그것은 아마 당신이 원하지 않을 것이다.)

몇몇 함수들은 암시적으로 sleep 에 들어갈 수 있음에 주의하자: 일반적으로 사용자 모드 접근 함수 (*_user) 나 GFP_ATOMIC 플래그 없이 호출하는 메모리 할당 함수가 이런 류에 속한다.

만약 위에서 이야기한 원칙들을 어긴다면 당신의 머신은 결국 다운되고 말 것이다.

정말로.


6. 공통 루틴


6.1. printk() <include/linux/kernel.h>


printk() 는 커널의 메세지를 콘솔이나 dmesg, syslog 데몬에게 넘겨주는 일을 한다. 이것은 디버깅시에나 에러를 알려주는 데 유용하며 interrupt context 내에서도 사용이 가능하지만 주의가 필요하다: printk() 메세지에 의해 콘솔이 가득찬 (flooded) 머신은 사용할 수 없다. printk() 에서 사용되는 형식 문자열은 대부분 ANSI C 의 printf() 와 호환된다. 그리고 C 언어의 문자열 결합 기능을 이용하여 맨 처음 인자로 중요도(priority) 를 사용한다:

printk(KERN_INFO "i = %u\n", i);

<include/linux/kernel.h> 파일에 사용할 수 있는 중요도에 대한 매크로가 정의되어 있다. 이 중요도들은 syslog 대몬이 메세지의 단계(level) 로 해석한다. 특별한 경우로 IP 주소값을 출력하고자 하는 경우에는 다음을 이용할 수 있다.

__u32 ipaddress;
printk(KERN_INFO "my ip: %d.%d.%d.%d\n", NIPQUAD(ipaddress));

printk() 는 내부적으로 1K 의 버퍼를 사용하며 버퍼가 넘치는 것을 검사하지 않는다. 이를 넘기지 않도록 주의한다.

(!) 참고: 당신이 사용자 모드의 프로그램에서 printf() 대신 printk() 를 사용하고 있음을 알게될 때, 진정한 커널 해커가 되었음을 느끼게 될 것이다.

(!) 참고: 또한, 원래 Unix Version 6 의 소스에는 printf() 함수 위편에 다음과 같은 주석이 달려 있다. "printf() 는 잡담을 위해 사용되서는 안된다." 이를 명심하기 바란다.

6.2. copy_to/from_user()/get/put_user() <include/asm/uaccess.h>


'''SLEEPS'''

put_user()get_user() 는 (int, char, long 과 같은) 하나의 값을 사용자 영역과 주고받기 위해 사용된다. 사용자 영역의 포인터는 (user context 내에서) 단순히 그 값을 참조해서는 안된다: 데이타는 반드시 이 함수들을 이용해 복사되어야 한다. 두 함수 모두 -EFAULT0 을 리턴한다.

copy_to_user()copy_from_user() 는 좀더 일반적인 함수이다: 이 함수들은 임의의 양의 데이타를 사용자 영역과 주고받는다.

/!\ 주의: put_user()get_user() 와 달리 copy_to_user()copy_from_user() 에서는 복사되지 않은 데이타의 양을 리턴한다. (즉, (마찬가지로) 0 은 성공을 의미한다.)

그렇다. 이 이상한 인터페이스는 나를 짜증나게 만들었다. 제발 여기에 대한 패치를 보내주어 나의 영웅이 되어 주길 바란다. -- RR

이 함수들은 암시적으로 sleep 에 들어갈 수 있다. 그래서 이 함수들은 user context 밖에서나 (user context 밖에서는 별 의미가 없다), 인터럽트가 비활성화된 상태 혹은 spinlock 이 걸린 상태에서 절대 사용되서는 안된다.

6.3. kmalloc()/kfree() <include/linux/slab.h>


이 루틴들은 (사용자 모드에서의 malloc()/free() 처럼) 동적으로 메모리를 요청할 때 사용된다. 하지만 kmalloc() 에서는 별도의 플래그 하나를 더 취하는데 그중에서 중요한 값들로는 다음과 같은 것들이 있다:

  • GFP_KERNEL - sleep 되거나 swap 될 수 있다. user context 내에서만 사용가능하지만, 메모리를 할당받는 가장 신뢰할 수 있는 방법이다.
  • GFP_ATOMIC - sleep 되지 않는다. GFP_KERNEL 보다는 신뢰성이 떨어지지만 interrupt context 내에서도 사용할 수 있는 강점이 있다. 이 경우 에러에 대한 처리가 무척 중요하다.
  • GFP_DMA - 16MB 보다 하위의 ISA DMA 영역의 메모리를 요청할 때 사용된다. 만약 이것이 무슨 말인지 모르겠다면 사용할 필요가 없을 것이다. 매우 신뢰성이 떨어진다.

만약 'kmem_grow: Called nonatomically from int' 라는 경고 메세지를 보았다면, 당신이 짠 프로그램에서 interrupt context 상에서 GFP_ATOMIC 플래그를 설정하지 않은 상태로 메모리 할당을 요청한 것이다. 이 에러는 즉시 고쳐져야 한다.

만약 PAGE_SIZE (include/linux/page.h) 바이트 이상의 메모리를 할당받고자 하는 경우에는 __get_free_pages() (include/linux/mm.h) 함수의 사용을 고려해 보자. 이 함수는 order (2의 승수, 0 이면 1 페이지, 1 이면 2 페이지, 2 이면 4 페이지, ...) 인자와 위에서 말한 메모리 우선순위 플래그 인자 (GFP_*) 를 취한다.

한 페이지 단위? 이상의 (more than a page worth of bytes) 메모리를 할당받고자 하는 경우에는 vmalloc() 함수를 이용할 수 있다. 이 함수는 kernel map 상의 가상 메모리를 할당한다. 이 블럭들은 물리적으로 연속된 메모리 영역이 아니지만, MMU [4] 가 마치 연속된 메모리인 것 처럼 처리해 준다. (즉, 오직 CPU 에게만 연속적으로 보이는 것이지 다른 외부의 디바이스 드라이버에서는 연속적으로 보이지 않는다.) 만약 어떤 (이상한) 장치에서 물리적으로 연속된 큰 메모리 영역을 필요로 한다면 문제가 될 수 있다. 이것은 Linux 에서는 잘 지원이 되지 않는 부분인데 커널이 실행되고 나면 발생하는 메모리 단편화 현상이 이것을 어렵게 하기 때문이다. 이러한 메모리를 할당받는 가장 좋은 방법은 부트 프로세스에서 alloc_bootmem() 함수를 이용해서 미리 할당받아 두는 것이다.

자주 사용되는 객체를 위해 새로운 캐시를 만들기 전에 include/linux/slab.h 에 있는 슬랩 캐시의 사용을 고려해 보자.

6.4. current <include/asm/current.h>


이 전역 변수 (실제로는 매크로이다) 는 현재 실행중인 task_struct 구조체에 대한 포인터이다. 그래서 오직 user context 에서만 사용할 수 있다. 예를 들어 프로세스가 시스템 콜을 호출했다면 current 변수는 호출한 프로세스의 task_struct 를 가리킬 것이다. 이 값은 interrupt context 내에서도 NULL 이 아니다.

6.5. udelay()/mdelay() <include/asm/delay.h> / <include/linux/delay.h>


프로세스를 잠시 중단시키기 위해 udelay() 함수를 이용할 수 있다. udelay() 함수에 큰 값을 사용하는 것은 오버 플로우 문제를 일으킬 수 있다 - 이를 보조하기 위한 함수인 mdelay() 함수를 이용하거나 schedule_timeout() 함수를 이용하도록 한다.

6.6. cpu_to_be/le32()/be/le32_to_cpu() <include/linux/byteorder/*.h>


cpu_to_be32() 계통의 매크로 들은 커널내의 엔디안에 관한 변환을 위한 일반적인 방법을 제공한다 (32 대신 64 나 16 이 쓰일수 있고, be 대신 le 가 쓰일 수 있다): 이들은 변환된 값을 리턴한다. 이와 반대되는 일을 하는 것들도 역시 존재한다: be32_to_cpu 등..

이 함수들은 크게 두가지 형태로 변화되어 사용된다: 그 첫번째는 cpu_to_be32p() 처럼 포인터로 변화된 형태이다. 이 함수는 명시된 타입의 포인터를 받아서 변환된 값을 리턴한다. 또다른 형태는 cpu_to_be32s() 와 같은 in-situ 계열인데, 이것은 포인터로 주어진 값을 변화시킨 후에 void 를 리턴한다.

6.7. local_irq_save/restore() <include/asm/system.h>


이 루틴들은 현재 CPU 의 하드웨어 인터럽트를 활성화/비활성화 시킨다. 이 함수들은 재진입이 가능하다; 이전의 상태값을 unsigned long flags 인자에 저장시킨다. 만약 인터럽트가 활성화되어 있는지 아닌지 알고 있다면 단순히 local_irq_disable()local_irq_enable() 함수를 이용할 수 있다.

6.8. local_bh_disable/enable() <include/asm/softirq.h>


이 루틴들은 현재 CPU 의 소프트웨어 인터럽트를 활성화/비활성화 시킨다. 이 함수들도 재진입이 가능하다; 만약 이전에 소프트웨어 인터럽트가 비활성화되어 있었다면, 이 함수들이 쌍으로 호출된 후에도 여전히 비활성화 된 채로 남아있을 것이다. [5] 이 함수들은 현재 CPU 에서 softirq, tasklet, bottom halves 가 실행되는 것을 금지한다.

6.9. smp_processor_id()/cpu_number/logical_map() <include/asm/smp.h>


smp_processor_id() 함수는 0과 NR_CPUS (Linux 에서 지원하는 최대 CPU 의 갯수로 현재는 32 이다) 사이의 값인 현재 프로세서 번호를 알려준다. 이 값은 반드시 연속적일 필요는 없다: 0 과 smp_num_cpus() (이 머신이 가지고 있는 실제 프로세서의 갯수) 사이의 값을 알기 위해서는 cpu_number_map() 함수를 사용해서 프로세서 번호를 논리적인 번호로 바꿀 수 있다. [6] cpu_logical_map() 함수는 이와 반대되는 일을 수행한다.

6.10. init/exit/__initdata <include/linux/init.h>


부팅이 되고 난 후에, 커널은 특정 영역의 메모리를 해제한다; init 로 선언된 함수들과, initdata 라고 선언된 데이타는 부팅이 완료된 후에 해제된다. (모듈내에서는 이러한 지시자들이 무시된다) __exit 는 종료될 때에만 필요한 함수들을 선언하는데 사용된다: 이 함수들은 파일이 모듈로 컴파일되지 않으면 해제된다. 이 지시자들을 사용한 헤더 파일들을 살펴보기 바란다. __init 지시자와 함께 선언된 함수에서 EXPORT_SYMBOL() 매크로를 이용해 심볼을 모듈로 공개하는 것은 의미가 없다는 것에 주의하자.

__initdata 로 선언된 정적 자료 구조는 (0 으로 초기화 되는 BSS 영역의 일반적인 정적 데이타와는 달리) 반드시 초기화 되어야 하고 상수가 되어서는 안된다.

6.11. __initcall()/modult_init() <include/linux/init.h>


커널의 많은 부분은 (동적으로 로드할 수 있는 부분인) 모듈로서도 잘 동작한다. module_init()module_exit() 매크로는 #ifdef 와 같은 전처리기 지시자 없이도 모듈이나 커널에 정적으로 포함되는 것을 둘 다 지원하는 코드를 쉽게 작성할 수 있도록 해 준다.

module_init() 매크로는 (파일이 모듈로 컴파일되는 경우) 모듈이 추가될 때, 혹은 부팅시에 어떤 함수가 불려야 할지를 결정한다: 파일이 모듈로 컴파일되지 않는다면 module_init() 매크로는 __initcall() 매크로와 동일한 역할을 하는데 이것은 링커에 의해 부팅시에 함수가 불려지도록 설정해 준다.

이 함수는 모듈을 로딩하는 데 실패하는 경우 음수값인 에러 번호를 리턴할 수 있다 (불행히도 커널에 정적으로 포함되는 경우에는 아무런 영향을 미치지 못한다). 모듈의 경우에는, 이 함수는 인터럽트가 활성화 되어 있고, 커널 lock 이 걸려있는??? 상태의 user context 에서 호출되므로 sleep 될 수 있다.

6.12. module_exit() <include/linux/init.h>


이 매크로는 모듈이 제거될 때 호출될 함수를 정의한다 (커널내에 정적으로 포함되는 경우에는 호출지지 않는다). 그 함수는 오직 모듈의 사용 횟수 (usage count) 가 0 이 되는 경우에만 호출될 것이다. 이 함수도 역시 sleep 될 수 있지만, 실패하지는 않는다: 모든 자료들은 리턴될 때 해제되어야 한다.

6.13. MOD_INC_USE_COUNT/MOD_DEC_USE_COUNT <include/linux/module.h>


이 매크로들은 모듈의 사용 횟수를 관리해서 모듈이 잘못 제거되는 것을 방지하기 위해 사용된다. (다른 모듈에서 현재 모듈에서 공개하고 있는 심볼을 사용하고 있는 경우에 모듈은 제거될 수 없다: 아래를 참고하기 바란다). (소켓이나 모든 자료구조와 같은) 사용자 공간에서의 모듈에 대한 참조는 항상 함수가 sleep 에 들어가기 전에 이 사용 횟수에 반영되어야 한다. Tim Waugh 의 코드를 인용하면:

/* THIS IS BAD */
foo_open (...)
{
        stuff..
        if (fail)
                return -EBUSY;
        sleep.. (might get unloaded here)
        stuff..
        MOD_INC_USE_COUNT;
        return 0;
}

/* THIS IS GOOD /
foo_open (...)
{
        MOD_INC_USE_COUNT;
        stuff..
        if (fail) {
                MOD_DEC_USE_COUNT;
                return -EBUSY;
        }
        sleep.. (safe now)
        stuff..
        return 0;
}

혹은 이러한 문제를 해결하기 위해 모듈의 file_operations 구조체의 owner 필드를 사용할 수 있다. 이 값은 THIS_MODULE 이라는 매크로를 이용해 설정한다.

좀더 복잡한 모듈을 언로드하기 위해 lock 이 필요할 수도 있다. 이 경우 모듈내에 can_unload 라는 함수를 정의해서 이를 알아볼 수 있다. 이 함수는 모듈을 언로드할 수 있는 경우 0 을 리턴하고, 그렇지 않으면 -EBUSY 를 리턴해야 한다.


7. 대기 큐 (Wait Queues) <include/linux/wait.h>


''SLEEPS''

대기 큐는 특정 조건이 만족될 때 깨어나서 실행되기를 원할 때 사용된다. 대기 큐를 사용할 때는 경쟁 조건이 발생하지 않도록 조심해서 사용해야 한다. 먼저 wait_queue_head_t 를 선언하고, 특정 조건을 기다리는 프로세스 들을 자신을 참조하는 wait_queue_t 로 선언하여 큐에 넣는다.

7.1. 선언하기


초기화 코드 부분에서 DECLARE_WAIT_QUEUE_HEAD() 매크로나 init_waitqueue_head() 함수를 이용하여 wait_queue_head_t 를 선언할 수 있다.

7.2. 큐에 넣기


자신을 대기 큐 안에 넣는 것은 꽤 복잡한 일이다. 왜냐하면 조건을 검사하기 전에 큐에 넣어야 하기 때문이다..? 이러한 일을 해주는 매크로가 있다: wait_queue_interruptible() <include/linux/sched.h>. 이 매크로의 첫번째 인자는 wait queue head 가 되고, 두번째 인자는 평가될 계산식 (expression) 이 된다. 이 매크로는 계산식이 참일 때 0 을 리턴하고, 시그널을 받으면 -ERESTARTSYS 를 리턴한다. (interruptible 이 없는) wait_queue() 버전은 같은 일을 하지만 시그널을 무시한다.

sleep_on() 계열의 함수들을 사용하지 않도록 한다 - 이 함수들은 경쟁 조건을 불러일으키곤 한다. 거의 모든 경우에 있어서 wait_event() 계열의 함수들이 같은 일을 해 줄 것이다. 혹은 schedule_timeout() 함수를 이용하여 루프를 돌리는 방법도 가능하다. 만약 schedule_timeout() 과 루프를 사용하기로 했다면 매 반복 주기마다 (set_current_state() 를 이용하여) 태스크 상태를 설정해야 busy-looping 을 피할 수 있다.

7.3. 큐에 들어있는 태스크 깨우기


wake_up() <include/linux/sched.h> 를 호출한다; 이 함수는 큐에 들어있는 모든 프로세스를 깨우게 될 것이다. 만약 어떤 태스크가 TASK_EXCLUSIVE 로 설정되어 있다면 큐에 들어있는 나머지 프로세스들은 깨어나지 않을 것이다.


8. 원자적 연산


특정 연산들은 모든 플랫폼에서 원자적으로 동작한다. 이러한 연산들의 첫번째 부류는 <include/asm/atomic.h> 에 정의된 atomic_t 라는 타입과 함께 수행되는 연산들이다; atomic_t 는 부호화된 정수형 (최소 24 비트) 이며 atomic_t 로 선언된 변수에 접근할 때는 반드시 이러한 함수들을 이용해야 한다. atomic_read()atomic_set() 함수는 변수값을 읽어오고 설정하는 데 사용되며 atomic_add(), atomic_sub(), atomic_inc(), atomic_dec(), atomic_dec_and_test() (0 으로 감소되면 true 를 리턴한다) 등의 함수가 있다.

이러한 함수들은 일반적인 산술 연산들에 비해 느리게 동작하므로, 불필요하게 사용되어서는 안된다. 또한 spinlock 을 사용하는 32-bit Sparc 머신과 같은 특정한 플랫폼에서는 더욱 느리게 동작한다.

원자적으로 수행되는 연산의 두번째 부류는 <include/asm/bitops.h> 에 정의된 long 타입에 적용되는 원자적인 비트 연산이다. 이러한 연산들은 일반적으로 비트 패턴의 포인터와 비트 번호를 인자로 받는다: 비트 번호 0 은 LSB (Least Significant Bit) 이다. set_bit(), clear_bit(), change_bit() 와 같은 함수들은 각각 특정 비트값을 1로 설정하거나, 0으로 설정하거나, 현재값을 바꾸는 (flip) 연산을 수행한다. test_and_set_bit(), test_and_clear_bit(), test_and_change_bit() 도 같은 일을 수행하지만, 해당 비트가 이미 1로 설정되어 있는 경우에는 true 를 리턴한다. 이 함수들은 아주 간단한 locking 을 구현하는데 유용하게 사용된다.

이러한 함수들을 BITS_PER_LONG 보다 큰 비트 번호와 함께 호출하는 것도 가능하다. 하지만 big-endian 플랫폼에서 이상한 동작을 일으키게 되므로 이렇게 하는것은 그리 좋은 아이디어가 아니다.

비트의 순서는 플랫폼에 따라 달라질 수 있다는 것과, 특히 이 함수들의 인자로 넘겨지는 비트 패턴의 길이는 최소한 long 타입보다 커야 한다는 사실에 주의하라.


9. 심볼


커널 내에서는 일반적인 링킹 법칙이 적용된다. (즉, static 이라는 키워드를 통해 file scope 로 선언된 심볼이 아니라면, 커널 내의 어느 곳에서나 사용될 수 있다.) 하지만, 모듈에 대해서는 커널에 대한 접근점을 제한하기 위해 특별히 외부로 공개 (export) 된 심볼 테이블을 유지한다. 물론 모듈도 심볼을 공개할 수 있다.

9.1. EXPORT_SYMBOL() <include/linux/module.h>


이 방법은 심볼을 공개하는 일반적인 방법이며, 모듈이건 아니건 다 사용할 수 있는 방법이다. 커널 내에서는 이러한 모든 선언들은 genksyms (커널 심볼을 생성하는 프로그램 - 이러한 선언들을 찾기 위해 모든 파일을 검색한다.) 를 위해서 하나의 파일로 모아놓기도 한다. genksyms 나 Makefile 의 주석을 살펴보기 바란다.

9.2. EXPORT_NO_SYMBOLS <include/linux/module.h>


만약 모듈이 아무런 심볼도 공개하지 않는다면 모듈 내의 어느곳에서건 다음과 같이 적어주면 된다.

EXPORT_NO_SYMBOLS;

커널 2.4 버전이나 그 이전 버전에서는, 모듈이 EXPORT_SYMBOL() 이나 EXPORT_NO_SYMBOLS 를 포함하지 않으면 기본적으로 static 이 아닌 모든 전역 심볼들을 공개한다. 커널 2.5 버전 이후에는 심볼을 공개할지 말지를 명시적으로 표시해야 한다.

9.3. EXPORT_SYMBOL_GPL() <include/linux/module.h>


EXPORT_SYMBOL() 과 동일하지만 EXPORT_SYMBOL_GPL() 로 공개된 심볼들은 GPL 과 호환되는 라이센스로 MODULE_LICENSE() 를 등록한 모듈에서만 보여진다.


10. Routines and Conventions


10.1. Doubly-Linked Lists <include/linux/list.h>


커널의 헤더에는 세가지 종류의 링크드 리스트 루틴이 존재한다. 그중에서도 가장 많이 사용되는 것이 바로 위의 것이다. (Linus 도 이것을 사용한다) 만약 당신이 반드시 단순 링크드 리스트를 사용해야 하는 상황이 아니라면 이것을 사용하는 것이 좋은 선택일 것이다. 사실, 나는 이것이 좋으냐 좋지 않으냐를 떠나서, 단지 다른 링크드 리스트 구현들을 없애버릴 목적으로 사용하고 있다.

10.2. 리턴값에 대한 전통


user context 내에서 호출되는 함수들은 대부분 C 언어의 전통 (Convention) 을 무시한다 - 성공시에는 0 을, 실패시에는 (-EFAULT 같은) 음수값인 에러 코드를 리턴한다. 처음에는 이것이 직관적이지 않을 수도 있겠지만, 네트워크 프로그래밍 같은 곳에서는 꽤 널리 사용되고 있다.

파일 시스템 코드에서는 ERR_PTR() <include/linux/fs.h> 매크로를 이용한다; 음수값인 에러 코드를 포인터로 인코딩한 후, IS_ERR()PTR_ERR() 와 같은 매크로를 이용하여 다시 얻어온다; 이를 이용하면 에러 코드를 얻기위한 포인터를 따로 인자로 넘기지 않아도 된다. 좀 이상해 보이지만, 좋은 방법이다.

10.3. Breaking Compilation


Linus 나 다른 커널 개발자들은 가끔 개발중인 커널내의 함수나 구조체 이름을 바꾸기도 한다. 이것은 부분적인 변화만이 아닌 기본적인 변화를 말하는 것이다. (즉, 더이상 인터럽트가 활성화 된 상태에서 호출되거나, 별도의 체크가 이루어지는 부분, 이전에 체크했던 부분 등이 검사되지 않을 수도 있다는 것을 말한다) 보통 이러한 변화가 일어나는 경우에는 리눅스 커널 메일링리스트에 그에 해당하는 설명이 있을 것이다; 아카이브를 검색해 보자. 단순히 파일 내에서 전역적인 문자열 치환을 하는 것은 문제를 더욱 커지게 할 것이다.

10.4. 구조체 변수 초기화


구조체의 멤버를 초기화하는 좋은 방법으로는 ISO C99 에 정의된 designated initializer 를 사용하는 것이 있다.

static struct block_device_operations opt_fops = {
        .open               = opt_open,
        .release            = opt_release,
        .ioctl              = opt_ioctl,
        .check_media_change = opt_media_change,
};

이 방법은 grep 으로 검색하는 것을 더욱 용이하게 해주며, 어떠한 멤버들이 지정되는지를 명확하게 보여준다. 당신도 이 방법을 사용하는 것이 좋을 것이다.

10.5. GNU 확장


GNU 확장 (Extension) 은 리눅스 커널 내에서 명시적으로 허용된다. 다른 복잡한 것들은 일반적으로 사용되지 않으므로 그다지 잘 지원되지 않지만, 다음과 같은 것들은 거의 표준과 같이 생각되고 있다 (더욱 자세한 사항은 GCC info 페이지의 "C Extension" 절을 보기 바란다 - man 페이지는 info 페이지의 짧은 요약본에 불과하다):

  • inline 함수
  • statement expression (즉 ({}) 같은 것)
  • 함수/변수/타입의 속성 선언 (attribute)
  • labeled elements
  • typeof
  • 길이가 0 인 배열
  • 가변길이 인자를 가지는 매크로 (Macro varargs)
  • void 포인터의 산술연산
  • 상수가 아닌 초기값
  • 어셈블러 명령 (arch/include/asm/ 내부)
  • 문자열 함수명 (FUNCTION)
  • __builtin_constant_p()

커널 내에서 long long 타입을 사용할 때에는 주의를 기하라. gcc 가 생성하는 코드는 끔찍하다: GCC 의 runtime 함수는 커널 환경에서 제외되기 때문에 곱셈과 나눗셈은 i386 에서 동작하지 않을 것이다...? (division and multiplication does not work on i386 because the GCC runtime functions for it are missing from the kernel environment.)

10.6. C++


커널 내에서 C++ 를 사용하는 것은 보통 좋지 않은 생각이다. 커널 내에서는 필요한 runtime environment 를 제공하지 않으며 테스트되지 않은 파일들을 포함해야 하기 때문이다. C++ 를 사용하는 것은 가능하지만, 권하고 싶지 않다. 만약 당신이 진정 C++ 를 사용하고 싶다면, 최소한 예외 (exception) 에 관한 것들은 잊기를 바란다.

10.7. #if


일반적으로 소스 코드 내에서 #if 전처리기 지시문을 사용하는 것보다 헤더 파일 내에서 (혹은 소스 파일의 최상위 부분에서) 매크로를 사용하여 함수의 기본형을 뽑아두는 것이 더 깔끔하다.


11. 당신의 코드를 커널내에 넣는 방법


당신이 작성한 코드를 커널 내에 정식으로 포함되는 형태로 만들고 싶거나, 단지 패치의 형태의 만들고 싶을때라도 다음과 같은 관리적인 차원의 작업이 필요해 진다:

  • 당신이 작업한 부분에 대한 관리를 맡고있는 사람이 누구인지 확인하라. 소스 파일의 맨 윗부분과, MAINTAINERS 파일과, CREDITS 파일을 살펴보기 바란다. 중복된 노력을 피하거나 혹은 이미 하지 않기로 결정된 작업에 뛰어들지 않기 위해서 당신은 이 사람들과 의견을 조정해야 한다.

    당신이 새로 만들거나 크게 수정한 파일의 맨 윗부분에 당신의 이름과 e-mail 주소를 적어두는 것을 잊지마라. 사람들이 버그를 찾거나 수정을 요구할 때 가장 먼저 보게되는 부분이 바로 그 부분이다.

  • 보통 당신이 작업한 부분에 대한 커널 설정 옵션을 넣기를 원할 것이다. 적절한 디렉토리의 Config.in 파일을 편집하라 (하지만 arch/ 디렉토리에서는 (소문자) config.in 파일이다). 설정 파일에 사용되는 언어는 bash 가 아니지만, bash 와 비슷하게 보인다; 안전한 방법은 이미 Config.in 파일들 안에서 사용된 형태만을 사용하는 것이다. (자세한 사항은 Documentation/kbuild/config-language.txt 파일을 보기 바란다) 설정 파일을 작성한 후에 테스트를 위해 (정적 parser 를 이용하는) make xconfig 를 최소한 한번 이상 실행시켜 보는 것이 좋다.

    (CONFIG_ 로 시작하는) 설정 변수는 Y 나 N 의 둘 중의 하나의 값을 가진다. 삼상 함수 (tristate function) 도 이와 동일하지만 CONFIG_MODULE 이 활성화 된 경우, M 을 택할 수 있다. (이 경우 설정 변수 이름은 CONFIG_FOO 가 아닌 CONFIG_FOO_MODULE 이 될 것이다)

    또는 당신의 옵션을 CONFIG_EXPERIMENTAL 이 활성화 된 경우에만 보여주게 하고 싶을 수도 있다; 이것은 사용자에게 경고를 해 주는 것이다. 이 밖에도 여러가지 일들이 가능하다; 아이디어를 얻기 위해 여러 Config.in 파일을 살펴보라.

  • Makefile 을 수정하라: 설정 변수들은 여기서 읽어올 수 있으므로 ifeq 를 이용하여 선택적으로 컴파일 하도록 조정할 수 있다. 당신의 파일이 심볼을 공개한다면 export-objs 부분에 파일 이름을 추가해서 genksyms 가 심볼을 찾을 수 있게 한다.

    /!\ 시스템을 구성할 커널에서는 심볼을 공개하는 객체는 반드시 유일한 이름을 가져야 한다는 제한사항이 있다. 만약 당신의 객체가 유일한 이름을 가지지 않는다면, 유일한 이름을 가지는 객체를 생성하여 EXPORT_SYMBOL() 문을 그곳으로 옮기는 방법이 일반적이다. 이것은 몇몇 시스템에서 ksyms 로 끝나는 공개된 객체들이 존재하는 이유이다.

  • 당신의 의견을 Document/Configure.help 파일에 작성하라. 비호환성과 문제점 등을 여기에 기록한다. 그리고 마지막에 잘 모르는 사람들을 위해 "if in doubt, say N" (혹은 "Y" 가 될수도 있다) 과 같은 안내문구를 남겨놓은 것이 중요하다.

  • 만약 당신이 중요한 공헌을 했다면 (보통 하나 이상의 파일에 대해 작업했을 것이다) CREDITS 파일에 자신을 추가한다. (물론 소스 파일의 가장 위에 당신의 이름이 적혀있어야 한다) MAINTAINERS 는 당신이 수정된 내용에 의해 만들어진 하부 구조에 대해 상담해 주거나 버그에 관한 내용을 듣기를 원한다는 것을 의미한다; 이것은 코드의 특정 부분에 대해 책임을 지는 것 이상의 일이 될 것이다.

  • 마지막으로, Documentation/SubmittingPatches 과 가능한한 Documentation/SubmittingDrivers 파일을 읽는 것을 잊지 말자.


12. Kernel Cantrips


다음은 소스 코드를 살펴보다가 발견한 흥미로운 것들이다. 자유롭게 추가해 주길 바란다.

include/linux/brlock.h:

extern inline void br_read_lock (enum brlock_indices idx)
{
        /*
         * This causes a link-time bug message if an
         * invalid index is used:
         */
        if (idx >= __BR_END)
                __br_lock_usage_bug();

        read_lock(&__brlock_array[smp_processor_id()][idx]);
}

include/linux/fs.h:

/*
 * Kernel pointers have redundant information, so we can use a
 * scheme where we can return either an error code or a dentry
 * pointer with the same return value.
 *
 * This should be a per-architecture thing, to allow different
 * error and pointer decisions.
 */
 #define ERR_PTR(err)    ((void *)((long)(err)))
 #define PTR_ERR(ptr)    ((long)(ptr))
 #define IS_ERR(ptr)     ((unsigned long)(ptr) > (unsigned long)(-1000))

include/asm-i386/uaccess.h:

#define copy_to_user(to,from,n)                         \
        (__builtin_constant_p(n) ?                      \
         __constant_copy_to_user((to),(from),(n)) :     \
         __generic_copy_to_user((to),(from),(n)))

arch/sparc/kernel/head.S:

/*
 * Sun people can't spell worth damn. "compatability" indeed.
 * At least we *know* we can't spell, and use a spell-checker.
 */

/* Uh, actually Linus it is I who cannot spell. Too much murky
 * Sparc assembly will do this to ya.
 */
C_LABEL(cputypvar):
        .asciz "compatability"

/* Tested on SS-5, SS-10. Probably someone at Sun applied a spell-checker. */
        .align 4
C_LABEL(cputypvar_sun4m):
        .asciz "compatible"

arch/sparc/lib/checksum.S:

/* Sun, you just can't beat me, you just can't.  Stop trying,
         * give up.  I'm serious, I am going to kick the living shit
         * out of you, game over, lights out.
         */


13. 감사의 글


아이디어를 주고, 내 질문에 답해 주었으며, 실수를 고쳐주고, 내용을 풍부하게 해 주는 등 많은 일을 도와준 Andi Kleen 에게 감사한다. 철자를 확인해 주고, 불명료한 부분에 대해 지적해 준 Philipp Rumpf 에게도 감사한다. disable_irq() 부분을 잘 정리해 준 Werner Almesberger 와, 특허권 보호 신청? (caveat) 을 추가해준 Jes Sorensen 와 Andrea Arcangeli 에게도 감사한다. Michael Elizabeth Chastain 은 설정 부분에 대해 검사해주고 내용을 추가해 주었다. DocBook 을 가르쳐준 Telsa Gwynne 에게도 감사한다.
----
  • [1] 역자주: 프로세스가 커널 모드에서 동작하고 있는 상태를 말하는 것이다. 이 글을 읽는 독자들이 커널영역에서 프로그래밍을 하기 때문에 이렇게 부르는 것 같다.
  • [2] 역자주: 하나의 tasklet 이 여러 CPU 에서 중복되어 실행되지 않는 것을 보장한다.
  • [3] 역자주: 정수 연산을 말하는 것이다.
  • [4] Memory Management Unit
  • [5] 역자주: 각 CPU 마다 local_bh_count 라는 값을 유지한다
  • [6] 역자주: i386 에서는 동일한 값이다



sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2005-06-17 09:48:21
Processing time 0.0449 sec