리눅스 커널 모듈 프로그래밍 가이드

 

리눅스 커널 모듈 프로그래밍 가이드

 

Peter Jay Salzman

 

Ori Pomerantz

 

Copyright © 2001 Peter Jay Salzman

 

2003-04-04 ver 2.4.0

 

번역 운형 cloudshape (at) hanafos.com

 

2003-07-11 ver 2.4.0-trans-0.1

 

Contents

[-]

1 서문
1.1 감사말
1.2 Nota Bene
2 소개
2.1 커널 모듈이란
2.2 어떻게 모듈을 커널에 넣을 것인가?
2.2.1 시작하기 전에
2.2.1.1 모듈 버전
2.2.1.2 X 사용하기
2.2.1.3 컴파일 문제와 커널 버전
3 Hello World
3.1 Hello, World (part 1): The Simplest Module
3.1.1 printk()함수 소개
3.2 커널 모듈 컴파일
3.3 Hello World (part 2)
3.4 Hello World (part 3): The __init and __exit Macros
3.5 Hello World (prt 4): 저작권과 모듈 문서
3.6 커맨드 라인 인자 모듈에 넘기기
3.7 다중 파일 모듈
4 준비 단계
4.1 모듈 vs 프로그램
4.1.1 모듈은 어떻게 시작하고 끝나는가
4.1.2 모듈에 사용 가능한 함수들
4.1.3 사용자 공간 vs 커널 공간
4.1.4 이름 공간
4.1.5 코드 영역
4.1.6 장치 드라이버
4.1.6.1 장치 주 번호와 장치 부 번호
5 문자 장치 파일
5.1 문자 디바이스 드라이버
5.1.1 file_operations 구조체
5.1.2 file 구조체
5.1.3 Registering A Device
5.1.4 장치의 등록해제
5.1.5 chardev.c
5.1.6 여러 커널 버전을 위한 모듈 작성
6 /proc파일 시스템
6.1 /proc File System
7 입력을 위한 /proc의 사용
7.1 입력을 위한 /proc의 사용
8 Talking to Device Files
8.1 Talking to Device Files (writes and IOCTLs)
9 시스템 콜
9.1 시스템 콜
10 Blocking Processes
10.1 Blocking Processes
10.1.1 printk() 대체하기
11 printK()대체하기
11.1 printK()대체하기
12 작업 스케줄링
12.1 작업 스케줄링하기
13 인터럽트 핸들러
13.1 인터럽트 핸들러
13.1.1 인터럽트 핸들러
13.1.2 인텔 아키텍쳐에서의 키보드
14 대칭형 다중 프로세싱
14.1 대칭적 다중 처리(프로세싱)
15 일반적으로 주의할 사항
15.1 흔한 함정
16 부록 A. 2.0에서 2.2로의 변화
16.1 2.0에서 2.2로의 변화
16.1.1 2.0에서 2.2로의 변화
17 변경점

 

1 서문

1.1 감사말

Ori Pomerantz는 Yoav Weiss의 많은 조언과 문서 공개전에 문서에서 잘못된 부분들을 찾아 준 것에 감사의 말을 전한다. 또한 네덜란드의 Frodo Looijaard, 뉴질랜드의 Stehpen Judd, 스웨덴의 Ahltorp, 그리고 캐나다 퀘벡의 Emmanuel Papirakis에게도 감사한다.

 

내가 이 문서를 관리할 수 있도록 해준 점에 대해 Ori Pomerantz에게 감사한다. 그에게 있어 이것은 상당한 노력이었다. 내가 이 문서에 작업한 것을 그가 좋아하길 바란다.

 

나를 지도해준 Jeff Newmiller와 Rhonda Bailey에게도 감사한다. 그들이 바쁨에도 불구하고 인내심을 가지고 나에게 그들의 경험을 전해 주었다. David Porter는 LaTeX 소스를 DocBook으로 컨버팅하는 지루하고 골치 아픈 직업을 가지고 있지만 누군가는 그 일을 해야만 한다. David에게도 감사한다.

 

http://www.tldp.org/LDP/lkmpg/www.kernelnewbies.org 의 사람들에게도 감사를 한다. 특히, kernelnewbies.org에 드나들며 초보들 가르치는 것 외에도 할 일이 무척 많을 것임이 분명한 Mark McLoughlin과 John Levon에게 감사를 보낸다. If this guide teaches you anything, they are partially to blame. 만약 이 문서가 당신에게 별 의미가 없다면 그들에게도 책임이 있다(역주: 그만큼 그들의 역할이 중요했음 ).

 

Ori와 나는 Richard M. Stallman과 Linus Tovalds에게 성능 좋은 OS를 사용하게 해준 것과 그것이 어떻게 작동하는지 자세히 알 수 있는 기회를 준 점에 대해 감사한다. 나는 Linus를 만난 적도 없고, 앞으로도 못 만나겠지만, 그는 나의 인생에 상당한 변화를 주었다.

 

다음은 나에게 좋은 제안을 하거나 오류수정의 메시지를 보낸 사람들이다. Ignacio Martin and David Porter

 

1.2 Nota Bene

Ori’s original document was good about supporting earlier versions of Linux, going all the way back to the 2.0 days. I had originally intended to keep with the program, but after thinking about it, opted out. My main reason to keep with the compatibility was for Linux distributions like LEAF, which tended to use older kernels. However, even LEAF uses 2.2 and 2.4 kernels these days.

 

Both Ori and I use the x86 platform. For the most part, the source code and discussions should apply to other architectures, but I can’t promise anything. One exception is Chapter 12, Interrupt Handlers, which should not work on any architecture except for x86.

 

2 소개

2.1 커널 모듈이란

당신은 커널 모듈을 작성하려고 한다. C를 알고 프로세스로 작동하는 일반적인 프로그램을 작성해 봤고 이제는 어디서 실질적인 작업이 행해지는지, 하나의 와일드 포인터가 파일 시스템을 완전히 지울 수 있고 코어 덤프가 시스템의 리부팅을 의미한다는 것을 알고 있다.

 

커널 모듈이란 정확히 무엇인가? 모듈이란 요구에 따라 커널에 적재 되거나 해제 될 수 있는 코드다. 시스템의 재가동 없이 커널의 기능을 확장을 가능케하는 것이다. 예로 모듈의 한 종류는 디바이스 드라이버다. 그리고 그것은 시스템에 연결된 하드웨어에 커널이 접근할 수 있도록 해 준다. 모듈이 없다면 우리는 모놀리틱 커널을 다시 빌드 해야 하며, 커널 이미지에 새로운 기능을 직접적으로 추가 시켜야 한다. 대규모 커널에서는, 우리가 원하는 새로운 기능을 추가 하기 위해서 매번 커널을 다시 빌드해야 하고, 다시 부팅해야하는 단점을 갖게 된다.

 

2.2 어떻게 모듈을 커널에 넣을 것인가?

lsmod를 실행함으로써 당신은 이미 커널에 적재되있는 모듈을 볼 수 있으며, lsmod는 /proc/modules 파일을 읽어 정보를 얻어 온다.

 

모듈은 커널에서 어떻게 자신의 위치를 찾아낼까? 커널 내부에 존재하지 않는 특징을 커널이 알 필요가 있을 때, 커널 모듈 데몬인 kmod(이전 버전의 리눅스에서는 kerneld로 알려져 있다.) 모듈을 로드 시키기 위해 modprobe를 실행 시킨다. modprobe에 다음의 두 형태로 문자열이 전달된다.

 

  • softdog 혹은 ppp 같은 모듈이름
  • char-major-10-30 같은 일반적인 아이덴티파이어

modprobe가 일반적인 아이덴티파이어를 전달 받는다면, 그것은 /etc/modules.conf 파일에서 그 문자열을 찾는다. 다음과 같은 알리어스 행을 찾는다면

 alias char-major-10-30 softdog

일반적인 아이덴티파이어는 softdog.o라는 모듈을 참조한다는 사실을 알게 된다.

 

다음으로 modprobe는 /lib/modules/version/modules.dep 파일을 조사해서 다른 모듈이 요구되는 모듈이 적재되기 전에 먼저 적재되야 하는가를 본다. 이 파일은 depmod -a 에 의해 생성되며, 모듈의 의존성을 담고 있다. 예를 들어 msdos.o는 fat.o모듈이 먼저 커널에 적재된 상태를 요구한다. 요청된 모듈이 사용하는 심볼(변수나 함수)을 다른 모듈이 정의 했는가라는, 다른 모듈에 대한 의존성을 갖는다.

 

마지막으로 modprobe는 선행 모듈을 커널에 적재하기 위해 insmod를 사용하고, 요구된 모듈을 적재한다. modprobe는 insmod에 표준 모듈 디렉토리인 /lib/modules/version/을 사용하도록 지시한다. insmod는 모듈의 위치에 대한 정보를 전혀 모르게 되있다. 반면에 modprobe는 모듈의 기본 위치를 알고 있다. 예를 들어 msdos모듈을 적재하기 원한다면 다음의 두 가지를 실행해야 한다.

 

insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o

혹은 “modprobe -a msdos”를 실행하자.

 

리눅스는 modprobe, insmode, depmod를 modutils혹은 mod-utils라 불리는 패키지로 제공한다

 

이장을 마치기 전에 /etc/modules.conf를 살펴보자.

#This file is automatically generated by update-modules
path[misc]=/lib/modules/2.4.?/local
keep
path[net]=~p/mymodules
options mydriver irq=10
alias eth0 eepro

#으로 시작하는 행은 주석이며, 빈 행은 무시된다.

 

path[misc] 행은 modprobe에 /lib/modules/2.4.?/local 디렉토리에서 misc 모듈에 대한 경로를 찾아 대체하도록 한다. 보듯이, 쉘 메타 캐릭터가 사용 가능하다.

 

path[net] 행은 modprobe로 하여금 net 모듈을 ~p/modules/ 디렉토리에서 찾도록 한다. 그러나 pathnet에 바로 선행되는 “keep”은 modprobe에게 misc모듈에서 한 것처럼 표준 검색 경로를 대체하지 않고, 해당 디렉토리를 net모듈을 찾을 때 표준 검색 경로로 추가하도록 한다.

 

kmod가 일반적인 아이덴티파이어인 ‘eth0’를 적재하라는 요구할 때마다, eepro.o를 적재하라고 한다.

 

/etc/modules.conf 에서 “alias block-major-2 floppy”같은 행은 발견하지 못할 것이다. 왜냐햐면, modprobe는 대부분의 시스템에서 사용되는 표준 드라이버들에 대해 알고 있기 때문이다.

 

이제 모듈이 어떻게 커널에 적재되는지 알았을 것이다. ‘stacking modules’라고 부르는 모듈에 의존적인 모듈을 작성한다면 몇 가지 더 언급할 것들이 있다. 이것은 다음으로 미룬다. 상대적으로 고 난이의 쟁점을 다루기 전에 다뤄야 할 부분이 많이 있다.

 

2.2.1 시작하기 전에

코드를 파헤치기 전에 우리가 다뤄야할 몇몇 주제가 더 있다. 모든 사람의 시스템이 상이하고 그들만의 관습이 있다. “hello world” 프로그램을 컴파일하고 올바르게 적재하는 것은 때론 trick(?)일수 있다. 처음 몇몇 장애를 극복하고 나면, 순풍에 돛을 단 듯 진행될 것이라 생각한다.

 

2.2.1.1 모듈 버전
CONFIG_MODVERSIONS를 커널에서 활성화 시키지 않았다면, 특정 커널에서 컴파일한 모듈은 다른 커널로 부팅한 시스템에는 적재 되지 않는다. 이문서 후반부 까지는 모듈 버전에 관한 문제를 다루지 않을 것이다. 이 문서의 예제는 modversioning기능을 활성화한 커널을 사용한다면 작동하지 않을 것이다. 대부분의 배포판 커널은 이 기능을 활성화 시킨 상태로 나온다. 만약 모듈을 적재할 때 버전 문제로 에러가 난다면 modversioning 기능을 비활성화 시킨 후 커널을 다시 컴파일 해야 할 것이다.

 

2.2.1.2 X 사용하기
이 문서에 나온 모든 예제를 타이핑하고 컴파일하고 적재해보기를 강력히 권하다. console에서 작업하기 역시 권한다. X 환경에서의 작업을 하지 마라.

 

모듈은 printf()처럼 화면에 출력을 하지 못하고 로그 정보와 경고들을 콘솔로만 출력한다. 만일 xterm에서 모듈을 적재한다면, 정보와 경고들은 당신의 로그 파일에만 기록될 것이다. 로그 파일을 확인 하기 전까지는 그것을 보지 못할 것이다. 즉각적인 정보를 위해서 모든 작업을 콘솔에서 하라.

 

2.2.1.3 컴파일 문제와 커널 버전
종종 리눅스는 표준이 아닌 방식으로 다양하게 패치되서 배포되고 그것은 문제를 야기한다.

 

더 흔한 문제는 몇몇 배판들이 불완전한 커널 헤더를 가지고 배포된다는 점이다. 당신의 코드를 컴파일 할 때 리눅스 커널의 다양한 헤더 파일을 사용해서 컴파일 해야 할 것이다. 머피의 법칙은 모듈이 작동하기에 필요한 헤더만 빠져있다고 말한다.(썰렁 조크-_-“ – 필자가 쓴것임) 이런 문제를 피하기 위해서는 리눅스 커널 미러 사이트에서 다운 받아 컴파일하고 부팅시켜 시스템을 사용할 것을 강력히 권한다. 자세한 사항은 Linux Kernel HOWTO를 참고하기 바란다.

 

역설적이지만 이것 역시 문제를 만든다. 기본적으로, 당신의 시스템에 있는 GCC는 당신이 설치한 버전의 커널 헤더파일을 찾는 것이 아니고 기본으로 설치됐던 커널 헤더(보통 /usr/src/)를 찾을 것이다. 이것은 gcc의 -l 옵션을 사용해서 수정할 수 있다.

 

3 Hello World

3.1 Hello, World (part 1): The Simplest Module

원시 프로그래머가 처음으로 원시 컴퓨터에 첫 프로그램을 새겨 넣었을 때, 그것은 ‘Hello World’라는 문자를 양떼 그림에 새겨 넣은 프로그램이었다. 로마의 프로그램 책은 ‘Salut Mundi’(hello world의 로마어인 듯.. J)로 시작한다. 이런 관습을 깨뜨린 사람에게 어떤 일이 일어났는지 잘은 모르지만, 별로 신경 쓰지 않아도 될 듯 하다. 커널 모듈을 작성하는 기본이 되는 다른 측면에서의 예로 Hello World류의 프로그램을 가지고 시작해보자.(어색한 번역…)

 

다음은 가장 간단한 모듈이다. 아직 컴파일하지 말자; 다음 장에서 모듈을 컴파일하는 것에 대해 다룰 것이다.

 

Example 2-1. hello-1.c

 

/*  hello-1.c - The simplest kernel module. */
#include <linux/module.h>  /* 모든 모듈에 필요 */
#include <linux/kernel.h>  /* KERN_ALERT에 필요 */

int init_module(void)
{
printk(“<1>Hello world 1.\n);

// 0이 아닌 값을 리턴하는 것은 init_module이 실패한 것을 의미한다. 고로 모듈은 로드되지 못한다.
return 0;
}

void cleanup_module(void)
{
printk(KERN_ALERT “Goodbye world 1.\n);
}

 

커널 모듈은 최소 두 개의 함수를 포함해야 한다: 모듈이 커널에 적재될 때 호출되는 시작(초기화) 함수인 init_module() 그리고 모듈이 해제되기 직전에(rmmod) 호출되는 종료(해제) 함수인 cleanup_module()이 바로 그것이다. 실제, 커널 2.3.13부터 이런 부분에 많은 변화가 있었다. 당신은 시작하거나 종료할 때 마음에 드는 것을 사용할 수 있으며, 이런 것들을 Section 2.3에서 배울 것이다. 사실 새로운 방식이 더 낫다. 그러나 많은 사람들이 여전히 시작과 끝에 init_module() 과 cleanup_module()함수를 사용한다.

 

전형적으로 init_module()함수는 커널에 어떤 헨들러를 등록하거나, 커널이 가지고 있는 함수의 코드를 대체한다. 일반적으로 특정한 작업을 수행한 후, 원래의 함수를 다시 호출한다.). cleanup_module()함수는 init_module()함수가 행한 것을 복귀시켜 모듈을 안전하게 해제(unload)할 수 있게 한다.

 

마지막으로 모든 커널 모듈은 linux/module.h를 포함해야 한다. linux/kernel.h는 printk()함수의 log level인 KERN_ALERT를 위해서만 여기서 필요하며 이런 내용은 printk()함수 소개에서 배울 것이다.

 

3.1.1 printk()함수 소개

당신의 생각과 다르게 printk()는 사용자와 정보 교환을 위한 것은 아니다.(우리가 이런 목적으로 hello-1에서 printk()를 사용할 지라도) 이것은 커널에 기록(logging)하는 방식이며, 경고를 하거나 정보를 남기기 위해 사용된다. 각각의 printk()는 우선 순위를 가지고 사용되며, /which is the <1> and KERN_ALERT you see /이것은 <1> 혹은 KERN_ALERT 와 같은 형태로 사용된다. 8개의 우선순위가 있으며, 커널은 그에 상응하는 매크로를 가지고 있어 이해하기 어려운 번호를 사용할 필요가 없다. 이들은 linux/kernel.h에서 찾아볼 수 있다. 우선 순위를 정하지 않고 사용한다면 기본적인 우선순위인 DEFAULT_MESSAGE_LOGLEVEL이 사용될 것이다.

 

우선 순위 매크로를 읽기를 권한다. 헤더 파일은 각 우선 순위가 의미하는 바도 설명해 준다. 실제 <4>와 같은 번호는 쓰이지 않으며 대신 KERN_WARNNING과 같은 매크로를 사용한다.

 

만약 int console_loglevel보다 우선 순위가 낮으면, 해당 메시지는 당신의 현재 터미널에 보여질 것이다. syslogd와 klogd가 실행 중이라면 메지시가 콘솔로 출력여부와 관계 없이 /var/log/messages에 추가된다. printk()의 메시지가 로그 파일에만 남겨지지 않고 콘솔로 반드시 출력되도록 하기 위해 KERN_ALERT와 같이 높은 우선순위를 사용한다. 실제 모듈을 작성할 때 적절히 상황에 맞는 우선순위를 사용하기 원할 것이다.

 

3.2 커널 모듈 컴파일

커널 모듈은 재대로 작동하기 위해서 GCC의 특정한 옵션과 함께 컴파일 되야 한다. 정의된 특정한 심볼과 함께 컴파일 될 필요도 있다. 실행 파일을 컴파일하는지 커널 모듈을 컴파일하는지에 따라 커널 헤더 파일이 다르게 동작해야 하기 대문이다. GCC의 –D옵션을 사용하거나 #define 프리프로세서 명령을 사용해 심볼을 정의할 수 있다. 이 장에서 커널 모듈을 컴파일하기 위해 필요한 것들을 모두 다룬다.

 

  • -c: 커널 모듈은 독립적으로 실행 가능한 파일이 아니며, insmod를 사용하여 실행 시간 중에 커널에 링크되는 오브젝트 파일 이다. 결론적으로 모듈은 -c옵션을 주고 컴파일 해야 한다.
  • -O2: 커널은 인라인 함수를 주로 사용하기 때문에 모듈은 이 옵션 플래그를 사용해야 한다. 이 옵션을 사용하지 않은 경우 어떤 어셈블러 매크로는 함수 호출 시 정상적으로 작동하지 않을 것이다. insmod는 커널에서 원하는 함수를 찾지 못하고 결국 모듈의 적재는 실패할 것이다.
  • -W -Wall:프로그램에서의 실수는 당신의 시스템을 다운 시킬 수도 있다. 컴파일러 경고 기능은 항상 켜둬라, 이것은 모듈 컴파일 뿐 아니라 당신의 모든 컴파일 행위에 적용된다.
  • -isystem /lib/modules/uname -r/build/include: 컴파일 대상이 되는 커널의 헤더를 사용해야만 한다. 기본적인 /usr/include/linux를 사용하는 것은 작동하지 않을 것이다.
  • -DKERNEL:이 심볼을 정의 하는 것은 헤더 파일에 이 코드가 유저 프로세스로 동작하지 않고 커널 모드에서 작동한다는 사실을 알린다.
  • -DMODULE:이 심볼은 헤더 파일에 커널 모듈을 위한 올바른 정의를 하게 한다.

module.h를 포함 시킬 때 –W –Wall이 유발시키는 사용하지 않는 변수에 대한 경고를 GCC로 하여금 막기 위해, -I 옵션 대신 GCC의 -isystem옵션을 사용해야 한다. GCC-3.0하에서 –isystem을 사용함으로써 커널 헤더를 주의 깊게 다루어지고 경고는 절재 된다. -I옵션을 대신 사용한다면 (gcc 2.9x하에서 -isystem옵션을 사용하는 것도 ) 사용하지 않는 변수에 대한 경고가 출력될 것이다. 그렇다면 무시하면 된다.

 

hello-1.c 모듈 컴파일을 위한 Makefile을 살펴보자

 

Example 2-2. Makefile for a basic kernel module

 

TARGET  := hello-1
WARN    := -W -Wall -Wstrict-prototypes -Wmissing-prototypes
INCLUDE := -isystem /lib/modules/`uname -r`/build/include
CFLAGS  := -O2 -DMODULE -D__KERNEL__ ${WARN} ${INCLUDE}
CC      := gcc-3.0

${TARGET}.o: ${TARGET}.c

.PHONY: clean

clean:
rm -rf {TARGET}.o

 

연습으로 hello-1.c를 컴파일하고 insmod ./hello-1.o를 커널에 올려보자. 잘되는가? 커널에 적재된 모든 모듈은 /proc/modules에 리스트된다. 당신의 모듈이 커널의 일부가 됐는지 알아 보기 위해 그 파일을 출력해보자. 축하한다. 당신은 리눅스 커널코드의 작성자가 됐다. 즐거움은 잠시 미뤄두고 rmmod hello-1를 사용해 커널로부터 당신의 모듈을 제거 하자. 당신의 시스템 로그파일에 기록이 됐는지 보기 위해 /var/log/messages 파일을 살펴 보자.

 

독자를 위한 또 다른 예제가 있다. 앞서 언급한 init_module()함수의 리턴 값을 0이 아닌 다른 값으로 변경한 후 다시 컴파일 하고 로드해 보자. 어떤 일이 일어나는가

 

3.3 Hello World (part 2)

여러분은 여러분의 init 와 cleanup 함수 이름을 바꿀 수 있다. 이후로 그들의 이름이 반드시 init_moduel(), cleanup_module()일 필요는 없다. 이는 module_init()와 module_exit()매크로에 의해 이뤄진다. 이 매크로는 linux/init.h에 저의 되있다. 선행되야 할 것은 매크로를 호출하기 전에 init와 cleanup함수라 정의 돼있어야 한다 것 뿐, 그렇지 않다면 컴파일 에러를 만날 것이다. 이런 테크닉의 예가 있다.

 

Example 2-3. hello-2.c

/*  hello-2.c - Demonstrating the module_init() and module_exit() macros.
    This is the preferred over using init_module() and cleanup_module().
hello-2.c &#8211; module_init()와 module_exit()매크로의 예. 이것은 init_module()과 cleanup_moduel() 보다 낫다. */
#include <linux/module.h>   // Needed by all modules
#include <linux/kernel.h>   // Needed for KERN_ALERT#include <linux/init.h>     // Needed for the macrosstatic

int hello_2_init(void)
{
printk(KERN_ALERT “Hello, world 2\n);
return 0;
}

static void hello_2_exit(void)
{
printk(KERN_ALERT “Goodbye, world 2\n);
}

module_init(hello_2_init);
module_exit(hello_2_exit);

 

이제 우리는 두개의 실제 커널 모듈을 체험했다. 생산성을 높이기 위해 우리는 Makefile을 활용해야 한다. 다음은 앞 두 개의 모듈을 모두 컴파일 할 수 있는 개선된 Makefile 이다. 간결성, 규모면에서 최적화된 것이다. 다음을 이해할 수 없다면 GNU Makefile매뉴얼 혹은, makefile info페이지를 읽기 바란다.

 

Example 2-4. Makefile for both our modules

WARN    := -W -Wall -Wstrict-prototypes -Wmissing-prototypes
INCLUDE := -isystem /lib/modules/`uname -r`/build/include
CFLAGS  := -O2 -DMODULE -D__KERNEL__ ${WARN} ${INCLUDE}
CC      := gcc-3.0
OBJS    := ${patsubst %.c, %.o, ${wildcard *.c}}

all: ${OBJS}

.PHONY: clean

clean:
rm -rf *.o

 

독자들을 위한 예로, 만약 같은 디렉토리에 hello-3.c 같은 모듈이 하나 더 있다면 그 모듈을 자동으로 컴파일 하기 위해 당신은 Makefile을 어떻게 수정할 것인가?

 

3.4 Hello World (part 3): The __init and __exit Macros

다음은 커널 2.2나 혹은 그 후 버전에서의 예다. init와 cleanup 함수의 정의에서의 변화를 주목하라. 내장된 드라이버에 대해 init함수가 수행되면, __init매크로는 init함수가 버려지고(-_-“) 메모리가 반환된다. 그러나 모듈은 적재 불가능하다. 언제 init함수가 호출되는 가를 생각한다면 이것은 완전히 타당하다고 느낄 것이다.

 

init함수 자체에 대한 것이라기 보다는 init변수에 대해 작용하는 __init와 유사한 매크로인 __initdata라는 것도 있다. __exit매크로는 모듈이 커널로 빌트인(모듈로 컴파일 하는 것이 아닌 커널 일부로 컴파일 하는 것)될 때, 함수의 호출을 생략하며 __exit처럼 적재 가능한 모듈에 어떤 영향도 미치지 않는다. 마찬가지로 언제 cleanup함수가 작동하는 고려한다면 이해될 것이다. 내장된 드라이버는 cleanup함수가 필요 없다. 반면에 적재 가능한 모듈은 필요하다.

 

이 매크로들은 linux/init.h에 정의 돼 있고 사용된 커널 메모리를 해제하는데 사용된다. 부팅 후 ‘Freeing unused kernel memory: 236k freed’ 와 같은 메시지를 볼 때 , 커널이 메모리를 해제하는 것이다.

 

Example 2-5. hello-3.c

/*  hello-3.c - Illustrating the __init, __initdata and __exit macros. */
#include <linux/module.h>      /* Needed by all modules */
#include <linux/kernel.h>      /* Needed for KERN_ALERT */
#include <linux/init.h>        /* Needed for the macros */
static int hello3_data __initdata = 3;

static int __init hello_3_init(void)
{
printk(KERN_ALERT “Hello, world %d\n, hello3_data);
return 0;
}

static void __exit hello_3_exit(void)
{
printk(KERN_ALERT “Goodbye, world 3\n);
}

module_init(hello_3_init);
module_exit(hello_3_exit);

 

리눅스 2.2 커널을 위한 드라이버 모듈에서 __initfunction() 같은 함수를 보았을 것이다.

__initfunction(int init_module(void))
{
   printk(KERN_ALERT "Hi there.\n");
   return 0;
}

이 메크로는 __init와 같은 동작을 한다. 그러나 __init에 비해 매우 적게 사용된다. 그저, 커널에서 당신이 보았을 수도 있기에 언급한 것이다. 2.4.18 버전의 커널에 __initfunction()은 38군데에서 보이고, 2.4.20에서는 37군데에서만 보인다. 그러나 당신의 코드에서는 사용하지 말 것을 권한다.

 

3.5 Hello World (prt 4): 저작권과 모듈 문서

커널 버전 2.4나 그 후 버전을 사용한다면, 앞서 언급한 모듈 예를 적재할 때 다음과 같은 메시지를 보게 될 것이다.

# insmod hello-3.o
Warning: loading hello-3.o will taint the kernel: no license
  See http://www.tux.org/lkml/#export-tainted for information about tainted modules
Hello, world 3
Module hello-3 loaded, with warnings

 

2.4버전 이후의 커널에서 GPL 라이센스 코드를 식별하기 위한 메커니즘이 고안돼서, 오픈 소스가 아닌 경우 사용자들에게 경고를 할 수 있게 돼있다. 이는 다음 코드에서 제시된 MODULE_LICENSE() 매크로에 의해 충족된다. GPL라이센스로 설정함으로써 경고가 출력되는 것을 방지 할 수 있다. 이런 라이센스 메커니즘은 linux/module.h에 정의되있고 문서화 되어 있다.

 

비슷하게 MOUDLE_DESCRIPTION() 은 모듈이 무엇을 하는가를 , MOUDLE_AUTHOR()은 모듈의 저자를 MODULE_SUPPORTED_DEVICE()는 어떤 타입의 장치를 모듈이 지원하는 가를 알려준다.

 

이런 매크로들은 linux/module.h에 정의되있고 커널 자체에서는 사용되지 않는다. 이것들은 문화를 간단히 하며, objdump와 같은 툴을 이용해 볼 수 있다. 독자들의 연습을 위해 linux/drivers에서 어떻게 모듈 저자들이 모듈을 문서화 하기 위해 이 매크로들을 사용했는가 찾아 보길 바란다.

 

Example 2-6. hello-4.c

/*  hello-4.c - Demonstrates module documentation. */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR "Peiter Jay Salzman <p@dirac.org>"
#define DRIVER_DESC   "A sample driver"

int init_hello_3(void);
void cleanup_hello_3(void);

static int init_hello_4(void){
printk(KERN_ALERT “Hello, world 4\n);
return 0;
}

static void cleanup_hello_4(void)
{
printk(KERN_ALERT “Goodbye, world 4\n);
}

module_init(init_hello_4);
module_exit(cleanup_hello_4);
/* You can use strings, like this: */
MODULE_LICENSE(“GPL”); // Get rid of taint message by declaring code as GPL..
/* Or with defines, like this: */
MODULE_AUTHOR(DRIVER_AUTHOR); // Who wrote this module?
MODULE_DESCRIPTION(DRIVER_DESC); // What does this module do?
/* This module uses /dev/testdevice.
The MODULE_SUPPORTED_DEVICE macro might be used in the future to help automatic configuration of modules,
but is currently unused other than for documentation purposes.
*/
MODULE_SUPPORTED_DEVICE(“testdevice”);

 

3.6 커맨드 라인 인자 모듈에 넘기기

모듈은 커맨드라인 인자를 받을 수 있다. 기존에 사용하던 것처럼 argc/argv를 가지고 할 수는 없다.

 

인자를 당신의 모듈로 넘기기 위해서는 커맨드라인 인자의 값을 저장할 변수를 전역으로 선언한 후 메커니즘을 활성화 시키기 위해 MODULE_PARAM()매크로를 사용한다. 실행 시간에 insmod는 주어진 인자로 변수를 채울 것이다. 변수의 선언과 매크로들은 명확성을 위해 모듈 서두에 위치해야 한다. 다음의 예는 어눌한 내 설명을 명백히 해준다.(내 번역도 ^^)

 

MODULE_PARAM() 매크로는 2개의 인자를 받는다 변수의 이름과 타입 지원하는 타입은 1byte인”b”, short int인 “h”, integer인 “i”, long인 “l”, string인 “s” 등 이다. 문자열(strings – string과 strings와 구분)은 “char *”로 해야 하며, insmod는 그 문자열(strings)을 위한 메모리를 할당한다. 늘 변수를 초기화하는 습관을 갖길 권한다. 이것은 커널 코드이다. 당신은 반드시 방어적으로 프로그래밍 해야 한다. 다음은 예제다.

int myint = 3;
char *mystr;

MODULE_PARM (myint, “i”);
MODULE_PARM (mystr, “s”);

배열 역시 지원된다. MODULE_PARM에 선행되는 integer 값은 배열의 최대 길이를 알려준다. ‘-‘에 의해 분리된 두 번호는 최대 최소 값을 알려 준다. 예를 들어 최소값 2과 최대값 4를 갖는 short형의 배열은 다음과 같이 선언될 수 있다.

int myshortArray[4];
MODULE_PARM (myintArray, "2-4i");

어떤 IO포트 혹은 IO 메모리가 사용되는지와 같이 모듈의 변수 값은 디폴트 값으로 세팅 되는 것이 좋다. 만약 변수가 기본값을 갖는다면 자동인식(차후에 설명이 나옴)를 수행 할 수 있다. 그렇지 않다면 현재의 값을 유지한다. 이것은 앞으로도 명확하다. 지금은 모듈로 인자를 넘기는 것만을 예로 들겠다.

 

Example 2-7. hello-5.c

 

/*  hello-5.c - Demonstrates command line argument passing to a module. */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“Peiter Jay Salzman”);

static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = “blah”;

MODULE_PARM (myshort, “h”);
MODULE_PARM (myint, “i”);
MODULE_PARM (mylong, “l”);
MODULE_PARM (mystring, “s”);

static int __init hello_5_init(void)
{
printk(KERN_ALERT “Hello, world 5\n=============\n);
printk(KERN_ALERT “myshort is a short integer: %hd\n, myshort);
printk(KERN_ALERT “myint is an integer: %d\n, myint);
printk(KERN_ALERT “mylong is a long integer: %ld\n, mylong);
printk(KERN_ALERT “mystring is a string: %s\n, mystring);
return 0;
}

static void __exit hello_5_exit(void)
{
printk(KERN_ALERT “Goodbye, world 5\n);
}

module_init(hello_5_init);
module_exit(hello_5_exit);

 

Supercalifragilisticexpialidocious(저자가 쓴 암호 중 하나… -_-)

 

3.7 다중 파일 모듈

때로는 커널 모듈을 여러 개의 소스파일로 나누는 것이 좋을 때가 있다. 이런 경우 다음과 같은 사항이 필요하다.

 

  1. 하나의 파일을 제외하고 모든 소스 파일에 #define NO_VERSION 이 필요하다. module.h는 일반적으로 kernel_version, 모듈이 컴파일되는 커널의 전역 변수를 포함하기 때문에 이것은 중요하다. 만약 version.h가 필요하다면 그것을 포함시켜야 한다. 왜냐하면 NO_VERSION를 가진 module.h 은 이것을 해주지 않는다.
  2. 일반적으로 모든 소스 파일을 컴파일 해야 한다.
  3. 모든 오브젝트 파일을 하나로 컴파일 해야 한다. X86환경에서는 ld -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>를 사용한다.

다음은 앞서 언급한 것에 대한 예제다.

 

Example 2-8. start.c

/*  start.c - Illustration of multi filed modules */
#include <linux/kernel.h>       /* We're doing kernel work */
#include <linux/module.h>       /* Specifically, a module */
int init_module(void)
{
  printk("Hello, world - this is the kernel speaking\n");
  return 0;
}

다음 파일

 

Example 2-9. stop.c

/*  stop.c - Illustration of multi filed modules */
#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
   #include <linux/modversions.h> /* Will be explained later */
   #define MODVERSIONS
#endif
#include <linux/kernel.h>  /* We're doing kernel work */
#include <linux/module.h>  /* Specifically, a module  */
#define __NO_VERSION__     /* It's not THE file of the kernel module */
#include <linux/version.h> /* Not included by module.h because of __NO_VERSION__ */

void cleanup_module()
{
printk(“<1>Short is the life of a kernel module\n);
}

 

마지막으로 Makefile

 

Example 2-10. Makefile for a multi-filed module

CC=gcc
MODCFLAGS := -O -Wall &#8211;DMODULE -D__KERNEL__

hello.o: hello2_start.o hello2_stop.o
ld -m elf_i386 -r -o hello2.o hello2_start.o hello2_stop.o

start.o: hello2_start.c
${CC} ${MODCFLAGS} -c hello2_start.c

stop.o: hello2_stop.c
${CC} ${MODCFLAGS} -c hello2_stop.c

 

4 준비 단계

4.1 모듈 vs 프로그램

4.1.1 모듈은 어떻게 시작하고 끝나는가

일반적으로 프로그램은 main()함수에서 시작한다, 명령어들의 집합을 실행하고 명령어들의 완료 시 종료된다. 커널 모듈은 약간 다르게 작동한다. 모듈은 항상 init_module()함수, 혹은 당신이 module_init()라는 함수에 지정한 곳에서 시작한다. 이것이 모듈의 진입 함수다(entry function); 모듈이 필요해질 때 이 함수들은 커널에게 그 모듈의 기능은 무엇이며 어떻게 설정되는 가를 알려 준다. 이런 설정이 이뤄지면 모듈을 시작하는 함수는 리턴하고 모듈이 제공하는 코드를 가지고 커널이 무엇인가를 하려할 때까지 모듈은 아무런 일을 하지 않는다.

 

모든 모듈은 cleanup_module(), 혹은 module_exit()에 지정되 있는 함수를 호출할 때 종료된다. 이것이 모듈의 종료 함수(exit function)가 된다. 이 함수는 진입 함수가 무엇을 하던 종료 시킨다. 진입 함수에 등록되 있는 기능을 해제한다..

 

모든 모듈은 진입함수와 종료함수를 갖는다. 하나 이상의 진입함수와 종료함수를 지정하는 방법이 있기에, `진입함수(entry function)’와 `종료함수(exit function)’라는 단어를 사용하겠다. 그러나 내가 간단히 init_module() 그리고 cleanup_module()이라 하더라도, 독자들은 내가 무엇을 의미하는지 알 것이라 생각하겠다.

 

4.1.2 모듈에 사용 가능한 함수들

프로그래머들은 자신들이 정의한 함수만을 사용하는 것은 아니다. 대표적인 예가 printf()다. 프로그래머들은 표준 C라이브러리에서 제공하는 이런 라이브러리 함수들을 사용한다. 실제 이런 함수들의 정의는 링킹 스테이지(linking stage)가 될 때까지 프로그램에 들어가지 않는다. 그리고 이런 것은 코드가 사용 가능하다는 것을 보장해주고 그 위치에 명령어(instruction)를 위치 시켜준다.

 

여기서도 커널 모듈은 다는 양상을 띤다. 전술한 예제에서, 우리는 printk()라는 함수를 사용했다 그리고 그것은 표준 라이브러리 함수가 아님을 알 것이다. 모듈은 insmod에 의해 해석되지는 심볼을 갖는 오브젝트 파일이기 때문이다. 심볼의 정의는 커널 자체에 있다. 당신이 사용 가능한 유일한 외부 함수는 커널에 의해 제공되는 것들에 한정되어진다. 어떤 심볼(역주-모듈에서 직접 정의하지 않고 사용할 수 있는 함수)들이 사용가능한지 궁금하다면 /proc/ksyms을 살펴봐라.

 

라이브러리 함수와 시스템 콜의 차이점을 명심하자. 라이브러리 함수들은 상위 레벨에 있으면서, 완벽하게 유저 레벨에서 실행되며, 프로그래머가 실제 작업을 하는데 편의를 제공해준다. 시스템 콜은 커널에 의해 제공되며 사용자의 행위에 따라 커널 레벨에서 실행된다. 라이브러리 함수인 printf()는 가장 일반적인 출력함수처럼 보인다. 그러나 그것이 실제로 하는 일은 데이터를 스트링으로 포맷하고 스트링 데이터를 로우레벨 시스템 콜인 write()를 이용하는 것이다, 그리고 그것은 그 데이터를 표준 출력으로 내보낸다.

 

Printf()가 무엇으로 구성되있는 보고 싶은가? 그것을 알아보는 것은 굉장히 쉽다. 다음의 프로그램을 컴파일 해보자.

#include <stdio.h>
int main(void)
{ printf("hello"); return 0; }

gcc -Wall -o hello hello.c. strace hello을 실행해보자. 놀랐는가? 당신이 보는 각각의 행이 시스템 콜에 해당된다. strace[1]는 어떤 시스템콜로 프로그램이 구성됬는가, 어떤 아규먼트가 넘겨지는가, 무엇을 리턴하는가를 보여주는 간단한 프로그램이다. 이 프로그램은 프로그램이 어떤 파일에 접근하는가와 같은 것들을 설명하는데 매우 중요한 도구다. 끝 부분에 write(1, “hello”, 5hello)라는 부분을 볼수 있을 것이다. 이것이 printf()의 정체다. 대부분의 사람들은 파일 입출력에 fopen(), fputs(), fclose()와 같은 라이브러리 함수를 사용하기 때문에, write()에 익숙하지 않을 것이다. 만약 당신도 그렇다면 , man 2 write을 보라. 매뉴얼 페이지의 2번째 섹션은 kill(), read()와 같은 시스템 콜에 할당되있다. 3번째 섹션은 독자들이 익숙한 cosh(), random() 등의 라이브러리 함수에 할당 되어 있다.

 

당신도 커널의 시스템 콜을 대체하기 위해 모듀을 쓸 것이다.(우리가 간단히 할것이다) 크래커들이 백도어용으로 이런 종류의 것들을 사용한다. 누군가 당신의 시스템에 있는 파일을 지우려 시도할 때마다 당신은 모듈을 이용해서 “간지러 장난 치지마”(역주Tee hee, that tickles! 의역)라는 식의 온화한 대응을 할 수 있을 것이다.

 

4.1.3 사용자 공간 vs 커널 공간

커널은 비디오 카드, 하드 드라이브, 심지어 메모리까지 모든 리소스에 접근한다. 프로그램은 종종 같은 리소스에 대해 경쟁한다. 내가 이 문서를 저장할 때, updatedb가 위치에대한 데이터 베이스를 갱신하기 시작한다. 나의 vim 세션과 updatedb가 같은 하드 드라브를 동시에 사용하는 것이다. 커널은 이런 것들을 순서대로 유지할 필요가 있으며, 사용자가 그들이 원하는 자원에 직접 접근하도록 권한을 주어서는 안 된다. 이런 이유로 CPU는 다른 방식으로 작동한다. 각 모드는 당신이 시스템에 하고자 하는 일에 대한 서로 다른 레벨의 권한을 준다. 인텔의 30386 아키텍쳐는 4개의 모드를 가지고 있다. 유닉스는 모든 것이 가능한 관리자 모드로 알려진 0 모드(최상위 모드 – ring의 단어선택이… -_-)와 유저 모드로 불리는 최하위 모드, 두 가지만을 사용한다.

 

라이브러리 함수와 시스템 콜에 대한 논의를 상기하자. 당신은 전형적으로 사용자유저모드에서 라이브러리 함수를 사용한다. 라이브러리 함수는 하나 혹은 그 이상의 시스템 콜을 호출한다. 그리고 이런 시스템 콜은 라이브러리 함수처럼 행동한다. 그런 관리자 모드에서는 커널의 일부이기 때문에 그렇게 행동한다. 시스템콜이 그 작업을 완료하면 복귀(return)하고 유저보드로 복귀한다.

 

4.1.4 이름 공간

당신이 C프로그램을 작성할 때, 당신은 코드를 보는 사람들의 가독성을 위해 변수이름을 사용했을 것이다. 규모가 큰 프로그램의 일부를 작성하고 있을 때, 당신의 전역변수가 같이 일하는 사람의 전역변수의 이름과 같다면 변수이름으로 인해 문제가 될 수 있다. 서로 구분할 만큼 의미 있는 이름을 갖지 못한 전역변수가 많이 존재한다면 이름공간이 오염될 것이다. 대규모 프로젝트에서 사용 보류된 이름에 대해 이런 노력은 반드시 있어야 하며, 심볼과 변수의 유니크한 명명 법에 대한 계획이 필요하다.

 

커널 코드를 작성할 때 그것이 아무리 작은 모듈이라도 커널 전체에 링크되기 때문에 이것은 분명 문제가 된다. 이것을 해결하는 가장 좋은 방법은 변수를 정적 변수로 선언하고, 잘 정의된 접두어를 당신의 심볼에 사용하는 것이다. 관습적으로 커널의 접두어는 소문자다. 모든 것을 정적으로 선언하기 원치 않는다, symbol table을 선언하고 그것을 커널에 등록시키는 방법을 쓴다. 차후에 이것을 하게 될 것이다.

 

/proc/ksyms는 커널이 알고 있는 모든 심볼을 가지고 있고, 그 심볼은 커널 코드 스페이스에서 공유되기 때문에, 당신의 모듈이 접근/사용할 수 있다.

 

4.1.5 코드 영역

메모리 관리는 매우 복잡한 주제다. `Understanding The Linux Kernel의 상당 부분이 메모리 관리에 중점을 둔다. 우리는 비록 메모리 관리에 전문가는 아니지만 실제 모듈을 작성하는데 고려해야 할 두 가지 사실에 대해 알 필요는 있다.

 

실제로 세그멘트 폴트가 무엇을 의미하는지 생각해보지 않았다면 포인터가 실제 메모리 위치를 지시하고 있지 않다는 것을 듣게 되면 놀랄 것이다. 어쨌든 어느 포인터도 그렇지 않다. 프로세스가 생성될 때, 커널은 실제 메모리의 일부를 할당해, (컴퓨터 학자나 알법한 것들) 프로세스가 사용하는 실행코드, 변수, 스택, 힙 등으로 사용하도록 프로세스에게 넘긴다. 이 메모리는 $0$에서 시작해 프로세스가 필요한 만큼 확장된다. 서로 다른 두 프로세스의 메모리 영역은 겹치지 않기 때문에 0xbffff978에 접근하는 모든 프로세스는 실제 물리적 메모리의 서로 다른 지점에 접근할 것이다. 프로세스들은 특정 프로세스에게 할당된 메모리 영역으로의 오프셋의 한 종류를 지시하는 0xbffff978로 이름 지어진 인덱스에 접근하려 할 것이다. 이후에 다루게 될 방법이 있음에도 불구하고, 우리의 Hello, World와 같은 대부분의 경우 다른 프로세스의 영역에 접근할 수 없다.

 

커널 역시 자신만의 메모리 영역이 있다. 모듈은 동적으로 커널에 적재 되거나 제거될 수 있는 코드기 때문에, 모듈은 자신만의 커널 코드 영역을 가지기 보다는 커널 코드 영역을 공유한다. 즉, 반 독립적 객체에 상반되는 개념이다. (as opposed to a semi-autonomous object 의 의역) 그러므로 우리의 세그먼트 폴트는 커널의 세그먼트 폴트가 된다. off-by-one 에러 때문에 데이터를 겹쳐 쓰기 시작한다면, 커널 코드를 망가트릴 것이다. 이건 생각보다 심각하므로 주의를 기울여야 한다. ( off-by-one error : n=0에서 시작할 것을 n=1에서 시작함으로 해서 일어나는 류의 에러를 의미함) 위에서 언급한 사실들은 모놀리틱 커널을 사용하는 모든 오퍼레이팅 시스템에서 적용된다는 것을 지적하고 싶다. 자기 자신만의 코드 영역을 갖는 마이크로 커널이라는 것도 있다. GNU Hurd와 QNX Neutrino가 마이크로 커널의 예다.

 

4.1.6 장치 드라이버

모듈의 한 종류가 장치 드라이버이며, 그들은 TV카드나 시리얼 포트 같은 하드웨어의 기능을 제공한다. 유닉스에서 하나의 하드웨어는 하드웨어와 의사 통신하는 수단을 제공하는 named device 파일(/dev/ 아래 위치한다.)로 보여진다. 디바이스 드라이버는 유저 프로그램을 대신해, 의사 소통 수단을 제공한다. es1370 사운드카드 드라이버는 Ensoniq IS1370 사운드 카드에 연결하기 위해 /dev/sound device에 연결한다. mp3blaster와 같은 유저 스페이스 프로그램은 어떤 종류의 사운드 카드가 설치됐는지 모른 체 /dev/sound라는 장치를 사용할 수 있다.

 

4.1.6.1 장치 주 번호와 장치 부 번호
디바이스 파일을 살펴보자. 다음은 프라이머리 마스터 IDE하드 드라이버의 첫 세 개 파티션을 나타내는 디바이스 파일이다.

 

# ls -l /dev/hda[1-3]
brw-rw----  1 root  disk  3, 1 Jul  5  2000 /dev/hda1
brw-rw----  1 root  disk  3, 2 Jul  5  2000 /dev/hda2
brw-rw----  1 root  disk  3, 3 Jul  5  2000 /dev/hda3

콤마에 의해 구분된 번호의 열을 주시하자 첫 번째 번호를 장치 주 번호라 부르며 두 번째 번호는 부 번호라 부른다. 주 번호는 드라이버가 어떤 하드웨어에 엑세스하는지 알려준다. 각 드라이버는 유일한 주 번호를 할당 받으며, 동일한 주 번호를 갖는 모든 디바이스 파일은 같은 드라이버에 의해 컨트롤 된다. 위의 주 번호가 모두 3인 것은, 그들이 같은 드라이버에 의해 콘트롤 되기 때문이다.

 

부 번호는 드라이버가 자신이 컨트롤하는 하드웨어를 구분하기 위해 사용된다. 위의 예로 돌아가 보자, 세 개의 장치들이 같은 드라이버에 의해 운영될지라도 서로 다른 고유의 부 번호를 갖는데, 이는 드라이버가 그들(하드 디스크 파티션들)을 서로 다른 하드웨어로 인식하기 때문이다.

 

디바이스는 캐릭터 디바이스와 블록 디바이스의 두 타입으로 나뉜다. 블록 디바이스는 버퍼를 가지고 있어, 어떤 순서로 응답하는 것이 가장 좋은 것인가 선택할 수 있다는 것이 차이점이다. 이점은 물리적으로 떨어져있는 섹터보다 가까이 있는 섹터에 읽기/쓰기를 하는 것이 빠르다는 점에서 저장 장치에 있어 중요하다. 또 다른 차이점은 블록 디바이스만이 입출력 시 블록 단위로 접근할 수 있다는 점이다(블록의 크기는 장치에 따라 다르다). 반면에 캐릭터 디바이스는 몇 바이트 되지 않는 크기만을 허용한다. 대부분의 장치는 캐릭터 디바이스이다. 왜냐하면 장치들 대부분이 이런 종류의 버퍼일을 필요로 하지 않고 고정된 블록 크기에 대해 작동하지 않기 때문이다. ls –l의 결과에서 첫 번째 문자를 살펴 봄으로써 디바이스가 블록 디바이스인지 캐릭터 디바이스인지 구분할 수 있다. 만인 첫 문자가 ‘b’이면 블록디바이스고, ‘c’이면 캐릭터 디바이스이다. 다음은 캐릭터 디바이스의 예다(시리얼 포트).

crw-rw----  1 root  dial 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r-----  1 root  dial 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw----  1 root  dial 4, 66 Jul  5  2000 /dev/ttyS2
crw-rw----  1 root  dial 4, 67 Jul  5  2000 /dev/ttyS3

만일 어떤 주 번호가 할당됐는가 알기를 원한다면, /usr/src/linux/Documentation/devices.txt 파일을 참고하기 바란다.

 

시스템이 설치될 때, 이런 디바이스 파일들은 mknod에 의해 생성된다. 주번호/부번호 12, 2의 ‘coffee’라는 새로운 캐릭터 디바이스를 생성하고자 한다면, 단순히 mknod /dev/coffee c 12 2만 실행시키면 된다. 디바이스 파일을 /dev/에 넣을 필요는 없다. 단순히 관습일 뿐이다. 리누즈가 그의 디바이스 파일을 /dev/에 넣었기 때문에 당신도 그렇게 하는 것이 낫을 것이다. 그러나 테스트 목적으로 디바이스 파일을 생성한다면, 커널 모듈을 컴파일한 작업 디렉토리에 디바이스 파일을 넣어도 무방하다. 디바이스 드라이버 작성이 완료됐을 때 올바른 위치에만 넣으면 된다.

 

나는 이전에 언급했던 것들과 상충되는 몇 가지 지적을 하기를 좋아 한다. 하지만 그것들은 단지 몇몇 경우에만 국한된다. 디바이스 파일이 액세스될 때 커널은 장치 주 번호를 어떤 드라이버를 이용해 그 액세스를 처리할 것인가를 결정하기 위해 사용한다. 이 이야기는 커널은 장치 부 번호를 사용할 필요가 없고, 심지어 몰라도 된다는 말이다. 장치 부 번호에 관심을 갖는 것은 드라이버 자체 뿐이다. 동일한 종류의 하드웨어 중에 어떤 장치인가를 구분하기 위해서만 장치 부 번호는 사용된다.

 

그런데 내가 ‘하드웨어’라고 했을 때, 난 당신의 손에 있는 PCI카드 이상의 좀더 추상적인 것을 의미하는 것이다. 다음의 디바이스 파일을 보자.

% ls -l /dev/fd0 /dev/fd0u1680
brwxrwxrwx   1 root  floppy   2,  0 Jul  5  2000 /dev/fd0
brw-rw----   1 root  floppy   2, 44 Jul  5  2000 /dev/fd0u1680

이제 두 개의 디바이스 파일을 보고 그들이 블록 디바이스라는 것과 동일한 드라이버에 의해 처리된다는 것을 즉시 알 수 있을 것이다. 두 개의 디바이스 파일은 당신의 플로피 드라이브를 나타낸다는 것을 알 것이다. 그런데 당신은 하나의 플로피 드라이버만을 가지고 있지 않은가. 왜 두 개인가? 하난 1.44MB의 플로피 드라이브를 나타낸다. 다른 하나는 흔히 말하는 ‘superformatted’ 드라이브에 해당되는 1.68MB의 동일한 저장 장치다. 표준 포맷 플로피 보다 많은 양의 데이터를 저장하는 것이다. 이것이 동일한 실제 하드웨어에 서로 다른 장치 부 번호를 갖는 경우다. 우리의 논의에서 ‘하드웨어’라고 하는 단어가 상당히 추상적이라는 것을 알기 바란다.

 

5 문자 장치 파일

5.1 문자 디바이스 드라이버

5.1.1 file_operations 구조체

file_operations 구조체는 linux/fs.h에 정의되 있으며 장치들에 대해 다양한 작동을 하는 드라이버에 의해 정의된 함수들을 가지고 있다. 구조체의 각 필드는 요구되는 작동을 하기 위해 드라이버에 의해 정의된 함수들의 주소에 대응된다.

 

예를 들어, 모든 문자 장치 드라이버는 장치로부터 데이터를 읽어 오는 함수를 정의 할 필요가 있다. file_operations 구조체는 그런 기능을 하는 함수 모듈의 주소를 가지고 있는 것이다. 커널 2.4.2에서 그 정의는 다음과 같다.

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
  int (*mmap) (struct file *, struct vm_area_struct *);
  int (*open) (struct inode *, struct file *);
  int (*flush) (struct file *);
  int (*release) (struct inode *, struct file *);
  int (*fsync) (struct file *, struct dentry *, int datasync);
  int (*fasync) (int, struct file *, int);
  int (*lock) (struct file *, int, struct file_lock *);
  ssize_t (*readv)  (struct file *, const struct iovec *, unsigned long, loff_t *);
  ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};

 

어떤 동작들은 드라이버에 의해 구현되지 않는다. 예로 비디오카드를 다루는 드라이버는 디렉토리 구조체에서 데이터를 읽어올 필요는 없다. 이 경우 file_operations구조체의 해당 필드는 NULL로 세팅된다.

 

gcc의 확장 버전은 이 구조체에 인자 할당을 좀더 편하게 해준다. 최근 만들어진 드라이버들에서 이런 것들을 볼 수 있다, 아마도 (사용 방법 때문에) 놀라움을 줄 수도 있다. 다음은 이런 새로운 할당 방식을 보여준다.

struct file_operations fops = {
  read: device_read,
  write: device_write,
  open: device_open,
  release: device_release;
}

 

그러나 C99방식의 구조체 멤버 할당방식도 여전히 존재하며 GNU확장 버전을 사용함에 오히려 이 방식이 선호되고 있다. 내가 현재 사용하고 있는 gcc 2.95버전은 C99 문법의 새로운 부분을 지원하고 있다. 누군가 당신의 드라이버를 포팅하기 원하는 경우 당신은 이 문법을 사용해야 할 것이다. 이 경우 두 종류의 신텍스가 공존할 수 있다.

struct file_operations fops = {
  .read = device_read,
  .write = device_write,
  .open = device_open,
  .release = device_release
};

 

의미는 명확하다 그리고 당신이 할당하지 않은 구조체의 멤버는 gcc에 의해 NULL로 초기화된다는 사실을 기억하자. file_operations 구조체의 포인터는 일반적으로 fops라는 이름을 갖는다.

 

5.1.2 file 구조체

각각의 장치들은 file구조체에 의해 커널 내부에 보여진다. 그리고 그것은 linux/fs.h에 정의 되있다. 구조체는 커널 내부에 존재하며 사용자 공간의 프로그램에서는 절대 보이지 않는다는 것을 명심하자. 구조체와 같지 않다. FILE구조체는 glibc에 정의되 있고 커널영역에는 절대 보이지 않는다. 이름 때문에 잘못 이해되기도 한다. 그것은 열린 파일을 의미하지 디스크상의 inode구조체에 이해 지시되는 파일을 의미하는 것은 아니다.

 

file구조체의 포인터는 일반적으로filp라는 이름을 갖는다. struct file file와 혼돈하지 말자(완전 통밥-_-)

 

file구조체의 정의를 살펴보자. 대부분의 멤버들이 dentry구조체처럼 디바이스 드라이버에서 사용되지 않는다. 또한 그런 부분은 무시해도 좋다. 디바이스 드라이버는 file구조체를 직접적으로 채우지 않고 어디선가 생성한 파일 구조체의 내용을 단지 이용하기만 할 뿐이다.

 

5.1.3 Registering A Device

앞서 논의한 것처럼 문자장치는 보통 dev[1] 에 있는 디바이스 파일을 통해 접근된다. 주장치 번호는 어느 드라이버가 어는 디바이스 파일을 사용하는가 알려준다. 부 장치번호는 드라이버가 하나 이상의 장치를 작동시킬 때, 어떤 장치가 가동되는가를 구분하기 위해 드라이버 내부에서 사용된다.

 

시스템에 드라이버를 추가한다는 것은 커널에 그것을 등록시키는 것을 의미한다. 모듈이 초기화되는 동안 주장치 번호를 부여한다는 것과도 같은 의미다. linux/fs.h에 정의된 register_chrdev()함수를 사용함으로써 이런 작업을 수행한다.

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

 

unsigned int major는 요청할 주장치 번호이고, const char *name은 /proc/devices에 나오는 장치 이름이고 struct file_operations *fops는 드라이버를 위한 file_operations table의 포인터다. 음수를 리턴 할 경우 장치 드라이버 등록에 실패한 것이다. 부장치 번호를 register_chrdev()에 넘기지 않는다는 것에 유의하자. 커널은 부장치 번호에 전혀 신경쓰지 않기 때문이다. 우리의 드라이버만 그 번호를 사용한다.

 

이제 어떻게 이미 사용중인 번호를 가로채지 않고 주장치 번호를 얻어오느냐는 생각이 든다. 가장 쉬운 방법은 Documentation/devices.txt를 보고 사용하지 않는 것을 선택하는 것이다. 그러나 이 번호가 나중에 사용될 수 있으므로 좋은 방법은 아니다. 해결책은 커널에게 동적으로 주장치 번호를 할당해 줄 것을 요청하는 것이다.

 

register_chrdev()에 주장치 번호 0으로 넘기면, 동적으로 할당된 주장치 번호를 리턴 해준다. 주장치 번호를 알 수 없기 때문에 미리 디바이스 파일을 만들 수 없다는 단점이 있다. 몇 가지 해결 책이 있다. 우선, 드라이버가 자신에게 할당된 주장치 번호를 출력하고 수작업으로 그 장치 파일을 만드는 것이다. 다음으로, /proc/devices에 새로 등록된 장치가 있을 것이다. 우리는 직접 디바이스 파일을 만들던지 혹은 파일을 읽어 들여 디바이스 파일을 만드는 쉘 스크립트 만들면 된다. 세 번째로 장치 드라이버를 등록한 후mknod를 이용해 디바이스 파일을 만들고 cleanup_module()을 호출한 후 rm으로 삭제하는 것이다.

 

5.1.4 장치의 등록해제

root가 그러고 싶다고 해서 커널 모듈을 rmmod하게 할 수는 없다. 프로세스에 의해 장치 파일을 열고 모듈을 제거한 후, 장치 파일의 사용시도는 read/write등 올바른 함수가 사용하던 메모리위치를 다른 것으로 하여금 사용하게 할 것이다. 만일 운이 좋다면 그곳에 아무런 코드도 로드되지 않을 것이고 에러 메시지를 받을 것이다. 우리가 운이 없다면 같은 위치로 다른 커널 모듈이 올라올 것이고 그것은 커널 내의 다른 함수의 중간 어딘가로 실행위치를 바꿔 버릴 것이다. 결과는 예상할 수 없으며 매우 부정적이다.

 

일반적으로 우리가 무엇인가 허가 하고 싶지 않다면 그 일을 처리하는 함수에서 에러코드(음수)를 리턴하도록 한다. void 타입이기 때문에 cleanup_module()에서는 이것이 불가능 하다. 그러나 얼마나 많은 프로세스가 그 모듈을 이용하고 있는지 추적하는 카운터가 있다. /proc/modules파일의 세 번째 필드의 값이 바로 이것이다. 이 번호가 0이 아니라면, rmmod는 실패한다. linux/module.c에 정의 되있는 sys_delete_module()에 의해 그 수가 체크되고 있으므로 cleanup_module()에서 카운트 할 필요가 없다. 직접적으로 카운터를 사용하지 말자, linux/modules.h에 정의된 매크로가 있다. 그것은 이 카운터를 증가시키거나 감소시키는 일을 한다.

 

  • MOD_INC_USE_COUNT:카운터를 증가시킨다.
  • MOD_DEC_USE_COUNT:카운터를 감소시킨다.
  • MOD_IN_USE:카운터를 보여준다.

카운터를 정확하기 유지시키는 것이 중요하다. 만약 정확한 카운터의 수를 잃어 버린다면, 모듈을 해제(unload)하는 것은 불가능하다. 부팅을 다시 하자. 모듈 개발을 하는 동안 언젠가는 닥칠 일이다.

 

5.1.5 chardev.c

다음의 코드는chardev라는 이름의 문자 드라이버를 만드는 간단한 예제다. 장치파일의 내용을 화면에 출력 (혹은 다른 프로그램을 이용해 열어 볼 수 있다)할 수 있다. 그러면 드라이버는 파일이 읽혀진 횟수를 파일에 기록한다. echo “hi” > /dev/hello와 같이 파일에 기록하는 것을 지원하지 않는다. 그러나 이런 시도를 감지해 사용자에게 쓰기 기능을 지원하지 않는 다는 것을 알려 준다. 우리가 읽어 버퍼에 기록하는 데이터를 가지고 우리가 하는 일을 볼 수 없다고 걱정하지 마라; 우리는 그것을 가지고 많은 일을 하지 않는다. 단지 데이터를 읽어서 그 데이터를 받았다는 메시지만 출력한다.

 

Example 4-1. chardev.c

/* chardev.c: 얼마나 많이 디바이스 파일에 접근했는지 알려주는 모듈 */
#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
#include <linux/modversions.h>
#define MODVERSIONS
#endif
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>  /* for put_user */

/* Prototypes – this would normally go in a .h file */
int init_module(void);
void cleanup_module(void);
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);
#define SUCCESS 0
#define DEVICE_NAME “chardev” /* /proc/devices에 나타나는 장치 이름 */
#define BUF_LEN 80 /* 장치로부터 메시지의 최대 길이 */

/* 정적 변수로 전역변수 선언 */
tatic int Major; /* 주장치 번호 */
static int Device_Open = 0; /* 장치가 열렸는가? 중복사용 방지 */

/* access to the device */
static char msg[BUF_LEN]; /* 요청이 있을 때 장치가 보내는 메시지 */
static char *msg_Ptr;
static struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};

/* Functions */
int init_module(void)
{
Major = register_chrdev(0, DEVICE_NAME, &fops);
if (Major < 0) {
printk (“Registering the character device failed with %d\n, Major);
return Major;
}
printk(“<1>I was assigned major number %d. To talk to\n, Major);
printk(“<1>the driver, create a dev file with\n);
printk(“‘mknod /dev/hello c %d 0′.\n, Major);
printk(“<1>Try various minor numbers. Try to cat and echo to\n);
printk(“the device file.\n);
printk(“<1>Remove the device file and module when done.\n);
return 0;
}

void cleanup_module(void)
{
/* Unregister the device */
int ret = unregister_chrdev(Major, DEVICE_NAME);
if (ret < 0) printk(“Error in unregister_chrdev: %d\n, ret);
}

/* Methods */
/* “cat /dev/mycharfile” 처럼 프로세스가 디바이스 파일을 열려고 할 때 호출됨 */
static int device_open(struct inode *inode, struct file *file)
{
static int counter = 0;
if (Device_Open) returnEBUSY;

Device_Open++;
sprintf(msg,“I already told you %d times Hello world!\n, counter++);
msg_Ptr = msg;
MOD_INC_USE_COUNT;
return SUCCESS;
}

/* 디바이스 파일이 닫힐 때 호출됨. */
static int device_release(struct inode *inode, struct file *file)
{
Device_Open –; /* We’re now ready for our next caller */
/* 사용 카운트 감소 혹은 한번 열어본 파일이 아니라면 모듈을 제거할 수 없음 */
MOD_DEC_USE_COUNT;
return 0;
}

/* 이미 열린 장치 파일에서 무엇인가 읽으려 할 때 호출 */
static ssize_t device_read(struct file *filp,
char *buffer, /* The buffer to fill with data */
size_t length, /* The length of the buffer */
loff_t *offset) /* Our offset in the file */
{
/* 버퍼에 실제 쓰여진 바이트 수 */
int bytes_read = 0;
/* 메시지의 끝에 메시지의 끝임을 알리기 위해 0리턴 */
if (*msg_Ptr == 0) return 0;
/* 버퍼에 실제 데이터를 입력 */
while (length && *msg_Ptr) {
/* 버퍼는 커널 세그먼트가 아니고 사용자 세그먼트다
* 할당은 일어 나지 않는다. 커널 데이터영역에서 사용자 영역으로
* 데이터를 복사하는 put_user() 사용
*/
put_user(*(msg_Ptr++), buffer++);
length–;
bytes_read++;
}
/* 읽혀진 데이터의 바이트 수를 버퍼에 기록 */
return bytes_read;
}

/* Called when a process writes to dev file: echo “hi” > /dev/hello */
static ssize_t device_write(struct file *filp,
const char *buff,
size_t len,
loff_t *off)
{
printk (“<1>Sorry, this operation isn’t supported.\n);
returnEINVAL;
}

 

5.1.6 여러 커널 버전을 위한 모듈 작성

 

커널과 프로세스 사이의 주요한 인터페이스인 시스템 콜은 일반적으로 버전을 일치시켜 사용한다. 새 버전의 시스템 콜이 추가되더라도 구 버전의 시스템 콜은 여전히 동작한다. 이것은 구 버전과의 공존을 위해 필요하다. –새 버전의 커널은 일반적인 프로세스를 중지시키려 하지 않는다. 대부분의 경우, 장치 파일은 그대로 남아있다. On the other hand반면에 , 커널 내부의 인터페이스는 버전마다 바뀐다.

 

리눅스 커널은 안정버전과 (n.$<$짝수$>$.m) 개발버전으로 (n.$<$홀수$>$.m) 나뉜다. 개발 버전은 다음 버전에서 다시 구현해야 한다거나 오류로 취급될 수도 있는 것을 내포한 모든 참신한 아이디어를 포함하고 있다. 결과적으로, 이런 개발버전의 인터페이스는 신뢰할 수 없다 (내가 이 글에서 그것들을 다루지 않는 이유이기도 하다. 그것은 고된 일이고 바뀌기도 빨리 바뀐다.). 반면에 안정버전에서, 버그 수정 여부와 관계없이 같은 인터페이스를 기대할 수 있다.

 

서로 다른 버전의 커널에는 다른 차이점들이 있다. 만일 여러 버전의 커널을 지원하려 한다면, 조건 지향적인 코드 컴파일 방식이 필요함을 알게 될 것이다.. LINUX_VERSION_CODE 와 KERNEL_VERSION를 비교하는 방법. 커널의 a.b.c버전에서, 매크로의 값은 $2^{16}a+2^{8}b+c$ 이다. 커널2.0.35이전 에는 이 매크로가 없다는 것을 알아두자. 구식 커널을 지원하는 모듈을 작성하고자 한다면, 다음과 같이 정의 해야 한다.

 

Example 4-2. some title

#if LINUX_KERNEL_VERSION >= KERNEL_VERSION(2,2,0)
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

 

물론 이런 매크로가 있기 때문에, 당신은 매크로의 존재여부를 테스트하기 위해 커널 버전을 테스트 하는 것보다 #ifndef KERNEL_ VERSION 을 사용하는 것이 좋을 것이다.

 

6 /proc파일 시스템

6.1 /proc File System

 

리눅스에는 커널과 커널 모듈이 프로세스로 정보를 보내는 추가적인 방법이 있다. /proc파일 시스템이 그것이다. 원래 이것은 프로세스에 관한 정보에 쉽게 접근하도록 설계된 것이다(이름을 보라.). 모듈의 리스트를 가지고 있는 /proc/modules, 그리고 메모리 사용량을 보여주는 /proc/meminfo등과 같이 커널이 관심을 갖는 것들에 상용된다.

 

/proc파일 시스템을 사용하는 방법은 디바이스 드라이버를 이용하는 방법과 매우 유사하다. 여러분은 함수의 핸들러 포인터를 포함해 /proc파일에 필요한 모든 정보를 가지고 구조체를 생성한다.( 우리의 경우 한가지 정보, 누군가 /proc 파일로부터 무엇인가 읽으려 시도할 때 그것이 호출된다.). 그런 후 커널에 init_module()이 그 구조체를 커널에 등록하고 cleanup_module이 등록을 해제한다.

 

우리가 우리의 파일이 이용할 inode 번호를 결정하기를 원하지 않고 커널이 결정하게 해서 시스템 다운을 방지하기 원하기 때문에 우리는 proc_register_dynamic()[1]함수를 사용한다. 일반적인 파일은 메모리에 존재하기보다는 디스크에 존재한다.(그것이 /proc이다.), 또한 그런 경우 파일의 인덱스 노드(index-node, inode는 약어다)가 위치한 디스크를 inode번호가 지시한다. inode구조체는 파일에 대한 정보를 담고 있다, 예를 들어 파일의 퍼미션, 디스크 어디서 파일의 데이터를 찾을 수 있는가 등등.

 

파일이 열리거나 닫힐 때 우리가 호출하지 않았기 때문에 , 모듈 내부에서 MOD_INC_USE_COUNT와 MOD_DEC_USE_COUNT에 접근할 방법이 없다. 또한 파일이 열린 상태에서 모듈이 제거될 때 생기는 문제를 피할 방법이 없다. 다음 장에서 우리는 더 복잡한 구현을 본다, 그러나 그것은 /proc파일을 다루는데 있어 더 유연한 방법을 제시해줄 것이다. 그리고 그것은 문제에 봉착하지 않게 해주기도 한다.

 

Example 5-1. procfs.c

/*  procfs.c -  create a "file" in /proc  */
#include <linux/kernel.h>   /* 커널 작업 */
#include <linux/module.h>   /* 모듈 작업 */
/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        /* proc fs를 다룬다 */
#include <linux/proc_fs.h>
/* 2.2.3 버전 이전에는 다음의 매크로가 없다. 필요하다면 여기서 정의 한다.*/
#ifndef KERNEL_VERSION#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

/* proc fs에 데이터 전달.
Argument =========
1. 데이터가 삽입되는 버퍼, 만일 사용하기 원한다면
2. 문자열에 대한 포인터, 만일 커널이 버퍼를 할당하기 원하지 않는 경우 사용
3. 파일에서 현재 위치
4. 첫 인자 버퍼의 크기
5. 0(미래의 사용을 위해?).
Usage and Return Value ======================
만일 자신만의 버퍼를 원한다면 내가 한 것처럼 두번 째 인자에 넣고 버퍼에서 사용한 바이트 수를 리턴하라
리턴 값이 NULL인것은 더 이상의 정보가 없음을 의미한다(end of file). 또한 음수인 경우 에러 상황이다.
For More Information ====================
The way I discovered what to do with this function
내가 이 함수에서 무엇인가를 발견한 방법은 문서를 읽는 것이 아니고, 사용되는 코드를 읽는 것이었다.
proc_dir_entry구조체의 get_info필드를 어디다 쓰는가를 살펴봤다.
또한 /fs/proc/array.c 파일이 사용되는 것을 살펴 본 것이다.
If something is unknown about the kernel, this is
무언가 커널에서 잘 모르는 부분이 있다면 이런 방식이 일반적으로 행해진다.
리눅스에서 우리는 커널 코드를 얻을 수 있다는 것은 대단한 장점이다. 활용하라. */

int procfile_read(char *buffer,
char **buffer_location, off_t offset,
int buffer_length,
int zero)
{
int len; /* 실제 사용되는 바이트 수 */
/* 우리가 이 함수를 나갈 때도 이 스테틱 변수이기 때문에 메모리에 남겨 진다. */

static char my_buffer[80];
static int count = 1;
/* 우리(직역)는 한번에 모든 정보를 주기 때문에 유저(?직역)가 추가적인 정보를 요구할 때 우리의 대답은 NO다.
* 표준 라이브러리 함수는 커널이 더 이상 줄 정보가 없다고 말할 때까지, 혹은 표준 라이브러리 함수의 버퍼가 다 찰 때까지
* read 시스템 콜을 하기 때 문에 중요하다. */

if (offset > 0) return 0; /* Fill the buffer and get its length */
len = sprintf(my_buffer, “For the %d%s time, go away!\n, count,
(count % 100 > 10 && count % 100 < 14) ? “th” : (count % 10 == 1) ? “st” : (count % 10 == 2) ? “nd” :
(count % 10 == 3) ? “rd” : “th” );
count++; /* 함수에 버퍼의 위치를 알린다. */
*buffer_location = my_buffer; /* Return the length */
return len;
}

struct proc_dir_entry Our_Proc_File =
{0, /* Inode 번호 &#8211;여기서는 무시한다. 이것은 proc_register[_dynamic]에 의해 채워진다. */
4, /* 파일의 이름 길이 */
“test”, /* 파일이름 */
S_IFREG | S_IRUGO, /* File mode &#8211; 일반 파일, 소유자, 그룹, 모든 사람들이 있을 수 있다.*/
1, /* 파일이 참고하는 디렉토리의 링크 개수*/
0, 0, /* uid, gid 루트에게 권한을 준다.*/
80, /* ls에 의해 보고되는 파일의 크기. */
NULL, /* link, remove등 inode상에 행해지는 함수- 우리는 아무것도 지원하지 않는다. */
procfile_read, /* 읽기 함수, 무엇인가를 읽으려 할 때 호출되는 함수 */
NULL /* 퍼미션, 소유권 등을 다루는 inode를 채우는 함수의 포인터 */ };

/* Initialize the module – register the proc file */
int init_module()
{
/* proc_register[_dynamic]이 성공하면 성공, 아니면 실패 */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
/* 구조체에서 그 값이 0이면 2.2버전에서는 proc_register()함수가 inode번호를 동적으로 할당한다.
* proc_register_dynamic()함수가 필요없게 된다. */
return proc_register(&proc_root, &Our_Proc_File);
#else
return proc_register_dynamic(&proc_root, &Our_Proc_File);
#endif
/* proc_root는 proc파일 시스템의 루트(/proc)
* 우리가 파일을 위치시키기 원하는 위치다.
*/
}

/* Cleanup – unregister our file from /proc */
void cleanup_module()
{
proc_unregister(&proc_root, Our_Proc_File.low_ino);
}

 

7 입력을 위한 /proc의 사용

7.1 입력을 위한 /proc의 사용

지금까지 우리는 커널 모듈로부터 출력을 얻는 두 가지 방법을 알아 보았다. 우리는 장치 드라이버와 mknod를 등록하거나 /proc 파일을 생성할 수 있다. 이것은 커널 모듈로 하여금 우리에게 무엇인가를 전달할 수 있도록 해준다. 그 반대의 경우가 불가능하다는 것이 문제가 된다. 우선 커널 모듈에 데이터를 입력하는 것은 /proc 파일에 데이터를 쓰는 방법을 택한다.

 

/proc파일 시스템은 커널이 프로세스의 상태를 보고할 수 있도록 한 것이기 때문에 , 입력에 대한 특별한 배려는 없다. proc_dir_entry 구조체에는 출력 함수와 같은 방식의 입력 함수에 대한 포인터가 없다. /proc파일에 쓰기를 하는 대신, 표준 파일 시스템 메커니즘을 사용할 필요가 있다. 리눅스에는 파일 시스템 등록을 위한 표준 메커니즘이 있다. 모든 파일 시스템은 inode와 file operation을 다루기 위한 고유한 함수를 가지고 있어야 하기 때문에 이 모든 함수의 포인터를 가지고 있는 inode_operations 라는 구조체가 있으며, 그리고 그것은 file_operations구조체를 지시하는 포인터를 포함하고 있다. 새 파일을 등록할 때마다, /proc에서 그것에 접근하기 위해 어느 inode_operations 구조체를 사용할 것인지 결정한다. 우리가 사용할 module_input(), module_output()함수를 지시하는 포인터를 포함한 file_operation 구조체를 지시하는 포인터를 포함한 inode_operations 구조체, 이것이 우리가 사용하는 메커니즘이다.

 

읽기와 쓰기를 위한 표준 롤 셋이 커널에서는 반대로 지정돼있다는 것을 명심하자. 읽기 함수는 출력을 위해, 반대로 쓰기함수는 입력을 위해 사용 된다. 일기와 쓰기는 사용자의 관점에서 본 것이기 때문이다. — 만일 프로세스가 커널에서 무언가 읽는다면 커널은 그것을 출력해 주어야 한다. 그리고 프로세스가 커널에 무언가를 쓴다면 커널은 그것을 입력으로 받아 들여야 한다.

 

흥미 있는 또 다른 것은 module_permissions() 함수다. 프로세스가 /proc 파일에 무언가를 하려고 할 때마다, 이 함수는 호출 된다. 그리고 이 함수는 이 접근을 허용할 것인가 말 것인가를 결정하게 된다. 당장은 이것이 현재 사용자의 uid와 행위(operation) 자체에만 근거 하지만, (그리고 이것은 현재 실행되는 프로세스의 정보를 담고 있는 구조체의 포인터에서 찾아 질 수 있는 것들) 이것은 프로세스가 무엇을 하는 것인가, 작업 시각, 마지막으로 무엇을 입력 받았는가 등, 우라기 원하는 어떤 것에 근거할 수도 있다.

 

인텔 아키텍쳐에서(물론 다른 프로세서 하에서는 달라질 수도 있지만) put_user() 매크로와 get_user()매크로를 사용하는 이유는 리눅스의 메모리가 세그먼트 이기 때문이다. 포인터 자체는 유니크한 메모리 위치를 지시할 수 없고, 메모리 세그먼트 위치만 지시할 수 있다. 또한 어느 메모리 세그먼트를 사용할 수 있는지 알 필요가 있다. 커널을 위한 메모리 하나의 세그먼트가 있고, 각 프로세스들마다 하나의 세그먼트가 존재한다.

 

프로세스 자신의 메모리 세그먼트에만 접근이 가능하다. 그래서 일반적인 프로그램이 프로세스로서 실행될 때 세그먼트에 관해 신경 쓰지 않아도 된다. 커널 모듈을 작성할 때, 일반적으로 시스템에 의해 자동으로 다뤄지는 커널 메모리 세그먼트에 접근하기를 원한다. 그러나 메머리 버퍼의 내용이 현재 실행 중인 프로세스와 커널 사이에서 전달 되야 할 때, 커널 함수는 프로세스 세그먼트에 있는 메모리 포인터를 받게 된다. put_user() 매크로와 get_user() 매크로가 그 메모리에 접근 가능케 해준다.

 

Example 6-1. procfs.c

/*  procfs.c -  create a "file" in /proc, which allows both input and output. */
#include <linux/kernel.h>   /* We're doing kernel work */
#include <linux/module.h>   /* Specifically, a module */
/* Necessary because we use proc fs */
#include <linux/proc_fs.h>
/* In 2.2.3 /usr/include/linux/version.h includes a  
 * macro for this, but 2.0.35 doesn't - so I add it  
 * here if necessary. */
#ifndef KERNEL_VERSION#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>  /* for get_user and put_user */
#endif

/* The module’s file functions ********************** */
/* Here we keep the last message received, to prove
* that we can process our input */
#define MESSAGE_LENGTH 80
static char Message[MESSAGE_LENGTH];
/* Since we use the file operations struct, we can’t
* use the special proc output provisions – we have to
* use a standard read function, which is this function */
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(2,2,0)
static ssize_t module_output(
struct file *file, /* The file read */
char *buf, /* The buffer to put data to (in the
* user segment) */
size_t len, /* The length of the buffer */
loff_t *offset)
/* Offset in the file – ignore */
#else
static int module_output(
struct inode *inode, /* The inode read */
struct file *file, /* The file read */
char *buf, /* The buffer to put data to (in the
* user segment) */
int len)
/* The length of the buffer */
#endif
{
static int finished = 0;
int i;
char message[MESSAGE_LENGTH+30];

/* We return 0 to indicate end of file, that we have
* no more information. Otherwise, processes will
* continue to read from us in an endless loop. */
if (finished)
{
finished = 0;
return 0;
}

/* We use put_user to copy the string from the kernel’s
* memory segment to the memory segment of the process
* that called us. get_user, BTW, is
* used for the reverse. */
sprintf(message, “Last input:%s, Message);

for(i=0; i&lt;len && message[i]; i++)
put_user(message[i], buf+i);
/* Notice, we assume here that the size of the message
* is below len, or it will be received cut. In a real
* life situation, if the size of the message is less
* than len then we’d return len and on the second call
* start filling the buffer with the len+1’th byte of
* the message. */
finished = 1;

return i;

/* Return the number of bytes “read” */
}
/* This function receives input from the user when the
* user writes to the /proc file. */

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t module_input(
struct file *file, /* The file itself */
const char *buf, /* The buffer with input */
size_t length, /* The buffer’s length */
loff_t *offset) /* offset to file – ignore */
#else
static int module_input(
struct inode *inode, /* The file’s inode */
struct file *file, /* The file itself */
const char *buf, /* The buffer with the input */
int length) /* The buffer’s length */
#endif
{
int i;
/* Put the input into Message, where module_output
* will later be able to use it */

for(i=0; i<MESSAGE_LENGTH-1 && i<length; i++)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
get_user(Message[i], buf+i);
/* In version 2.2 the semantics of get_user changed,
* it not longer returns a character, but expects a
* variable to fill up as its first argument and a
* user segment pointer to fill it from as the its
* second.
*
* The reason for this change is that the version 2.2
* get_user can also read an short or an int. The way
* it knows the type of the variable it should read
* is by using sizeof, and for that it needs the
* variable itself. */
#else
Message[i] = get_user(buf+i);#endif Message[i] = ‘\0’;
/* we want a standard, zero
* terminated string */

/* We need to return the number of input characters
* used */

return i;
}

/* This function decides whether to allow an operation
* (return zero) or not allow it (return a non-zero
* which indicates why it is not allowed).
*
* The operation can be one of the following values:
* 0 – Execute (run the “file” – meaningless in our case)
* 2 – Write (input to the kernel module)
* 4 – Read (output from the kernel module)
*
* This is the real function that checks file
* permissions. The permissions returned by ls -l are
* for referece only, and can be overridden here.
*/
static int module_permission(struct inode *inode, int op)
{
/* We allow everybody to read from our module, but
* only root (uid 0) may write to it */

if (op == 4 || (op == 2 && current->euid == 0))
return 0;
/* If it’s anything else, access is denied */
returnEACCES;
}

/* The file is opened – we don’t really care about
* that, but it does mean we need to increment the
* module’s reference count. */
int module_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;

return 0;
}

/* The file is closed – again, interesting only because
* of the reference count. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
int module_close(struct inode *inode, struct file *file)
#else
void module_close(struct inode *inode, struct file *file)
#endif
{
MOD_DEC_USE_COUNT;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
return 0;
/* success */
#endif
}

/* Structures to register as the /proc file, with
* pointers to all the relevant functions.
*/

/* File operations for our proc file. This is where we
* place pointers to all the functions called when
* somebody tries to do something to our file. NULL
* means we don’t want to deal with something. */
static struct file_operations File_Ops_4_Our_Proc_File =
{ NULL, /* lseek */
module_output, /* “read” from the file */
module_input, /* “write” to the file */
NULL, /* readdir */
NULL, /* select */
NULL, /* ioctl */
NULL, /* mmap */
module_open, /* Somebody opened the file */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
NULL, /* flush, added here in version 2.2 */
#endif
module_close, /* Somebody closed the file */
/* etc. etc. etc. (they are all given in
* /usr/include/linux/fs.h). Since we don’t put
* anything here, the system will keep the default
* data, which in Unix is zeros (NULLs when taken as
* pointers). */
};

/* Inode operations for our proc file. We need it so
* we’ll have some place to specify the file operations
* structure we want to use, and the function we use for
* permissions. It’s also possible to specify functions
* to be called for anything else which could be done to
* an inode (although we don’t bother, we just put * NULL). */
static struct inode_operations Inode_Ops_4_Our_Proc_File =
{ &File_Ops_4_Our_Proc_File,
NULL, /* create */
NULL, /* lookup */
NULL, /* link */
NULL, /* unlink */
NULL, /* symlink */
NULL, /* mkdir */
NULL, /* rmdir */
NULL, /* mknod */
NULL, /* rename */
NULL, /* readlink */
NULL, /* follow_link */
NULL, /* readpage */
NULL, /* writepage */
NULL, /* bmap */
NULL, /* truncate */
module_permission /* check for permissions */
};/* Directory entry */

static struct proc_dir_entry Our_Proc_File =
{ 0, /* Inode number – ignore, it will be filled by
* proc_register[_dynamic] */
7, /* Length of the file name */
“rw_test”, /* The file name */
S_IFREG | S_IRUGO | S_IWUSR,
/* File mode – this is a regular file which
* can be read by its owner, its group, and everybody
* else. Also, its owner can write to it.
*
* Actually, this field is just for reference, it’s
* module_permission that does the actual check. It
* could use this field, but in our implementation it
* doesn’t, for simplicity. */
1, /* Number of links (directories where the
* file is referenced) */
0, 0, /* The uid and gid for the file –
* we give it to root */
80, /* The size of the file reported by ls. */
&Inode_Ops_4_Our_Proc_File,
/* A pointer to the inode structure for
* the file, if we need it. In our case we
* do, because we need a write function. */
NULL
/* The read function for the file. Irrelevant,
* because we put it in the inode structure above */
};

/* Module initialization and cleanup ******************* */

/* Initialize the module – register the proc file */
int init_module()
{
/* Success if proc_register[_dynamic] is a success,
* failure otherwise */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* In version 2.2, proc_register assign a dynamic
* inode number automatically if it is zero in the
* structure , so there’s no more need for
* proc_register_dynamic */
return proc_register(&proc_root, &Our_Proc_File);
#else
return proc_register_dynamic(&proc_root, &Our_Proc_File);
#endif
}

/* Cleanup – unregister our file from /proc */
void cleanup_module()
{
proc_unregister(&proc_root, Our_Proc_File.low_ino);
}

 

8 Talking to Device Files

8.1 Talking to Device Files (writes and IOCTLs)

디바이스 파일은 물리적 장치를 나타내는 것으로 간주된다. 대부분의 물리적 장치들은 입출력을 위해 사용된다. 그래서 커널 안에서 디바이스 드라이버가 프로세스로부터 디바이스로 보낼 아웃풋을 얻기 위해 특별한 메커니즘이 필요하다. 파일에 쓰기를 하는 것처럼, 아웃풋을 쓰기 위해 디바이스 파일을 오픈 하는 것으로 된다. 다음의 예에서 이것은 device_write()함수에 의해 구현된다.

 

이것으로 항상 충분한 것은 아니다. 모뎀에 연결된 시리얼 포트가 있다고 상상해보자. (내장 모뎀이라 해도 CPU의 관점에서 모뎀에 시리얼 포트가 연결된 것으로 구현된다. 너무 지나치게 깊이 상상하지 말자). 해야 할 것은 디바이스 파일을 이용해 모뎀에 무언가(모뎀 명령어 혹은 전화선을 통해 나가는 데이터)를 기록하는 것이고, 모뎀으로부터 무언가(명령에 대한 응답, 혹은 전화선을 통한 데이터)를 읽어 오는 것이다. 예를 들어 어떤 속도록 데이터를 보낼 것인가? 와 같은 질문은 여전히 남는다.

 

유닉스에서 Input Output Control 의 준 말인ioctl()이라는 특수한 함수를 호출하는 것이 답이 된다. 모든 디바이스드라이버는 자신만의 ioctl 명령을 갖을 수 있고, 그리고 그것은 읽기를 위한 ioctl 커맨드(프로세스로부터 커널로 정보를 보내는 )이거나 쓰기를 위한 ioctl 커맨드(프로세스로 정보를 돌리는), 혹은 둘 모두, 혹은 둘 모드 아니다. ioctl함수는 다음과 같은 세 개의 인자를 가진다. 해당되는 디바이스 파일의 디스크립터, ioctl번호, 롱형으로 어떤 타입으로던지 케스팅후 사용할 수 있는 인자[2]

 

ioctl의 번호는 장치 주번호, ioctl의 타입(읽기/쓰기), 커맨드, 인자의 타입으로 만들어 진다. 이 ioctl 번호는 헤더파일에 있는 _IO, _IOR, _IOW or _IOWR(이들은 보통 타입에 따라 달리 쓰임) 와 같은 매크로에 의해 보통 만들어진다. 헤더 파일은 ioctl을 사용하는 프로그램(적당한 ioctl명령을 만들기 위해 )이나, 커널 모듈(만들어 진 ioctl명령을 이해하기 위해) 모두에 포함되야 한다. 다음의 예에서, chardev.h가 헤더 파일이고 그것을 사용하는 프로그램은 ioctl.c이다.

 

만일 당신의 커널 모듈에서 ioctl을 사용하기 원한다면, ioctl 명령을 할당하는 것이 가장 좋다. 그래서 우연히 다른 사람의 ioctl 커맨드를 받게 되거나, 다른 사람이 당신의 ioctl커맨드를 받아도 그것이 잘못됐다는 것을 알게 된다. 좀더 많은 정보를 위해서 커널소스의 Documentation/ioctl-number.txt파일을 참고하길 바란다.

 

Example 7-1. chardev.c

/*  chardev.c - Create an input/output character device
 */

#include <linux/kernel.h> /* We’re doing kernel work */
#include <linux/module.h> /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif

/* For character devices */

/* The character device definitions are here */
#include <linux/fs.h>

/* A wrapper which does next to nothing at
* at present, but may help for compatibility
* with future versions of Linux */
#include <linux/wrapper.h>

/* Our own ioctl numbers */
#include “chardev.h”

/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t – so I add it
* here if necessary. */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h> /* for get_user and put_user */
#endif

#define SUCCESS 0

/* Device Declarations ******************************** */

/* The name for our device, as it will appear in
* /proc/devices */
#define DEVICE_NAME “char_dev”

/* The maximum length of the message for the device */
#define BUF_LEN 80

/* Is the device open right now? Used to prevent
* concurent access into the same device */
static int Device_Open = 0;

/* The message the device will give when asked */
static char Message[BUF_LEN];

/* How far did the process reading the message get?
* Useful if the message is larger than the size of the
* buffer we get to fill in device_read. */
static char *Message_Ptr;

/* This function is called whenever a process attempts
* to open the device file */
static int device_open(struct inode *inode,
struct file *file)
{
#ifdef DEBUG
printk (“device_open(%p)\n, file);
#endif

/* We don’t want to talk to two processes at the
* same time */
if (Device_Open)
returnEBUSY;

/* If this was a process, we would have had to be
* more careful here, because one process might have
* checked Device_Open right before the other one
* tried to increment it. However, we’re in the
* kernel, so we’re protected against context switches.
*
* This is NOT the right attitude to take, because we
* might be running on an SMP box, but we’ll deal with
* SMP in a later chapter.
*/

Device_Open++;

/* Initialize the message */
Message_Ptr = Message;

MOD_INC_USE_COUNT;

return SUCCESS;
}

/* This function is called when a process closes the
* device file. It doesn’t have a return value because
* it cannot fail. Regardless of what else happens, you
* should always be able to close a device (in 2.0, a 2.2
* device file could be impossible to close).
*/
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static int device_release(struct inode *inode,
struct file *file)
#else
static void device_release(struct inode *inode,
struct file *file)
#endif
{
#ifdef DEBUG
printk (“device_release(%p,%p)\n, inode, file);
#endif

/* We’re now ready for our next caller */
Device_Open –;

MOD_DEC_USE_COUNT;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
return 0;
#endif
}

/* This function is called whenever a process which
* has already opened the device file attempts to
* read from it. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_read(
struct file *file,
char *buffer, /* The buffer to fill with the data */
size_t length, /* The length of the buffer */
loff_t *offset) /* offset to the file */
#else
static int device_read(
struct inode *inode,
struct file *file,
char *buffer, /* The buffer to fill with the data */
int length) /* The length of the buffer
* (mustn’t write beyond that!) */
#endif
{
/* Number of bytes actually written to the buffer */
int bytes_read = 0;

#ifdef DEBUG
printk(“device_read(%p,%p,%d)\n, file, buffer, length);
#endif

/* If we’re at the end of the message, return 0
* (which signifies end of file) */
if (*Message_Ptr == 0)
return 0;

/* Actually put the data into the buffer */
while (length && *Message_Ptr) {

/* Because the buffer is in the user data segment,
* not the kernel data segment, assignment wouldn’t
* work. Instead, we have to use put_user which
* copies data from the kernel data segment to the
* user data segment. */
put_user(*(Message_Ptr++), buffer++);
length –;
bytes_read ++;
}

#ifdef DEBUG
printk (“Read %d bytes, %d left\n, bytes_read, length);
#endif

/* Read functions are supposed to return the number
* of bytes actually inserted into the buffer */
return bytes_read;
}

/* This function is called when somebody tries to
* write into our device file. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_write(struct file *file,
const char *buffer,
size_t length,
loff_t *offset)
#else
static int device_write(struct inode *inode,
struct file *file,
const char *buffer,
int length)
#endif
{
int i;

#ifdef DEBUG
printk (“device_write(%p,%s,%d)”,
file, buffer, length);
#endif

for(i=0; i<length && i<BUF_LEN; i++)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
get_user(Message[i], buffer+i);
#else
Message[i] = get_user(buffer+i);
#endif

Message_Ptr = Message;

/* Again, return the number of input characters used */
return i;
}

/* This function is called whenever a process tries to
* do an ioctl on our device file. We get two extra
* parameters (additional to the inode and file
* structures, which all device functions get): the number
* of the ioctl called and the parameter given to the
* ioctl function.
*
* If the ioctl is write or read/write (meaning output
* is returned to the calling process), the ioctl call
* returns the output of this function.
*/
int device_ioctl(
struct inode *inode,
struct file *file,
unsigned int ioctl_num,/* The number of the ioctl */
unsigned long ioctl_param) /* The parameter to it */
{
int i;
char *temp;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
char ch;
#endif

/* Switch according to the ioctl called */
switch (ioctl_num) {
case IOCTL_SET_MSG:
/* Receive a pointer to a message (in user space)
* and set that to be the device’s message. */

/* Get the parameter given to ioctl by the process */
temp = (char *) ioctl_param;

/* Find the length of the message */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
get_user(ch, temp);
for (i=0; ch && i<BUF_LEN; i++, temp++)
get_user(ch, temp);
#else
for (i=0; get_user(temp) && i<BUF_LEN; i++, temp++)
;
#endif

/* Don’t reinvent the wheel – call device_write */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
device_write(file, (char *) ioctl_param, i, 0);
#else
device_write(inode, file, (char *) ioctl_param, i);
#endif
break;

case IOCTL_GET_MSG:
/* Give the current message to the calling
* process – the parameter we got is a pointer,
* fill it. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
i = device_read(file, (char *) ioctl_param, 99, 0);
#else
i = device_read(inode, file, (char *) ioctl_param, 99);
#endif
/* Warning – we assume here the buffer length is
* 100. If it’s less than that we might overflow
* the buffer, causing the process to core dump.
*
* The reason we only allow up to 99 characters is
* that the NULL which terminates the string also
* needs room. */

/* Put a zero at the end of the buffer, so it
* will be properly terminated */
put_user(‘\0’, (char *) ioctl_param+i);
break;

case IOCTL_GET_NTH_BYTE:
/* This ioctl is both input (ioctl_param) and
* output (the return value of this function) */
return Message[ioctl_param];
break;
}

return SUCCESS;
}

/* Module Declarations *************************** */

/* This structure will hold the functions to be called
* when a process does something to the device we
* created. Since a pointer to this structure is kept in
* the devices table, it can’t be local to
* init_module. NULL is for unimplemented functions. */
struct file_operations Fops = {
NULL, /* seek */
device_read,
device_write,
NULL, /* readdir */
NULL, /* select */
device_ioctl, /* ioctl */
NULL, /* mmap */
device_open,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
NULL, /* flush */
#endif
device_release /* a.k.a. close */
};

/* Initialize the module – Register the character device */
int init_module()
{
int ret_val;

/* Register the character device (atleast try) */
ret_val = module_register_chrdev(MAJOR_NUM,
DEVICE_NAME,
&Fops);

/* Negative values signify an error */
if (ret_val < 0) {
printk (%s failed with %d\n,
“Sorry, registering the character device “,
ret_val);
return ret_val;
}

printk (%s The major device number is %d.\n,
“Registeration is a success”,
MAJOR_NUM);
printk (“If you want to talk to the device driver,\n);
printk (“you’ll have to create a device file. \n);
printk (“We suggest you use:\n);
printk (“mknod %s c %d 0\n, DEVICE_FILE_NAME,
MAJOR_NUM);
printk (“The device file name is important, because\n);
printk (“the ioctl program assumes that’s the\n);
printk (“file you’ll use.\n);

return 0;
}

/* Cleanup – unregister the appropriate file from /proc */
void cleanup_module()
{
int ret;

/* Unregister the device */
ret = module_unregister_chrdev(MAJOR_NUM, DEVICE_NAME);

/* If there’s an error, report it */
if (ret < 0)
printk(“Error in module_unregister_chrdev: %d\n, ret);
}

 

Example 7-2. chardev.h

/*  chardev.h - the header file with the ioctl definitions.
 *
 *  The declarations here have to be in a header file, because
 *  they need to be known both to the kernel module
 *  (in chardev.c) and the process calling ioctl (ioctl.c)
 */

#ifndef CHARDEV_H
#define CHARDEV_H

#include <linux/ioctl.h>

/* The major device number. We can’t rely on dynamic
* registration any more, because ioctls need to know
* it. */
#define MAJOR_NUM 100

/* Set the message of the device driver */
#define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *)
/* _IOR means that we’re creating an ioctl command
* number for passing information from a user process
* to the kernel module.
*
* The first arguments, MAJOR_NUM, is the major device
* number we’re using.
*
* The second argument is the number of the command
* (there could be several with different meanings).
*
* The third argument is the type we want to get from
* the process to the kernel.
*/

/* Get the message of the device driver */
#define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *)
/* This IOCTL is used for output, to get the message
* of the device driver. However, we still need the
* buffer to place the message in to be input,
* as it is allocated by the process.
*/

/* Get the n’th byte of the message */
#define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int)
/* The IOCTL is used for both input and output. It
* receives from the user a number, n, and returns
* Message[n]. */

/* The name of the device file */
#define DEVICE_FILE_NAME “char_dev”

#endif

 

Example 7-3. ioctl.c

/*  ioctl.c - the process to use ioctl's to control the kernel module
 *
 *  Until now we could have used cat for input and output.  But now
 *  we need to do ioctl's, which require writing our own process. 
 */

/* device specifics, such as ioctl numbers and the
* major device file. */
#include “chardev.h”

#include <fcntl.h> /* open */
#include <unistd.h> /* exit */
#include <sys/ioctl.h> /* ioctl */

/* Functions for the ioctl calls */

ioctl_set_msg(int file_desc, char *message)
{
int ret_val;

ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);

if (ret_val < 0) {
printf (“ioctl_set_msg failed:%d\n, ret_val);
exit(-1);
}
}

ioctl_get_msg(int file_desc)
{
int ret_val;
char message[100];

/* Warning – this is dangerous because we don’t tell
* the kernel how far it’s allowed to write, so it
* might overflow the buffer. In a real production
* program, we would have used two ioctls – one to tell
* the kernel the buffer length and another to give
* it the buffer to fill
*/
ret_val = ioctl(file_desc, IOCTL_GET_MSG, message);

if (ret_val < 0) {
printf (“ioctl_get_msg failed:%d\n, ret_val);
exit(-1);
}

printf(“get_msg message:%s\n, message);
}

ioctl_get_nth_byte(int file_desc)
{
int i;
char c;

printf(“get_nth_byte message:”);

i = 0;
while (c != 0) {
c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++);

if (c < 0) {
printf(
“ioctl_get_nth_byte failed at the %d‘th byte:\n, i);
exit(-1);
}

putchar(c);
}
putchar(‘\n’);
}

/* Main – Call the ioctl functions */
main()
{
int file_desc, ret_val;
char *msg = “Message passed by ioctl\n;

file_desc = open(DEVICE_FILE_NAME, 0);
if (file_desc < 0) {
printf (“Can’t open device file: %s\n,
DEVICE_FILE_NAME);
exit(-1);
}

ioctl_get_nth_byte(file_desc);
ioctl_get_msg(file_desc);
ioctl_set_msg(file_desc, msg);

close(file_desc);
}

 

9 시스템 콜

9.1 시스템 콜

지금까지 우리가 한 것은 /proc파일과 디바이스 헨들러를 등록하기 위해 잘 정의된 커널 메커니즘을 사용한 것이다. 장치 드라이버를 작성하는 것과 같은 커널 프로그램을 하는 것에는 지금까지의 것도 좋다. 그러나 특정한 방식으로 시스템의 행위를 변경하는 것과 같은 독특한 것을 원하는가? 그런 것들은 전적으로 당신의 몫이다.

 

이 부분이 커널 프로그램에서 주의해야 할 부분이다. 아래 예제에서 open() 시스템 콜을 죽였다. 이것은 어떤 파일도 열거나, 어떤 프로그램을 실행하거나, 컴퓨터를 셧다운 시키지도 못하게 됨을 의미한다. 전원 스위치를 강제로 꺼야 한다. 다행히도 어떤 파일도 손상을 입지 않았다. 어떤 파일도 잃지 않기 위해서는 insmod를 하거나 rmmod를 실행하기 전에 sync를 실행 시키자.

 

/proc파일이나 디바이스 파일은 잊자. 그들은 간단한 문제다. 모든 프로세스가 사용하는 단 하나의 실제 프로세스에서 커널로의 통신 메커니즘은 시스템 콜이다. 파일을 열거나 새로운 프로세스를 생성하거나 추가적인 메모리를 요구하는 등, 프로세스가 커널에게 서비스를 요구할 때, 이 메커니즘이 사용된다. 원하는 방식으로 커널이 움직이기를 원한다면 바로 여기가 작업해야 할 부분이다. 어떤 시스템 콜이 프로그램에 의해 사용되는 가를 알기 원한다면, strace <arguments> 실행 시키자.

 

일반적으로 프로세스는 커널에 직접적으로 접근할 수 없다. 프로세스는 커널 메모리에 접근한다거나 커널의 함수를 호출할 수 없다. 하드웨어의 CPU가 그렇게 하며 이것이 보호모드라 부르는 이유다.

 

시스템 콜은 일반적인 규칙에서의 예외다. 프로세스가 적당한 값으로 레지스터를 채우고, 커널 안에 미리 정의 되있는 위치로 점프하는 인스트럭션(명령)을 호출하는 것이 바로 이것이다. 인텔 CPU에서는 이것이 인터럽트 0x80을 의미한다. 일단 이 위치로 이동하면 하드웨어는 제한 적인 사용자 모드가 아니고 운영체계의 커널로 인식한다. 비로소 원하는 것을 할 수 있다. 커널 내부의 위치에서 프로세스는 시스템 콜로 점프가 가능하다. 이 위치에서 프로시져는 시스템 콜의 번호를 확인한다. 그리고 그것은 커널에게 어떤 서비스를 프로세스가 요구했는지 알려준다. 그런 후, 시스템 콜 테이블(sys_chall_tabel)을 커널 함수를 호출하기 위한 주소를 알아보기 위해 조사한다. 그 후 함수를 호출하며, 함수가 리턴한 후 몇 가지 시스템 체크를 하고, 프로세스로 돌아온다. 이 코드를 보기를 원한다면, /$<$architecture$>$/kernel/entry.S의 다음 라인인 ENTRY(system_call)에 있으니 보기 바란다. /

 

특정 시스템 콜의 작동을 바꾸고 싶다면, 해야 할 일은 그 함수를 우리의 것으로 새로 구현하는 것이다.(보통 몇 라인되는 우리의 코드를 넣고 원래의 함수를 호출한다.) 그리고sys_call_table에 우리가 만든 함수의 포인터로 대체하면 된다. 나중에 우리가 만든 것을 제거하고 시스템이 불안정한 상태로 남기를 바라지 않는 다면, cleanup_module()에서 원래의 상태로 sys_call_table을 돌려 놓는 것이 필요하다. 여기의 소스코드는 그런 커널 모듈의 예다. 특정 사용자에 대한 ‘스파이’를 원해 그 사용자가 어떤 파일을 열 때마다 prink()로 메시지를 출력한다. 이것을 위해 open 시스템 콜의 코드를 우리의 함수인 our_sys_open()으로 바꿔야 한다. 이 함수는 현재 프로세스의 uid를 검사해본 후 우리가 감시하고자 하는 uid와 일치 할 경우, printk()를 호출해 열린 파일의 이름을 출력한다. 같은 방식으로 실제 파일을 열기 위해 같은 인자를 가지고 open()함수를 호출한다.

 

init_module() 함수는 sys_call_table에서 맞는 위치를 대체하고 원래 함수의 포인터를 변수에 보관한다. cleanup_module() 함수는 그 변수를 원상 복구해 모두 정상 상태로 돌려 놓는 다. 두 개의 커널 모듈이 동시에 같은 시스템 콜을 변경할 가능성 대문에 이 접근은 위험하다. A와 B라는 커널 모듈이 있다고 가정하자. 그리고 A의 open()함수인 A_open 그리고 B의 B_open이라 하자. A 모듈이 커널에 적재 될 때, 시스템 콜은 A_open으로 바뀌고, 일을 마쳤을 때 원래 것인 sys_open으로 되돌아 갈 것이다. 다음으로 B 모듈이 적재 되면, 시시템 콜을 B_open으로 바꾸고, 그것은 해제 시 복구 시켜야 할 시스템 콜을 A_open으로 생각할 것이다.

 

B모듈이 먼저 제거 될 경우 모든 것은 괜찮다. — 그것은 단순히 시스템 콜을 A_open으로 되돌려 놓을 것이다. 그런 A모듈이 먼저 제거되고 B가 제거되면, 시스템은 다운될 것이다. A 모듈을 제거하는 것은 원래의 시스템 콜을 복구 시키고 B는 이 루프를 빠져 나온다. B모듈이 제거 될 때, B는 원래의 시스템 콜을 A_open이라고 생각할 것이다. 그러나 이것은 더 이상 메모리에 존재하지 않는다. 간단히 보아, 시스템 콜이 우리가 작성한 함수와 같은가, 변화가 없는가를 확인함으로써 간단히 해결할 수 있을 것 같아 보인다.(B모듈이 제거 될 때 아무것도 제거하지 않음으로써) 그러나 이것은 더 나쁜 문제를 유발한다. A모듈이 제거 될 때, 시스템 콜이 B_open으로 바뀐 것으로 보고 더 이상 A_open을 지시하지 않을 것이다. 이는 메모리에서 제거되기 전에 sys_open()을 복구 시기지 않을 것이다. 불행히도 B_open은 더 이상 존재하지 않는 A_open을 호출하려고 하고 B모듈을 제거하지 않은 상태에서 시스템은 다운될 것이다.

 

두 가지 방법으로 이를 해결할 수 있다. 첫 방법은 원래 시스템 콜인 sys_open()을 복구 하는 것이다. 불행하게도 sys_open은 더 이상 커널 시스템 콜 테이블인 /proc/ksyms의 일부가 아니어 우리가 접근할 수 없다. 다른 방법은 레퍼런스 카운터를 사용해 모듈이 사용중이라면 root가 rmmod를 실행 할 수 없게 하는 것이다. 이것은 상용제품에서는 좋은 방법이나 교육용으로는 부적합하다. — 내가 여기서 이것을 사용하지 않는 이유다.

 

Example 8-1. procfs.c

/*  syscall.c 
 * 
 *  System call "stealing" sample.
 */

/* Copyright (C) 2001 by Peter Jay Salzman */

/* The necessary header files */

/* Standard in kernel modules */
#include <linux/kernel.h> /* We’re doing kernel work */
#include <linux/module.h> /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif

#include <sys/syscall.h> /* The list of system calls */

/* For the current (process) structure, we need
* this to know who the current user is. */
#include <linux/sched.h>

/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t – so I add it
* here if necessary. */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>
#endif

/* The system call table (a table of functions). We
* just define this as external, and the kernel will
* fill it up for us when we are insmod’ed
*/
extern void *sys_call_table[];

/* UID we want to spy on – will be filled from the
* command line */
int uid;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
MODULE_PARM(uid, “i”);
#endif

/* A pointer to the original system call. The reason
* we keep this, rather than call the original function
* (sys_open), is because somebody else might have
* replaced the system call before us. Note that this
* is not 100% safe, because if another module
* replaced sys_open before us, then when we’re inserted
* we’ll call the function in that module – and it
* might be removed before we are.
*
* Another reason for this is that we can’t get sys_open.
* It’s a static variable, so it is not exported. */
asmlinkage int (*original_call)(const char *, int, int);

/* For some reason, in 2.2.3 current->uid gave me
* zero, not the real user ID. I tried to find what went
* wrong, but I couldn’t do it in a short time, and
* I’m lazy – so I’ll just use the system call to get the
* uid, the way a process would.
*
* For some reason, after I recompiled the kernel this
* problem went away.
*/
asmlinkage int (*getuid_call)();

/* The function we’ll replace sys_open (the function
* called when you call the open system call) with. To
* find the exact prototype, with the number and type
* of arguments, we find the original function first
* (it’s at fs/open.c).
*
* In theory, this means that we’re tied to the
* current version of the kernel. In practice, the
* system calls almost never change (it would wreck havoc
* and require programs to be recompiled, since the system
* calls are the interface between the kernel and the
* processes).
*/
asmlinkage int our_sys_open(const char *filename,
int flags,
int mode)
{
int i = 0;
char ch;

/* Check if this is the user we’re spying on */
if (uid == getuid_call()) {
/* getuid_call is the getuid system call,
* which gives the uid of the user who
* ran the process which called the system
* call we got */

/* Report the file, if relevant */
printk(“Opened file by %d: “, uid);
do {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
get_user(ch, filename+i);
#else
ch = get_user(filename+i);
#endif
i++;
printk(%c, ch);
} while (ch != 0);
printk(\n);
}

/* Call the original sys_open – otherwise, we lose
* the ability to open files */
return original_call(filename, flags, mode);
}

/* Initialize the module – replace the system call */
int init_module()
{
/* Warning – too late for it now, but maybe for
* next time… */
printk(“I’m dangerous. I hope you did a “);
printk(“sync before you insmod’ed me.\n);
printk(“My counterpart, cleanup_module(), is even”);
printk(“more dangerous. If\n);
printk(“you value your file system, it will “);
printk(“be \”sync; rmmod\” \n);
printk(“when you remove this module.\n);

/* Keep a pointer to the original function in
* original_call, and then replace the system call
* in the system call table with our_sys_open */
original_call = sys_call_table[__NR_open];
sys_call_table[__NR_open] = our_sys_open;

/* To get the address of the function for system
* call foo, go to sys_call_table[__NR_foo]. */

printk(“Spying on UID:%d\n, uid);

/* Get the system call for getuid */
getuid_call = sys_call_table[__NR_getuid];

return 0;
}

/* Cleanup – unregister the appropriate file from /proc */
void cleanup_module()
{
/* Return the system call back to normal */
if (sys_call_table[__NR_open] != our_sys_open) {
printk(“Somebody else also played with the “);
printk(“open system call\n);
printk(“The system may be left in “);
printk(“an unstable state.\n);
}

sys_call_table[__NR_open] = original_call;
}

 

10 Blocking Processes

10.1 Blocking Processes

10.1.1 printk() 대체하기

누군가 지금 당장 무얼 해달라고 하면 어떻게 하겠는가? 당신이 사람이고 다른 사람이 귀찮게 한다면 당신은 “지금은 안 돼, 바빠, 저리가!” 라고 하면 된다. 그러나 당신이 커널 모듈이고 프로세스가 귀찮게 할 때는 다른 방식을 취해야 한다. 당신이 프로세스의 요구를 수행할 수 있을 때까지 프로세스를 잠들게 해야 한다. 프로세스는 커널에 의해 잠들거나 깨어날 수 있다. (이것이 하나의 CPU에서 여러 개의 프로세스가 실행되는 방식이다.)

 

다음의 커널 모듈은 이런 예다. /proc/sleep파일은 오직 하나의 프로세스에 의해서만 사용될 수 있다. 만약 파일이 이미 열려 있다면, 커널 모듈은 module_interruptible_sleep_on()함수를 호출한다. 이 함수는 작업의 상태(task – 작업은 프로세스와 호출되는 시스템 콜에 관한 정보를 가진 커널의 데이터 구조를 말한다.)를 TASK_INTERRUPTIBLE로 변경한다. 이것은 작업이 깨어나 파일에 접근할 수 있는 작업 대기 큐에 진입됨을 의미한다. 그리고 이 함수는 스케줄러를 호출해 다른 프로세스로 문맥전환(context switch)를 하게 하며, 그 프로세스는 CPU를 사용한다.

 

프로세스가 파일에 대한 작업을 마치면, 파일을 닫고, module_close()함수를 호출한다. 이 함수는 큐에 있는 프로세스를 깨운다. (대기 중인 프로세스를 깨우는 특별한 메커니즘은 없다) 파일을 닫은 프로세스는 다른 작업을 계속 수행한다. 이때 스케줄러는 그 프로세스가 충분히 수행됐으며, 다른 프로세스에게 CPU점유권을 넘기도록 결정한다. 결국 프로세스 중 하나는 스케줄러에 의해 CPU점유를 허가 받게 된다. 이런 작업은 module_interruptible_sleep_on()함수를 호출한 직후 수행된다. 이 프로세스는 전역 변수 값을 설정하고, 여전히 해당 파일을 사용하려고 기다리는 프로세스에게 통보를 한다. 다른 프로세스들이 CPU를 점유하면, 해당 전역변수의 값을 확인하고 다시 대기 상태로 돌아간다.

 

흥미롭게도, module_close()함수는 파일에 접근하기 위해 기다리는 프로세스를 깨우는 것에 대해 독점권을 가지지 못한다. Ctrl+c (SIGINT)같은 시그널도 프로세스를 깨울 수 있다.[3] 이런 경우 우리는 –EINTR로 설정된 값을 즉각 돌려 받는다. 일례로 프로세스가 파일을 열기 전에 사용자가 그것을 죽일 수 있다는 점에서 중요하다.

 

기억해 야할 사항이 하나 더 있다. 가끔 대기 상태로 바뀌는 것을 원치 않는 프로세스가 있다. 그들은 그들이 원하는 것을 즉각적으로 갖기를 원하거나 작업을 진행할 수 없다고 보고 하길 원한다. 그런 프로세스들은 파일을 열 때O_NONBLOCK 플래그를 설정한다. 다음의 예와 같이 파일을 열 때 블록 당하기 보다 -EAGAIN 이라는 에러코드를 리턴함으로써 커널이 응답한다. 이 장에서 예로 볼 수 있는 cat_noblock 프로그램은 O_NONBLOCK 플래그를 설정한 체, 파일을 열게 될 것이다.

 

Example 9-1. sleep.c

 

11 printK()대체하기

11.1 printK()대체하기

섹션 Section 1.2.1.2에서 X와 커널 모듈 프로그래밍을 함께 사용하지 말라고 했다. 커널 모듈을 개발하는 과정에서는 맞는 말이다. 그러나 실제의 경우에서는 모듈을 적재하기 위한 tty명령을 보낼 수 있기를 원할 것이다.

 

현재 실행 중인 현재 태스크의 tty 구조체를 구하기 위해, 태스크를 지시하는 current 매크로를 사용해서 해결할 수 있다. 문자열을 쓸 수 있는 함수의 포인터를 찾기 위해 tty구조체를 들여다본다, 그것을 이용해 tty에 문자열을 출력 한다.

 

Example 10-1. print_string.c

 

12 작업 스케줄링

12.1 작업 스케줄링하기

우리는 종종 “가정부” 일을 한다. 그리고 그 일은 자주이던 가끔이던 특정 시각에 행해져야 한다. 그 일을 프로세스가 한다면, crontab파일에 입력하는 것으로 해결할 수 있다. 만일 이런 일을 커널 모듈이 해야 한다면, 두 가지 방법이 있다. 첫째로 필요할 때, 시스템 콜에 의해 모듈을 깨우는 프로세스를 crontab 파일에 집어 넣는 것이다. 그러나 이것은 끔찍하게도 비효율적이다. 이것은 새 프로세스를 crontab에서 실행 시키고, 실행 가능 코드를 메모리에서 읽는다. 단지 메모리에 이미 존재하는 커널 모듈을 깨우기 위해서 말이다.

 

이렇게 하는 것 말고, 타이머 인터럽트 때마다 호출되는 함수를 만들 수 있다. 이런 일을 하는 방법은 새로운 작업(task)를 만들고, 함수의 포인터를 가지고 있는 tq_struct 구조체에 보관하는 것이다. 그리고나서 queue_task() 함수를 tq_timer(이것은 다음 타임 인터럽트 때 실행될 작업의 리스트를 가지고 있다.)라고 불리는 작업 리스트에 집어 넣기 위해 사용한다. 이 함수가 실행 중인 것으로 유지되기 바라기 때문에, 이것이 호출될 때마다 다음 시간 인터럽트를 대비해, 다시 tq_timer에 집어넣는다.

 

기억할 사항이 하나 더 있다. 모듈이 rmmod에 의해 제거 될 때, 레퍼런스 카운트가 점검된다. 그 값이 0일 때, module_cleanup()이 호출된다. 그제서야 모듈이 이 함수에 의해 메모리에서 제거된다. 어느 누구도 timer’s task list에 이런 더 이상 사용하지 않는 함수의 포인터가 있는지 확인하지 않는다. 한참 전에( 이것은 어디까지나 컴퓨터의 관점이다. 인간의 관점에서 보면 많아야 수 백 초 전이다.) 커널은 타이머 인터럽트를 받았고, 작업리스트에 있는 함수를 호출했다. 불행히도 그 함수는 더 이상 그곳에 존재하지 않는다. 대부분의 경우, 함수가 존재하던 메모리의 페이지는 더 이상 사용되지 않으며, 당신은 지저분한 에러 메시지를 받을 것이다. 그러나 같은 메모리의 위치에 새로운 코드가 자리잡았다면, 사태는 더 나빠진다. 불행히도 우리는 작업리스트(task list)에서 작업을 제거하는 쉬운 방법이 없다.

 

cleanup_module()함수는 에러코드를 리턴할 수 없기 때문에(이것은 void를 리턴한다.), 해결책은 아무것도 리턴하게 하지 못하는 것이다. 대신 sleep_on() 혹은 module_sleep_on()을 호출해 rmmod프로세스를 잠들게 한다. 그러기 전에, 타이머 인터럽트에서 호출된 함수에게 전역변수를 설정함으로써 자신을 더 이상 추가하지 말도록 알린다. 다음 번 타임 인터럽트 때에 rmmod프로세스가 깨어나고, 그때는 우리의 함수가 더 이상 큐에 있지도 않고, 모듈을 제거하는데도 안전하게 된다.

 

Example 11-1. sched.c

 

13 인터럽트 핸들러

13.1 인터럽트 핸들러

13.1.1 인터럽트 핸들러

최신 컴퓨터를 제외하고, 프로세스가 요청한 것에 대해, 특수 파일을 다루거나 ioctl()을 사용하거나, 시스템 콜을 사용함으로써 우리가 지금까지 커널에 한 모든 작업을 했다. 그러나 커널의 역할은 프로세스의 요청에 응답하는 것만은 아니다. 모든 순간 중요한 또 다른 커널의 작업은 장비에 연결된 하드웨어와 의사 소통하는 것이다.

 

CPU와 나머지 하드웨어간에 의사 소통을 하는 방식은 두 가지가 있다. 첫 방식은 CPU가 하드웨어에 명령을 내리고, CPU에게 하드웨어가 무언가 필요하다고 말하는 방식이다. 인터럽트라 불리는 두 번째 방식은 CPU가 아닌 하드웨어의 편의를 다루는 것이기 때문에 구현이 더 어렵다. 하드웨어 장치는 보편적으로 매우 작은 양의 RAM을 가지고 있고, 만약 가능할 때 당신이 그 정보를 읽지 않는 다면 그것은 사라진다.

 

유닉스에서는 하드웨어 인터럽트를 IRQ라 부른다. short와 long 타입의 두 가지 IRQ가 존재한다. short IRQ는 매우 짧은 시간이 걸리는 것이고, 그 시간 동안 다른 인터럽트가 처리되지 않고 장비의 나머지 장비가 모두 블록된다. long IRQ는 비교적 긴 시간이 걸리고, 그 시간 동안 다른 인터럽트가 발생할 수도 있다. (그렇지만 같은 장치로부터 발생할 수는 없다.) 이 모든 가능성을 고려한다면, 인터럽트 핸들러를 long 타입으로 서언 하는 것이 낫다.

 

CPU가 인터럽트를 받으면, 그것은 하던 모든 일을 멈추고(더 중요한 인터럽트 처리를 하고 있지 않다면, 이 경우, 좀더 중요한 인터럽트 처리가 처리된 후 그것을 처리한다.), 스택에 특정 파라미터를 저장한 후 인터럽트 핸들러를 호출한다. 이것은 시스템이 어떤 상태에 있는지 알지 못하기 때문에, 어떤 것들은 인터럽트 핸들러 내부에서는 허가 되지 않음을 의미한다. 이 문제의 해결책은 즉각 처리되어야 하는 인터럽트 핸들러가 일반적으로 하드웨어에서 무엇인가를 읽거나 하드웨어에 어떤 것을 전달하고 나서 새로운 정보를 처리하는 스케줄러를 실행 시키는 것이다.(우리는 이것을 bottom half라 부른다. 커널은 가능한 빨리 bottom half를 호출할 것이라 여겨진다. — 그리고 그렇게 됐을 때, 커널 모듈에서 허가된 모든 것이 수행 될 수 있다.

 

이 방식은 상대적인 IRQ를 받았을 때, 인터럽트 핸들러를 찾기 위해 request_irq()함수를 호출함으로써 구현된다. 이 함수는 IRQ번호, 함수의 이름, 플래그 값, /proc/interupts에 대한 이름, 인터럽트 핸들러에 전달될 인자를 전달 받는다. 이 때, 플래그는 다른 인터럽트 핸들러와 IRQ를 공유하겠다는 것을 알리는 SA_SHIRQ(보통, 하드웨어 디바이스의 번호가 같은 IRQ에 의해 발생하기 때문에), 빠른 처리돼야 함을 알리는 SA_INTERRUPT를 포함한다. 서로 공유하려고 하거나, IRQ에 다른 핸들러가 존재하지 않을 때만 이 함수는 성공한다.

 

인터럽트 핸들러 내부로부터, 우리는 하드웨어와 통신할 수 있고, bottom half를 스케줄 하기 위해, queue_task_irq()함수와 mark_bh(BH_IMMEDIATE)를 사용한다. 우리가 표준의 queue_task()함수를 버전 2.0에서 사용하지 못하는 것은 인터럽트가 다른 사람의 queue_task()함수 실행 중에 발생할 수 있기 때문이다. 초기 버전의 리눅스만이 32개 배열의 bottom half를 가졌고, 그 중 하나(BH_IMMEDIATE)는 bottom half의 엔트리를 할당하지 않은 드라이버의 bottom half의 링크드 리스트를 위해 사용한다.

 

13.1.2 인텔 아키텍쳐에서의 키보드

이 장의 나머지 부분의 전적으로 인텔 계열에만 적용된다. 다른 플렛폼에서 작동시켰다면, 작동하지 않을 것이다. 만일 그렇다면 컴파일 시도도 하지 말자.

 

이 장의 예제 코드를 작성하는데 문제가 있었다. 한편으로는 모든 사람의 컴퓨터에서 작동 하면서 유용한 예제, 다른 한편으로는 커널이 이미 포함하고 있는 일반적인 디바이스 드라이버, 그리고 내가 지금 작성하려고 하는 것과 일치하지 않는 것. 찾아낸 해결책은 키보드 인터럽트에 대해 무엇인가 작성하는 것이고, 우선은 일반적인 키보드 인터럽트를 중지 시키는 것이다. static symbol로 이미 커널 내부에 정의 돼있기 때문에(특히 drivers/char/keyboard.c), 그것을 복구할 방법은 없다. 이 코드를 적재하기 전에 120 동안 다른 터미널을 잠재우자.(sleep 120); 만일 당신의 파일 시스템을 중요하게 여긴다면 reboot하자.

 

이 코드는 자신을 IRQ1에 바인드 시키며, IRQ1은 인텔 아키텍쳐 아래서 키보드 IRQ이다. 이 코드가 키보드 인터럽트를 받을 때, 키보드의 상태를 읽어오고(inb(0x64)를 하기 위한 것이다.), 코드를 스캔한다. 그리고 그 코드는 키보드에 의해 리턴되는 값이다. 커널이 이것을 실행 가능한 것이라고 판단하자마자, got_char()함수를 실행 시키며, 이 함수는 사용된 키의 코드를 주고(스캔한 코드의 첫 7비트), 그것이 눌렸는지(이 경우 8번째 비트가 0이다), 눌렸다 떨어졌는지(이 경우는 1) 알려준다.

 

Example 12-1. intrpt.c

 

14 대칭형 다중 프로세싱

14.1 대칭적 다중 처리(프로세싱)

하드웨어 성능 향상을 위한 가장 쉽고 저렴한 방법 중 하나가 보드에 CPU를 하나 더 설치하는 것이다. 서로 다른 CPU가 서로 다른 작업을 수행하게 하는 것(비대칭적 다중 처리)으로 실현될 수 있으며, 같은 일을 병행적으로 실행하게 함(대칭적 다중 처리)으로써도 가능하다. 효과적인 비대칭 다중 처리를 위해서는 컴퓨터가 하는 작업에 대한 특별한 지식이 필요하며, 리눅스 같은 일반적인 목적의 운영체계에서는 사용할 수 없는 것이다. 반면에, 대칭적 다중 처리는 상대적으로 구현하기 쉽다.

 

상대적으로 쉽게, 실제 쉽지 않았음을 말할 수 있다. 대칭 다중 프로세스 환경에서, CPU는 같은 메모리를 공유하고 결과적으로, 하나의 CPU에서 실행되는 코드는 다른 CPU가 사용하는 메모리를 변경시킬 수 있다. 이전에 값을 넣은 변수가 여전히 같은 값을 가지고 있다고 확신할 수 없다. 당신이 주의를 기울이지 않는 사이에 다른 CPU가 그 변수를 어떻게 했을 수도 있다. 명백하게도, 프로그램을 이런 식으로 할 수는 없다. 프로세스 프로그래밍의 경우에서, 프로세스는 일반적으로 하나의 CPU에서 실행되기 때문에 일반적으로 이런 것은 문제가 되지 않는다. 반면에 커널은 서로 다른 CPU에서 실행되는 프로세스에 의해 호출 될 수 있다.

 

전체 커널이 하나의 거대한 spinlock() 이기 때문에, 2.0.x버전에서는 이것이 문제가 되지 않았다. 예를 들어, 하나의 CPU가 커널을 사용할 때, 시스템 콜에 의해 다른 CPU가 커널을 요청하면, 그것은 처음 CPU가 일을 마칠 때까지, 기다려야 한다. 이것은 SMP를 안전하게 만들었지만 효율을 떨어뜨렸다.

 

버전 2.2.x에서는 여러 개의 CPU가 동시에 커널을 사용할 수 있다. 이것이 모듈을 작성하는 사람이 주의 해야 할 것이다.

 

15 일반적으로 주의할 사항

15.1 흔한 함정

당신이 세상으로 나가 커널 모듈을 작성하기 전에, 당신에게 주의를 주어야 할 것들이 몇 가지 있다. 내가 충고를 주지 못해 나쁜 일이 발생한다면, 그 문제를 나에게 알려주길 바란다.

 

표준 라이브러리를 사용하는 것

표준 라이브러리는 사용할 수 없다. 당신이 커널 모듈에서 사용 할 수 있는 함수는 /proc/ksyms에서 보이는 함수들 뿐이다.

인터럽트를 불가능하게 하는 것

이것은 잠시 동안 써야 할 필요도 있고, 그런 것은 괜찮다. 그러나 다시 인터럽트를 가능하게 해놓지 않는 다면, 당신의 시스템에 문제가 발생할 것이고, 전원을 내려야만 할 것이다.

머리를 대형 육식동물의 입 안에 찔러 넣는 일

이런 것까지 충고해야 할 필요는 없을 것 같지만, 혹시나 해서 어쨌든 언급한다.

16 부록 A. 2.0에서 2.2로의 변화

16.1 2.0에서 2.2로의 변화

16.1.1 2.0에서 2.2로의 변화

전체적인 커널이 모든 변화에 대해 충분히 문서화하고 있는지 모르겠다. 예제들을 변환(혹은 실제로 Emmanuel Papirakis의 changes를 적용)하는 과정에서 나는 다음의 차이점들을 발견했다. 나는 모듈 프로그래머들(중에서 특히 이 책의 이전 판부터 배워온 사람들과 내가 쓰고 새버전으로 변환한 기술들에 가장 익숙한 사람들)을 돕기 위해서 그것들을 모조리 나열했다.

 

2.2로 변환하기를 바라는 이들을 위한 추가적인 리소스들은 [http]Richard Gooch의 사이트에서 찾을 수 있다.

 

asm/uaccess.h

put_user 나 get_user가 필요하면, 그것을 포함(#include)시켜야 한다.

get_user

2.2 버전에서, get_user는 유저 메모리에 대한 포인터와 커널 메모리에 들어있는 변수를 둘다 받아서 해당되는 정보들을 채운다. 우리가 읽어들이는 변수가 2바이트 혹은 4바이트 길이를 가질 경우, get_user가 한번에 2바이트 또는 4바이트를 읽을 수 있기 때문이다.

file_operations

현재 이 구조체는 open 함수와 close 함수 사이에 flush 함수를 가지고 있다.

close in file_operations

2.2 버전에서, close 함수가 integer값을 반환하기 때문에, so it’s allowed to fail.

file_operations에서의 read, write

이 함수들에 대한 헤더가 변경되었다. 그것들은 현재 integer 대신 ssize_t를 반환하며, 파라미터 목록도 다르다. inode는 더 이상 파라미터가 아니며, 반대로 파일에 들어가는 오프셋은 파라미터가 되었다.

proc_register_dynamic

이 함수는 더 이상 존재하지 않는다. 대신, 정규 proc_register를 호출하며, 구조체의 inode 필드에 zero를 넣는다.

Signals

태스크 내의 시그널들은 더 이상 32비트 정수값이 아니고 _NSIG_WORDS 정수들의 배열이다.

queue_task_irq

태스크를 인터럽트 핸들러 내부에서 발생하게끔 지정하는 경우에도 queue_task_irq 대신 queue_task를 쓰기 바란다.

Module Parameters

더 이상 모듈 파라미터를 전역 변수로서 선언하지 않아도 된다. 2.2에서도 역시 MODULE_PARM을 써서 그것들의 타입을 선언해주도록 한다. 이것은 커다란 발전이다. 왜냐하면, 모듈로 하여금 digit(역자주: 0 또는 1)으로 시작하는 문자열 파라미터를 (예컨데, 혼동하지 않고도) 받을 수 있게 되었기 때문이다.

Symmetrical Multi-Processing

커널은 더 이상 거대한 spinlock 안에 존재하지 않으며, 이는 커널모듈이 SMP를 알고 있어야 함을 의미한다.

17 변경점

http://bbs.kldp.org/viewtopic.php?t=28376 을 보고 일단 시작합니다. —세벌

C 소스는 올리시기 전에 줄맞춤을 해서 올려주시기 바랍니다. —pyrasis

줄맞춤(?)을 시키려고 했는데, Example 7-1, 7-2, 7-3, 8-1은 번역 안되어 있던 것 맞죠? —검은해

괜한 오역으로 도움은 커녕 오해만 주는 것은 아닐런지.. —[http]HotPotato

출처 : Tong – redyoon님의 Unix_Programming통

Leave a Reply