驅動程序:使硬件工作的軟件。
linux驅動程序的分類:
◎字符設備驅動(重點)
◎網絡接口驅動(重點)
◎塊設備驅動
1)字符設備:
字符設備是一種按字節來訪問的設備,字符驅動則負責驅動字符設備,這樣的驅動通
常實現open,close,read和write系統調用。
2)塊設備:
◎在大部分unix系統,塊設備不能按字節處理數據,只能一次傳送一個或多個長度是
512字節(或一個更大的2次冪的數)的整塊數據。
◎而Linux則允許塊設備傳送任意數目的字節。
因此塊和字符設備的區別:僅僅是驅動的接口函數與內核的接口函數不同。
3)網絡接口:
任何網絡事物都通過一個接口來進行,一個接口通常是一個硬件(eth0),但它也可以
是一個純粹的軟件設備,比如迴環接口(lo)。一個網絡接口負責發送和接收數據報文。
字符設備與塊設備:隨機訪問?塊設備隨機訪問,字符設備必須按順序訪問
驅動程序安裝方式:有兩種
◎ 模塊方式(已知)(insmod, rmmod)
◎直接編譯進內核
如何直接把驅動程序(內核模塊)編譯進內核?
需要修改兩個文件:Kconfig,Makefile(Kconfig用來產生配置菜單,到處有Kconfig):
第一步:首先把驅動程序或內核模塊源文件(hello.c)放到內核源代碼相應的目錄下(根據
功能選擇目錄)(如drivers/char下)
第二步:修改Kconfig,修改所放源文件目錄下的Kconfig,在內核源代碼的頂層目錄執行:
vi drivers/char/Kconfig(打開Kconfig),然後在其中加上如下兩行代碼(照着寫):
config HELLO_WORLD
Bool “helloworld”
第三步:通過make menuconfig ARCH=arm進入配置菜單,選中剛添加(要編譯進內核)
的項。配置結果體現在(.config)中,.config文件位於內核源代碼頂層目錄下,通
過 vi .config可以查看。可以看到:CONFIG_HELLO_WORLD=y
第四步:修改Makefile,修改所放源文件目錄下的Makefile。照着寫
obj-$(CONFIG_HELLO_WORLD) += hello.o (加上這一項,hello.c)
第五步:編譯內核。修改好後,回到內核源代碼頂層目錄執行如下命令編譯內核。:
male uImage ARCH=armCROSS_COMPILE=arm-linux-
***************************** hello.c代碼如下:**********************************
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init()
{
printk("hello world!\n");
return 0;
}
static void __exit hello_exit()
{
printk(KERN_EMERG "hello exit!\n");
}
module_init(hello_init);
module_exit(hello_exit);
*****************************************************************************
編譯完成啓動內核的過程中,會打印出:Hello World!信息。因爲執行了模塊初始化函數。
__init標誌表示此函數將被放置到初始化代碼段;內核在啓動時,會依次調用初始化代碼段中的函數指針。__exit類似。
驅動程序使用:
linux用戶程序通過設備文件(也稱:設備節點)來使用驅動程序操作字符設備和塊設備(根據*fp找到在內核裏面對應的sturct file結構,從而找到相應的read,write函數)
網絡設備沒有設備文件,設備文件在dev目錄下面。
1、主次設備號
字符設備通過字符設備文件來存取。字符設備文件由使用ls –l命令後輸出的第一列的“c”標識。在dev目錄下使用ls –l命令可以看到設備文件項中有2個數字,由逗號分隔,這些數字就是設備文件的主次設備編號。前主後次。
1.1設備號的作用
思考:字符設備文件與字符設備驅動如何建立聯繫?——通過主設備號
設備文件所對應的主設備號和驅動程序所對應的主設備號相同的話,那麼這個驅動程序就對應這個設備文件。(設備文件的主設備號通過創建設定,驅動程序通過申請獲得)
主設備號:用來標識與設備文件相連的驅動程序。
次設備號:被驅動程序用來辨別操作的是哪個設備。
總結:主設備號用來反映設備類型;次設備號用來區分同類型的設備。
主次設備號的描述:內核描述
內核中通過類型dev_t來描述設備號,其實質是unsigned int 32位整數,其中高12位爲主設備號,低20位爲次設備號。
◎分離出主設備號MAJOR(dev_t dev);
◎分離出次設備號MINOR(dev_t dev)
◎定義主次設備號dev_t devno = MKDEV(mem_major, mem_minor)
1.2分配主設備號
Linux內核通過靜態申請和動態分配兩種方法來給設備分配主設備號。
1.2.1靜態申請(簡單但易導致衝突)
方法如下:
1、根據Documentation/deices.txt,確定一個沒有使用的主設備號;
2、使用register_chrdev_region函數註冊設備號。
優點:簡單,
缺點:一旦驅動程序被廣泛使用,這個隨機選定的主設備號可能會導致設備號衝突,而使驅
動程序無法註冊。
原型:int register_chrdev_region(dev_t from,unsigned count,const char* name)
功能:申請使用從from開始的count個設備號(主設備號不變,次設備號增加)
參數:from:希望申請使用的設備號
count:希望申請使用的設備號數目
name:設備名(體現在 /proc/devices)
1.2.2動態分配(簡單,但無法在安裝驅動前創建設備文件,因爲安裝前還沒有分配到主設備號)
方法如下:
使用 alloc_chrdev_region 分配設備號
原型:int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
功能:請求內核動態分配count個設備號,且次設備號從baseminor開始。
參數:dev:分配到的設備號位於dev指針所指向的內存中。,不需要填值,用於獲取值
baseminor:起始次設備號
count:需要分配的設備號數目
name:設備名(體現在 /proc/devices)
優點:簡單,易於驅動推廣
缺點:無法在安裝驅動前創建設備文件(因爲安裝前還沒有分配到主設備號)
解決辦法:安裝驅動後,從/proc/devices中查詢設備號,然後再創建設備文件。
1.3註銷設備號(不用時應該釋放這些設備號)
原型:void unregister_chrdev_region(dev_t from,unsigned count)
功能:釋放從from開始的count個設備號
2、創建設備文件——2種方法:
2.1、使用mknod命令手工創建
用法:mknod filename type major minor
參數:filename:設備文件名
type:設備文件類型“c”,“b”
major:主設備號
minor:次設備號
例如:mknod serial0 c 100 0 //設備文件的主次設備號是確定的,一個設備文件只能對應一個
設備。要操作哪個設備,首先要創建對應主次設備號的設備文件,再操作此設備文件。
2.2、自動創建——後面課程介紹
3、重要結構
在Linux字符設備驅動程序設計中,有三種非常重要的內核數據結構:
3.1、struct file:代表一個打開的文件。系統中每個打開的文件在內核空間都有一個關聯的struct file。它由內核在打開文件時創建,在文件關閉後釋放。(每打開一次創建一個)
重要成員:
loff_t f_pos /*文件讀寫位置*/
struct file_operations *f_op
3.2、struct inode:用來記錄文件的物理上的信息(如存放位置、設備號等)。因此它和代表打開文件的file結構是不同的,一個文件可以對應多個file結構,但只有一個inode結構。
重要成員:
dev_t i_rdev:設備號 //inode代表設備文件(設備節點)?
3.3、struct file_operations:一個函數指針的集合(更像一個轉化表),定義能在設備上進行的操作。結構中的成員指向驅動中的函數,這些函數實現一個特殊的操作,對於不支持的操作保留爲NULL。
struct file_operations mem_fops = {
.owner = THIS_MODULE,
.llseek = mem_seek,
.read = mem_read,
.write = mem_write,
.ioctl = mem_ioctl,
.open = mem_open,
.release = mem_release,
};
思考:應用程序如何訪問驅動程序?
解析:當應用程序執行read系統調用,對設備文件進行讀的時候,驅動程序就會做出mem_read函數調用。當應用程序執行write系統調用,對設備文件進行寫的時候,驅動程序就會做出mem_write函數調用。把應用程序中對文件的操作轉化成驅動程序中相應的函數。(內核根據應用程序系統調用中傳遞的*fp指針找到在內核裏面對應的sturct file結構,從而找到驅動程序中相應的read,write函數)
4、設備註冊
在linux2.6內核中,字符設備使用struct cdev結構來描述。
字符設備的註冊分爲如下三個步驟:
4.1、分配cdev,分配空間:分配是對於指針而言,靜態的不需要分配
struct cdev的分配可使用cedv_alloc函數來完成。
原型:struct cdev *cdev_alloc(void)。分配完成後返回分配到的struct cdev函數指針
注意:如果cdev結構被定義爲靜態的,則不需要執行空間分配。
4.2、初始化cdev結構
struct cdev的初始化可使用cedv_init函數來完成。
原型:void cdev_init(struct cedv *cdev,const struct file_operations *fops)
參數:cdev:待初始化的cdev結構
fops:設備對應的操作函數集
4.3、添加cdev(即註冊字符設備驅動)
struct cdev的註冊可使用cedv_add函數來完成。
原型:int cdev_add(struct cdev *p,dev_t dev,unsigned count)
參數:p:待添加到內核的字符設備結構,要註冊的字符設備
dev:設備號,驅動程序對應的主設備號
count:添加的設備個數
5、設備操作(註冊完之後要實現struct file_operations結構中相關的設備操作)
◎1、int (*open)(struct inode *,struct file *) 對應open方法
在設備文件上的第一操作,可以不實現此方法,沒有(即該項爲NULL)時則認爲永
遠打開成功。
◎2、void (*release)(struct inode *,struct file *) 對應close方法
當設備文件被關閉時調用這個操作。release也可以沒有。
◎3、ssize_t(*read)(struct file *,char __user *,size_t,loff_t *)
從設備中讀取數據
◎4、ssize_t(*write)(struct file *,const char __user *,size_t,loff_t *)
向設備發送數據
◎5、unsigned int (*poll)(struct file *,struct poll_table_struct *)
對應select系統調用
◎6、int (*ioctl)(struct inode *,struct file *,unsigned int,unsigned long)
控制設備
◎7、int (*mmap)(struct file *,struct vm_area_struct *)
將設備映射到進程虛擬地址空間中。
◎8、off_t(*llseek)(struct file *,loff_t,int)
修改文件的當前讀寫位置,並將新位置作爲返回值。
open方法:
open方法是驅動程序用來爲以後的操作完成初始化準備工作的。在大部分驅動程序中,open完成如下工作:
◎初始化設備,設置寄存器等。
◎標明次設備號,這樣驅動程序才知道操作哪個設備。/*將設備描述結構指針賦值給文
件私有數據指針,然後在讀寫操作函數中就能知道該操作哪個設備*/
release方法:
release方法的作用正好與open相反。這個設備方法有時也稱爲close,它應該:
◎關閉設備
讀和寫方法:
讀和寫方法都完成類似的工作:從設備中讀取數據到用戶空間;將數據傳遞給驅動程序。它們的原型也相當類似:
原型:ssize_t xxx_read(struct file *filp,char __user *buff,size_t count,loff_t *offp);
原型:ssize_t xxx_write(struct file *filp,char __user *buff,size_t count,loff_t *offp);
參數:對於這兩個方法,其參數含義如下:
filp是文件指針,內核構造後傳給此函數的
count是請求傳輸的數據量。來自用戶空間
buff參數指向數據緩存。來自用戶空間
offp支出文件當前的訪問位置。來自內核
問題:read和write方法的buff參數是用戶空間指針。因此,他不能被內核代碼直接引用。
理由如下:用戶控件的指針在內核空間可能根本是無效的——沒有那個地址的映射。
解決:內核提供了專門的函數用於訪問用戶空間的指針,例如:
寫:int copy_from_user(void *to,const void __user *from,int n)
數據從用戶空間放到設備裏面去
讀:int copy_to_user(void __user *to,const void *from,int n)
從設備裏面讀,數據從設備到用戶空間,
6、設備註銷:
當不再使用驅動程序的時候應該把驅動程序註銷掉。
字符設備的註銷使用cdev_del函數來完成。
原型:int cdev_del(struct cdev *p)
參數:p:要註銷的字符設備結構
[4-6-3]字符設備驅動程序實例分析 memdev.c memdev.h app-mem.c
[4-6-4]競爭與互斥
4.1 驅動調試技術:
對於驅動程序設計來說,核心問題之一就是如何完成調試。
當前常用的驅動調試技術可分爲:
◎打印調試:printk
◎調試器調試:gdb
◎查詢調試:proc文件系統
合理使用printk
應該使用全局打開或關閉printk打印的宏開關來控制是否使用printk。
#ifdef PDEBUG
#define PLOG(fmt,args…) printk(KERN_DEBUG “scull:”fmt,##args)
#else
#define PLOG(fmt,args…) /* do nothing */
#endif
Makefile做如下修改:
DEBUG=y
ifeq($(DEBUG),y)
DEBFLAGS = -O2 –g –DPDEBUG
else
DEBFLAGS = -O2
endif
CFLAFS +=$(DEBFLAGS)
4.2 併發控制:
4.2.1 概念:併發與競態
併發:多個執行單元同時被執行
競態:併發的執行單元對共享資源(硬件資源和軟件上的全局變量等)的訪問導致的競爭狀態
例:
if(copy_from_user(&(dev->data[pos]),buf,count))
ret = -EFAULT;
goto out;
假設有2個進程試圖同時向一個設備的相同位置寫入數據,就會造成數據混亂。
4.2.2 併發控制技術:
處理併發的常用技術是加鎖或者互斥,即確保在任何時候只有一個執行單元可以操作共享資源。在linux內核中主要通過semaphore機制和spin_lock機制實現。
4.2.2.1 信號量:
linux內核的信號量在概念和原理上與用戶態的信號量是一樣的,但是它不能在內核之外使用,它是一種睡眠鎖。如果有一個任務想要獲得已經被佔用的信號量時,信號量會將這個進程放入一個等待隊列,然後讓其睡眠。當持有信號量的進程將信號釋放以後,處於等待隊列中的任務將被喚醒,並讓其獲得信號量。
◎信號量在創建時需要設置一個初始值,表示允許可以有幾個任務同時訪問該信號量保護的資源,初始值爲1就變成互斥鎖(Mutex),即同時只能有一個任務可以訪問信號量保護的資源。
◎當任務訪問完被信號量保護的共享資源後,就必須釋放信號量,釋放信號量通過把信號量的值加1實現,如果釋放後信號量的值爲非正數,表明有任務等待當前信號量,因此要喚醒等待該信號量的任務。
信號量的使用
信號量的實現也是與體系結構相關的,定義在<asm/semaphonre.h>中,struct semaphore類型用來表示信號量。
1、定義信號量
◎struct semaphore sem;
2、初始化信號量
◎void sema_init(struct semaphore *sem,int val)
該函數用於初始化設置信號量的初值,它設置信號量sem的值爲val。
◎void init_MUTEX(struct semaphore *sem)
該函數用於初始化一個互斥鎖,即把信號量sem的值設置爲1。
互斥鎖的值只能爲0或者1
◎void init_MUTEX_LOCKED(struct semaphore *sem)
該函數也用於初始化一個互斥鎖,但它把信號量sem的值設置爲0,
即一開始就處在已鎖狀態。
定義與初始化的工作可以由如下宏一步完成:
◎DECLARE_MUTEX(name)
定義一個信號量name,並初始化它的值爲1。
◎DECLARE_MUTEX_LOCKED(name)
定義一個信號量name,但把它的初始值設置爲0,即鎖在創建時就處在已鎖狀態。
3、獲取信號量
◎void down(struct semaphore *sem)
獲取信號量sem,可能會導致進程睡眠,因此不能在中斷上下文使用該函數。該函數將
把sem的值減1,如果信號量sem的值非負,就直接返回,否則調用者將被掛起,直到
別的任務釋放該信號量才能繼續運行。
◎ int down_interruptible(struct semaphore *sem)
獲取信號量sem。如果信號量不可用,進程將被置爲TASK_INTERRUPTIBLE類型的睡
眠狀態。該函數由返回值來區分是正常返回還是被信號中斷返回,如果返回0,表示獲
得的信號量正常返回,如果被信號打斷,返回-EINTR。
◎down_killable(struct semaphore *sem)
獲取信號量sem。如果信號量不可用,進程將被置爲TASK_KILLABLE類型的睡眠狀態。
注:down()函數現已不建議繼續使用。
建議使用down_killable()或down_interruptible()函數。
4、釋放信號量
void up(struct semaphore *sem)
該函數釋放信號量sem,即把sem的值加1,如果sem的值爲非正數,表明有任務在等
待該信號量,因此喚醒這些等待者。
4.2.2.2 自旋鎖
自旋鎖最多隻能被一個可執行單元持有。自旋鎖不會引起調用者睡眠,如果有一個執行線程試圖獲得一個已經被持有的自旋鎖,那麼線程就會一直進行忙循環,一直等待下去,在那裏看是否該自旋鎖的保持者已經釋放了鎖,“自旋”就是這個意思。
自旋鎖的使用
1、初始化自旋鎖
◎spin_lock_init(x)
該宏用於初始化自旋鎖x,自旋鎖在使用前必須先初始化。
2、獲取自旋鎖
◎spin_lock(lock)
獲取自旋鎖lock,如果成功,立即獲得鎖,並馬上返回,否則它將一直自旋在那裏,直
到該自旋鎖的保持者釋放。
◎spin_trylock(lock)
試圖獲取自旋鎖lock,如果能立即獲得鎖,並返回真,否則立即返回假。它不會一直等
待被釋放。
3、釋放自旋鎖
◎spin_unlock(lock)
釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。
4.2.2.3 信號量與自旋鎖對比
◎信號量可能允許有多個持有者,而自旋鎖在任何時候只能允許有一個持有者。當然也有
信號量叫互斥信號量(只有一個持有者),允許有多個持有者的信號量叫計數信號量。
◎信號量適合於保持時間較長的情況;而自旋鎖適合於保持時間非常短的情況,在實際應
用中自旋鎖控制的代碼只有幾行,而持有自旋鎖的時間也一般不會超過兩次上下文切換
的時間,因爲線程一旦要進行切換,就至少花費切出切入兩次,自旋鎖的佔用時間如果
遠遠長於兩次上下文切換,我們就應該選擇信號量。