天天看點

Linux驅動程式開發 - Kbuild系統

一個簡單的驅動

下面我們來編寫第一個驅動程式,它很簡單,在運作時會輸出‘Hello World’消息。

// hello.c

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

static int __init hello_init(void)

{

printk(KERN_ALERT "Hello World!/n");

return 0;

}

static void __exit hello_exit(void)

{

printk(KERN_ALERT "Goodbye World!/n");

}

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("GPL");

這就是一個簡單的驅動程式,它什麼也沒做,僅僅是輸出一些資訊,不過對于我們來說這已經足夠了。儲存這個程式,命名為hello.c。在寫一個Makefile檔案用來編譯它,Makefile和hello.c檔案儲存在同一個目錄下。

##Makefile

ifneq ($(KERNELRELEASE),)

MODULE_NAME = helloworld

$(MODULE_NAME)-objs := hello.o

obj-m := $(MODULE_NAME).o

else

KERNEL_DIR = /lib/modules/`uname -r`/build

MODULEDIR := $(shell pwd)

.PHONY: modules

default: modules

modules:

make -C $(KERNEL_DIR) M=$(MODULEDIR) modules

clean distclean:

rm -f *.o *.mod.c .*.*.cmd *.ko

rm -rf .tmp_versions

endif

編譯并運作這個子產品:

//需要root權限來運作

make

insmod helloworld.ko

rmmod helloworld.ko

盡管我們對它的一些細節還不夠了解,它确實神奇的工作了,這個Hello World資訊輸出到了螢幕終端上(不是VT),或者系統的Kenrel log裡(/var/log/messages),你可以通過運作dmesg來看到這些資訊。

驅動基礎

我們通過分析上面的代碼來了解一個驅動程式的基本概念。

  • 頭檔案

就像你寫C程式需要包含C庫的頭檔案那樣,Linux核心程式設計也需要包含Kernel頭檔案,大多的Linux驅動程式需要包含下面三個頭檔案:

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

    • init.h 定義了驅動的初始化和退出相關的函數,
    • kernel.h 定義了經常用到的函數原型及宏定義
    • module.h 定義了核心子產品相關的函數、變量及宏。
  • 初始化

任何一個驅動都去需要提供一個初始化函數,當驅動加載到核心中時,這個初始化函數就會被自動執行,初始化的函數原型定義如下:

typedef int (*initcall_t)(void);

驅動程式是通過module_init宏來聲明初始化函數的:

static int __init hello_init(void)

{

printk(KERN_ALERT "Hello World!/n");

return 0;

}

module_init(hello_init);

__init 宏告訴編譯器如果這個子產品被編譯到核心則把這個函數放到(.init.text)段,這樣當函數初始化完成後這個區域可以被清除掉以節約系統記憶體。 Kenrel啟動時看到的消息“Freeing unused kernel memory: xxxk freed”同它有關。

初始化函數是有傳回值的,隻有在初始化成功是才傳回0,否則傳回錯誤碼(errno)。

  • 解除安裝

如果驅動程式編譯成子產品(動态加載)模式,那麼它需要一個清理函數。當移除一個核心子產品時這個函數被調用執行清理工作。清理函數的函數原型定義為:

typedef void (*exitcall_t)(void);

驅動程式是通過module_exit宏來聲明清理函數的:

static void __exit hello_exit(void)

{

printk(KERN_ALERT "Goodbye World!/n");

}

module_exit(hello_exit);

同__init類似,如果驅動被編譯進核心,則__exit宏會忽略清理函數,因為編譯進核心的子產品不需要做清理工作。顯然,__init和__exit對動态加載的子產品是無效的。

  • 版權資訊

Linux核心是按照GPL釋出的,同樣Linux的驅動程式也要提供版權資訊,否則當加載到核心中是系統會給出警告資訊。Hello World例子中的版權資訊是GPL。

MODULE_LICENSE("GPL");

二、Kbuild與Makefile

從Linux核心2.6開始,Linux核心的編譯采用Kbuild系統,這同過去的編譯系統有很大的不同,尤其對于Linux核心子產品的編譯。在 新的系統下,Linux編譯系統會兩次掃描Linux的Makefile:首先編譯系統會讀取Linux核心頂層的Makefile,然後根據讀到的内容 第二次讀取Kbuild的Makefile來編譯Linux核心。

Linux核心Makefile分類

  • Kernel Makefile

Kernel Makefile位于Linux核心源代碼的頂層目錄,也叫 Top Makefile。它主要用于指定編譯Linux Kernel目标檔案(vmlinux)和子產品(module)。這編譯核心或子產品是,這個檔案會被首先讀取,并根據讀到的内容配置編譯環境變量。對于内 核或驅動開發人員來說,這個檔案幾乎不用任何修改。

  • Kbuild Makefile

Kbuild系統使用Kbuild Makefile來編譯核心或子產品。當Kernel Makefile被解析完成後,Kbuild會讀取相關的Kbuild Makefile進行核心或子產品的編譯。Kbuild Makefile有特定的文法指定哪些編譯進核心中、哪些編譯為子產品、及對應的源檔案是什麼等。核心及驅動開發人員需要編寫這個Kbuild Makefile檔案。

  • ARCH Makefile

ARCH Makefile位于ARCH/$(ARCH)/Makefile,是系統對應平台的Makefile。Kernel Top Makefile會包含這個檔案來指定平台相關資訊。隻有平台開發人員會關心這個檔案。

Kbuild Makefile

Kbuild Makefile的檔案名不一定是Makefile ,盡管推薦使用Makefile這個名字。大多的Kbuild檔案的名字都是Makefile。為了與其他Makefile檔案相差別,你也可以指定Kbuild Makefile的名字為Kbuild 。而且如果“Makefile”和“Kbuild”檔案同時存在,則Kbuild系統會使用“Kbuild”檔案。

  • 目标定義

Kbuild Makefile的一個最主要功能就是指定編譯什麼,這個功能是通過下面兩個對象指定的obj-?和xxx-objs:

  • obj-?

obj-?指定編譯什麼,怎麼編譯?其中的“?”可能是“y”或“m”,“y”指定把對象編譯進核心中,“m”指定把對象編譯為子產品。文法如下;

obj-? = $(target).o

target為編譯對象的名字。如果沒有指定xxx-objs,這編譯這個對象需要的源檔案就是$(target).c或$(target).s。如果指 定了$(target)-objs,則編譯這個對象需要的源檔案由$(target)-objs指定,并且不能有$(target).c 或$(target).s檔案。

  • xxx-objs

xxx-objs指定了編譯對象需要的檔案,一般隻有在源檔案是多個時才需要它。 隻要包含了這兩行,Kbuild Makefile就應該可以工作了。

  • 嵌套編譯

有時一個對象可能嵌入到另一個對象的目錄下,那個如何編譯子目錄下的對象呢?其實很簡單,隻要指定obj_?的對象為子目錄的名字就可以了:

obj-? = $(sub_target)/

其中“?”可以是“y”或“m”,$(sub_target)是子目錄名字。

  • 編譯器選項

盡管在大多數情況下不需要指定編譯器選項,有時我們還是需要指定一些編譯選項的。

  • ccflags-y, asflags-y and ldflags-y

這些編譯選項用于指定cc、as和ld的編譯選項 編譯外部子產品

有時候我們需要在核心源代碼數的外面編譯核心子產品,編譯的基本指令是:

make -C $(KERNEL_DIR) M=`pwd` modules

我們可以把這個指令內建到Makefile裡,這樣我們就可以隻輸入“make”指令就可以了。回想上一章的那個Makefile,它把Normal Makefile 和Kbuild Makefile內建到一個檔案中了。為了差別Kbuild Makefile 和Normal Makefile,這樣我們改寫Makefile為如下形式,并且添加Kbuild Makefile - “Kbuild”。

##Makefile

ifneq ($(KERNELRELEASE),)

include "Kbuild"

else

KERNEL_DIR = /lib/modules/`uname -r`/build

MODULEDIR := $(shell pwd)

.PHONY: modules

default: modules

modules:

make -C $(KERNEL_DIR) M=$(MODULEDIR) modules

clean distclean:

rm -f *.o *.mod.c .*.*.cmd *.ko

rm -rf .tmp_versions

endif

## Kbuild

MODULE_NAME = helloworld

$(MODULE_NAME)-objs := hello.o

obj-m := $(MODULE_NAME).o

一般不需要在Makefile裡包含如下代碼,這樣寫完全是為了相容老版本的Kbuild系統。KERNELRELEASE變量在Kernel Makefile裡定義的,是以隻有在第二次由Kbuild讀取這個Makefile檔案時才會解析到Kbuild的内容。

ifneq ($(KERNELRELEASE),)

include "Kbuild"

else

...

endif

外部頭檔案

有時需要連接配接核心源代碼外部的系統頭檔案,但Kbuild系統預設的系統頭檔案都在核心源代碼内部,如何使用外部的頭檔案呢?這個可以借助于Kbuild系統的特殊規則:

  • EXTRA_CFLAGS

EXTRA_CFLAGS可以給Kbuild系統添加外部系統頭檔案,

EXTRA_CFLAGS += $(ext_include_path)

一般外部頭檔案可能位于外部子產品源檔案的目錄内,如何指定呢?這可以借助$(src)或$(obj)

  • $(src)/$(obj)

$(src)是一個相對路徑,它就是Makefile/Kbuild檔案所在的路徑。同樣$(obj)就是編譯目标儲存的路徑,預設就是源代碼所在路徑。

是以,我們修改Kbuild檔案添加 EXTRA_CFLAGS 來包含外部頭檔案盡管在這個驅動裡沒有引用外部系統頭檔案:

## Kbuild

MODULE_NAME = helloworld

$(MODULE_NAME)-objs := hello.o

EXTRA_CFLAGS := -I$(src)/include

obj-m := $(MODULE_NAME).o

繼續閱讀