ch8: 裝置驅動程式的基本知識

小弟不是電類的,若以下筆記有錯誤歡迎任何指教,謝謝。

環境

ubuntuDistributor ID:	Ubuntu
Description:	Ubuntu 16.04.1 LTS
Release:	16.04
Codename:	xenial
kernel version:4.9.0-999-generic

驅動裝置的基本概念

裝置驅動程式是為了隱藏硬體的複雜性和多變性,幫助使用者能夠簡單的呼叫函式即能完成複雜的工作,並和使用者的程式隔離,並免直接存取核心資料結構駭硬體裝置。

在本章裝置驅動程式(device driver)、可動態載入模組(loadable module)、可動態載入核心模組(loadable kernel device,LKM)、模組(module),通通都是指核心裝置驅動模組(kernel device driver module),因為好像沒有統一的術語。

可動態載入模組

linux提供界面方便使用者在開機後動態的移除或加入模組到核心中,當核心完成啟動後,開始安裝動態組(loadable module),或是透過script安裝模組,當模組需要使用時,才要求載入模組,那麼動態載入模組有什麼好處呢?

  1. 可以任意將組插入或移除正在運行的裝置
  2. 可以直接更新模組,不須重新啟動
  3. 嵌入式裝置的容量有限,可以將模組放在其他除存裝置中

當然我們也可以透過重新編譯核心將驅動裝置直接編譯進核心,或是透過Chapter 6 : User Space Initialization將需要的模組和載入模組的script放進Initial RAM Disk這樣就可以直接在開機時啟動模組。

裝置驅動程式架構

簡略分為兩類 |裝置|字元裝置(character devices)|區塊裝置(block devices)| |:-------|:-------------------------|:--------------------| |讀寫方式|serial|以block為單位,進行讀寫| |舉例|keyboard,arduino|硬碟,隨身碟|

裝置驅動程式範例

project tree: Uploading file..._ronik77fp

hello.c

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

static int __init hello_init(void) //初始化
{
    printk(KERN_INFO "Hello world!");
    return 0;
}

static void __exit hello_exit(void) //還原初始化
{
    printk(KERN_INFO "bye bye world");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("splasky");
MODULE_DESCRIPTION("Hello world example!");
MODULE_LICENSE("GPL");


接下來我們要將寫好的模組加入到kernel中,步驟如下

  1. 下載linux kernel source code
wget wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.8.13.tar.xz
tar Jxvf linux-4.8.13
cd linux 4.8.13
  1. 進入.../driver/char 並建立hello directory,這個資料夾就是我們等等要將我們寫的模組source code放置的位置
cd drivers/char
mkdir hello
cp hello.c .../drivers/char/hello
  1. 在hello中新增編譯hello模組的makefile
splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ cat drivers/char/hello/Makefile 
obj-$(CONFIG_HELLO) +=hello.o
  1. 在核心組態設定檔中加入選單表示hello模組,可以指定是否內建或是可動態載入核心模組
splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ cat drivers/char/Kconfig |head -n 20
#
# Character device configuration
#

menu "Character devices"

source "drivers/tty/Kconfig"

config HELLO
    tristate "Enable Hello"
	default m
	help
      Example Hello module

修改完後就可以在menuconfig看見核心組態檔(Kconfig)的編譯選項

  1. 修改.../drivers/char/Makefile,決定建構時要不要包含此目錄
splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ cat drivers/char/Makefile 
#
# Makefile for the kernel character device drivers.
#

obj-y				+= mem.o random.o
...
obj-$(CONFIG_HELLO) 		+= hello/

到這邊修改完後我們就可以開始建構驅動程式模組了!

建構與安裝驅動程式模組

splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
*** Default configuration is based on 'multi_v7_defconfig'
#
# configuration written to .config
#
splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- modules
...compiling...
splasky@splasky-XPS13→ [~/workspace/linux-4.8.13] $ ls drivers/char/hello/
hello.c  hello.ko  hello.mod.c  hello.mod.o  hello.o  Makefile  modules.order  Module.symvers

hello.ko模組就是我們的模組

不知道有沒有其他方法可以直接上傳模組到qemu虛擬機裡,或是直接透過qemu掛載模組,小弟這邊是透過NFS掛載於qemu,想知道一般大家都是怎麼做的? 如何掛載NFS可以參考這裡

載入模組

接下來我們嘗試在qemu上掛載我們編譯好的hello.ko

首先先更改console的級別才能將log的訊息列印到console上

$ cat /proc/sys/kernel/printk
4 4 1 7
$ cat /proc/sys/kernel/printk
7	4	1	7 

更詳習的規則可以參考這裡

接著掛載module

$ cp mnt/hello.ko /lib/modules/4.8.13/kernel/
$ depmod
$ modprobe hello
Hello world!$ 

就可以發現init的message列印於當前console下 移除已掛載的module也很簡單

modprobe -r hello

模組參數

現在讓我們為模組增加參數,讓其可以透過insmod輸入模組參數

splasky@school->[~/workspace/module] (master) 166h10m $ cat src/hello2.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int debug_enable = 0; //宣告靜態變數debug_enable
module_param(debug_enable, int, 0);
MODULE_PARM_DESC(debug_enable, "Enable module debug mode.");

static int __init hello_init(void)
{
    printk(KERN_INFO "debug mode is %s\n", debug_enable ? "enable" : "disable");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "bye bye world\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("splasky");
MODULE_DESCRIPTION("Hello world example!");
MODULE_LICENSE("GPL");

module_param定義於.../include/linux/moduleparam.h中,由modules.h引入,作用是將模組參數向核心模組子系統做註冊 MODULE_PARM_DESC向核心註冊參數說明的字串 接著讓我們看看如何在載入模組時使用引數

splasky@school->[~/workspace/module] (master) 166h26m $ sudo insmod src/hello2.ko debug_enable=1
splasky@school->[~/workspace/module] (master) 166h27m $ dmesg |tail -n 2
[451967.837106] bye bye world
[451972.059004] debug mode is enable

也可以直接引入

splasky@school->[~/workspace/module] (master) 166h28m $ sudo insmod src/hello2.ko
splasky@school->[~/workspace/module] (master) 166h28m $ dmesg |tail -n 2
[452079.503340] bye bye world
[452085.328312] debug mode is disable

模組工具

模組工具在前一小節已經介紹過,如果你想要更詳細的說明請參考manual

insmod

insmod 接收一個模組的==絕對路徑==,並將模組載入核心

sudo insmod src/hello2.ko

lsmod

將載入到核新的模組清單印出

splasky@school->[~/workspace/module] (master) 166h28m $ sudo lsmod
[sudo] password for splasky: 
Module                  Size  Used by
hello2                 16384  0
cdc_acm                36864  0
btrfs                 987136  0
ufs                    73728  0
gf128mul               16384  1 lrw
...

最右邊的欄位Uesd by表示該模組正在使用中,並列出相依性

modprobe

modprobe可以幫我們解決模組間相依的問題,舉例來說上例的gf128mul需要lrw模組才能使用,modprobe能幫我解決這個問題,並以正確的順序載入。

modprobe gf128mul

會將lrw與gf128mul都載入

modprobe -r gf128mul

則會將兩個模組都移除 :::info 使用modeprobe載入module時要先將module複製至/lib/modules/$(uname -r)/kernel 下 :::

depmod

modprobe必須依賴modules.dep來知道模組間的相依關係,而depmod負責為我們產生這個檔案,這個檔案內含一份核心建構系統有啟用的模組清單。 這個檔案放至於/lib/modules/4.4.0-($uname -r)/modules.dep 下

cat /lib/modules/4.4.0-51-generic/modules.dep

格式為 [模組]:[關聯模組]

rmmod

移除核心中的模組,不需要路徑,不會移除相依的模組

rmmod hello2

modinfo

檢視module的詳細資訊

splasky@school->[~/workspace/module] (master) 167h38m $ modinfo hello2 
filename:       /lib/modules/4.4.0-51-generic/kernel/hello2.ko
license:        GPL
description:    Hello world example!
author:         splasky
srcversion:     CE729C455AA625EF69A6DDB
depends:        
vermagic:       4.4.0-51-generic SMP mod_unload modversions 
parm:           debug_enable:Enable module debug mode. (int)

與驅動程式溝通

接下來我們將介紹如何由module提供介面函式給user space application

驅動程式的檔案系統操作函式

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

#define DEVICE_NAME "hello3"
static int debug_enable = 0;
module_param(debug_enable, int, 0);
MODULE_PARM_DESC(debug_enable, "Enable module debug mode.");

struct file_operations hello_fops;
static int hello_open(struct inode *inode, struct file *file)
{
    printk("Hello open: successful\n");
    return 0;
}

static int hello_release(struct inode *inode, struct file *file)
{
    printk("Hello release: successful\n");
    return 0;
}

static ssize_t hello_read(struct file *file, char *buf, size_t count, loff_t *ptr)
{
    printk("Hello read: returning zero bytes\n");
    return 0;
}

static ssize_t hello_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
    printk("Hello write: accepting zero bytes\n");
    return 0;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk("Hello ioctl:cmd=%ud, arg=%lu\n", cmd, arg);
    return 0;
}

static int __init hello_init(void)
{
    int ret=0;
    printk(KERN_INFO "debug mode is %s\n", debug_enable ? "enable" : "disable");

    ret = register_chrdev(234, DEVICE_NAME, &hello_fops);

    if (ret < 0)
    {
        printk(KERN_ALERT"ERROR registering hello device\n");
        goto hello_fail;
    }

    printk(KERN_INFO "Hello module registered successfully!\n");

    /* init process here */


    return 0;
hello_fail:
    return ret;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello exit\n");
}

/* 透過建立一個file operation 來和character裝製做connect */
struct file_operations hello_fops =
{
owner:
    THIS_MODULE,
read:
    hello_read,
write:
    hello_write,
unlocked_ioctl:
    hello_ioctl,
open:
    hello_open,
release:
    hello_release,
};

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("splasky");
MODULE_DESCRIPTION("Hello world example!");
MODULE_LICENSE("GPL");

裝置節點與mknod

裝置節點是一種特殊的檔案類型,linux用這種檔案來代表裝置,幾乎所有linux distribution都將裝置節點放在/dev下,我們會透過mknod產生裝置節點。

mknod /dev/hello3 c 234 0

mknod [裝置節點位置] [檔案類型] [主編號] [次編號] 我們透過裝置節點來跟已安裝的驅動程式做銜接,當程式透過open系統呼叫時,核心會透過主編號(此範例中為234)將驅動程式和裝置節點做連接,作業系統並不理會次編號,會把次編號直接傳給驅動裝置,讓一個驅動程式可以處理多個子裝置。 :::info 裝置節點一般是透過udev產生,後面章節會介紹到,另外本章的範例是透過手動直接把裝置主編號直接指定在source code中,這樣做並不好,應該透過kernel指定一個給驅動程式,詳細作法Linux Device Drivers的第三章有說明到。 :::

還要再研究

整合為一體

現在我們創立一個use-hello3.c 來運用我們寫的驅動程式

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    /* our file descriptor */
    int fd=0,rc=0;
    
    char *rd_buf[16];
    printf("%s: entered\n",argv[0]);

    /* open device */
    fd = open("/dev/hello3",O_RDWR);
    if(fd==-1){
        perror("open failed");
        rc=fd;
        exit(-1);
    }

    printf("open successful!\n");

    /* Issue a read */
    rc=read(fd,rd_buf,0);
    if(rc==-1){
        perror("read failed");
        close(fd);
        exit(-1);
    }
    printf("read:returning %d bytes!\n",rc);
    close(fd);
    return 0;
}

最後執行我們的use-hello

splasky@splasky-XPS13→ [~/workspace/kernel_module/module] (master) 237h25m $ ls -l /dev/hello*
ls: cannot access '/dev/hello*': No such file or directory
splasky@splasky-XPS13→ [~/workspace/kernel_module/module] (master) 237h26m $ sudo mknod /dev/hello3 c 234 0
[sudo] password for splasky: 
splasky@splasky-XPS13→ [~/workspace/kernel_module/module] (master) 237h26m $ sudo insmod src/hello3.ko 
splasky@splasky-XPS13→ [~/workspace/kernel_module/module] (master) 237h27m $ sudo ./use-hello 
./use-hello: entered
open successful!
read:returning 0 bytes!
splasky@splasky-XPS13→ [~/workspace/kernel_module/module] (master) 237h29m $ dmesg | tail -n 8
[  193.437511] hello3: loading out-of-tree module taints kernel.
[  193.437724] hello3: module verification failed: signature and/or required key missing - tainting kernel
[  193.438665] debug mode is disable
[  193.438678] Hello module registered successfully!
[  204.570530] Hello open: successful
[  204.570534] Hello read: returning zero bytes
[  204.570537] Hello release: successful
[  300.054926] mce: [Hardware Error]: Machine check events logged

GPL

https://www.gnu.org/licenses/quick-guide-gplv3.html

作者推薦的參考書目

Linux Device Drivers:臺灣:Linux 驅動程式, 3/e (Linux Device Drivers, 3/e) Essential Linux Device Drivers有中文版 Filesystem Hierarchy Standard

參考資料

Loda's blog The Linux Kernel Module Programming Guide 開機流程、模組管理與 Loader

本參考資料中也含有模組工具的使用

Linux Modules(1.1)module parameters


书籍推荐