linux設備驅動程序--hello-world

linux字符設備驅動程序--hello_world

基於4.14內核, beagleBone green平臺html

PC端的設備驅動程序

有過電腦使用經驗的人都知道,當咱們將外部硬件設備好比鼠標鍵盤插入到電腦端口(一般是USB口)時,在windows系統右下角會彈出"安裝設備驅動程序"的顯示框,那麼,爲何每一個硬件都須要安裝設備驅動程序才能使用呢?linux

首先,每一個硬件都有相應的功能,鼠標的功能就是將鼠標的位移與點擊狀態轉換成相應的數據,而後將數據傳輸給電腦,而後電腦根據收到的數據移動屏幕上的光標。算法

若是沒有相應的鼠標驅動程序,電腦並不知道鼠標的接口以什麼協議將數據傳輸過來,也不知道怎麼解析相應的數據,因此固然電腦上的光標不會跟隨鼠標的移動而移動,歸根結底,鼠標的移動和電腦上光標的移動是二者間數據同步的結果。shell

同理,打印機也是同樣,電腦將文件數據以某種格式傳遞給打印機,而後經過控制數據控制打印機的運行,打印機驅動程序基本上也是識別接收數據以及對數據的處理,這就是爲何通常外部設備都須要使用一根數據線與主機進行鏈接。編程

MCU中設備驅動程序

在基於MCU的普通嵌入式驅動程序開發中,並不會常常接觸到鼠標、鍵盤、硬盤這一類的設備,多數是一些較爲簡單的傳感器設備、小容量的存儲設備等等,一般數據的傳輸使用的是spi、i2c、串口這一類的串行通訊協議,一般一個設備驅動程序的開發就是這樣的流程:windows

  • 數據傳輸層,通常在MCU上集成相應的硬件控制器,配置寄存器便可
  • 數據處理層,根據收發的數據對數據進行解析,而後控制設備作相應處理。

linux設備驅動程序

在linux系統中,一個硬件設備想要運行一樣須要提供設備驅動程序,底層的原理和MCU中的設備驅動程序同樣:收發數據以及處理數據,只是因爲桌面操做系統的特殊性,設備驅動程序的流程會複雜不少。框架

在這一系列的文章中,我將介紹怎麼去編寫linux設備驅動程序,linux內核支持將設備驅動程序編譯進內核和運行時加載進內核兩種方式,在這一系列文章中,主要討論編譯可加載模塊,通常開發者會將設備驅動程序編譯成可加載模塊,在linux運行時動態加載進內核,以實現對應設備的驅動。函數

linux將內核與用戶分離,驅動模塊運行在內核空間中,而應用程序運行在用戶空間,內核主要對公共且有限的資源進行管理、調度,好比硬件外設資源、內存資源等。工具

當用戶須要使用系統資源時,經過系統調用進入內核,由內核基於某種調度算法對這部分資源進行調度。ui

在用戶空間看來,每一個用戶進程都請求到了相應的資源得以運行。

在內核空間看來,避免用戶程序與有限的硬件資源直接交互,保護了相應資源,且用戶程序之間相互隔離,互不影響,用戶程序的崩潰並不會影響內核空間,保障了系統的穩定運行。

可是,linux設備驅動程序的開發目的是將模塊加載到內核空間中,內核空間的操做向來都是危險的,一旦不當心就極可能致使內核的崩潰,同時咱們須要掌握一些內核調試的技巧。

實現一個內核驅動程序的hello_world

程序實現

話很少說,先來個hello_world吧。
hello_world.c:

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

//指定license版本
MODULE_LICENSE("GPL");              

//設置初始化入口函數
static int __init hello_world_init(void)
{
    printk(KERN_DEBUG "hello world!!!\n");
    return 0;
}

//設置出口函數
static void __exit hello_world_exit(void)
{
    printk(KERN_DEBUG "goodbye world!!!\n");
}

//將上述定義的init()和exit()函數定義爲模塊入口/出口函數
module_init(hello_world_init);
module_exit(hello_world_exit);

上述代碼就是一個設備驅動程序代碼框架,這套框架主要的任務就是將內核模塊中的init函數動態地註冊到系統中並運行,由module_init()和module_exit()來實現,分別對應驅動的加載和卸載。

只是它並不作什麼事,僅僅是打印兩條語句而已,若是要實現某些驅動,咱們就能夠在init函數中進行相應的編程。

編譯

編譯這個程序,咱們都知道,linux下編譯程序通常使用make工具(簡單的程序能夠直接命令行來操做),以及一個Makefile文件,在內核開發中,Makefile並不像應用程序那樣,而是通過了一些封裝,咱們只須要往其中添加須要編譯的目標文件便可:

obj-m+=hello_world.o
all:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean

其中hello_world.o就是目標文件,make工具會根據目標文件自動推導來編譯hello_world.c文件。
編譯:

make

編譯結果會在當前目錄生成hello_world.ko文件,這個文件就是咱們須要的內核模塊文件了。

針對編譯模塊makefile的介紹能夠看看我介紹makefile的博客:
linux可加載模塊makefile
linux 內核makefile總覽

加載

編譯生成了內核文件,接下來就要將其加載到內核中,linux支持動態地添加和刪除模塊,因此咱們能夠直接在系統中進行加載:

sudo insmod hello_world.ko

咱們能夠經過lsmod命令來檢查模塊是否被成功加載:

lsmod | grep "hello_world"

lsmod顯示當前被加載的模塊。

同時,咱們也能夠卸載這個模塊:

sudo rmmod hello_world.ko

一樣咱們也能夠經過lsmod指令來查看模塊是否卸載成功。

可是,在這裏咱們並無看到有任何打印信息的輸出,在程序中咱們使用printk函數來打印信息。

事實上,printk屬於內核函數,它與printf在實現上惟一的區別就是printk能夠經過指定消息等級來區分消息輸出,在在這裏,printk輸出的消息被輸出到/var/log/kern.log文件中,咱們能夠經過另開一個終端來查看內核日誌消息:

tail -f /var/log/kern.log

tail -f表示循環讀取/var/log/kern.log文件中的消息並顯示在當前終端中,這樣咱們就能夠在終端查看內核中printk輸出的消息。

若是你不想從新開一個終端來顯示內核日誌,但願直接顯示在當前終端,你能夠這樣作:

tail -f /var/log/kern.log &

僅僅是將這條指令放在當前進程後臺執行,當前終端關閉時,這個後臺進程也會被關閉。

咱們使用開第二種方式來顯示內核消息的方式來從新加載hello_world.ko模塊:

sudo insmod hello_world.ko

而後查看消息輸出,果真,在終端輸出日誌:

Dec 16 10:01:15 beaglebone kernel: [98355.403532] hello world!!!

而後卸載,一樣,終端中顯示相應的輸出:

Dec 16 10:01:50 beaglebone kernel: [98390.181631] goodbye world!!!

從結果能夠看出,在模塊加載時,執行了hello_world_init()函數,在卸載時執行了hello_world_exit()函數。

hello_world PLUS版本

在上面實現了一個linux內核驅動程序(雖然什麼也沒幹),接下來咱們再來添加一些小功能來豐富這個驅動程序:

  • 添加模塊信息
  • 模塊加載時傳遞參數。

廢話很少說,直接上代碼:
hello_world_PLUS.c:

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

MODULE_AUTHOR("Downey");      //做者信息
MODULE_DESCRIPTION("Linux kernel driver - hello_world PLUS!");  //模塊的描述,可使用modinfo xxx.ko指令來查看
MODULE_VERSION("0.1");             //模塊版本號
//指定license版本
MODULE_LICENSE("GPL");              

static char *name = "world";
module_param(name,charp,S_IRUGO);                                 //設置加載時可傳入的參數
MODULE_PARM_DESC(name,"name,type: char *,permission: S_IRUGO");        //參數描述信息

//設置初始化入口函數
static int __init hello_world_init(void)
{
    printk(KERN_DEBUG "hello %s!!!\n",name);
    return 0;
}

//設置出口函數
static void __exit hello_world_exit(void)
{
    printk(KERN_DEBUG "goodbye %s!!!\n",name);
}

//將上述定義的init()和exit()函數定義爲模塊入口/出口函數
module_init(hello_world_init);
module_exit(hello_world_exit);

與上一版本的區別

添加了MODULE_AUTHOR(),MODULE_DESCRIPTION(),MODULE_VERSION()等模塊信息

添加了module_param()傳入參數功能

編譯

編譯以前須要修改Makefile,將hello_world.o修改成hello_world_PLUS.o。

加載

在上述程序中咱們添加了module_param這一選項,module_param支持三個參數:變量名,類型,以及訪問權限,咱們能夠先試一試傳入參數:

sudo insmod hello_world_PLUS.ko Downey

查看日誌輸出,顯示:

Dec 16 10:07:38 beaglebone kernel: [98738.153909] hello Downey!!!

看到模塊中name變量被賦值爲Downey,代表參數傳入成功。
而後卸載:

sudo insmod hello_world_PLUS

日誌輸出:

Dec 16 10:08:12 beaglebone kernel: [98772.191127] goodbye Downey!!!

傳入多個參數

上面講解了傳入一個參數時的示例,若是傳入多個參數呢?該怎麼修改,這個就留給你們去嘗試了。

添加的模塊信息

在hello_world_PLUS中,咱們添加了一些模塊信息,可使用modinfo來查看:

modinfo hello_world_PLUS.ko

輸出:

filename:       /home/debian/linux_driver_repo/hello_world_PLUS/hello_world_PLUS.ko
license:        GPL
version:        0.1
description:    Linux kernel driver - hello_world PLUS!
author:         Downey
srcversion:     549C47CB670506CE16F56D8
depends:
name:           hello_world_PLUS
vermagic:       4.14.71-ti-r80 SMP preempt mod_unload modversions ARMv7 p2v8
parm:           name:type: char *,permission: S_IRUGO (charp)

sysfs

sysfs是一個文件系統,可是它並不存在於非易失性存儲器上(也就是咱們常說的硬盤、flash等掉電不丟失數據的存儲器),而是由linux系統構建在內存中,簡單來講這個文件系統將內核驅動信息展示給用戶。

當咱們裝載hello_world_PLUS.ko時,會在/sys/module/目錄下生成一個與模塊同名的目錄即hello_world_PLUS,目錄裏囊括了驅動程序的大部分信息,查看目錄:

ls /sys/module/hello_world_PLUS

輸出:

coresize  initsize   notes       refcnt    srcversion  uevent
holders   initstate  parameters  sections  taint       version

這一部分的知識僅僅是在這裏引出提一下,創建個映象,在這裏就再也不贅述,若是想進一步瞭解能夠參考博主的另外一篇博客linux設備驅動程序--sysfs

好了,關於linux驅動程序-hello_world就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索