질문&답변
클라우드/리눅스에 관한 질문과 답변을 주고 받는 곳입니다.
리눅스 분류

리눅스 커널 디바이스 드라이버

작성자 정보

  • 오영욱 작성
  • 작성일

컨텐츠 정보

본문

하루가 다르게 새로운 하드웨어가 나오고 있는 요즘, 이런 하드웨어를 사용하는 사람들은 마이크로소프트의 운영체제가 아닌 플랫폼에서 많은 어려움을 겪고 있다.

개발사들도 새로운 하드웨어를 개발하면 dos, windows, win95를 위한 드라이버만 지원을 하고 있다. 조금 신경을 쓰는 회사들도 windows/NT, OS/2 정도로 범위를 확대할 뿐 Unix 쪽은 거의 지원하지 않는다.

이제 마이크로소프트의 운영체제가 대세가 되고 있으므로 당연한 현상이라고 말할 수 있지만 사용자 입장에서는 큰 불만이 아닐 수 없다. 아무리 좋은 하드웨어라고 하더라도 사용하는 운영체제가 지원을 하지 않으면 전혀 쓸 수 없기 때문이다. 상용 운영체제는 사용자가 직접 만들어 쓰는 것도 쉽지 않다. 운영체제를 만드는 회사에 건의하거나 개발사에 적극적으로 요청하여 드라이버를 만들어 줄 때까지 기다려야 한다. 일반적으로 시장이 넓지 않은 분야에 대해 개발사들이 비용을 들여서 관심을 가질 이유도 여력도 없는 경우가 많다. 점점 지배적인 점유율을 가진 운영체제만이 살아 남게 되고 점유율이 낮은 운영체제는 하드웨어 디바이스 드라이버조차 변변찮게 되고 있는 것이다.

여기에 리눅스의 장점이 있다. 리눅스에서는 개발사가 지원하지 않아도 사용자가 필요한 하드웨어에 대한 명세를 구하여 스스로 만들 수 있다. 리눅스는 커널까지 완전히 개방되어 있기 때문이다. 그렇다고 새로운 하드웨어를 사용하기 위해서 직접 드라이버를 만들 필요는 없다. 리눅스의 개발자는 전세계의 모든 리눅스 사용자들이며, 새로운 하드웨어를 위한 디바이스 드라이버는 이미 누군가 먼저 개발해 놓고 누구나 사용할 수 있도록 인터넷에 올라와 있을 것이기 때문이다.

최근에는 하드웨어 개발자들도 마이크로소프트의 운영체제 뿐만 아니라 리눅스에서의 지원에 대해 최우선으로 관심을 가지는 경우가 늘고 있다. 소프트웨어는 이미 그런 현상이 뚜렷하다. 인터넷과 관련이 있는 소프트웨어는 윈도우계열 이외에 리눅스가 최우선으로 지원되고 있다. 요즘에는 인기 있는 게임 프로그램들도 리눅스용으로 포팅되고 있다.    ==>more 

공개 오에스인 리눅스의 성장은 경이적인 일이다. 무섭게 성장하는 리눅스 때문에 coherent 라는 작은 유닉스는 이미 사라졌고, 리눅스의 기초가 된 minix는 유료에서 무료로 돌아섰으며, 스코 유닉스는 일인 사용자에 한하여 무료 시디를 배포하고 있고, 선에서는 커널소스를 배포하기에 이르렀다. DEC에서는 윈도우NT와 함께 리눅스를 alpha cpu의 주요 플랫폼으로 키우기 위해 노력 중이다. 리눅스는 인텔, alpha, sparc, PowerPC, MIPS, ARM, SGI, 모토롤라 68계열 컴퓨터에서 돌아간다. 이미 하드웨어 개발사들에게 무시하지 못할 플랫폼이 되어 가고 있는 것이다.

이 글은 이런 추세에 맞추어 리눅스를 위한 하드웨어 커널 디바이스 드라이버를 제작하기를 원하는 제작사나 자신의 필요 때문에 직접 드라이버를 만들기 원하는 사용자를 위해서 리눅스 커널 디바이스 드라이버 작성법에 대해 쓰는 글이다.

리눅스 디바이스 드라이버를 완성하고 FreeBSD에 포팅하기 위해서 FreeBSD 커널 소스를 살펴 볼 때 막막한 느낌을 받았다. 코드 한 줄마다, 이름 모를 함수들이 생소하고 어떻게 코딩을 시작해야 할 지 알 수 없었다.

간단하더라도 줄기가 되는 드라이버 작성법 설명이 있었다면 그렇게 막막하지는 않았을 것이다. 줄기만 이해한다면 각 디바이스마다 달라지는 부분은 점차 이해할 수 있기 때문이다. 이 글도 마찬 가지이다. 여기서 디바이스 작성의 모든 것을 설명할 수 없다. 이 글의 목적은 리눅스 커널 디바이스 드라이버 작성을 위해서 최소한의 비빌 언덕을 제공하려는 것이다. 드라이버를 작성할 때에는 관련 프로그램을 모두 살펴야 하지만 이 글에서 언급한 부분이 코드의 구조를 이해하는 데 도움이 되기를 바란다.

이미 나와있는 디바이스 드라이버를 고찰하는 것 뿐만 아니라 가상의 하드웨어를 위한 디바이스 드라이버를 직접 작성해 보도록 하자. 리눅스로 도움을 받은 사용자라면 그 보답을 해야 하기 때문에, 리눅스에서 사용하지 못하는 새 하드웨어가 있다면 쓸 수 있게 해야 하기 때문에, 개발사라면 무시 못할 정도의 리눅스 사용자가 전 세계에 있기 때문이기도 하다. 새로운 하드웨어를 개발한 회사라면 이제 리눅스를 위한 드라이버 없이 하드웨어의 점유율을 높일 수 없다는 점을 명심해야 할 것이다.    ==>more

리눅스 커널에 대한 이해

리눅스 커널은 리누스 토발즈가 미닉스에서 개발했다. 처음부터 멀티태스킹이 가능한 유닉스호환을 지향했고 현재는 POSIX에서 제안한 유닉스 표준을 지키고 있다. 초기에는 리누스가 모든 개발을 진행했지만 인터넷에 커널 소스를 개방시킨 결과 수많은 개발자들이 기여를 하여 기능 첨가, 버그 개선 등이 빠른 속도로 진행되었다. 이 과정에서 개발자들이 사용자가 되고 사용자가 개발자가 되는 상승효과로 리눅스 사용자 또한 빠르게 증가했다. 여기에 더해서 리눅스 상용 패키지 회사까지 경쟁적으로 나타나 유닉스에 경험이 전혀 없는 초보자도 몇 시간 안에 리눅스를 설치하고 설정까지 최적화 시킬 수 있을 정도로 발전했다.

개발자가 증가하면서 커널 소스도 방대해지고 멀티플랫폼, 코드최적화, 지원 디바이스 드라이버의 추가 등의 개발 방면도 다양해져서 이미 한 명의 개발자가 모든 부분을 전담하기에는 벅차게 되었다. 현재는 커널의 각 부분에 대한 관리자가 각각의 분야를 전담하고 리누스는 전체적인 부분의 유기적 연결과 중요한 개발 방향에 대한 정책적 결정을 하는 형태로 진행되고 있다.

알렌 콕스는 커널 모듈화, 네트워크 코드, SMP에 대해서 중요한 기여를 했고 개발관리 부분에 많은 일을 하고 있다. 리눅스는 이미 리누스의 의지로만 결정되지 않아서 개별적인 패치가 존재하기도 하고, 리누스가 찬성하지 않는 방향으로 개발을 진행하는 개발팀도 존재한다.

리눅스 커널 버전에 따라 안정 버전과 개발 버전으로 나뉜다. 2.0.x처럼 중간 번호가 짝수인 버전은 안정버전으로 일반 사용자가 안심하고 사용할 수 있을 정도의 안정성을 보장하는 버전이며, 2.1.x처럼 중간 번호가 홀수인 버전은 각종 패치와 최적화, 새로운 기능의 첨가가 진행되고 있는 개발 버전이다.

개발 버전은 안정 버전에는 없는 특수한 드라이버들이 필요한 사용자나 커널 개발에 관심이 있는 개발자들이 사용하며 버그보고나 기능 개선에 대한 조언을 할 수 있도록 공개된 버전이다. 개발 버전은 최신의 드라이버가 포함되고 새로운 알고리즘이 포함되어 있으므로 속도에 이점이 있고 더 많은 지원 드라이버가 있지만 안정성은 보장할 수 없으므로 일반 사용자는 주의해서 사용해야 한다.

만약 새로운 커널 디바이스 드라이버를 개발하는 중이라면 최신 안정버전을 이용해서 개발하고 개발이 끝나면 개발 버전을 따라가면서 변경사항을 그때 그때 적용해 나가는 것이 개발 시간을 줄일 수 있는 방법이다. 개발 버전은 결국 다음 안정 버전이 되기 때문이다.   ==>more

리눅스 커널 소스의 구조

커널 2.2 버전을 중심으로 리눅스 커널 소스를 살펴보자. 받아온 커널을 풀면 /usr/src/linux 에 소스트리가 생성된다. /usr/src/linux/ 루트 트리에는 기본적인 설명서, 관리자들의 목록, 컴파일 방법 등에 대한 간단한 설명서가 있다.

리눅스 커널 프로그래밍을 위해서는 리눅스에 대한 이해와 여러 가지 프로그램의 사용법에 대해서 알고 있다고 가정을 하고 세세한 과정에 대한 설명은 생략한다. arch/ 디렉토리에는 지원하고 있는 CPU에 의존적인 코드가 있다. 부팅과정, 메모리 관리방법, 프로세스 처리 부분들이 모두 다르므로 가장 하드웨어와 밀접한 코드가 모여 있다. 여기서 CPU의 차이를 블랙박스로 만들어 주게 된다.

fs/ 디렉토리에서는 논리적인 파일 시스템을 처리하는 부분이다. 리눅스에서는 가상파일 시스템을 사용하여 동시에 여러 가지 파일 시스템을 지원할 수 있다.

mm/ 디렉토리에서는 메모리 처리 관련 프로그램이 net/ 에는 네트워크 관련 코드가 있다. 그 외 여러 디렉토리에는 잘 정리된 형태로 필요한 프로그램들이 나뉘어져 있다.

커널 디바이스 드라이버를 만들기 위해 가장 주의 깊게 보아야 할 디렉토리는 Documentation/ 디렉토리와 drivers/ 디렉토리이다. Doc../ 디렉토리에는 커널 프로그래밍에 필요한 다양한 설명서가 있으며 drivers/ 디렉토리에는 커널용 디바이스 드라이버가 모두 모여 있다. 가능하다면 Doc../ 디렉토리의 모든 문서를 프린트 해서 읽어 보기 바란다.

커널 프로그래밍을 위해서는 직접 파일들을 살펴보고 파일 안에 있는 설명을 읽고 소스트리에 있는 설명서를 모두 조사해야 하기 때문에 개략적으로 디렉토리 구조만을 말했다. 직접 프로그래밍을 시작하기 전에 우선 모든 디렉토리를 살펴 보기 바란다.   ==>more

리눅스 커널 컴파일하기

새로운 디바이스 드라이버를 만들기 전에 먼저 이미 만들어진 것들을 조사해 보기로 하자. 가장 먼저 커널 컴파일을 해보는 것이 좋다. 루트에서

make clean cd /usr/include rm -rf asm linux scsi ln -s /usr/src/linux/include/asm-i386 asm ln -s /usr/src/linux/include/linux linux ln -s /usr/src/linux/include/scsi scsi make mrproper

를 실행한다. 다음에 엑스윈도우라면 make xconfig, 콘솔이라면 make menuconfig를 실행한다. 위 두 명령은 메뉴방식으로 커널을 설정할 수 있다. 뭔가 문제가 있다면 make config 명령을 쓸 수 있다. 이 명령은 모든 설정을 단계적으로 하게 되기 때문에 불편하다.

[make xconfig 를 실행한 화면]

각 버튼은 커널 설정의 큰 제목이고 해당 버튼을 누르면 다시 세분된 옵션을 선택할 수 있다. 옵션은 서로 연관되어 있어서 어떤 옵션이 선택되면 다른 옵션은 선택할 수 없는 상태로 된다. 큰 메뉴끼리의 연관성도 있다. 화면에서처럼 SCSI SUPPORT 메뉴 안에 있는 첫 번째 옵션인 SCSI SUPPORT 옵션을 "n"로 만들었을 때 다음 큰 메뉴인 SCSI low-level drivers 메뉴에 있는 모든 옵션이 꺼지는 것을 볼 수 있다. 옵션끼리의 연관은 한 메뉴 안에서 정해지거나 비슷한 설정 메뉴끼리 관련 되거나 베타버전의 드라이버를 포함하지 않겠다는 등의 정책적인 옵션에 영향받기도 한다. 모든 옵션을 살펴보고 연관성을 이해하기 바란다.

[scsi support 메뉴가 열린 모습]

[scsi low-level drivers 메뉴가 열린 모습]

그림에서 Adaptec AIC7xxx support 옵션을 살펴보자. 이 것은 Adaptec 2940 스커지 컨트롤러를 지원하기 위한 드라이버이다. 만약 SCSI SUPPORT 옵션이 "n"로 설정되어 있다면 선택을 할 수 없게 된다. "y"로 되어 있다면 y,m,n를 선택할 수 있고 SCSI SUPPORT 가 "m"으로 되어 있다면 m,n만을 선택할 수 있다. 다시 Adaptec AIC7xxx support 옵션을 y,m으로 만들어야 "Maxinum number of commands per LUN"옵션에 적절한 값을 써 넣을 수 있게 된다. 여기서 "y"는 드라이버를 커널에 직접 포함시키겠다는 것을, "m"은 모듈화 시켜서 필요할 때 직접 커널에 삽입 할 수 있게 하겠다는 것을 나타낸다. "n"는 이 드라이버가 지원하는 하드웨어를 사용하지 않겠다는 뜻이다. 이것은 어디에 있을까?

drivers/scsi/Config.in을 살펴보자.

dep_tristate 'Adaptec AIC7xxx support' CONFIG_SCSI_AIC7XXX $CONFIG_SCSI if [ "$CONFIG_SCSI_AIC7XXX" != "n" ]; then bool ' Enable tagged command queueing' CONFIG_AIC7XXX_TAGGED_QUEUEING Y dep_tristate ' Override driver defaults for commands per LUN' CONFIG_OVERRIDE_CMDS N if [ "$CONFIG_OVERRIDE_CMDS" != "n" ]; then int ' Maximum number of commands per LUN' CONFIG_AIC7XXX_CMDS_PER_LUN 8 fi

CONFIG_SCSI 옵션이 설정되었다면 "Adaptec AIC7xxx support" 옵션이 세가지 상태 (dep_tristate)를 가질 수 있게 된다. 다른 옵션에 영향받지 않는다면 독자적인 세가지 상태(tristate)를 가질 수 있다. CONFIG_SCSI_AIC7XXX 옵션이 커널에 포함 되거나 모듈로 선택되었다면 바로 아래 줄의 옵션을 선택할 수 있다. bool이란 두 가지 상태를 선택할 수 있다는 뜻이며 줄 마지막의 "Y"는 기본값을 y로 한다는 뜻이다. 다음 줄의 CONFIG_OVERRIDE_CMDS를 N로 선택하지 않았으면 이제 비로소 CONFIG_AIC7XXX_CMDS_PER_LUN 값을 써넣을 수 있다. int라는 키워드는 여기에 써넣은 숫자를 취한다는 뜻이다. 기본값은 여기서 8로 되어 있다.

그렇다면 CONFIG_SCSI 옵션은 어디에서 선택하는 것일까? drivers/scsi 디렉토리의 상위 디렉토리인 drivers/ 의 Makefile에 있다.

ifeq ($(CONFIG_SCSI),y) SUB_DIRS += scsi MOD_SUB_DIRS += scsi else ifeq ($(CONFIG_SCSI),m) MOD_SUB_DIRS += scsi endif endif

ifeq-endif 짝을 잘 살펴보면 별 무리 없이 이해할 수 있을 것이다. 필요한 설정값을 넣는 방법, 드라이버의 상호연관성에 대한 것, 세 가지 혹은 두 가지 설정 상태, 옵션이 있을 수 있는 파일 위치는 제작하려는 드라이버의 종류에 따라 다르지만 대략 위와 같다.

그림과 같이 세세한 옵션에 대해서 어떻게 설정해야 하는지 정확히 알 수 없을 때는 "help" 버튼을 눌러서 자세한 설명을 볼 수 있다. 이 버튼을 눌렀을 때 나오는 설명은 Documentation/Configure.help 파일에 있다. 그 형식은

Enable vendor-specific extensions (for SCSI CDROM) CONFIG_BLK_DEV_SR_VENDOR This enables the usage of vendor specific SCSI commands. This is required to support multisession CD's on with old NEC/TOSHIBA cdrom drives (and HP Writers). If you have such a drive and get the first session only, try to say Y here; everybody else says N.

공백 한 줄, 간단한 설명 한 줄, 옵션 키워드, 필요한 설명, 공백 한 줄이다. 위치에 무관하게 설명을 누르면 키워드를 이용해 이 설명을 보여 주게 된다.

[scsi cdrom support 설명을 누른 모습]

이렇게 하드웨어에 맞는 옵션을 선택했다면 "make dep"라는 명령으로 선택한 옵션에 따라 파일들의 연관성이 올바로 되도록 만든다. 그 후에는 실제 컴파일을 수행하게 된다. 플로피에 커널을 보내려면 "make zdisk", 컴파일 수행과 동시에 lilo가 실행되게 하려면 "make zlilo", 커널의 크기가 너무 크면 "make bzlilo"를 사용하면 된다. 컴파일이 제대로 되고 커널이 만들어 졌으면 모듈을 만들기 위해 "make modules", "make modules_install"을 수행한다. 만들어진 모듈은 /lib/modules/uname -r/ 아래에 인스톨 된다.

이제 새 하드웨어는 리눅스 커널에서 인식하고 사용할 수 있다. 요즘 나오는 리눅스 배포본의 추세는 대부분의 드라이버를 모듈화 시켜서 필요할 때 커널에 삽입해 쓰게 하기 위해서 데몬을 만들고 옵션을 따로 기록해 놓는 등의 여러 방법을 쓴다. 이런 것은 사용자 편의를 위한 것일 뿐 커널 프로그래밍과 크게 관련이 없기 때문에 언급하지 않는다.   ==>more

MY_DEVICE

이제 각종 옵션과 필요한 파일의 위치, 고쳐야 하는 파일과 그 내용을 실제로 알아보기로 하자. 우선 가상의 하드웨어를 하나 정하기로 한다. 이 하드웨어의 성격은 다음과 같다.

문자 디바이스이다. 인터럽트 방식이다. 하드웨어에는 자체적인 CPU가 있다. PC의 운영체제와는 shared memory 방식으로 교신한다. 인터럽트와 shared memory는 카드 설정에 따라 바뀔 수 있기 때문에 부팅할 때나 모듈 적재 시에 옵션으로 바꿀 수 있다. 동시에 같은 하드웨어를 다른 인터럽트를 주고 2개 이상 사용할 수 있다.

위와 비슷한 하드웨어 중에 커널에 포함된 것은 지능형 멀티 시리얼 포트 종류가 있다. 크게 말하면 사운드 드라이버도 포함될 수 있을 것이다. 이 정도의 하드웨어를 가정한다면 커널 프로그래밍에 필요한 대부분의 테크닉이 모두 동원되어야 할 것이다. 문자 디바이스 보다 블록 디바이스가 좀더 복잡하지만 버퍼 입출력만 제외한다면 크게 다르지 않다. 이 하드웨어를 my_device라고 명명하고 리눅스 커널에서 제대로 동작하게 하기 위해서 필요한 작업을 해보도록 한다.   ==>more

장치 특수파일

프로그래밍을 할 때 어떤 장치를 열기 위해 open 함수를 호출한다고 하자. 표준 라이브러리에 있는 open이라는 함수를 사용하면 이 함수는 커널에 시스템 호출을 하고 커널은 이 호출이 요구하는 파일에 대한 요청 작업을 처리한다. 유닉스의 특성상 모든 디바이스의 입출력은 파일 입출력과 차이가 없다. 우리가 만들어야 하는 것은 추상화 되어 있는 파일 입출력이 최종적으로 호출하게 될 각 장치에 고유한 열기 방법에 관한 것이다. 어떤 장치에 접근하기 위해서 가장 먼저 해야 할 작업은 장치마다 다를 것이다. open이라는 호출이 오면 필요한 일을 하고 write/read 호출에 대비해서 준비를 하는 작업만 하면 된다.

이 작업은 프로그램 언어만 C를 사용할 뿐 C 라이브러리 함수를 하나도 사용할 수 없는 특수 작업이므로 응용 프로그래밍과 완전히 다른 작업이라고 할 수 있다. 파일 입출력의 상위 인터페이스는 리눅스에서도 다 완성되어 있기 때문에 우리가 신경을 쓸 필요는 없다. 장치 특수파일을 정의하고 표준 파일 입출력에 사용되는 시스템 호출이 사용할 적절한 함수를 만들어 내는 작업만으로 충분하다.

장치 특수파일을 위해서 파일유형, 주(major) 번호와 부(minor) 번호가 필요하다. 주번호는 장치유형을, 부번호는 그 유형의 단위기기를 나타낸다.

리눅스에서는 255번까지의 장치특수파일 주번호가 있다. 수많은 하드웨어 지원이 계속 추가되고 있어서 100번 이하는 거의 다 할당이 되었다. 특정한 하드웨어를 위한 디바이스 드라이버를 만들고 공식적으로 리눅스의 주번호를 받고 싶으면 Documentation/devices.txt 를 참고하여 리눅스 주번호 관리를 하는 사람에게 연락을 취하면 된다. 임의로 번호를 부여한다면 커널이 업그레이드되어 다른 디바이스 드라이버가 이 번호를 사용할 때 충돌이 있을 수 있다.

리눅스를 테스트하거나 실험적으로 디바이스 드라이버를 만드는 사람을 위해서 60-63, 120-127, 240-254번이 예약되어 있다. 이 번호 중에서 임시로 적당한 번호를 사용하여 테스트 하고 나중에 정식으로 번호를 할당받으면 된다. 우리가 만드는 장치를 위해서 125번을 선택하여 MY_DEVICE_MAJOR로 정하자.

이 번호를 커널에 등록하기 위해서는 include/linux/major.h에 이 값을 기록한다. 위치는 상관없다. 만약 계속 커널 업그레이드에 따라 갈 예정이고 아직 정식으로 커널 배포본에 등록이 안된 테스트 드라이버라면 커널 패치를 할 때 문제가 생기지 않도록 되도록 가장 뒷부분에 배치하는 것이 좋을 것이다. 우리가 삽입한 코드에 인접한 곳에서 커널 변경 사항이 생긴다면 패치할 때 문제가 생길 수 있다.

#define MY_DEVICE_MAJOR 125

그리고 이 번호로 된 장치 특별파일을 만든다. mknod /dev/my_device0 c 125 0 mknod /dev/my_device1 c 125 1 mknod /dev/my_device2 c 125 2 mknod /dev/my_device3 c 125 3 chown root.root /dev/my_device? chmod 660 /dev/my_device?

이제 my_device는 이 파일을 열고 쓰고 읽음으로써 조작할 수 있다. 참고로 c는 문자 특수파일임을 나타내고 125는 주번호, [0-3]은 부번호이다.    ==>more

MY_DEVICE를 커널 컴파일 옵션에 삽입

앞에서 얘기한 make config 시에 MY_DEVICE 항목이 나오게 하기 위해서 필요한 작업을 하자. 디바이스 드라이버는 계층상 가장 하위에 위치하기 때문에 디렉토리 위치도 살펴야 한다. drivers/[block/,char/] 디렉토리 이외의 디렉토리는 편의상 분류한 것이다. drivers/scsi는 블록 디바이스지만 스커지만의 특수한 상위 함수와 다양한 스커지 컨트롤러 제품에 따른 파일을 모아서 디렉토리를 나누어 놓은 것이다. 만약에 이런 유형에 해당하는 하드웨어에 맞는 드라이버를 만든다면 거기에 맞는 디렉토리를 선택해야 할 것이다.

MY_DEVICE는 문자 특수파일이며 isdn등과 같이 특수한 분류에 들어가지 않기 때문에 drivers/char 디렉토리에서 작업을 하면 된다. 문자 드라이버는 상위의 tty_io 루틴과 연관이 있다. 시리얼 포트나 모뎀 등의 디바이스는 터미널 기능을 수행하게 되기 때문에 이 기능이 보장되어야 한다. 그러므로 각종 초기화나 옵션의 설정 등이 상위 루틴과 관련이 있는 파일에 있다.

make config를 했을 때 MY_DEVICE 항목이 나오게 하기 위해서는 drivers/char/Config.in 에 my_driver에 해당하는 조건을 명시해야 한다.

tristate 'insert my device driver in kernel' CONFIG_MY_DEVICE if [ "$CONFIG_MY_DEVICE" = "y" -o "$CONFIG_MY_DEVICE" = "m" ]; then int ' my device value' CONFIG_MY_DEVICE_VALUE 1 bool ' support my device something' CONFIG_MY_DEVICE_SUPPORT fi

tristate는 커널에 직접 삽입되거나(y), 모듈로 만들거나(m), 컴파일하지 않는(n)다는 것을 정하는 것이며 bool은 (y,n) 두 가지 중에 선택하는 것이고 int는 필요한 수치값이 있으면 적어 주는 부분이다.

if-fi는 여러 계층을 둘 수 있다. 다른 if-fi 문장 사이만 아니라면 이 문장을 삽입하는 위치는 상관이 없다.

help 버튼을 눌렀을 때 설명이 나오게 하기 위해서 Documentation/Configure.help에 적절한 설명을 넣어 준다.

My device support CONFIG_MY_DEVICE This text is help message for my device driver    ==>more

커널 부팅 옵션의 처리

커널이 부팅할 때 디바이스 드라이버들은 제어할 수 있는 하드웨어에 대한 설정값을 스스로 찾거나 고정된 값을 사용할 수 있다. 만약 사용자가 어떤 디바이스에 대한 인터럽트값이나 베이스 어드레스등을 바꾸었다면 커널에게 알려 주어야 한다. 설정값을 스스로 찾을 수 없는 디바이스 드라이버도 마찬가지로 사용자가 하드웨어 설정값을 알려 주어야 한다. 이를 위해 리눅스 커널은 부팅 옵션을 지원한다.

lilo를 사용해 부팅할 때 append 옵션을 사용하여 커널의 my_device에게 옵션을 전달하기 위해서는 init/main.c을 편집한다.

#ifdef CONFIG_MY_DEVICE extern void my_device_setup(char *str, int *ints); #endif #ifdef CONFIG_MY_DEVICE { "my_device=", my_device_setup}, #endif

lilo.conf에 append="my_device=0x200,1" 이라고 적어 주면 이 값이 my_device_setup 함수에 전달된다. 이 함수는 앞으로 만들게 될 드라이버 함수 중에서 커널에서 넘어온 인자를 드라이버에 적절한 값으로 변환하여 전달하는 함수이다. 다른 ifdef-endif 속에 들어가지 않는다면 위치는 상관없다.

디바이스 드라이버를 위한 파일 drivers/char/my_device.c을 만들고 헤더파일 include/linux/my_device.h를 만든다. 컴파일 할 때 이 프로그램도 컴파일 시키기 위해서 drivers/char/Makefile의 적당한 곳에 이 파일을 적어준다.

ifeq ($(CONFIG_MY_DEVICE),y) L_OBJS += my_device.o else ifeq ($(CONFIG_MY_DEVICE),m) M_OBJS += my_device.o endif endif

커널에 직접 삽입(y)하라는 옵션과 모듈로 만들(m)라는 옵션일 때 각각에 대해서 다른 처리를 한다. 그 외에 Makefile 을 살펴보면 서브디렉토리를 포함하라는 옵션 등이 있다. 컴파일 속도를 위해서 리누스가 자체로 만든 mkdep 라는 프로그램으로 파일 간의 관계를 조사하기 때문에 메이크파일이 거의 암호화 수준이지만 잘 살펴보면 크게 어려운 점은 없을 것이다.

drivers/char/mem.c에서 문자 디바이스 드라이버를 초기화한다. 드라이버 초기화 함수는 대부분 *_init 형식이므로 my_driver_init라고 정하고 적당한 곳에 넣어 주면 된다. 비슷한 드라이버 초기화 루틴이 삽입되어 있는 위치에 다른 ifdef-endif와 상관없는 곳에 두면 된다.

#ifdef CONFIG_MY_DEVICE my_device_init(); #endif

my_device_setup 함수는 부팅할 때 커널 인자로 넘어온 값을 처리하기 위해 한 번 호출되는 함수이고 my_device_init 함수는 my_device를 초기화 시킬 때 필요한 각종 작업을 할 때 한 번 실행되는 함수이다. my_device_init 함수에서 my_device_open, my_device_write 함수를 커널에 등록하게 된다.

my_device_init 함수는 block/char 디바이스 드라이버 초기화 루틴을 전체적으로 실행하는 부분에 알려 주어야 실행이 될 수 있다. 전체 디바이스 드라이버 초기화 루틴을 수행하는 함수 선언은 block device driver라면 include/linux/blk.h에, char device driver라면 include/linux/tty.h에 있다. my_device는 char device 이므로 .../tty.h에 적어 준다. 마찬가지로 위치는 크게 상관없다.

extern int my_device_init(void);

[ 그림 : make xconfig에서 my_device의 help 화면을 잡은 모습]

make dep 과정에서 Config.in을 참조하여 Makefile에 정의된 디바이스 드라이버 파일을 컴파일 하여 커널에 삽입할 것인지, 모듈로 만들 것인지 여부를 결정한다. 만약 커널에 삽입되었다면 부팅하면서 커널로 넘어온 인자(my_device=0x200,1) 중에서 init/main.c에 정의된 문자열(my_device=)과 맞는 것이 있으면 이 인자들을 파싱하여 이 드라이버의 인자 셋업함수(my_device_setup)에 인자(str="0x200,1", ints[0]=2, ints[1]=0x200, ints[2]=1)를 전달하고 셋업함수를 실행한다. 셋업함수는 간단히 넘어온 인자값을 조사하여 디바이스 드라이버의 번지값 등을 적절히 바꾸고 리턴한다.

나머지 부팅과정을 진행한 후에 커널은 디바이스 드라이버 초기화 루틴(drivers/char/mem.c)으로 뛰어 각 디바이스를 실제로 초기화(my_device_init)한다. 각각의 디바이스 초기화 함수는 하드웨어 디바이스가 컴퓨터에 존재하는지 검사하고, 하드웨어가 필요로 하는 초기화를 한 후에 system call 루틴을 위해서 read, write, release, ioctl 등의 함수가 정의된 file_operation 함수배열을 등록한다.

모듈로 만들었을 때는 커널 부팅과정을 insmod가 해 준다. 인자 파싱도 마찬가지로 insmod 몫이다. 디바이스 드라이버는 인자 배열을 넘겨 받게 되고 init_module(my_device_init)에서 마찬가지로 하드웨어 검색, 초기화, 인자를 사용한 설정값 변경을 한다. 모듈로 했을 때는 드라이버 프로그램에서 약간의 부가 작업이 필요할 뿐 직접 삽입된 드라이버와 다르지 않다.   ==>more

디바이스 드라이버의 적재와

리눅스 커널 코딩 스타일

리눅스 커널 프로그래밍을 하기 전에 리누스가 제안한 코딩 스타일을 살펴볼 필요가 있다. 스타일을 알아야 커널 소스를 빨리 이해할 수 있고, 우리가 쓴 드라이버를 다른 프로그래머가 쉽게 파악 할 수 있기 때문이다.

리누스가 제안한 코딩 스타일은 일반적인 C 프로그래밍과 크게 다르지 않다. GNU 코딩스타일과도 크게 차이나지 않는다.

1. 들여쓰기는 8자 단위로 할 것

2자나 4자 단위로 하면 들여쓰기의 목적인 블록 구분이 불분명하게 된다. 8자 단위일 때 80칼럼을 넘는 줄이 많아지는 문제가 있지만 리누스는 소스를 고쳐서 들여쓰기를 3단계 이하로 고치라고 말한다.

2. 괄호 사용법

조건문은 다음과 같이 사용해야 한다.

if (x == y) { .. } else if (x > y) { ... } else { .... }

함수는 다음과 같이 사용해야 한다.

int function(int x) { body of function }

이렇게 하면 가독성을 높이면서도 빈 줄을 최소한으로 할 수 있다.

3. 명명 규칙

파스칼과 같이 대소문자를 섞어 ThisVariableIsATemporaryCounter처럼 쓰는 것은 좋지 않다. 전역변수나 함수는 그 기능을 충분히 설명할 수 있게 사용해야 한다. count_active_users();와 같이 써야 할 것을 cntusr();로 쓰면 안 된다. 함수형은 컴파일러가 검사하니까 함수형을 함수 이름에 집어 넣을 필요는 없다. 지역변수는 되도록 짧게 쓸 것.

4. 함수 크기

함수는 한 가지 일만 하게 하고 가능한 짧게 해야 이해하기 쉽고, 버그를 줄일 수 있다.

5. 주석문

소스에 주석문을 달아 주는 것이 좋지만 코드가 어떻게 동작하는지 일일이 설명하는 장황한 주석은 피해야 한다. 코드자체로 명확하게 동작방식을 보여 주어야 한다. 주석문은 왜 이 코드가 여기에 존재하는지 이 코드가 하는 일은 무엇인지를 알려주는 것으로 족하다.

이 외에 소스를 찾아 보면 알겠지만 리누스가 쓴 코드는 함수를 적절히 배치하여 함수 원형을 거의 선언하지 않고 있다. 다른 사람이 만든 코드는 대부분 함수 원형을 선언해 놓았다. 어떤 방법을 사용할지는 스스로 판단하기 바란다.    ==>more

 

 

자료 :  네트워크,보안계열,IT취업 관련사이트

커널 부팅 옵션 처리 함수

MY_DEVICE가 사용하는 번지와 인터럽트를 바꾸기 위해서 lilo옵션으로 값을 주었을 때 커널에서 my_device_setup을 호출한다.

lilo 옵션

append="my_device=0x200,5,0x300,7,0x400,8,0x500,9

셋업으로 넘어온 인자

void my_device_setup(char *str, int *ints) char *str="0x200,5,0x300,7,0x400,8,0x500,9"; int *ints={ 8, 0x200, 5, 0x300, 7, 0x400, 8, 0x500, 9};

옵션으로 준 문자열(*str)은 값으로 변환되어(*ints) 넘어온다. 커널에서 한 디바이스가 취할 수 있는 값의 개수는 9개로 제한되어 있다. 그 보다 많은 값이 필요하다면 *ints를 무시하고 *str을 다시 파싱해야 한다. *str에는 lilo옵션으로 넘겨준 문자열이 그대로 전송되기 때문이다. *ints 값 보다는 문자열이 필요한 경우에는 *str만을 사용할 수도 있다. 단순한 방법을 사용한 예는 char/lp.c, 문자열 만을 취하는 경우는 scsi/aic7xxx.c에 있고 그 외의 드라이버들은 문자열과 값을 적절히 사용하고 있다. 지면이 허락하지 않을 뿐만 아니라 완전히 공개되어 있는 커널 소스를 모두 보일 이유도 없으므로 앞으로 꼭 필요한 부분을 제외하고는 커널 코드는 생략하려고 하니까 여기서 거론된 부분은 반드시 직접 찾아 보기 바란다.

모듈로 적재할 때에는 약간 달라진다. 이때에는 my_device_setup이 호출되지 않는다. 곧바로 초기화 루틴인 my_device_init가 호출된다. 모듈 적재 함수 명은 모두 init_module로 통일되어 있으므로 드라이버 파일을 살펴보면 모두 아래와 같이 선언된 부분을 찾을 수 있다.

#define my_device_init init_module

초기화 루틴을 하나로 통일시키고 커널 삽입 방법과 모듈 적재 방법을 #ifdef MODULE - #else - #endif 짝으로 구분해서 실행하도록 해놓았다. 만약 인자를 lilo옵션과 동일하게 쓸 수 있도록 했다면 my_device_init의 #ifdef MODULE 안에서 문자열을 파싱해야 한다. 대부분의 드라이버들이 이런 작업을 피하기 위해서 아래처럼 모듈을 적재한다.

insmod my_device io=200,300,400 irq=5,7,8

이렇게 하면 insmod가 인자를 파싱하여 드라이버의 io와 irq 배열에 이 값을 초기화 시켜 주니까 드라이버를 작성할 때 io와 irq배열을 정의해 놓아야 한다. 배열명이 꼭 io,irq일 필요는 없다. 이때에는 개수의 제한이 io 배열의 크기와 같다. 모듈 적재와 커널 삽입 때의 호환성을 위해서는 크기를 커널 삽입시와 같게 해야 문제가 없을 것이다.

커널 2.2에서는 MODULE_PARM(my_device,"1-10i") 함수를 사용한다. 함수 인자를 살펴보면 어떤 방식인지 쉽게 알 수 있을 것이다. 일부 드라이버들은 my_device_setup과 my_device_init를 통합시켜서 모든 작업을 my_device_init에서 하게 한 드라이버들도 있다. 이런 드라이버는 my_device_setup 함수가 간단히 아래처럼 한 줄이다.

return(my_device_init());

옵션 값이 필요 없거나 스스로 디바이스를 찾을 수 있게 만든 드라이버들이 주로 이 방법을 사용한다. setup 함수는 디바이스 드라이버의 제일 뒤쪽에 주로 있고 함수 명은 *_setup이다. 드라이버마다 특별한 방법이 많기 때문에 가능하다면 많은 드라이버를 조사해 보기 바란다.

드라이버 초기화 함수

초기화 함수에서는 특정 주소로 값을 보내기, 바이오스 다운로드, 지원 디바이스 중에서 장착된 디바이스를 구별하는 작업들을 해야 한다. 커널 쪽으로는 인터럽트 등록, 드라이버 등록, 디바이스 개수와 번지를 지정하는 작업이 필요하다. 개발 초기에는 모듈적재 방법을 사용 해야 커널 재컴파일 시간을 줄일 수 있으므로 모듈 적재 방법을 중심으로 살펴보자.

my_device_init에서 인자를 파싱하는 드라이버는 코드 처음에 있다. setup 함수에서 말했으니까 설명하지 않는다. 초기화 함수에서 그 다음으로 할 일은 setup에서 지정한 번지, 인터럽트가 유효한 것인지 검사하는 일이다. 설정값을 잘못 지정했거나, 장착된 디바이스보다 더 많이 지정했거나, 디바이스가 작동하지 않는 경우 등을 모두 걸러내야 한다.

우선 my_device 드라이버를 등록하자. 등록 함수는 register_chrdev이다. 이 함수는 주 장치 번호, 디바이스 이름, 파일 조작 함수 인터페이스를 등록한다. 커널에서 이 디바이스를 사용하는 인터페이스는 다음과 같다.

static struct file_operations my_device_fops = { my_device_lseek, my_device_read, my_device_write, my_device_readdir, my_device_select, my_device_ioctl, my_device_mmap, my_device_open, my_device_release };

커널에서는 어떤 디바이스라도 파일과 같이 접근하므로 file_operations 구조체로 정의하고 read/write 루틴을 my_device에 고유한 방법으로 작성하면 된다. 문자 디바이스와 블록 디바이스에 따라 지원하는 함수는 차이가 있다. 예를 들어, my_device_readdir 함수는 문자 디바이스인 my_device에서는 전혀 의미가 없다. 이 함수는 커널 자체에서 전혀 사용하지 않으므로 NULL로 정의해도 아무 상관이 없다. 어떤 함수가 필요하고 어떤 함수가 필요 없는지는 크게 블록 디바이스와 문자 디바이스에 따라 차이가 난다. 만들려는 디바이스의 특성을 고려하여 연관이 있는 드라이버를 조사해 파악하기 바란다.   ==>more 

디바이스가 사용할 번지 주소가 유효한지는 검사하는 함수는 check_region이다. 주소가 유효하다면 request_region으로 이 주소를 점유한다. 해제는 release_region이다. 초기화 과정에서 드라이버나 디바이스가 문제가 있으면 반드시 이 함수로 영역을 반환하여야 한다. 함수 정의는 소스를 찾아 보기 바란다. 앞에서 커널 코드를 생략한다고 말했듯이 함수 설명도 가능한 한 함수 명만을 보이고 정확한 정의는 생략한다.

인터럽트를 사용한다면 이것을 사용하겠다고 요청해야 한다. request_irq/free_irq를 사용하면 된다. request_irq의 세 번째 인자는 SA_INTERRUPT를 사용한다. SA_INTERRUPT는 빠른 irq라고 정의되어 있다. 이 플래그는 신속한 인터럽트 서비스를 위해서 다른 인터럽트를 금지하고 가장 우선으로 인터럽트 처리 루틴을 수행한다. 문맥교환, 재인터럽트, 프로세스 블록킹이 일어나지 않는다. 여기에 0을 쓰면 느린 irq로 작동된다. 이 인터럽트는 인터럽트를 금지하지 않으므로 또 다시 인터럽트가 발생할 수 있다. SA_SHIRQ는 pci 디바이스가 인터럽트를 공유할 때 사용한다. 함수 원형과 자세한 옵션 플래그 설명은 include/asm/sched.h 에서 찾아 볼 수 있다.

request_region, request_irq를 수행했을 때 요청한 값을 사용할 수 있으면 디바이스가 실제로 장착되어 있는지 검사해야 한다. 디바이스의 정해진 주소에 장치 종류를 나타내는 문자열 같은 식별자가 있다면 그것을 읽어 오면 된다. 인터럽트를 사용한다면 테스트를 위해 카드가 인터럽트를 일으키도록 한 다음에 돌아오는 인터럽트를 검사하면 된다. 이 방법은 register_chardev(또는 register_blkdev,tty_register_driver)를 사용하여 미리 인터럽트 함수를 등록하고 사용해야 한다. 이런 방법을 쓸 수 없다면 지정한 번지에 디바이스가 있다고 가정하고 초기화를 해야 한다. lilo 옵션이 틀리지 않았다는 가정을 해야 하기 때문에 다른 장치와 충돌한다면 심각한 문제가 생길 수 있으므로 가능하다면 확실한 방법을 강구해야 한다. 지정한 장치가 실제로 있는지 검사하는 루틴은 모든 드라이버에 있으므로 잘 살펴보기 바란다.

드라이버가 사용할 메모리 영역이 필요하다면 kmalloc 함수로 일정 영역을 확보해 놓는다. kmalloc은 데이터를 저장할 메모리 영역을 확보하는데 사용하고 request_region 함수는 특정 주소의 메모리 영역을 확보하는 데 사용한다.

디바이스에 바이오스를 다운로드 하여 활성화 시켜야 한다면 상당한 고민을 해야 한다. 바이오스의 크기가 적다면 바이너리 파일을 바이트 배열로 만들어 정의해 두고 memcpy_toio 함수를 이용해서 다운로드 할 수 있다.(drivers/char/digi_bios.h) 몇 메가씩 되는 데이터를 다운로드 해야 한다면 이 방법을 사용할 수 없다. 바이트 배열은 정적 데이터가 되어 커널을 적재할 수도 없도록 크게 만들게 되기 때문이다. 가능한 방법은 일단 드라이버를 등록하고 ioctl 루틴을 이용해서 디바이스를 열어 다운로드 하고, 그 결과에 따라 드라이버를 활성화하거나 우회적으로 드라이버를 사용금지 또는 강제 삭제를 해야 한다.    ==>more

드라이버 삭제 함수

모듈 방식으로 드라이버를 적재했으면 rmmod로 모듈을 삭제하는 함수를 작성해야 한다. cleanup_module로 함수 명이 통일되어 있고 모듈일 때만 필요하므로 #ifdef MODULE - #endif 안에서 정의해야 한다. 우선 kmalloc으로 할당 받은 메모리를 kfree 함수로 반납한다. request_region으로 확보한 주소는 release_region으로 해제하고 request_irq로 인터럽트를 사용했다면 free_irq로 반납한다. 마지막으로 장치 특수번호를 해제하여 커널이 디바이스를 사용하지 않도록 한다. 이 함수는 unregister_chrdev/tty_unregister_device 이다.

그외에 ioremap등의 함수를 사용했으면 이들과 짝이 되는 iounmap 등의 함수를 사용하여 모든 자원을 반납해야 한다. 자원을 제대로 반납하지 않고 모듈을 삭제하면 엄청난 양의 에러 메시지가 /var/log/messages에 쌓이게 된다. 좀 더 심각한 상황이 생겨서 파일 시스템 전체를 잃게 될 수도 있기 때문에 할당 받은 자원은 반드시 반납하는 함수를 철저히 확인하여 사용해야 한다.

2.2 버전에서는 initfunc(my_device_setup())으로 초기화 때만 필요한 함수를 정의해서 초기화가 끝난 후에 이 함수가 할당 받은 메모리를 회수하여 커널 메모리 사용량을 줄이고 있다.

드라이버를 적재하고 초기화하는 함수들은 몇 가지 공통사항만 정의되어 있을 뿐 함수 내부의 작성 규칙은 완벽하게 정해지지 않았다. 사용할 수 있는 함수도 수시로 없어지거나 인터페이스가 달라져서 프로그래머를 혼란스럽게 한다. 또한 사용 가능한 함수를 정리해 놓은 곳도 없고 설명도 충분하지 않다. 커널 프로그래밍을 위해서 만든 함수를 사용하지 않고 같은 기능을 수행하는 함수를 스스로 만들어서 사용한 드라이버도 많다. 아마 함수를 찾지 못했거나 있는 것을 모르고 드라이버를 작성해서 그럴 것이다. 필자도 필요한 기능을 하는 함수를 만들어 사용하다가 커널에 이미 그 함수가 있는 것을 알고 코드를 고친 경험이 있다. 비주얼 툴로 최상위 응용 프로그램을 만드는 일보다 쓸 함수도 변변찮은 로우레벨의 커널 디바이스 프로그래밍이 오히려 풍부한 상상력을 필요로 하는 작업인 이유가 여기에 있을 것이다.

헤더파일의 구조

헤더파일은 반드시 아래와 같이 시작하고 끝내야 한다.

#ifndef LINUX_MY_DEVICE #define LINUX_MY_DEVICE ... ... #endif

이렇게 해야 여러 파일에 헤더파일이 인클루드 되었어도 문제가 생기지 않는다. 커널에서 사용하는 변수이지만 사용자 프로그램에서 보여서는 안 되는 변수가 있다면 아래와 같이 막아 놓아야 한다.

#ifdef KERNEL ... #endif

그리고 my_device_init는 외부에서 참조할 수 있도록 헤더파일에 꼭 선언해야 한다. init 함수는 커널에서만 사용하므로 KERNEL 내부에 존재해도 상관은 없다.

초기화 할 때 많이 쓰는 함수들

outb/outw, inb/inw 함수는 물리 주소에 쓰기/읽기 함수이다. 이름이 보여 주듯이 바이트, 워드를 읽고 쓴다. readb/writeb 함수는 memory mapped I/O 장비에 읽고 쓰는 함수이다. memcpy_toio/memcpy_fromio 함수는 특정 주소에 데이터를 인자로 준 바이트만큼 쓴다. 각 플랫폼에 따라 커널이 보는 주소와 cpu가 보는 주소, 그리고 물리 주소의 차이를 없애는 역할을 한다. 물리 주소와 가상주소 시스템 버스와의 관계가 복잡하고 여러 플랫폼에 따라 주소 지정법이 다르다. x86 아키텍쳐에서는 물리주소와 memory mapped 주소가 동일하지만 다른 플랫폼에서는 x86과 다르기 때문에 호환성을 위해서 상당한 주의를 해야 한다. Documentation/IO-mapping.txt를 살펴보면 리누스가 메모리 접근함수 사용시 주의할 점에 대해서 설명해 놓았다. 디바이스가 범용 플랫폼에서 동작하기를 바란다면 꼭 읽어 보아야 한다.

cli() 함수를 사용하여 인터럽트를 금지 시키고 중요한 작업을 한 다음에 sti() 함수로 인터럽트를 가능하게 만든다. 드라이버 프로그램을 부르는 함수에서 부르기 전후에 플래그 상태가 변화한다면 문제가 발생할 수 있기 때문에 cil/sti 짝으로만 쓰지는 않고 save_flags(flags); cli(); sti(); restore_flags(flags); 형식으로 쓴다. 이렇게 하면 불리기 전의 상태가 보존되므로 드라이버 프로그램 안에서 안심하고 플래그를 조작할 수 있다. sti()는 드라이버 프로그램 안에서 인터럽트를 가능하게 할 필요가 있을 때 사용하면 된다. 인터럽트 가능 불가능에관계없이 드라이버가 불릴 때의 상태에서 동작해도 상관없다면 save_flags;cli;restore_flags를 사용하면 된다. 원 상태가 인터럽트 가능이라면 restore_flags가 sti 함수 역할도 하기 때문이다. 주의할 것은 드라이버 함수에서 여러 하위 루틴으로 뛰는 동안에 save_flags;cli;restore_flags 순서가 유지되어야 하는 것이다. 함수가 조건문 등으로 분기할 때 자칫 restore_flags 함수가 수행되지 않는 등의 오류가 있으면 시스템이 정지하게 된다.

이런 에러는 상당히 발견하기 어려운 것이다. 어디서 시스템이 정지했는지 정보를 얻기가 힘들다. printk 함수를 사용해서 소스의 위치를 추적하더라도 찾기 힘들다. printk 함수는 수행되는 즉시 문자열을 /var/log/messages나 콘솔 화면에 쓰지 않고 쓸 데이터가 많거나 시스템이 바쁘지 않은 동안에 flash를 하므로 printk 함수가 아직 완전히 수행되지 않은 상태에서 시스템이 정지하는 경우가 많기 때문이다. 그러므로 코딩 시에 철저히 save_flags;cli;restore_flags의 순서와 흐름을 따져서 사용해야 한다.

디바이스에 어떤 조작을 하고 일정 시간을 기다릴 때에 사용할 수 있는 다양한 함수가 존재한다. 디바이스를 조작하고 결과를 체크하는 일을 수 밀리 초의 간격 안에 모두 해야 하는 경우가 있다. MY_DEVICE에 차체 cpu가 들어 있고 이 cpu를 활성화 시키기 위해서는 MY_DEVICE의 base 주소에 0을 쓰고 400ms와 500ms 사이에 1을 써야 한다고 하자. 이때에는 절대적인 대기 함수를 사용해야 한다. 여러 문서에 대기에 대한 함수 설명이 있지만 필자의 경험으로는 delay() 함수만 이런 기능을 정상적으로 수행했다. 인자는 delay((loops_per_sec/100) * (원하는ms))를 사용하면 된다. 이 함수는 문맥교환이 일어나지 않기 때문에 실행되는 동안 시스템이 정지해 있게 된다. 드라이버의 일반 작업에 사용하면 효율이 엄청나게 떨어지므로 절대적인 시간이 필요한 초기화 작업 같은 경우를 제외하고는 이 함수를 사용해서는 안 된다.

일반 작업에서 대기 함수가 필요하면 다음과 같이 사용하면 된다.

current->state = TASK_INTERRUPTIBLE; current->timeout = jiffies + HZ * 3; schedule();

current란 현재 수행되고 있는 프로세스를 말한다. 이 프로세스는 일정한 시스템 시간을 할당 받아 현재 이 라인을 수행하고 있다. 인터럽트가 가능하게 만들고(TASK_INTERRUPTIBLE) 깨어나는 시간을 앞으로 3초 후로 정하고 (HZ*3) 잠들게(schedule) 한다. jiffies란 시스템의 현재 시간을 의미한다. HZ는 1초에 해당하는 값이고 schedule은 현재의 프로세스를 블록 시키는 함수이다. 커널은 이 프로세스가 잠들고 있는 동안 다른 작업을 할 수 있게 되기 때문에 효율을 높일 수 있다. timeout이 3초로 되어 있지만 반드시 이 시각에 깨어난다는 보장은 없다. 깨어날 수 있는 보장이 3초 후부터라는 것일 뿐이다. timeout 값이 너무 작으면 문맥교환이 자주 일어나서 효율이 떨어지고 너무 크면 드라이버 작업 수행 성능이 떨어지므로 대기 시간을 잘 조사해서 적당한 값을 설정해야 한다.

커널은 프로세스에게 각종 signal을 줄 수 있다. include/asm/signal.h에서 정의된 여러 시그널을 받은 프로세스는 가능한 한 신속하게 작업을 끝내기 위해서 블록되지 않으므로 schedule이 무시되기 때문에 코딩을 할 때 이 것을 염두에 두고 깨어났을 때 정상상태에서 깨어난 것인지 signal을 받았는지 구별해서 동작하게 해야 한다. signal을 받았는지는 깨어난 후에 다음과 같이 알아 낼 수 있다.

if(current->signal & ~current->blocked) //signal else // no signal

시그널을 받은 프로세스는 더 이상 작업을 진행하는 것이 의미가 없기 때문에 디바이스를 연 프로세스의 개수가 기록된 변수의 처리 등 꼭 필요한 일만 하고 신속하게 끝내야 한다. 2.2 버전에서는 signal_pending(current)로 바뀌었다.

앞에서 말한 대기 함수는 대기하는 프로세스가 일정 시간 이후에 깨어나서 바뀐 조건을 검사하는 것들이다. 대기하고 있는 동안에 다른 루틴에서 조건을 변화시키고 대기 프로세스를 깨워 준다면 조건이 만족하기 전에 깨어났다가 다시 잠드는 오버헤드를 줄일 수 있을 것이다. 이를 위해서 sleep/wake_up 함수가 있다.

sleep_on/interruptible_sleep_on, wake_up/wake_up_interruptible 함수는 잠들고 깨우는 일을 한다. interruptible_sleep_on 함수는 signal이 오거나 wake_up_interruptible 함수가 깨워 주거나 current->timeout 값에 시스템 시간이 이르면 깨어난다. 이 함수를 실행하기 전에 timeout 값을 조정해 줄 수 있으므로 깨우는 함수가 제대로 실행되지 않았다고 판단할 만큼 충분한 시간을 주고 잠들게 하면 된다. 이 함수는 대기 큐가 필요하므로 사용하기 전에 전역변수로 큐를 정의하고 초기화 시켜 놓아야 한다. 함수 명은 init_waitqueue 이다.

interruptible_sleep_on/wake_up_interruptible 함수는 인터럽트를 사용하는 드라이버에서 디바이스에 인터럽트를 걸어 놓고 잠들면 디바이스가 처리를 끝내고 인터럽트를 걸어 깨워 주는 경우에 많이 사용한다. 이 함수도 schedule과 마찬가지로 signal을 받으면 무조건 깨어 나기 때문에 꼭 상태를 체크해야 한다.

sleep_on/wake_up 짝은 wake_up 함수가 반드시 수행된다는 확신이 있는 경우에 사용된다. sleep_on으로 잠들면 timeout 값이 지나거나 시그널이 와도 깨어 나지 않고 오로지 wake_up만이 깨울 수 있다. wake_up 함수가 수행되지 않는다면 시스템이 교착상태에 빠질 수 있기 때문에 100% 확신이 없으면 사용하지 않는 것이 좋다. drivers/char/lp.c에서 사용되었지만 2.0.33에서는 interruptible로 바뀌었다. drivers/block 의 일부 드라이버에 사용예가 있으므로 참고하기 바란다.

그 외에 커널 해커 가이드의 지원함수 부분에 나머지 대기 함수에 대한 설명이 있다. 함수 설명을 읽고 드라이버를 조사해서 사용 방법을 알아 두기 바란다. 드라이브가 shared memory 방식으로 디바이스와 교신하기 위해 메모리를 확보하고 이것을 사용한다고 하자. 이 메모리 영역을 메일박스라고 부른다. 이 메일박스를 위해서 일정한 양의 메모리가 필요하다. 메일박스를 만들 때 마다 메모리를 할당 받는다면 상당한 오버헤드가 생기므로 디바이스 초기화 때에 영역을 확보해 놓고 계속 사용하면 좋을 것이다. 이때 사용할 수 있는 함수는 kmalloc이다. kmalloc이 한 번에 할당할 수 있는 메모리 양은 16KB로 제한되어 있다.

할당 받은 메모리 영역을 초기화 할 때는 memset 함수를 쓸 수 있다. 인자로 주는 값으로 메모리 영역을 채운다. 메모리에서 메모리로 데이터를 복사할 때는 memcpy를 사용할 수 있다. 이 함수는 커널 메모리사이에서 복사할 때만 쓰는 함수이다. 절대로 유저 데이터 영역과 커널 메모리사이의 복사에서 사용해서는 안 된다.

개발 초기에는 insmod 인자가 정상적으로 전달되었는지 확인한다든지 어떤 디바이스가 인식되었는지 보여 주는 출력루틴을 가능한 많이 삽입하는 것이 좋다. C 라이브러리 루틴의 printf는 사용할 수 없지만 커널 프로그래밍을 위해서 printk가 있다. 몇 가지 제한이 있지만 printf와 거의 유사하다. 정확한 형식은 lib/vsprintf.c에서 볼 수 있다.    ==>more

디바이스의 I/O 컨트롤

디바이스 드라이버가 제대로 적재되고 나서 설정값을 바꿀 필요가 있을 때 ioctl 함수를 부른다. C 라이브러리의 ioctl 함수를 이용해서 커널 드라이버의 ioctl 함수에 접근 할 수 있다. C 라이브러리의 ioctl 함수 원형은 다음과 같다.

int ioctl(int d, int request,...);

d는 파일기술자이며 /dev/my_device0에 해당한다. 우선 open 함수로 my_device0를 열어 보고 정상적으로 열리면 ioctl을 실행할 수 있다. ioctl의 두 번째 인자에 원하는 명령을 넣는다. 만약 my_device0의 어떤 값을 바꾸게 하고 싶으면 include/linux/my_device.h에 아래와 같이 정의하고 유저 프로그램에서 사용할 수 있도록 하면 된다.

#define MY_DEVICE_CHANGE_VALUE _IOWR(MY_DEVICE_MAJOR, 1, int[2])

_IOWR/_IOR 매크로는 include/asm/ioctl.h에 정의되어 있고 여러 가지 비슷한 매크로가 있다. 함수 명이 보여주는 대로 세 번째 인자에서 지정한 영역을 읽기만 가능/읽고 쓰기 가능한 형태로 커널에 전달하게 된다. 이 매크로는 첫 번째 인자와 두 번째 인자를 결합해서 커널에 전달한다. 두 번째 인자에 들어 갈 수 있는 값의 크기가 얼마나 되는가는 ioctl.h를 조사해 스스로 알아보기 바란다.

ioctl의 세 번째 인자는 커널에 전달하는 값이나 값의 배열 또는 커널로부터 받을 데이터 영역이다. 커널에 값(값배열)을 전달하고 같은 곳에 커널로부터 값을 받을 수도 있다.

커널의 ioctl은 보내온 명령에 따라 완전히 독립된 작업을 해야 하기 때문에 가장 지저분한 부분이다. 대부분의 드라이버들이 switch 문을 사용해서 명령에 따른 작업을 하는 코드를 작성해 놓았다. 해야 하는 작업에 따라 내용이 다르지만 커널 프로그래밍과 크게 차이가 나는 것은 아니다. 가장 중요한 것은 C 라이브러리 함수인 ioctl에서 보내온 데이터를 주고 받는 방법이다. 커널이 보는 메모리 영역과 사용자 프로그램이 보는 메모리 영역은 완전히 다르기 때문에 memcpy 등의 함수를 사용해서는 안 된다.

커널에서 유저 프로그램에서 데이터를 읽어 오거나 쓰기 위해서는 우선 읽어 올 수 있는지 확인해야 한다. verify_area(VERIFY_READ/VERIFY_WRITE ..) 함수를 사용해서 읽거나 쓰는 데 문제가 없으면 memcpy_fromfs/memcpy_tofs 함수를 사용할 수 있다. 이 함수 명은 사용자 데이터 세그먼트와 커널 데이터 세그먼트를 구별하는 인텔 CPU의 레지스터 명이 fs인 데서 유래했다. 2.2 이상에서는 여러 플랫폼에서 일반 명칭으로 사용하기 위해서 copy_from_user/copy_to_user로 바뀌었다. 2.2에서는 또한 verify_area를 할 필요가 없다.

ioctl 함수는 사용하기에 따라 수많은 일을 할 수 있다. ioctl을 사용하여 할당 받은 인터럽트를 바꿀 수도 있고 점유하고 있는 물리 주소도 변경할 수 있다. linux-ggi 프로젝트 그룹에서는 모든 VGA드라이버를 단일화 시키고 ioctl을 사용해서 각 VGA카드의 특성을 조정해서 사용하자고 제안하고 있다. 이 방법은 리눅스 VGA드라이버 작성을 위한 노력을 대폭 줄일 수 있는 획기적인 방법이다.

 네트워크,보안계열,IT취업 관련사이트

디바이스 드라이버 입출력 함수

이제 디바이스 드라이버의 핵심인 입출력 루틴에 대해서 알아보자. 이 부분은 하드웨어와 가장 밀접한 부분으로 하드웨어의 기계적인 작동까지도 고려해야 하는 복잡하고 힘든 작업을 필요로 한다. 게다가 책에서만 보던 세마포, 교착상태 스케줄링 등 운영체제에서 가장 중요한 부분을 직접 구현해야 하는 일이기도 하다. 온갖 개발툴이 난무하고 마우스만으로 프로그래밍을 끝낼 수 있는 최상위 응용프로그램 개발 환경이 지배하는 요즈음, 이렇게 원론적이고 근본적인 작업을 할 수 있다는 것은 즐거움이기도 하다. 심각한 소프트웨어 위기가 닥치면 닥칠 수록 컴퓨터를 배우는 사람들, 컴퓨터를 사용하여 무엇인가를 이루어 보려는 사람들은 가장 근본적인 부분을 다시 들여다 보아야 할 필요가 있다. 특히 컴퓨터를 배우고 있는 학생이라면 반드시 리눅스 커널에 관심을 가져야 하는 이유가 바로 여기에 있는 것이다.

커널이 발전하면서 필요에 따라 변수명이나 함수 명들을 바꿀 필요가 생긴다. 유저프로그래밍에서는 라이브러리 함수 인터페이스가 바뀔 수도 있지만 write같은 가장 기본적인 인터페이스는 거의 바뀌지 않는다. 커널 프로그래밍에서는 이런 것을 기대할 수 없다. 커널의 외부인터페이스만 유지될 뿐 내부에서는 모든 것이 바뀔 수 있다고 생각해야 한다. 특히 리눅스는 개발버전과 안정버전의 두 가지 버전이 공존하기 때문에 제작한 드라이버가 새버전의 커널에서 문제 없이 컴파일 될 수 있는지 확인할 필요가 있다. 리눅스 커널 프로그래밍의 변경 내용을 공존시키기 위해서 만들어진 변수가 LINUX_VERSION_CODE 이다.

커널 소스의 Makefile 가장 앞부분에 다음과 같은 내용이 있다.

VERSIO

관련자료

댓글 0
등록된 댓글이 없습니다.

공지사항


뉴스광장


  • 현재 회원수 :  60,039 명
  • 현재 강좌수 :  35,845 개
  • 현재 접속자 :  112 명