基本知識簡介:linux驅動開發教程

最近在搞一個linux的項目,其中主要是在編寫一些應用模塊,對內核及其驅動模塊涉及很少,遇到了一些驅動模塊的問題時,臨時查了些資料,大致了解了一下驅動模塊開發的基本步驟和常規步驟,並從網上也收集到了一些相關的資料,於是對其進行了一下簡單的總結,記錄於此,便於日後查閱,並與同道中人共享。

什麼是linux內核驅動模塊

Linux內核的整體結構已經非常龐大,而其包含的組件也非常多。我們怎樣把需要的部分都包含在內核中呢?

一種方法是把所有需要的功能都編譯到Linux內核。這會導致兩個問題,一是生成的內核會很大,二是如果我們要在現有的內核中新增或刪除功能,將不得不重新編譯內核。

有沒有一種機制使得編譯出的內核本身並不需要包含所有功能,而在這些功能需要被使用的時候,其對應的代碼被動態地加載到內核中呢?

答案是肯定的,Linux提供了這樣的一種機制,這種機制被稱為模塊(Module)。模塊具有這樣的特點:

模塊本身不被編譯入內核映像,這控制了內核的大小。

模塊一旦被加載,它就和內核中的其它部分完全一樣。

那麼,問題來了。如何編寫內核驅動模塊呢?別急,我們一步一步來介紹。

文章福利】小編自己整理了一些個人覺得比較好的linux內核學習書籍、視頻資料共享在群文件裡面,有需要的可以私信【內核】自行添加免費領取哦!!!(含視頻教程、電子書、實戰項目及代碼)

「inux內核」驅動模塊開發步驟及實例入門介紹

一、先從一個最簡單的例子入手

先來看一個最簡單的內核模塊“Hello World”。

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

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_INFO " Hello World entern");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO " Hello World exitn ");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("Song Baohua");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

這個最簡單的內核模塊只包含內核模塊加載函數、卸載函數和對Dual BSD/GPL許可權限的聲明以及一些描述信息。編譯它會產生hello.ko目標文件,通過“insmod ./hello.ko”命令可以加載它,通過“rmmod hello”命令可以卸載它,加載時輸出“Hello World enter”,卸載時輸出“Hello World exit”。

內核模塊中用於輸出的函數是內核空間的printk()而非用戶空間的printf(),printk()作為一種最基本的內核調試手段,其用法和printf()基本相似,但可定義輸出級別。

1、查看系統中已經加載的模塊列表

在Linux中,使用lsmod命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關係,例如:

root@imx6:~$  lsmod
Module             Size         Used by
hello              1568         0 
ohci1394           32716        0 
ide_scsi           16708        0 
ide_cd             39392        0 
cdrom              36960        1 ide_cd

lsmod命令實際上讀取並分析“/proc/modules”文件,與上述lsmod命令結果對應的“/proc/modules”文件如下:

root@imx6:~$ cat /proc/modules 
hello        1568        0     -          Live    0xc8859000
ohci1394     32716       0     -          Live    0xc88c8000
ieee1394     94420       1     ohci1394,  Live    0xc8840000
ide_scsi     16708       0     -          Live    0xc883a000
ide_cd       39392       0     -          Live    0xc882f000
cdrom        36960       1     ide_cd,    Live    0xc8876000

內核中已加載模塊的信息也存在於/sys/module目錄下,加載hello.ko後,內核中將包含/sys/module/hello目錄,該目錄下又包含一個refcnt文件和一個sections目錄,在/sys/module/hello目錄下運行tree –a得到如下目錄樹:

root@imx6:~$  tree -a
.
|-- refcnt
`-- sections
    |-- .bss
    |-- .data
    |-- .gnu.linkonce.this_module
    |-- .rodata
    |-- .rodata.str1.1
    |-- .strtab
    |-- .symtab
    |-- .text
    `-- __versions

2、查看某個具體模塊的詳細信息

使用“modinfo <模塊名>”命令可以獲得模塊的信息,包括模塊作者、模塊的說明、模塊所支持的參數等。

root@imx6:~$ modinfo hello.ko
filename:       hello.ko
license:        Dual BSD/GPL
author:         Song Baohua
description:    A simple Hello World Module
alias:          a simplest module
vermagic:       2.6.15.5 686 gcc-3.2
depends:   

二、模塊程序的基本結構

一個Linux內核模塊主要由如下幾個部分組成:

  • 模塊加載函數一般需要
    當通過insmod或modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。
  • 模塊卸載函數一般需要
    當通過rmmod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。
  • 模塊許可證聲明必須
    許可證(LICENSE)聲明描述內核模塊的許可權限,如果不聲明LICENSE,模塊被加載時,將收到內核被污染 (kernel tainted)的警告。在Linux 2.6內核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多數情況下,內核模塊應遵循GPL兼容許可權。Linux 2.6內核模塊最常見的是以MODULE_LICENSE( “Dual BSD/GPL” )語句聲明模塊採用BSD/GPL雙LICENSE。
  • 模塊參數可選
    模塊參數是模塊被加載的時候可以被傳遞給它的值,它本身對應模塊內部的全局變量。
  • 模塊導出符號可選
    內核模塊可以導出符號(symbol,對應於函數或變量),這樣其它模塊可以使用本模塊中的變量或函數。
  • 模塊作者等信息聲明可選
    用於申明模塊作者的相關信息,一般用於備註作者姓名、郵箱等。

1、內核模塊的加載函數

模塊加載函數Linux內核模塊加載函數宜被以__init標識聲明,典型的模塊加載函數的形式如下所示:

static int __init initialization_function(void)
{     
    /* 初始化代碼 */
}
module_init(initialization_function);

模塊加載函數必須以“module_init(函數名)”的形式被指定。它返回整型值,若初始化成功,應返回0。而在初始化失敗時,應該返回錯誤編碼errno【在Linux內核里,錯誤編碼errno是一個負值,在頭文件”linux/errno.h”中定義,包含-ENODEV、-ENOMEM之類的符號值】。總是返回相應的錯誤編碼是種非常好的習慣,因為只有這樣,用戶程序才可以利用perror等方法把它們轉換成有意義的錯誤信息字符串。

在Linux 2.6內核中,可以使用request_module(const char *fmt, …)函數加載內核模塊,驅動開發人員可以通過調用request_module(module_name);或request_module(“char-major-%d-%d”, MAJOR(dev), MINOR(dev));這種靈活的方式加載其它內核模塊。

註:在Linux中,所有標識為__init的函數在連接的時候都放在.init.text這個區段內,此外,所有的__init函數在區段.initcall.init中還保存了一份函數指針,在初始化時內核會通過這些函數指針調用這些__init函數,並在初始化完成後,釋放init區段(包括.init.text,.initcall.init等)。

2、內核模塊的卸載函數

Linux內核模塊加載函數宜被以__exit標識聲明,典型的模塊卸載函數的形式如下所示:

static void __exit cleanup_function(void)
{
    /* 釋放代碼 */
}
module_exit(cleanup_function);

模塊卸載函數在模塊卸載的時候執行,不返回任何值,必須以“module_exit(函數名)”的形式來指定。

通常來說,模塊卸載函數要完成與模塊加載函數相反的功能,例如:

  • 若模塊加載函數註冊了XXX,則模塊卸載函數應該註銷XXX;
  • 若模塊加載函數動態申請了內存,則模塊卸載函數應釋放該內存;
  • 若模塊加載函數申請了硬件資源(中斷、DMA通道、I/O端口和I/O內存等)的佔用,則模塊卸載函數應釋放這些硬件資源;
  • 若模塊加載函數開啟了硬件,則卸載函數中一般要關閉之;

和__init一樣,__exit也可以使對應函數在運行完成後自動回收內存。實際上,__init和__exit都是宏,其定義分別為:

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

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

數據也可以被定義為__initdata和__exitdata,這兩個宏分別為:

#define __initdata   __attribute__ ((__section__ (".init.data")))
和
#define __exitdata  __attribute__ ((__section__(".exit.data")))

3、內核模塊的參數傳遞

我們可以用“module_param(參數名,參數類型,參數讀/寫權限)”為模塊定義一個參數,例如下列代碼定義了1個整型參數和1個字符指針參數:

static char *book_name = "深入淺出Linux設備驅動";
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);

在裝載內核模塊時,用戶可以向模塊傳遞參數,形式為“insmode(或modprobe)模塊名 參數名=參數值”,如果不傳遞,參數將使用模塊內定義的缺省值。

參數類型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指針)、bool 或invbool(布爾的反),在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較,判斷是否一致。

模塊被加載後,在/sys/module/目錄下將出現以此模塊名命名的目錄。當“參數讀/寫權限”為0時,表示此參數不存在sysfs文件系統下對應的文件節點,如果此模塊存在“參數讀/寫權限”不為0的命令行參數,在此模塊的目錄下還將出現parameters目錄,包含一系列以參數名命名的文件節點,這些文件的權限值就是傳入module_param()的“參數讀/寫權限”,而文件的內容為參數的值。

除此之外,模塊也可以擁有參數數組,形式為“module_param_array(數組名,數組類型,數組長,參數讀/寫權限)”。從2.6.0至2.6.10 版本,須將數組長變量名賦給“數組長”,從2.6.10 版本開始,須將數組長變量的指針賦給“數組長”,當不需要保存實際輸入的數組元素個數時,可以設置“數組長”為NULL。

運行insmod或modprobe命令時,應使用逗號分隔輸入的數組元素。

4、內核模塊的符號導出

模塊可以使用如下宏導出符號到內核符號表:

  • EXPORT_SYMBOL(符號名);
  • EXPORT_SYMBOL_GPL(符號名);

導出的符號將可以被其它模塊使用,使用前聲明一下即可。EXPORT_SYMBOL_GPL()只適用於包含GPL許可權的模塊。下面的代碼給出了一個導出整數加、減運算函數符號的內核模塊的例子(這些導出符號毫無實際意義,僅僅只是為了演示)。

#include <linux/init.h>                                
#include <linux/module.h>                                
MODULE_LICENSE("Dual BSD/GPL");                                

int add_integar(int a,int b)                                
{                                
    return a+b;                             
} 

int sub_integar(int a,int b)                                
{                                
    return a-b;                             
}                            

EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(sub_integar);

5、內核模塊的信息聲明

在Linux內核模塊中,我們可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分別聲明模塊的作者、描述、版本、設備表和別名,例如:

  • MODULE_AUTHOR(author);
  • MODULE_DESCRIPTION(description);
  • MODULE_VERSION(version_string);
  • MODULE_DEVICE_TABLE(table_info);
  • MODULE_ALIAS(alternate_name);

對於USB、PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE。

6、內核模塊的編譯方法

我們可以為代碼清單4.1的模板編寫一個簡單的Makefile:

obj-m := hello.o

並使用如下命令編譯Hello World模塊:

make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules

如果當前處於模塊所在的目錄,則以下命令與上述命令等效:

make –C /usr/src/linux-2.6.15.5 M=$(pwd) modules

C後指定的是Linux內核源代碼的目錄,而M=後指定的是hello.c和Makefile所在的目錄,編譯結果如下:

root@imx6:~$ make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules
make: Entering directory `/usr/src/linux-2.6.15.5'
  CC
  /driver_study/hello.o
/driver_study/hello.c:18:35: warning: no newline at end of file
  Building modules, stage 2.
  MODPOST
  CC      /driver_study/hello.mod.o
  LD
  /driver_study/hello.ko
make: Leaving directory `/usr/src/linux-2.6.15.5'

從中可以看出,編譯過程中,經歷了這樣的步驟:先進入Linux內核所在的目錄,並編譯出hello.o文件,運行MODPOST會生成臨時的hello.mod.c文件,而後根據此文件編譯出hello.mod.o,之後連接hello.o和hello.mod.o文件得到模塊目標文件hello.ko,最後離開Linux內核所在的目錄。

7、實例解析

現在我們定義一個包含2個參數的模塊,並觀察模塊加載時被傳遞參數和不傳遞參數時的輸出。

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static char *book_name = "dissecting Linux Device Driver";
static int num = 4000;

static int book_init(void)
{
  printk(KERN_INFO " book name:%sn",book_name);
  printk(KERN_INFO " book num:%dn",num);
  return 0;
}

static void book_exit(void)
{
  printk(KERN_INFO " Book module exitn ");
}

module_init(book_init);
module_exit(book_exit);
module_param(num, int, S_IRUGO);                                
module_param(book_name, charp, S_IRUGO);

MODULE_AUTHOR("Song Baohua, author@linuxdriver.cn");
MODULE_DESCRIPTION("A simple Module for testing module params");
MODULE_VERSION("V1.0");

對上述模塊運行“insmod book.ko”命令加載,相應輸出都為模塊內的默認值,通過察看“/var/log/messages”日誌文件可以看到內核的輸出:

root@imx6:~$ tail -n 2 /var/log/messages
Jul  2 01:03:10 localhost kernel:  <6> book name:dissecting Linux Device Driver
Jul  2 01:03:10 localhost kernel:  book num:4000

當用戶運行“insmod book.ko book_name=’GoodBook’ num=5000”命令時,輸出的是用戶傳遞的參數:

root@imx6:~$ tail -n 2 /var/log/messages
Jul  2 01:06:21 localhost kernel:  <6> book name:GoodBook
Jul  2 01:06:21 localhost kernel:  book num:5000

三、總結

本文主要講解了Linux內核模塊的概念和基本的編程方法。內核模塊由加載/卸載函數、功能函數以及一系列聲明組成,它可以被傳入參數,也可以導出符號供其它模塊使用。

由於Linux設備驅動以內核模塊的形式而存在,因此,掌握上述內容是編寫任何類型設備驅動的必須。在具體的設備驅動開發中,將驅動編譯為模塊也有很強的工程意義,因為如果將正在開發中的驅動直接編譯入內核,而開發過程中會不斷修改驅動的代碼,則需要不斷的編譯內核並重啟Linux,但是如果編譯為模塊,則只需要rmmod並insmod即可,開發效率為大為提高。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/219235.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-09 10:56
下一篇 2024-12-09 10:56

相關推薦

發表回復

登錄後才能評論