Linux核心模組學習

zhengcixi發表於2022-01-09

注:本文是《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

相關文章