Linux內核模組學習

註:本文是《Linux設備驅動開發詳解:基於最新的Linux 4.0內核 by 宋寶華 》一書學習的筆記,大部分內容為書籍中的內容。

書籍可直接在微信讀書中查看:Linux設備驅動開發詳解:基於最新的Linux4.0內核-宋寶華-微信讀書 (qq.com)

1 簡介

模組(Module)具有以下特點:

  • 模組本身不編譯進內核映像
  • 內核載入之後,和其它內核中的部分完全一樣。

一個簡單的示例:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world enter\n");
    return 0;
}
module_init(hello_init);  //內核載入函數

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello world exit\n");
}
module_exit(hello_exit);  //內核卸載函數

MODULE_AUTHOR("Test Hello");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("A simple module");

Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

編譯,並且插入ko:

$ make 
$ sudo insmod hello.ko 

插入ko時,可能沒有列印資訊,可以使用dmesg命令查看:

可以通過lsmod命令查看當前系統插入了哪些ko,lsmod命令查看的結果對應/proc/modules文件,內核中已經載入模組資訊也存在於/sys/module目錄中:

$ lsmod | grep "hello"
Module                  Size  Used by
hello                  12425  0 
$ cat /proc/modules | grep "hello"
hello 12425 0 - Live 0xffffffffc06fc000 (OE)
$ ls /sys/module/hello/ 
coresize  initsize   notes/  rhelversion  srcversion  uevent
holders/  initstate  refcnt  sections/    taint

modinfo命令可以獲得模組的資訊,包括模組作者、模組的說明、模組所支援的參數以及vermagic:

$ modinfo hello.ko 
filename:       /home/grace/driver_study/code/modules/hello.ko
alias:          A simple module
description:    A simple Hello World Module
license:        GPL
author:         Test Hello
rhelversion:    7.4
srcversion:     E4D5379B55084D8ED2D94E8
depends:        
vermagic:       3.10.0-693.el7.x86_64 SMP mod_unload modversions 

卸載模組命令rmmod:

$ rmmod hello

2 模組程式結構

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

(1)模組載入函數

通過insmod或者modprobe命令載入模組時,模組的載入函數會自動被內核執行,完成模組的初始化工作。

(2)模組卸載函數

通過rmmod命令卸載模組時,模組的卸載函數會自動被內核執行,完成模組相關的卸載功能。

(3)模組許可證聲明

許可證(LICENSE)聲明描述了內核模組的許可許可權,如果不申明LICENSE,模組載入時會收到內核被污染(Kernel Tainted)的警告。

[10688.585888] hello: module license 'unspecified' taints kernel.

在Linux內核模組中可接受的LICENSE包括:GPL、GPL v2等。大多數情況下,內核模組應遵循GPL兼容許可證。

(4)模組參數(可選)

模組參數是模組被載入時可以傳遞給它的值,它本身對應模組內部的全局變數。

(5)模組導出符號(可選)

內核模組可以導出的符號(symbol,對應於函數或變數),若導出,其它模組可以使用本模組的變數或函數。

(6)模組作者等資訊聲明(可選)

3 模組載入函數

Linux模組載入函數一般以__init標識聲明,典型的模組載入函數的形式如下:

static int __init init_func(void)
{
	/* 初始化程式碼 */
}
module_init(init_func);

模組載入函數module_init(函數名)的方式指定,返回整型值,若初始化成功,返回0;初始化失敗,返回錯誤編碼。

Linux內核中,錯誤碼是一個接近0的負值,定義在<lnux/errno.h>中。

Linux內核中,可以使用request_module(const char *fmt, …)函數載入內核模組,使用方式:

request_module(module_name);

初始化數據可以定義為__initdata,對於只在初始化階段所需要的數據,內核在初始化完成之後會釋放它們佔用的記憶體。

static int hello_data __initdata = 1;
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world enter %d\n", hello_data);
    return 0;
}
module_init(hello_init);  //內核載入函數

4 模組卸載函數

模組卸載函數一般以__exit標識聲明,常見的用法如下:

static void __exit clean_func(void)
{
    /* 釋放程式碼 */
}
module_exit(clean_func);  //內核卸載函數

模組卸載函數在模組卸載的時候執行,不返回任何值,且必須以module_exit(函數名)的方式使用。

5 模組參數

可使用“module_param(參數名, 參數類型, 參數讀/寫許可權)”為模組定義一個參數。

在載入模組時,用戶可以向模組傳遞參數,形式為:

insmod 模組名 參數名=參數值

如果不傳遞,參數使用模組定義的預設值。如果模組被內置無法insmod,在bootloader中可以通過bootargs設置”模組名.參數名=值”的形式給內核的模組傳遞參數。

參數的類型可以是:

byte short ushort int uint long ulong charp(字元指針) bool invbool(布爾的反)

在模組編譯時會將module_param中的聲明類型和變數定義的類型進行比較,判斷是否一致。

模組也可以有參數數組,形式為:

module_param_array(數組名, 數組類型, 數組長, 參數讀/寫許可權)

模組參數舉例:

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

static char *book_name = "dissection Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 400;
module_param(book_num, int, S_IRUGO);

static int __init hello_init(void)
{
    printk(KERN_INFO "book name:%s\n", book_name);
    printk(KERN_INFO "book  num:%d\n", book_num);

    return 0;
}
module_init(hello_init);  //內核載入函數

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello world exit\n");
}
module_exit(hello_exit);  //內核卸載函數

MODULE_AUTHOR("Test Hello");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("A simple module");

編譯並且載入不帶參數,可以看出輸出的是默認的參數值:

$ make
$ insmod para.ko 
$ dmesg 
[13186.787598] book name:dissection Linux Device Driver
[13186.787601] book  num:400

載入時帶參數,列印的是輸入的參數:

$ insmod para.ko book_name="LDD3" book_num=500 
$ dmesg
[13423.036831] book name:LDD3
[13423.036835] book  num:500

在/sys/module/模組名/parameters目錄下也可以看到模組的參數:

$ ls /sys/module/para/parameters/
book_name  book_num
$ cat /sys/module/para/parameters/book_num 
500
$ cat /sys/module/para/parameters/book_name 
LDD3

6 導出符號

Linux下內核符號表在/proc/kallsyms下,它記錄了符號以及符號所在的記憶體地址:

$ more /proc/kallsyms
ffffffff8109f320 T sys_kill
ffffffff8109f330 T SyS_tgkill

模組可以使用以下方式導出符號到符號表中,導出的符號可以被其它模組使用,使用前需要進行聲明:

EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);  //只適用於包含GPL許可權的模組

測試用例:

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

int add_integer(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL_GPL(add_integer);

int sub_integer(int a, int b)
{
    return a - b;
}
EXPORT_SYMBOL_GPL(sub_integer);

MODULE_LICENSE("GPL v2");

載入並且查看相關資訊:

$ insmod export_symb.ko 
ffffffffc0701000 t add_integer  [export_symb]
ffffffffc0701010 t sub_integer  [export_symb]

7 模組聲明與描述

Linux內核模組中,使用以下函數進行聲明:

MODULE_AUTHOR  //作者
MODULE_DESCRIPTION //描述
MODULE_VERSION  //版本
MODULE_DEVICE_TABLE  //設備表,對於USB、PCI驅動,表明該驅動模組所支援的設備
MODULE_ALIAS //別名

8 模組的使用計數

Linux2.6之後的模組計數管理介面為:try_module_get(&module)和module_put(&module)。模組的使用計數不用模組自己管理。

/* 用於增加模組使用計數;返回為0,表示調用失敗,希望使用的模組沒有載入或正在卸載 */
int try_module_get(struct module *module);

/* 用於減少模組的使用計數 */
void module_put(struct module *module);

9 模組的編譯

簡單的Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0  #是否使用調試資訊

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

該makefile和源碼hello.c在同一個目錄,運行make命令得到模組hello.ko。

如果需要包括多個.c文件,需要更改:

obj-m += modulename.o
modulename-objs := file1.o file2.o