linux字符設備驅動程序--hello_world
基於4.14內核, beagleBone green平台
PC端的設備驅動程序
有過電腦使用經驗的人都知道,當我們將外部硬件設備比如鼠標鍵盤插入到電腦端口(通常是USB口)時,在windows系統右下角會彈出"安裝設備驅動程序"的顯示框,那么,為什么每個硬件都需要安裝設備驅動程序才能使用呢?
首先,每個硬件都有相應的功能,鼠標的功能就是將鼠標的位移與點擊狀態轉換成相應的數據,然后將數據傳輸給電腦,然后電腦根據收到的數據移動屏幕上的光標。
如果沒有相應的鼠標驅動程序,電腦並不知道鼠標的接口以什么協議將數據傳輸過來,也不知道怎么解析相應的數據,所以當然電腦上的光標不會跟隨鼠標的移動而移動,歸根結底,鼠標的移動和電腦上光標的移動是兩者間數據同步的結果。
同理,打印機也是一樣,電腦將文件數據以某種格式傳遞給打印機,然后通過控制數據控制打印機的運行,打印機驅動程序基本上也是識別接收數據以及對數據的處理,這就是為什么一般外部設備都需要使用一根數據線與主機進行連接。
MCU中設備驅動程序
在基於MCU的普通嵌入式驅動程序開發中,並不會經常接觸到鼠標、鍵盤、硬盤這一類的設備,多數是一些較為簡單的傳感器設備、小容量的存儲設備等等,通常數據的傳輸使用的是spi、i2c、串口這一類的串行通信協議,通常一個設備驅動程序的開發就是這樣的流程:
- 數據傳輸層,一般在MCU上集成相應的硬件控制器,配置寄存器即可
- 數據處理層,根據收發的數據對數據進行解析,然后控制設備做相應處理。
linux設備驅動程序
在linux系統中,一個硬件設備想要運行同樣需要提供設備驅動程序,底層的原理和MCU中的設備驅動程序一樣:收發數據以及處理數據,只是由於桌面操作系統的特殊性,設備驅動程序的流程會復雜很多。
在這一系列的文章中,我將介紹怎么去編寫linux設備驅動程序,linux內核支持將設備驅動程序編譯進內核和運行時加載進內核兩種方式,在這一系列文章中,主要討論編譯可加載模塊,一般開發者會將設備驅動程序編譯成可加載模塊,在linux運行時動態加載進內核,以實現對應設備的驅動。
linux將內核與用戶分離,驅動模塊運行在內核空間中,而應用程序運行在用戶空間,內核主要對公共且有限的資源進行管理、調度,比如硬件外設資源、內存資源等。
當用戶需要使用系統資源時,通過系統調用進入內核,由內核基於某種調度算法對這部分資源進行調度。
在用戶空間看來,每個用戶進程都請求到了相應的資源得以運行。
在內核空間看來,避免用戶程序與有限的硬件資源直接交互,保護了相應資源,且用戶程序之間相互隔離,互不影響,用戶程序的崩潰並不會影響內核空間,保障了系統的穩定運行。
但是,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不沾身.