1.1. 시작하기전에


image00.png
[PNG image (16.31 KB)]


리눅스 커널은 "리누스 토발즈"가 개발하여 1991년에 처음으로 v0.01이 Copyleft로 공개되어진 커널입니다. 현재는 v4.11.3 까지 개발중에 있으며 전세계 수많은 개발자들이 협력하여 커널이 개발되고 있습니다.

리눅스 커널의 주요 특징은 "멀티프로세서", "멀티테스킹", "멀티유저", "다양한 아키텍쳐지원", "페이징", "저장장치를 위한 동적캐시", "공유 라이브러리", "POSIX 1003.1호환", "여러형태의 실행파일형식 지원", "수치연산 에뮬레이션", "다양한 키보드 및 언어지원", "TCP/IP, SLIP, PPP등 다양한 네트웍 계층", "BSD Socket", "System V 기반 IPC", "가상콘솔"등이 있습니다.

본 문서는 개인적인 해석관점에서 작성되었으므로 사실과 다른 부분이 있을지도 모릅니다. 만약 올바르지 않은 사항이 있다면 알려주세요.

1.2. 리눅스 커널소스의 구조

리눅스 커널의 소스는 http://www.kernel.org/ 에서 옛날버젼부터 최근버젼까지 모두 구할수 있습니다.

리눅스 커널소스를 다운로드 받아서 압축을 해제하면 몇몇의 하위 디렉토리가 보이는데 주요 디렉토리는 다음과 같습니다.
  • Documentation : 기본적인 Kernel을 이해하는데 필요한 기초적인 문서들이 여기 있습니다.
  • kernel : 커널의 핵심적인 부분인 시스템콜, 스케쥴러, 시그널 처리등의 코드가 들어있습니다.
  • ipc : Process간의 통신, Semaphore, Shared memory, Message Queue등의 IPC관련 코드가 들어있습니다.
  • lib : 커널에서 사용하는 여러가지 라이브러리 함수들이 구현되어 있습니다.
  • mm : 메모리 관리에 대한 코드가 들어있습니다.
  • scripts : 커널빌드 및 유지에 필요한 script들이 들어있습니다. 코드의 의존성을 스캔하는 역할도 합니다.
  • arch : 아키텍쳐에 의존적인 사항들이 들어있습니다. 보통 아키텍쳐이식은 이 부분에서 구현됩니다.
  • fs : VFS Interface에 따르는 구조에 기반한 여러 파일시스템 지원코드가 들어있습니다.
  • init : 커널이 초기 수행되기 위한 절차가 구현되어 있습니다.
  • net : 다양한 네트웍 프로토콜에 대한 스택들이 구현되어 있습니다.
  • driver : 장치에 대한 디바이스 드라이버가 들어있습니다.

1.3. 리눅스 커널을 빌드하기 위한 준비단계

우선 커널을 빌드하기 위에서 필요한 최소 요구사항에 대한 것은 linux/Documentation/Changes 파일을 보면 명시되어 있습니다.

현재 Linux kernel v2.6.34에서는 다음과 같은 사항들이 요구되는것을 볼수 있습니다.
o  Gnu C                  3.2                     # gcc --version
o  Gnu make               3.80                    # make --version
o  binutils               2.12                    # ld -v
o  util-linux             2.10o                   # fdformat --version
o  module-init-tools      0.9.10                  # depmod -V
o  e2fsprogs              1.41.4                  # e2fsck -V
o  jfsutils               1.1.3                   # fsck.jfs -V
o  reiserfsprogs          3.6.3                   # reiserfsck -V 2>&1|grep reiserfsprogs
o  xfsprogs               2.6.0                   # xfs_db -V
o  squashfs-tools         4.0                     # mksquashfs -version
o  btrfs-progs            0.18                    # btrfsck
o  pcmciautils            004                     # pccardctl -V
o  quota-tools            3.09                    # quota -V
o  PPP                    2.4.0                   # pppd --version
o  isdn4k-utils           3.1pre1                 # isdnctrl 2>&1|grep version
o  nfs-utils              1.0.5                   # showmount --version
o  procps                 3.2.0                   # ps --version
o  oprofile               0.9                     # oprofiled --version
o  udev                   081                     # udevinfo -V
o  grub                   0.93                    # grub --version
o  mcelog                 0.6
o  iptables               1.4.1                   # iptables -V


1.4. 커널의 빌드옵션 설정

커널의 빌드관련 옵션을 설정하기 위해서는 크게 3가지가 많이 사용됩니다.
make config    # console상에서 질의/답변을 통한 대화식의 설정으로 상당한 집중을 요하는 방법입니다.
make menuconfig # console상에서 메뉴방식으로 설정하는 방법으로 가장 널리 사용됩니다.
make xconfig # X Windows를 이용중일때 사용할수 있는 방법으로 그래픽컬한 메뉴로 설정할수 있습니다.


image02.png
[PNG image (8.57 KB)]

위와 같이 3가지 방법중에서 편한것으로 설정하고 저장하게 되면 .config 파일이 생성되는데 다음과 같은 형태로 만들어질겁니다. 이것을 직접 수정하셔서 설정하셔도 됩니다. Embedded system에서는 "arch/" 하위에 특정 Board에 맞는 pre-config 파일들이 존재할수도 있는데 자신의 Board에 맞는게 있다면 해당 pre-config파일을 복사해서 .config로 저장하여 설정을 마칠수도 있습니다.
CONFIG_X86=y
...
CONFIG_MK7=y
...
CONFIG_MODULES=y
...
CONFIG_NET=y
...
# CONFIG_ACPI_DEBUG is not set
...
CONFIG_PARPORT=m
...


1.5. 커널의 빌드

커널의 빌드옵션에 대한 설정을 마쳤으면 다음과 같은 절차에 의해서 빌드하게 됩니다.
make dep        # v2.4 이하버젼의 커널에서만 필요하며 v2.6이상의 커널에서는 이 부분을 필요없습니다.
make modules
make bzImage
make modules_install


위의 빌드절차는 일반적인 PC용 커널로 빌드하는 방식이라고 할수 있으며 실제 Embedded환경을 위한 커널을 빌드하기 위해서는 Cross build를 해야 하기 때문에 추가적인 Cross compiler에 관한 정보를 지정해주어야 합니다. 예를 들자면 Target architecture가 mips이고 해당 Cross compiler에대한 prefix가 mips-linux- 라고 한다면 다음과 같이 빌드할수 있습니다. 경우에 따라서는 ARCH, CROSS_COMPILE등의 make변수가 Makefile에 직접 수정되어 사용하는 경우도 있는데 별도로 Architecture vendor로부터 받는 커널들이 대부분 그러합니다.
make ARCH=mips CROSS_COMPILE=mips-linux- modules bzImage modules_install

보통 개발하는 컴퓨터를 Host라고 지칭하고 실제로 빌드된 이미지가 수행되는 시스템을 Target이라고 지칭하게 됩니다. Cross compiler는 Host에서 실행되지만 그것이 만들어내는 이미지 파일은 Target에서 수행되는것으로 정의할수 있습니다. 이 경우 필요한 인자는 Host, Target에 대한 정의와 Cross compiler의 파일명에 붙는 일관적인 Prefix가 있습니다. 그 밖에도 여러 요소들이 있는데 Makefile내의 Make 변수들로 그러한 사항을 조절하여 최종 Target에서 수행될수 있는 커널이미지를 얻게 됩니다. (Host의 경우는 커널의 Makefile내에서 자동검출하는 shell 명령이 이용되기 때문에 특별한 경우가 아니라면 지정할 필요가 없습니다.)

1.6. 커널이미지의 구조

image03.png
[PNG image (992 Bytes)]
  • Bootsector부분은 Floppy boot를 위한 boot code가 들어있으며 메모리상으로 탑재된 후에는 다양한 용도의 공간으로 활용됩니다. (예를 들면 Kernel argument영역)
  • Setup부분은 Memory의 기초적인 사용을 위한 설정과 압축을 해제하기 위한 사전단계를 설정하는 역할을 합니다.
  • Head+Misc는 압축된 커널이미지 영역을 해제하는 역할을 합니다.

1.7. 커널의 동작모드

인텔 CPU의 경우는 특권레벨이 총 4단계까지 하드웨어적으로 구현될수 있고 대부분의 마이크로프로세서들도 보통 2단계 이상의 특권레벨을지원합니다

유닉스계열의 운영체제는 대부분 2단계의 특권레벨을 구현하며 리눅스 역시 2단계의 특권레벨만을 사용합니다.

리눅스의 특권레벨은 응용프로그램이 시스템을망가뜨리는것을 방지할수 있도록 커널모드와 사용자모드 두가지의 동작모드로 정의하며 각각의 동작모드는 다음과 같은 동작을 허용합니다.
  • 커널모드 : 직접적인 하드웨어의 접근에 필요한 자료구조로의 접근 및 IRQ, DMA, I/O 등을 처리
  • 사용자모드 : 일반 응용프로그램들

image04.png
[PNG image (863 Bytes)]

이러한 동작모드간의 전환은 크게 다음의 두가지 경우에 발생합니다. 결국 이것은 콜게이트라는 하드웨어적인 특권레벨 전환규칙에 의해서 처리되게 됩니다. 즉, 콜게이트 진입시 사용자모드에서 커널모드로 진입할수 있게 되며 콜게이트에서 반환될때 커널모드로부터 빠져나올수 있게 됩니다. (콜게이트를 정의하는 이유는 직접적으로 사용자모드의 프로그램이 커널의 아무 영역이나 호출하는것을 방지할수 있기 때문입니다.)
  • 시스템콜을 호출하는 경우
  • IRQ(Exception포함)가 발생할때

특정 시점에서 다음과 같은 상태중에 하나에 놓이게 되며 선점될수 있는 조건이 제한되는데 사용자모드를 제외한 다른 상태들은 오직 자신보다 상위에 있는 상태에 의해서만 선점될수 있게 됩니다. 그리고 이러한 선점조건은 CPU별로 독립적으로 수행됩니다.
  • 특정 프로세스와 관련없는 H/W Interrupt 처리상태 (irq, top-half)
  • 특정 프로세스와 관련없는 S/W Interrupt 처리상태 (softirq, tasklet, bottom-half)
  • 특정 프로세스와 관련하여 커널모드에서의 동작
  • 사용자 모드에서 특정 프로세스의 수행

    예를 들면 softirq가 수행되는중에는 다른 softirq는 이를 선점할수 없지만 H/W irq가 발생하는 경우에는 선점될수 있습니다.

1.8. 시스템콜 (System Call)

시스템콜은 커널모드상에서 커널의 특정 루틴을 사용하기 위한 특별한 함수형태라고 할수 있으며 다음과 같은 처리를 위한 용도로 사용됩니다. (결국 커널자원을 제한적인 접근경로를 통해서만 호출할수 있도록 하는 역할)
  • I/O 장치의 접근요청
  • 커널의 자원에 대한 정보요청 (ProcessID, Scheduler의 정책, ...)
  • 프로세스 제어요청 (fork, exec)
  • 특정 동작요청 (chdir, kill, brk, signal, ...)

결국 시스템콜은 사용자모드의 입장에서는 시스템의 자원을 사용하기 위한 유일한 경로의 수단으로 볼수 있습니다. 이를 통해서 사용자모드에서 실행되는 응용프로그램은 직접적으로 시스템자원을 접근하지 않고 시스템콜을 통하여 커널로부터 자원을 중재하여 제공받게 됩며 커널은 자원을 관리하는 본연의 기능을 충실히 수행할수 있어야 합니다.

image06.png
[PNG image (2.9 KB)]

사용자 문맥(User context)는 사용자 모드(User mode)와 다른 개념의 용어입니다. 이것은 시스템콜이나 트랩등에 의해서 진입된 프로세스의 커널모드동작상태를 말하며 비선점성을 가집니다.

1.9. 메모리의 분할과 페이징

리눅스는 크게 4가지 성격의 메모리로 분할되어 설정되는데 커널모드의 Code영역과 Data영역, 그리고 사용자모드의 Code영역과 Data영역으로 분할되어 관리되며 커널공간과 사용자공간은 서로 겹치지 않게 관리됩니다.

각 프로세스는 독립적인 Page directory를 가지고 페이징을 수행하도록 되어 있어서 각 프로세스간의 메모리 간섭은 발생하지 않도록 보호됩니다.

보통 Page directory를 가르키는 register가 변경이 일어나면 TLB(Translation Lookaside Buffers)가 비워지도록 되어 있어서 태스크간의 문맥전환시 TLB를 관리하는 경우는 특별한 아키택쳐를 제외하고는 없다고 볼수 있습니다.

인텔CPU의 경우는 Page directory와 Table 두가지 Level로 Page를 관리하도록 하드웨어적으로 지원되며 Alpha나 UltraSparc의 경우는 Page directory, Table, Offset 세가지 Level을 제공합니다. 리눅스는 이를 보다 효율적으로 다루기 위해서 아키텍쳐와 관계없이 3-Level paging으로 추상화하여 관리합니다.

1.10. 멀티태스킹

리눅스의 태스크는 다음의 상태중에 한가지에 놓이게 됩니다.
  • TASK_RUNNING : 실행가능한 상태
  • TASK_INTERRUPTABLE : 시그널이나 시스템 자원을 기다리는 상태
  • TASK_UNINTERRUPTABLE : 시스템 자원을 기다리고 있고 대기큐에서 기다리는 상태
  • EXIT_ZOMBIE : 부모프로세스가 정보를 기다리고 있는 상태의 자식프로세스의 종료상태
  • TASK_STOPPED : 디버깅중인 태스크
  • TASK_TRACED : 디버거에 의해서 감시중인 상태
  • EXIT_DEAD : 프로세스를 시스템에서 제거하는 중인 상태
  • TASK_KILLABLE : 치명적인 Signal을 받을때 Wakeup될수 있는 상태 (UNINTERRUPTABLE속성에 WAKEKILL속성)

image05.png
[PNG image (4.57 KB)]

리눅스는 태스크마다 주어진 타임슬라이스에 따른 counter를 가지고 있고 Timer IRQ로부터 이것이 감소되어 0이 되면 태스크의 문맥전환이 일어나게 됩니다. 또한 특정 시스템 자원이나 시그널을 기다리는 상태에 진입할때도 태스크 문맥이 전환됩니다.

각 IRQ가 발생할때 마다 해당 IRQ를 그 시점에서 모두 처리하려고 하면 경우에 따라서 불필요한 자원소모와 함께 긴급한 성격의 IRQ처리 작업이 지연되는 경우가 발생할수 있습니다. 이를 방지하고 성능을 향상하기 위해서 Bottom Half에서 처리되도록 Task-Queue로 작업을 미루도록 하여 긴급한 IRQ처리는 즉시 처리하지만 그렇지 않은 경우는 후에 처리하도록 하고 있습니다.

fork는 새로운 Task를 생성하는데 사용되는 System call 인데 Parent Task로부터 Child Task로 대부분의 자료구조들이 복사되어져서 만들어집니다. 이때 Child Task의 모든 Page는 READ+EXEC권한으로 설정되어 Parent task의 Page와 같은 곳을 가르키게 됩니다. 그리고 Child Task에서 해당 Page에 Write동작을 수행하게 되면 Page fault가 발생되면서 별도의 page를 가르키도록 독립적인 page를 사용하게 됩니다. 이것한 구조는 Copy on write방식이라고 부르며 리눅스는이를 활용하여 메모리의 복사량이 적고 빠르게 Child task를 생성하면서 적은 메모리를 사용할수 있도록 도모할수 있게 되었습니다.

image07.png
[PNG image (3.47 KB)]

1.11. 커널개발시 주의사항

사용자모드에서는 잘못된 메모리 접근에 대한 메모리 보호가 되지만 커널모드에서는 보호되지 않기 때문에 시스템의 동작이 원치 않는 결과로 망가질수 있습니다.

커널모드에서는 FPU의 상태정보를 저장하지 않으며 관련된 MMX기술등에 대한 연산도 함께 저장되지 않기 때문에 부동소수점 연산은 피하는게 좋으며 만약 사용해야 한다면 인터럽트를 금지하고 상태정보를 저장하는등의 조치를 하여 구현하여야 합니다.

커널모드의 스택은 크기가 엄격한 제한으로 설정되어 있어서 재귀적호출이나 과도한 스택사용은 피하도록 하고 동적할당을 사용하도록 구현하여야 합니다.

printk() 함수는 내부적으로 1K정도의 버퍼를 사용하도록 구현되어 있고 이를 넘어가는지를 검사하는 부분이 구현되어 있지 않기 때문에 주의해야 합니다.

사용자 영역과 커널 영역간의 메모리 교환은 반드시 copy_to_user, copy_from_user 함수를 통해서 교환하며 인터럽트가 비활성화된 상태이거나 spinlock상태에 놓여있을때는 이 함수는 사용될수 없다는 점에 주의하여야 합니다. (왜냐하면 이 함수들 자체가 sleep상태로 들어갈수 있도록 되어 있기 때문입니다.)

Dead lock을 회피하기 위한 기본 규칙으로 다음과 같은 상황이 아닌 경우의 모든 경우에 sleep이 될수 있는 어떠한 루틴도 호출되어서는 안됩니다. (주의할 것은 일부 커널함수들이 sleep에 들어갈수 있도록 묵시적으로 구현되어 있기 때문에 항상 sleep여부를 파악하고 사용할 필요가 있습니다.)
  • 사용자 문맥(User context)에 있는 경우
  • 어떠한 spinlock도 소유하지 않은 경우
  • 인터럽트가 활성화하는 경우

리눅스 커널에서는 C++을 사용할수는 있으나 실행환경에 대한 충분한 C++지원코드를 탑재하고 있지 않기 때문에 명백하지 않은 실행을 야기할수 있어 권장하지 않고 있습니다.

1.12. 메모리의 할당

일반적으로 C언어의 표준 라이브러리 함수인 malloc, free함수는 커널에서 사용하지 않습니다. 프로세스가 동작하는 사용자프로세스에서는 glibc에 의해서 할당루틴을 제공받아서 malloc, free가 제공됩니다. 그리고 특별히 할당과 해제에 있어서 심각한 고려사항들이 발생하지 않습니다. 그러나 커널의 경우는 상황이 다르며 다음과 같은 상황에 대해서 면밀한 고려가 필요합니다.
  1. 커널에서는 물리적인 메모리를 직접 접근할수도 있고 MMU(메모리 관리장치)를 통한 접근도 고려해야 합니다.
  2. 메모리의 할당과 해제는 빈번하게 발생하며 이로 인하여 메모리의 단편화가 발생할수 있습니다. 이것을 방지하기 위해서 커널에서는 PAGE_SIZE와 PAGE_SHIFT라는 값으로 관리됩니다. (보통 PAGE_SHIFT는 12를 사용하며 1<<12 즉, 4KBytes가 PAGE_SIZE로 정의하여 사용합니다. 이것은 하드웨어적인 제약에 의해서 결정됩니다.)
  3. 응용프로그램에서는 malloc, free함수에 의해서 가상메모리를 할당받기 때문에 실패할 가능성이 거의 없습니다. 그렇지만 커널에서는 요구되는 메모리크기를 할당하는데 부족하거나 단편화로 인하여 적절한 메모리의 단편화 제거동작이 필요할수 있습니다. 이에 따라서 실패하였을때 할당이 성공할때까지 대기하면서 해당 메모리를 확보하도록 동작하던지 아니면 실패에 따른 복귀를 하던지 메모리의 요구성격에 따라서 고려되어야 합니다.
  4. 가상메모리기법에 의해서 실제 접근하고자 하는 메모리가 물리적 메모리가 아닌 보조저장장치에 있을수가 있는데 이러한 경우에 대한 추가적인 처리를 고려하여야 합니다.
  5. DMA같은 연속된 물리적 메모리 주소가 필요한 경우 이를 고려한 메모리관리루틴이 필요합니다. (요즘에는 가상 DMA를 이용하는 경우도 있다고 하는데 흔치는 않은것 같습니다.)
  6. Interrupt상황에서 메모리를 할당해야 하는 경우 일반적으로 메모리가 부족할때 해당 Interrupt구간에서 프로세스를 잠들게 하면 안되는 경우가 있으며 이를 고려하여야 합니다.

리눅스 커널은 기본적으로 __get_free_pages, free_page함수를 제공하여 PAGE_SIZE의 승수에 해당하는 메모리를 할당받거나 해제하는것이 기본 할당자로 제공됩니다. 이 함수는 커널의 할당특성을 만족시키기 위해서 flag(gfp_mask)를 추가로 인자로 넘겨받습니다. 이 함수는 실제로 승수인자를 MAX_ORDER값으로 제한받기 때문에 PAGE_SIZE * (1 << MAX_ORDER) 보다 큰 메모리는 할당받을수 없습니다. (어차피 승수가 커지면 단편화로 인하여 실제로 메모리가 더 있음에도 불구하고 실패할 확률은 높아집니다. 보통 MAX_ORDER는 11을 사용합니다.)

  • GFP_KERNEL : 이 flag가 사용되면 할당이 항상 성공하도록 요구하는 것으로 만약에 메모리가 모자란 경우에는 할당자에서 프로세스를 잠들게 하고 메모리가 확보될때 프로세스가 깨워집니다.. 하지만 인터럽트 구간내에서는 프로세스가 잠들면 안되므로 인터럽트 처리구간내에서는 사용하지 않아야 합니다.
  • GFP_ATOMIC : 이 flag가 사용되면 메모리가 부족할때 즉시 NULL을 반환하도록 요구하는것으로 프로세스가 잠드는 문제가 없기 때문에 인터럽트 구간내에서도 사용가능하게 됩니다. 단, 메모리 할당에 실패하는 경우에 대한 충분한 고려가 반드시 필요합니다.
  • GFP_DMA : 연속된 물리적 메모리를 할당고자 요구할때 사용합니다.

    unsigned long s_page;
    unsigned int s_order;
    
    s_order = MAX_ORDER;
    s_order = get_order((4 << 10) * (1 << s_order));
    if(s_order > ((unsigned int)(MAX_ORDER))) {
        printk("<0>too big order ! (%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
    }
    
    /* unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) */
    s_page = __get_free_pages(GFP_KERNEL, s_order);
    if(s_page != 0ul) {
        printk("<0>__get_free_pages success. (order=%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
        free_page(s_page);
    }
    else {
        printk("<0>__get_free_pages failed ! (order=%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
    }
    


kmalloc, kfree는 malloc, free함수와 매우 유사한 함수이지만 근본적으로 커널메모리의 특성에 따른 메모리할당을 구현하기 위해서 __get_free_pages함수처럼 flag(gfp_mask)를 추가인자로 사용합니다. __get_free_pages함수는 승수를 인자로 받지만 kmalloc함수는 size인자를 직접 받기 때문에 매우 편리하며 비교적 커널내에서 가장 많이 사용되는 할당함수라고 보시면 됩니다. 그러나 kmalloc 역시 MAX_ORDER에 따른 할당크기에 제약이 있다는 점에 주의해야 합니다.

void *s_page;

/* void *kmalloc(size_t size, gfp_t flags) */
s_page = kmalloc((size_t)1234u, GFP_KERNEL);
if(s_page != ((void *)0)) {
    printk("<0>kmalloc success.\n");

    /* void kfree(const void *objp) */
    kfree((const void *)s_page);
}
else {
    printk("<0>kmalloc failed !\n");
}


vmalloc, vfree는 size인자외에 특별한 인자를 사용하지 않으며 malloc, free함수와 가장 유사성을 띈 함수라고 할수 있습니다. __get_free_pages, kmalloc함수는 할당크기에 제약이 존재하지만 vmalloc은 가상메모리 공간을 할당하기 때문에 크기에 대한 물리적으로 허용하는 이상 크기제약은 없습니다. 하지만 인터럽트구간내에서 사용할수 없으며 가상메모리관리루틴이 수행되기 때문에 __get_free_pages, kmalloc에 비하여 상대적으로 느리고 연속적인 물리적 메모리를 기대할수 없다는 단점이 있습니다.

void *s_vpage;

/* void *vmalloc(unsigned long size) */
s_vpage = vmalloc(1234ul);
if(s_vpage != ((void *)0)) {
    printk("<0>vmalloc success.\n");

    /* void vfree(const void *addr) */
    vfree((const void *)s_vpage);
}
else {
    printk("<0>vmalloc failed !\n");
}


시스템이 순간적으로 대용량의 데이터를 처리할때 메모리가 부족해지며 가상메모리가 사용되면서 시스템의 성능저하가 발생할수 있습니다. 시스템이 원활하게 동작하도록 하려면 일부처리루틴에서는 일정량이 메모리를 미리 확보하여 메모리가 부족할때 이를 사용하는 방식도 필요성이 대두되었습니다. 그래서 고안된것이 바로 Memory pool관리 API입니다.

위의 할당자들에 대한 종합적인 예제는 다음과 같습니다.

#include <linux/module.h>
#include <linux/init.h>

#include <linux/mempool.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>

#if !defined(mzdriver_mempool_element_t)
typedef struct mzdriver_mempool_element_ts {
    unsigned char __dummy;
    unsigned char __shadow_area;
}__mzdriver_mempool_element_t;
# define mzdriver_mempool_element_t __mzdriver_mempool_element_t
#endif

static void *mzdriver_mempool_alloc_handler(gfp_t s_gfp_mask, void *s_pool_data)
{
    void *s_result;

    /* void *kmalloc(size_t size, gfp_t flags) */
    s_result = (void *)kmalloc(sizeof(mzdriver_mempool_element_t), s_gfp_mask);
    if(unlikely(s_result == ((void *)0))) {
        printk("<0>mempool_alloc failed !\n");
        return((void *)0);
    }

    printk("<0>mempool_alloc success.\n");
    return(s_result);
}

static void mzdriver_mempool_free_handler(void *s_element, void *s_pool_data)
{
    if(s_element == ((void *)0)) {
        printk("<0>mempool_free EINVAL !\n");
        return;
    }

    /* void kfree(const void *objp) */
    kfree((const void *)s_element);

    printk("<0>mempool_free success.\n");
}

static int __init mzdriver_init(void)
{
    printk("<0>Insert mzdriver module.\n");

    do { /* __get_free_pages, free_page */
        unsigned long s_page;
        unsigned int s_order;

        s_order = MAX_ORDER;
        s_order = get_order((4 << 10) * (1 << s_order));
        if(s_order > ((unsigned int)(MAX_ORDER))) {
            printk("<0>too big order ! (%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
        }

        /* unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) */
        s_page = __get_free_pages(GFP_KERNEL, s_order);
        if(s_page != 0ul) {
            printk("<0>__get_free_pages success. (order=%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
            free_page(s_page);
        }
        else {
            printk("<0>__get_free_pages failed ! (order=%u/%u)\n", s_order, (unsigned int)(MAX_ORDER));
        }
    }while(0);

    do { /* mempool */
        void *s_pool_data;
        mempool_t *s_mempool;
        int s_count, s_max_pool;

        void *s_pool[ 3 ];

        s_pool_data = (void *)0;
        s_max_pool = sizeof(s_pool) / sizeof(void *);

        /* mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data) */
        s_mempool = mempool_create(0, mzdriver_mempool_alloc_handler, mzdriver_mempool_free_handler, s_pool_data);
        if(s_mempool != ((mempool_t *)0)) {
            printk("<0>mempool_create success.\n");

            for(s_count = 0;s_count < s_max_pool;s_count++) {
                /* void * mempool_alloc(mempool_t *pool, gfp_t gfp_mask) */
                s_pool[s_count] = mempool_alloc(s_mempool, GFP_KERNEL);
                if(s_pool[s_count] != ((void *)0)) {
                   printk("<0>mempool_alloc[%d] success.\n", s_count);
                }
                else {
                   printk("<0>mempool_alloc[%d] failed !\n", s_count);
                }
            }

            printk("<0>mempool_alloc end.\n");

            for(s_count = 0;s_count < s_max_pool;s_count++) {
                if(s_pool[s_count] != ((void *)0)) {
                    /* void mempool_free(void *element, mempool_t *pool) */
                    mempool_free(s_pool[s_count], s_mempool);
                    printk("<0>mempool_free[%d] success.\n", s_count);
                }
                else {
                    printk("<0>mempool_free[%d] ignored !\n", s_count);
                }

            }

            /* void mempool_destroy(mempool_t *pool) */
            mempool_destroy(s_mempool);
        }
        else {
            printk("<0>mempool_create failed !\n");
        }
    }while(0);

    do { /* vmalloc, vfree */
        void *s_vpage;

        /* void *vmalloc(unsigned long size) */
        s_vpage = vmalloc(1234ul);
        if(s_vpage != ((void *)0)) {
            printk("<0>vmalloc success.\n");

            /* void vfree(const void *addr) */
            vfree((const void *)s_vpage);
        }
        else {
            printk("<0>vmalloc failed !\n");
        }
    }while(0);

    return 0;
}

static void __exit mzdriver_exit(void)
{
    printk("<0>Remove mzdriver module.\n");
}

module_init(mzdriver_init);
module_exit(mzdriver_exit);

/* End of source */


1.13. current 전역변수

current 전역변수는 현재 실행중인 task구조에 대한 포인터입니다. 이것은 오직 사용자 문맥(User context)에서만 사용할수 있는데 프로세스가 시스템콜을 호출하는 경우 Current 변수는 현재 시스템콜을 호출한 프로세스에 대한 task 구조를 가르키게 됩니다. (실제로 current는 전역변수인것처럼 사용하기는 하지만 실제로는 매크로로 구현되어 있습니다.)

1.14. 짧은 지연을 목적으로 하는 udelay, mdelay함수

매우 짧은 시간을 지연하기 위해서는 udelay 함수를 사용할수 있습니다. 하지만 이것이 수 msec이상이 되면 mdelay를 사용하여야 합니다. 그렇지 않으면 overflow로 인하여 시스템이 원치 않는 지연을 갖게 됩니다. mdelay역시 수초이상의 지연에는 적합하지 않으며 수 msec이하에서만 사용하는것이 좋습니다. 만약 비교적 긴 시간을 지연시키기 위해서는 schedule_timeout함수를 사용할수 있습니다. 결국 긴 시간의 Busy sleep은 커널모드에서는 바람직하지 않다는게 커널개발자들의 의견입니다.

1.15. local_irq_save/restore 함수

인터럽트를 금지하거나 진입가능하도록 하는 함수이며 재진입이 가능한 함수입니다. 만약 재진입에 대한 고려없이 명확하다면 local_irq_enable/disable함수를 사용할수 있습니다.

1.16. local_bh_enable/disable 함수

이것은 S/W irq를 활성화하거나 비활성화 하는 함수이며 재진입이 가능합니다. 결국 softirq, tasklet, bottm-half를 금지하거나 진입하도록 하게 됩니다.

1.17. 모듈의 구현

리눅스 커널 디바이스 드라이버는 드라이버를 초기화하는 역할을 담당할 함수와 해제를 담당하는 함수로 기본구성됩니다. 2.4 커널 이하 버젼에서는 init_module, exit_module로 그 함수명이 고정되어 개발되어 왔었습니다. 그러나 지금 2.6 커널에서는 함수명은 자유롭게 지정하고 대신 module_init, module_exit라는 매크로함수를 통해서 해당 초기화 및 해제함수를 가르키도록 수정되었습니다.

여기서 module_init 과 module_exit 매크로는 include/linux/init.h 에서 정의가 되어 있으며 그 원형은 다음과 같습니다.

#define __init __attribute__ ((__section__ (".init.text"))) __cold

#ifdef MODULE
# define __exit __attribute__ ((__section__(".exit.text"))) __cold
#else
# define __exit __attribute_used__ __attribute__ ((__section__(".exit.text"))) __cold
#endif

#define __define_initcall(level,fn) \
    static initcall_t __initcall_##fn __attribute_used__ \
    __attribute__((__section__(".initcall" level ".init"))) = fn

#define device_initcall(fn) __define_initcall("6",fn)

#define __exit_call __attribute_used__ __attribute__ ((__section__ (".exitcall.exit")))
#define __initcall(fn) device_initcall(fn)
#define __exitcall(fn) static exitcall_t __exitcall_##fn __exit_call = fn

#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);


결국 이것은 gcc에서 attribute((section("<section name>")))기능을 빌어서 특정 Section에 해당 심볼의 변수나 함수를 배치하는것이 목적입니다. (Linker Script 참고 : arch/sh/kernel/vmlinux.lds.S) 이것은 RunTime시에 커널의 init/main.c 에서 do_initcalls 함수에서 호출되도록 동작하며 rest_init함수에서 init 함수를 Kernel Thread로 Schedule 시킨후에 수행되게 됩니다. 가장 간단한 형태의 드라이버 모듈은 다음과 같이 작성할수 있습니다.

/* 모듈 작성시에는 반드시 아래의 header는 항상 포함되어야 합니다.. */
#include <linux/module.h>
#include <linux/init.h>
static int __init mzdriver_init(void)
{
    printk("<0>Insert mzdriver module.\n");
    return 0; /* 반환값이 0인 경우 모듈적재에 성공했다고 처리됩니다. */
}

static void __exit mzdriver_exit(void)
{
    printk("<0>Remove mzdriver module.\n");
}

module_init(mzdriver_init); /* mzdriver_init함수를 모듈의 초기화 함수로 지정합니다.. */
module_exit(mzdriver_exit); /* mzdriver_exit함수를 모듈의 해제함수로 지정합니다. */


이것을 빌드하기 위한 Makefile을 작성하여야 하는데 사실상 모듈빌드부분의 Makefile 작성은 오랜동안 커널개발에 참여하셨던 분들도 만들기 어려워 하는 부분입니다. 때문에 커널개발자들은 이것을 단순화 하도록 Kernel내의 기본 Makefile을 이용하여 빌드하도록 모듈빌드과정을 단순화 하는 작업을 하였습니다. 그래서 다음과 같은 형태의 Makefile을 자신의 입맞에 맞도록 수정하여 빌드하기만 하면 되도록 하였으며 Makefile의 구성은 신경쓰지 않아도 모듈을 개발하는데 문제업도록 단순화 시켰습니다. 아래의 Makefile은 mzdriver.c라는 소스파일을 모듈로 빌드하도록 하는 예제입니다.

ifneq ($(KERNELRELEASE),)
obj-m += mzdriver.o
else
KERNELDIR="/lib/modules/$(shell uname -r)/build"
#KERNELDIR="/usr/src/linux"
.PHONY: modules %
modules %: ;$(MAKE) -C $(KERNELDIR) M="$(abspath .)" $(@)
endif


위에서 보면 KERNELDIR이라는 Make변수는 해당 모듈을 적용하고자 하는 Kernel소스를 가르키도록 하며 해당 KERNELDIR의 Makefile을 이용하여 모듈을 빌드하도록 Makefile을 재귀적으로 호출하는식의 빌드방법입니다.

1.18. 대기큐 (Wait Queues)

대기큐는 특정 조건을 만족할때 문맥에서 깨어나기를 원하는 경우에 사용됩니다.

wait_queue_interruptable() 함수계열이 대기큐로 조건을 삽입하고 해당 조건을 만족하게 되면 문맥에서 깨어나게 됩니다.

그냥 대기큐에 들어있는 문맥을 모두 깨우기위해서는 wake_up() 함수를 호출하여 깨울수 있습니다.

1.19. 원자적 연산

원자적 연산이 보장되는 정수형타입의 크기나 명령의 제한은 해당 Archtecture에 따라서 크게 다릅니다. 이것을 단순히 C언어로 구현할수는 없으며 리눅스는 이것을 매크로 또는 함수로 어셈블리 구현을 통해서 얻어냅니다. 원자적 함수는 다음과 같은것들이 있으며 경우에 따라서 지원되지 않거나 추가적으로 지원되는 함수매크로가 존재할수 있습니다.

  atomic_read(), atomic_set(), atomic_add(), atomic_sub(), atomic_inc(), atomic_dec(), atomic_dec_and_text(), set_bit(), clear_bit(), change_bit(), test_and_set_bit(), test_and_clear_bit(), test_and_change_bit(), ...


원자적 연산에 대한 OP-code가 존재하는 경우는 spinlock() 함수보다 빠를수 있다는 기대를 할수 있지만 32-bit sparc과 같은 시스템에서는 원자적 연산 자체가 spinlock으로 구현되어 훨씬 느릴수도 있으므로 잘 고려해야 합니다.

1.20. 구조체의 특정 멤버만을 초기화 하기 위한 C99 designated initializer

구조체의 수많은 멤버를 모두 초기화 해야 하는 경우도 있지만 대부분은 그 일부만 초기화 해도 되는 경우가 많습니다. 리눅스커널에서는 코드의 초기화가 필요한 부분만 초기화하도록 C99 designated initializer 문법을 사용하는 경우가 많습니다.

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


1.21. 참조카운트

자원을 선점하고 반환하는 과정사이에는 자원의 유효시점을 판단하는 효과적인 방법이 필요합니다.

2개 이상의 자료구조가 상호 연관성이 있는 경우 어느 한쪽의 자료만 일방적으로 해제되는 것을 방지하기 위해서 참조카운트는 가장 효과적인 방법이 될것입니다. 그리고 검색루틴에서 검색된 자원은 일정 부분 해당 자원을 참조하는 부분을 위해서 존재하므로 검색시 참조카운트를 증가시켜 자원해제를 막고 해당 자원을 참조완료후 참조카운트를 감소시켜 이를 안전하게 보호할 수 있게 됩니다.

보통 XXX_hold 함수에 의해서 자원에 대한 참조카운트를 증가시키고 XXX_release 또는 XXX_put 계열의 함수에 의해서 참조카운트를 감소키시도록 하여 참조카운트가 0 이 되는 시점에서 자원의 사용이 모두 완료되었음을 확인할 수 있습니다.

이것은 0 이 되는 시점에서 자원을 즉각 반환(Synchronous, 동기적)하기도 하지만 좀더 지연된 자원반환(Asynchronous, 비동기적)의 형태로써 GC(Garbage Collection) 시점을 두어 일괄적인 자원 반환을 도모하기도 합니다.

이렇게 참조카운트를 사용하면 참조카운트의 증가 및 감소에 대한 균형을 잃지 않도록 관리하는게 가장 중요하며 이러한 균형을 잃게 되면 다음과 같이 2가지의 경우가 발생합니다.
  • 참조카운트 감소를 잊어버린 경우 메모리를 점진적으로 소진시켜버리는 현상이 나타납니다.
  • 참조카운트 증가를 잊어버린 경우 실제 참조하는 시점에 해당 자원이 해제되버릴 수 있어서 매우 치명적인 Kernel panic 이 발생할 수 있습니다.

1.22. 조건분기에 따른 코드배치의 최적화를 도모하는 likely/unlikely


대부분의 조건분기는 조건식의 판단에 따른 참(True) 또는 거짓(False)에 해당하는 수행부를 실행하는 흐름을 만드는데 있습니다.

여기서 조건에 따른 분기의 수행부가 특정 결과에 치우친 수행이 많다는 점과 빈도가 높은 수행부를 어떻게 배치하는가에 따라 성능이 다소 개선될 수 있다는 점이 있다는 것입니다.

즉, 조건분기의 수행부는 참(True)에 따른 수행부와 거짓(False)에 따른 수행부가 반드시 50% : 50% 의 비율로 수행되지 않으며 많은 부분은 한쪽 조건결과에 치우친 수행비율을 가진다는 점에 착안하여 효과적으로 C언어를 통해서 코드배치를 하도록 방법을 고민한 것이 likely 와 unlikely macro 입니다.

아래와 같이 error 처리부는 일반적으로 수행되지 않습니다. 이 경우 unlikely를 이용하여 error처리부의 진입확률이 낮다는 것을 컴파일러에게 의사전달하고 이를 통해서 컴파일러는 코드배치를 좀더 효율적으로 변경하게 됩니다. 반대로 success 처리부는 일반적으로 수행되기 때문에 likely를 사용하여 컴파일러에게 의사전달하여 최적화 하게 됩니다.
int s_check;

s_check = my_func(a, b);
if(unlikely(s_check < 0)) {
    /* error */
}

s_check = my_func2(c, d);
if(likely(s_check >= 0)) {
    /* success */

    return(0);
}
/* error */




/*

[ FrontPage | PrintView | RawView | RSS ]

Copyright ⓒ MINZKN.COM
All Rights Reserved.

MINZKN

----

*/