詳細介紹linux字符驅動相關內容

 

驅動程序:使硬件工作的軟件。

linux驅動程序的分類:

字符設備驅動(重點)

網絡接口驅動(重點)

塊設備驅動

 

1)字符設備:

字符設備是一種按字節來訪問的設備,字符驅動則負責驅動字符設備,這樣的驅動通

常實現openclosereadwrite系統調用。

2)塊設備:

在大部分unix系統,塊設備不能按字節處理數據,只能一次傳送一個或多個長度是

512字節(或一個更大的2次冪的數)的整塊數據

Linux則允許塊設備傳送任意數目的字節

因此塊和字符設備的區別:僅僅是驅動的接口函數與內核的接口函數不同

3)網絡接口:

任何網絡事物都通過一個接口來進行,一個接口通常是一個硬件(eth0),但它也可以

是一個純粹的軟件設備,比如迴環接口(lo)。一個網絡接口負責發送和接收數據報文

字符設備與塊設備:隨機訪問?塊設備隨機訪問,字符設備必須按順序訪問

 

驅動程序安裝方式:有兩種

  模塊方式(已知)insmod, rmmod

直接編譯進內核

 

如何直接把驅動程序(內核模塊)編譯進內核?

需要修改兩個文件:KconfigMakefileKconfig用來產生配置菜單,到處有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結構,從而找到相應的readwrite函數)

網絡設備沒有設備文件,設備文件在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.1struct file代表一個打開的文件。系統中每個打開的文件在內核空間都有一個關聯的struct file。它由內核在打開文件時創建,在文件關閉後釋放。(每打開一次創建一個

重要成員

loff_t f_pos /*文件讀寫位置*/

struct file_operations *f_op

3.2struct inode用來記錄文件的物理上的信息(如存放位置、設備號等)。因此它和代表打開文件的file結構是不同的,一個文件可以對應多個file結構,但只有一個inode結構

重要成員

dev_t i_rdev:設備號  //inode代表設備文件(設備節點)?

3.3struct 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結構,從而找到驅動程序中相應的readwrite函數)

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結構中相關的設備操作)

1int (*open)(struct inode *,struct file *) 對應open方法

在設備文件上的第一操作,可以不實現此方法,沒有(即該項爲NULL)時則認爲永

遠打開成功。

2void (*release)(struct inode *,struct file *) 對應close方法

    當設備文件被關閉時調用這個操作。release也可以沒有。

3ssize_t(*read)(struct file *,char __user *,size_t,loff_t *)

    從設備中讀取數據

4ssize_t(*write)(struct file *,const char __user *,size_t,loff_t *)

    向設備發送數據

5unsigned int (*poll)(struct file *,struct poll_table_struct *)

對應select系統調用

6int (*ioctl)(struct inode *,struct file *,unsigned int,unsigned long)

      控制設備

7int (*mmap)(struct file *,struct vm_area_struct *)

    將設備映射到進程虛擬地址空間中。

8off_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支出文件當前的訪問位置。來自內核

 

問題:readwrite方法的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_trylockspin_lock配對使用。

 

4.2.2.3 信號量與自旋鎖對比

信號量可能允許有多個持有者,而自旋鎖在任何時候只能允許有一個持有者。當然也有

信號量叫互斥信號量(只有一個持有者),允許有多個持有者的信號量叫計數信號量

 

信號量適合於保持時間較長的情況;而自旋鎖適合於保持時間非常短的情況,在實際應

用中自旋鎖控制的代碼只有幾行,而持有自旋鎖的時間也一般不會超過兩次上下文切換

的時間,因爲線程一旦要進行切換,就至少花費切出切入兩次,自旋鎖的佔用時間如果

遠遠長於兩次上下文切換,我們就應該選擇信號量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章