(57)Linux驅動開發之三Linux字符設備驅動

 

一、通常狀況下,對每一種設備驅動都會定義一個軟件模塊,這個工程模塊包含.h和.c文件,前者定義該設備驅動的數據結構並聲明外部函數,後者進行設備驅動的具體實現。

 

二、典型的無操做系統下的邏輯開發程序是:這種三層的裸機驅動模型是足夠知足低耦合、高內聚的特色的。

 

 

 

三、當有操做系統存在時,設備驅動成爲了鏈接硬件和內核的橋樑,這時候的設備驅動對外表現爲操做系統的API,與直接裸機開發不一樣,裸機開發時的設備驅動是應用工程師的API。若是設備驅動都按照操做系統給出的獨立於設備的接口而設計,應用程序將可使用統一的系統調用接口來訪問各類設備。

 

四、字符設備指須要經過串行順序(一個字節一個字節訪問)訪問的設備;而塊設備是能夠任意順序訪問的設備,可是以塊爲單位進行操做。字符涉筆驅動和網絡設備驅動都是使用文件系統的操做接口open(),close(),read(),write()函數來訪問;可是內核與網絡設備的通訊和內核與字符設備以及塊設備的通訊方式就徹底不一樣了。

 

 

五、編寫linux設備驅動的技術基礎:

 

(1)咱們在寫驅動代碼的時候,是直接在內核態下工做的,咱們使用的API是內核提供給咱們的,這套API(好比read()、printk()函數等)即設備驅動與內核的接口;咱們的內核統一一套這樣的API或者說驅動框架,就是爲了讓咱們不一樣設備的驅動能夠相互獨立出來。

 

(2)咱們學習設備驅動不僅是對一些內核與設備驅動接口的幾個函數或者是幾個數據結構瞭解就能夠了,應該使用總體思惟、點面結合。

 

六、數字信號處理器(DSP)包括定點DSP和浮點DSP,其中浮點DSP是由硬件來實現的,優於定點DSP。

 

七、咱們能夠得出的處理器分類:

 

 

 

 

八、存儲器的分類:

 

 

 

九、I2C總線:該種總線用於鏈接微控制器及其外圍設備,I2C總線支持多主控模式,任何可以進行發送和接收的設備都可以成爲主設備,主控可以控制數據的傳輸和時鐘頻率,在任意一個時刻只能有一個主控。組成I2C總線的兩個信號爲數據線SDA和時鐘線SCL。I2C設備上的串行數據線SDA接口電路是雙向的,輸出電路用於向總線發送數據,輸入電路用於接收總線上的數據。

 

十、硬件時序分析:

 

時序分析的意思是讓芯片之間的訪問知足芯片手冊中時序圖信號有效的前後順序、採樣創建時間和保持時間的要求,在電路板工做不正常的時候準確的定位時序方面的問題。

 

創建時間:

 

保持時間:

 

十一、CPLD和FPGA

 

十二、示波器個邏輯分析儀在嵌入式方面的應用

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

一、POSIX標準:可移植的操做系統接口,該標準用於保證編制的應用程序能夠在源代碼一級上在多種操做系統上進行移植。

 

二、linux內核的組成部分:進程調度(SCHED)、內存管理(MM)、虛擬文件系統(VFS)、網絡接口(NET)和進程間通訊(IPC)5個子系統組成。

 

(1)進程調度:

 

 

(2)內存管理:當CPU提供內存管理單元(MMU)時,內存管理系統會完成爲每一個進程進行虛擬地址到物理內存的轉化。0~3GB爲進程空間,3~4GB爲內核空間,內核空間對常規內存、I/O設備內存以及高端內存存在不一樣的處理方式。

 

(3)虛擬文件系統:它隱藏了硬件的各類細節,爲全部的設備提供了統一的接口,並且它獨立於各個具體的文件系統,是對各類文件系統的一個抽象。

 

(4)網絡接口:可分爲網絡協議和網絡驅動程序,網絡協議部分負責實現每一種可能的網絡傳輸協議,網絡設備驅動程序負責與硬件設備進行通訊。

 

三、Linux系統只可以經過系統調用和硬件中斷完成從用戶空間到內核空間的控制轉移。

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

                                                                                      linux內核的編譯及加載

 

一、解壓縮命令:tar -jxvf ~.tar.bz2

 

二、執行 make mrproper命令,確保沒有出錯的.o文件以及文件的互相依賴。

 

三、配置內核:make menuconfig命令

 

四、編譯內核命令:make  bzImage    生成的鏡像文件在:/usr

 

或者編譯內核模塊命令:make modules。

 

五、

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

一、linux內核模塊的可裁剪性,使內核體積不會特別大,動態加載。

 

二、lsmod命令能夠得到系統中加載了的全部模塊以及模塊間的依賴關係,該命令實際上等同於  cat /proc/modules。

 

三、insmod:模塊加載函數,insmod 某個目錄下/xx.ko;還有一個模塊加載命令 modprobe ,它在加載某個模塊的時候會同時加載該模塊所依賴的其餘模塊,使用 modinfo xxx.ko命令還能夠得到模塊的信息。

 

四、rmmod 用於卸載某個模塊,它會調用模塊卸載函數。

 

五、linux內核模塊的程序結構:

 

(1)模塊加載函數

 

static int  __init    init_function(void)     //__init  標識聲明內核模塊加載函數

 

{

 

/*初始化代碼*/

 

}

 

module_init(init_function );

 

(2)模塊卸載函數

 

static void  __exit   cleanup_function(void)     //__exit   標識聲明內核模塊卸載函數,無返回值

 

{

 

/*釋放代碼*/

 

}

 

module_exit(cleanup_function );

 

(3)模塊聲明與描述

 

 

 

(4)模塊參數

 

module_param(參數名,參數類型,參數 讀/寫權限);在裝載內核模塊的時候,用戶能夠向內核模塊傳遞參數,形式爲: insmod 模塊名 參數名=參數值。

 

eg:

 

 

 

 

(5)導出符號                     //沒有實際意義

 

(6)模塊的使用計數          //沒有實際意義

 

(7)模塊與GPL

 

爲了使公司產品所使用的Linux操做系統支持模塊,須要完成如下操做:

 

一、在內核編譯時應該選擇上"Enable loadable module support"

 

二、嵌入式產品在啓動過程當中就應該加載模塊,在這個啓動過程當中加載企業本身的驅動模塊最簡單的方法就是修改啓動過程當中的rc腳本,增長 insmod  /.../xx.ko這樣的命令

 

 

 

                                                                               (8)模塊的編譯

 

內核模塊是Linux內核向外部提供的一個插口,其全稱爲動態可加載內核模塊(LoadableKernelModule,LKM),咱們簡稱爲模塊。 Linux內核之因此提供模塊機制,是由於它自己是一個單內核(monolithickernel)。單內核的最大優勢是效率高,由於全部的內容都集成在一塊兒,但其缺點是可擴展性和可維護性相對較差,模塊機制就是爲了彌補這一缺陷。

 

1、什麼是模塊

 

模塊是具備獨立功能的程序,它能夠被單獨編譯,但不能獨立運行。它在運行時被連接到內核做爲內核的一部分在內核空間運行,這與運行在用戶空間的進程是不一樣的。模塊一般由一組函數和數據結構組成,用來實現一種文件系統、一個驅動程序或其餘內核上層的功能。

 

應用程序與內核模塊的比較

 

爲了加深對內核模塊的瞭解,表一給出應用程序與內核模塊程序的比較。

 

表一應用程序與內核模塊程序的比較

 

 

 

從表一咱們能夠看出,內核模塊程序不能調用libc庫中的函數,它運行在內核空間,且只有超級用戶能夠對其運行。另外,模塊程序必須經過module_init()和module-exit()函數來告訴內核「我來了」和「我走了」。

 

2、編寫一個簡單的模塊

 

模塊和內核都在內核空間運行,模塊編程在必定意義上說就是內核編程。由於內核版本的每次變化,其中的某些函數名也會相應地發生變化,所以模塊編程與內核版本密切相關。
1.程序舉例

 

 
  1. #include  <module.h >
  2.  
  3. #include  <kernel.h >
  4.  
  5. #include  <init.h >
  6.  
  7. MODULE_LICENSE("GPL");  
  8.  
  9. staticint__initlkp_init(void)  
  10.  
  11. {  
  12.  
  13. printk(KERN_ALERT"HelloWorld!\n");  
  14.  
  15. return0;  
  16.  
  17. }  
  18.  
  19. staticvoid__exitlkp_cleanup(void)  
  20.  
  21. {  
  22.  
  23. printk(KERN_ALERT"ByeWorld!\n");  
  24.  
  25. }  
  26.  
  27. module_init(lkp_init);  
  28.  
  29. module_exit(lkp_cleanup);  
  30.  
  31. MODULE_AUTHOR("heyutao");  
  32.  
  33. MODULE_DESCRIPTION("hello"); 

 

說明
全部模塊都要使用頭文件module.h,此文件必須包含進來。
頭文件kernel.h包含了經常使用的內核函數。
頭文件init.h包含了宏_init和_exit,它們容許釋放內核佔用的內存。
lkp_init是模塊的初始化函數,它必需包含諸如要編譯的代碼、初始化數據結構等內容。
使用了printk()函數,該函數是由內核定義的,功能與C庫中的printf()相似,它把要打印的信息輸出到終端或系統日誌。
lkp_cleanup是模塊的退出和清理函數。此處能夠作全部終止該驅動程序時相關的清理工做。
module_init()和cleanup_exit()是模塊編程中最基本也是必須的兩個函數。
module_init()是驅動程序初始化的入口點。而cleanup_exit()註銷由模塊提供的全部功能。

 

2編寫Makefile文件,與hello.c放在同一個目錄裏

 

 
  1. obj-m   :=hello.o
  2. KERNELBUILD:=/lib/modules/$(shelluname-r)/build
  3. default:
  4.          make -C $(KERNELBUILD) M=$(shellpwd) modules
  5. clean:
  6.          rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers.tmp_versions 

 

(注意makefile裏面要求的tab)
KERNELBUILD:=/lib/modules/$(shelluname-r)/build是編譯內核模塊須要的Makefile的路徑,Ubuntu下是
/lib/modules/2.6.31-14-generic/build
make-C$(KERNELBUILD)M=$(shellpwd)modules編譯內核模塊。-C將工做目錄轉到KERNELBUILD,指定的是內核源代碼的目錄,調用該目錄下的Makefile,並向這個Makefile傳遞參數。M的值是$(shellpwd)modules,咱們本身給他指定的目錄。
3.編譯模塊
#sudo make(調用第一個命令default)
這時,在hello.c所在文件夾就會有hello.ko,這個就是咱們須要的內核模塊
#sudo make clean
清理編譯垃圾,hello.ko也會清理掉。

 

4.插入模塊,讓其工做。注意必須是root權限

 

#sudo insmod  ./hello.ko咱們用dmesg就能夠看到產生的內核信息啦,Helloworld!

 

若是沒有輸出"hellofromhelloworld",由於若是你在字符終端而不是終端模擬器下運行的話,就會輸出,由於在終端模擬器下時會把內核消息輸出到日誌文件/var/log/kern.log中。

 

#sudo rmmod./hello再用dmesg能夠看到Byeworld!
備註:若是一個模塊包含多個.c文件(eg:1.c,2.c),則應該使用以下方式編寫Makefile,
obj-m :=modulename.o
module-objs := 1.o 2.o

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                          linux文件系統與設備文件系統

 

一、linux文件系統VFS目錄結構:

 

 

 

其中比較重要的有:

 

一、/bin目錄,包含基本命令,如ls,cp,mkdir等,這個目錄中的文件都是可執行的。

 

二、/dev目錄,該目錄是設備文件存儲目錄,應用程序經過對這些文件的讀寫和控制就能夠訪問實際的設備。

 

三、/etc目錄,系統配置文件的所在地,一些服務器的配置文件

 

四、/lib目錄,linux庫文件存放目錄。

 

五、/proc目錄,操做系統運行時進程及內核信息(好比CPU、硬盤分區內存信息等)存放在這裏,/proc目錄爲僞文件系統proc的掛載目錄,proc並非真正的文件系統,它只是內核裏一些數據結構在這一塊的映射,它存在於內存之中。

 

六、/var目錄,這個目錄的內容常常變更,如/var/log目錄被用來存放系統日誌。

 

七、/sys,linux內核所支持的sysfs文件系統被映射在此目錄,當內核檢測到在系統中出現了新的設備後,內核會在sysfs文件系統中爲該新設備生成一項新的記錄。

 

 

 

 

 

 

 -----------------------------------------------------------------------------------------------------------------------------------------------------------  

 

                                                                                       字符設備驅動

 

 

 

1、字符設備基礎知識
內核裏有驅動,咱們操做這些驅動文件的方法是操縱內核給咱們提供的文件操做API,好比OPEN(),close()函數等。

 

一、設備驅動分類

 

      linux系統將設備分爲3類:字符設備、塊設備、網絡設備。

 

 

字符設備:是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據須要按照前後數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制檯和LED設備等。

 

塊設備:是指能夠從設備的任意位置讀取必定長度數據的設備。塊設備包括硬盤、磁盤、U盤和SD卡等。

 

每個字符設備或塊設備都在/dev目錄下對應一個設備文件。linux用戶程序經過設備文件(或稱設備節點)來使用驅動程序操做字符設備和塊設備。

 

二、字符設備、字符設備驅動與用戶空間訪問該設備的程序三者之間的關係

 

 

 

在Linux內核中:

 

a -- 使用cdev結構體來描述字符設備;

 

b -- 經過其成員dev_t來定義設備號(分爲主、次設備號)以肯定字符設備的惟一性;

 

c -- 經過其成員file_operations來定義字符設備驅動提供給VFS的接口函數,如常見的open()、read()、write()等;

 

在Linux字符設備驅動中:

 

a -- 模塊加載函數經過 register_chrdev_region( ) 或 alloc_chrdev_region( )來靜態或者動態獲取設備號;

 

b -- 經過 cdev_init( ) 創建cdev與 file_operations之間的鏈接,經過 cdev_add( ) 向系統添加一個cdev以完成註冊;

 

c -- 模塊卸載函數經過cdev_del( )來註銷cdev,經過 unregister_chrdev_region( )來釋放設備號;

 

用戶空間訪問該設備的程序:

 

a -- 經過Linux系統調用,如open() 、                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             read( )、write( ),來「調用」file_operations來定義字符設備驅動提供給VFS的接口函數;

 

三、字符設備驅動模型

 

 

 

2、cdev 結構體解析
      在Linux內核中,使用cdev結構體來描述一個字符設備,cdev結構體的定義以下:

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. <include/linux/cdev.h>  
  2.   
  3. struct cdev {   
  4.     struct kobject kobj;                  //內嵌的內核對象.  
  5.     struct module *owner;                 //該字符設備所在的內核模塊的對象指針.  
  6.     const struct file_operations *ops;    //該結構描述了字符設備所能實現的方法,是極爲關鍵的一個結構體.  
  7.     struct list_head list;                //用來將已經向內核註冊的全部字符設備造成鏈表.  
  8.     dev_t dev;                            //字符設備的設備號,由主設備號和次設備號構成.  
  9.     unsigned int count;                   //隸屬於同一主設備號的次設備號的個數.  
  10. };  
 
內核給出的操做struct cdev結構的接口主要有如下幾個:

 

a -- void cdev_init(struct cdev *, const struct file_operations *);
其源代碼如代碼清單以下:

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)  
  2. {  
  3.     memset(cdev, 0, sizeof *cdev);  
  4.     INIT_LIST_HEAD(&cdev->list);  
  5.     kobject_init(&cdev->kobj, &ktype_cdev_default);  
  6.     cdev->ops = fops;  
  7. }  
      該函數主要對struct cdev結構體作初始化,最重要的就是創建cdev 和 file_operations之間的鏈接:

 

(1) 將整個結構體清零;
(2) 初始化list成員使其指向自身;
(3) 初始化kobj成員;
(4) 初始化ops成員;
 
 b --struct cdev *cdev_alloc(void);
     該函數主要分配一個struct cdev結構,動態申請一個cdev內存,並作了cdev_init中所作的前面3步初始化工做(第四步初始化工做須要在調用cdev_alloc後,顯式的作初始化即: .ops=xxx_ops).
其源代碼清單以下:

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. struct cdev *cdev_alloc(void)  
  2. {  
  3.     struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);  
  4.     if (p) {  
  5.         INIT_LIST_HEAD(&p->list);  
  6.         kobject_init(&p->kobj, &ktype_cdev_dynamic);  
  7.     }  
  8.     return p;  
  9. }  
     在上面的兩個初始化的函數中,咱們沒有看到關於owner成員、dev成員、count成員的初始化;其實,owner成員的存在體現了驅動程序與內核模塊間的親密關係,struct module是內核對於一個模塊的抽象,該成員在字符設備中能夠體現該設備隸屬於哪一個模塊,在驅動程序的編寫中通常由用戶顯式的初始化 .owner = THIS_MODULE, 該成員能夠防止設備的方法正在被使用時,設備所在模塊被卸載。而dev成員和count成員則在cdev_add中才會賦上有效的值。
 
c -- int cdev_add(struct cdev *p, dev_t dev, unsigned count);
       該函數向內核註冊一個struct cdev結構,即正式通知內核由struct cdev *p表明的字符設備已經可使用了。
固然這裏還需提供兩個參數:
(1)第一個設備號 dev,
(2)和該設備關聯的設備編號的數量。
這兩個參數直接賦值給struct cdev 的dev成員和count成員。
 
d -- void cdev_del(struct cdev *p);
     該函數向內核註銷一個struct cdev結構,即正式通知內核由struct cdev *p表明的字符設備已經不可使用了。
     從上述的接口討論中,咱們發現對於struct cdev的初始化和註冊的過程當中,咱們須要提供幾個東西
(1) struct file_operations結構指針;
(2) dev設備號;
(3) count次設備號個數。
 
3、設備號相應操做
1 -- 主設備號和次設備號(兩者一塊兒爲設備號):
      一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操做的是哪一個設備,用來區分同類型的設備。
  linux內核中,設備號用dev_t來描述,2.6.28中定義以下:
  typedef u_long dev_t;
  在32位機中是4個字節,高12位表示主設備號,低20位表示次設備號。
內核也爲咱們提供了幾個方便操做的宏實現dev_t:
1) -- 從設備號中提取major和minor
MAJOR(dev_t dev);                              
MINOR(dev_t dev);
2) -- 經過major和minor構建設備號
MKDEV(int major,int minor);
注:這只是構建設備號。並未註冊,須要調用 register_chrdev_region 靜態申請;

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. //宏定義:  
  2. #define MINORBITS    20  
  3. #define MINORMASK    ((1U << MINORBITS) - 1)  
  4. #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))  
  5. #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  
  6. #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>  
 
二、分配設備號(兩種方法):
a -- 靜態申請:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
其源代碼清單以下:
[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. int register_chrdev_region(dev_t from, unsigned count, const char *name)  
  2. {  
  3.     struct char_device_struct *cd;  
  4.     dev_t to = from + count;  
  5.     dev_t n, next;  
  6.   
  7.     for (n = from; n < to; n = next) {  
  8.         next = MKDEV(MAJOR(n)+1, 0);  
  9.         if (next > to)  
  10.             next = to;  
  11.         cd = __register_chrdev_region(MAJOR(n), MINOR(n),  
  12.                    next - n, name);  
  13.         if (IS_ERR(cd))  
  14.             goto fail;  
  15.     }  
  16.     return 0;  
  17. fail:  
  18.     to = n;  
  19.     for (n = from; n < to; n = next) {  
  20.         next = MKDEV(MAJOR(n)+1, 0);  
  21.         kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));  
  22.     }  
  23.     return PTR_ERR(cd);  
  24. }  
 
b -- 動態分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其源代碼清單以下:

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,  
  2.             const char *name)  
  3. {  
  4.     struct char_device_struct *cd;  
  5.     cd = __register_chrdev_region(0, baseminor, count, name);  
  6.     if (IS_ERR(cd))  
  7.         return PTR_ERR(cd);  
  8.     *dev = MKDEV(cd->major, cd->baseminor);  
  9.     return 0;  
  10. }  
能夠看到兩者都是調用了__register_chrdev_region 函數,其源代碼以下:
[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. static struct char_device_struct *  
  2. __register_chrdev_region(unsigned int major, unsigned int baseminor,  
  3.                int minorct, const char *name)  
  4. {  
  5.     struct char_device_struct *cd, **cp;  
  6.     int ret = 0;  
  7.     int i;  
  8.   
  9.     cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);  
  10.     if (cd == NULL)  
  11.         return ERR_PTR(-ENOMEM);  
  12.   
  13.     mutex_lock(&chrdevs_lock);  
  14.   
  15.     /* temporary */  
  16.     if (major == 0) {  
  17.         for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {  
  18.             if (chrdevs[i] == NULL)  
  19.                 break;  
  20.         }  
  21.   
  22.         if (i == 0) {  
  23.             ret = -EBUSY;  
  24.             goto out;  
  25.         }  
  26.         major = i;  
  27.         ret = major;  
  28.     }  
  29.   
  30.     cd->major = major;  
  31.     cd->baseminor = baseminor;  
  32.     cd->minorct = minorct;  
  33.     strlcpy(cd->name, name, sizeof(cd->name));  
  34.   
  35.     i = major_to_index(major);  
  36.   
  37.     for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)  
  38.         if ((*cp)->major > major ||  
  39.             ((*cp)->major == major &&  
  40.              (((*cp)->baseminor >= baseminor) ||  
  41.               ((*cp)->baseminor + (*cp)->minorct > baseminor))))  
  42.             break;  
  43.   
  44.     /* Check for overlapping minor ranges.  */  
  45.     if (*cp && (*cp)->major == major) {  
  46.         int old_min = (*cp)->baseminor;  
  47.         int old_max = (*cp)->baseminor + (*cp)->minorct - 1;  
  48.         int new_min = baseminor;  
  49.         int new_max = baseminor + minorct - 1;  
  50.   
  51.         /* New driver overlaps from the left.  */  
  52.         if (new_max >= old_min && new_max <= old_max) {  
  53.             ret = -EBUSY;  
  54.             goto out;  
  55.         }  
  56.   
  57.         /* New driver overlaps from the right.  */  
  58.         if (new_min <= old_max && new_min >= old_min) {  
  59.             ret = -EBUSY;  
  60.             goto out;  
  61.         }  
  62.     }  
  63.   
  64.     cd->next = *cp;  
  65.     *cp = cd;  
  66.     mutex_unlock(&chrdevs_lock);  
  67.     return cd;  
  68. out:  
  69.     mutex_unlock(&chrdevs_lock);  
  70.     kfree(cd);  
  71.     return ERR_PTR(ret);  
  72. }  
 經過這個函數能夠看出 register_chrdev_region和 alloc_chrdev_region 的區別,register_chrdev_region靜態申請的方式是直接將Major 註冊進入,而 alloc_chrdev_region動態分配的方式是從Major = 0 開始,逐個查找設備號,直到找到一個閒置的設備號,並將其註冊進去;

 

兩者應用能夠簡單總結以下:
                                     register_chrdev_region                                                alloc_chrdev_region 

 

    devno = MKDEV(major,minor);
    ret = register_chrdev_region(devno, 1, "hello"); 
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1);
    alloc_chrdev_region(&devno, minor, 1, "hello");
    major = MAJOR(devno);
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1)
register_chrdev(major,"hello",&hello

 

     能夠看到,除了前面兩個函數,還加了一個register_chrdev 函數,能夠發現這個函數的應用很是簡單,只要一句就能夠搞定前面函數所作之事;
下面分析一下register_chrdev 函數,其源代碼定義以下:

 

[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. static inline int register_chrdev(unsigned int major, const char *name,  
  2.                   const struct file_operations *fops)  
  3. {  
  4.     return __register_chrdev(major, 0, 256, name, fops);  
  5. }  
調用了 __register_chrdev(major, 0, 256, name, fops) 函數:
[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. int __register_chrdev(unsigned int major, unsigned int baseminor,  
  2.               unsigned int count, const char *name,  
  3.               const struct file_operations *fops)  
  4. {  
  5.     struct char_device_struct *cd;  
  6.     struct cdev *cdev;  
  7.     int err = -ENOMEM;  
  8.   
  9.     cd = __register_chrdev_region(major, baseminor, count, name);  
  10.     if (IS_ERR(cd))  
  11.         return PTR_ERR(cd);  
  12.   
  13.     cdev = cdev_alloc();  
  14.     if (!cdev)  
  15.         goto out2;  
  16.   
  17.     cdev->owner = fops->owner;  
  18.     cdev->ops = fops;  
  19.     kobject_set_name(&cdev->kobj, "%s", name);  
  20.   
  21.     err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);  
  22.     if (err)  
  23.         goto out;  
  24.   
  25.     cd->cdev = cdev;  
  26.   
  27.     return major ? 0 : cd->major;  
  28. out:  
  29.     kobject_put(&cdev->kobj);  
  30. out2:  
  31.     kfree(__unregister_chrdev_region(cd->major, baseminor, count));  
  32.     return err;  
  33. }  
能夠看到這個函數不僅幫咱們註冊了設備號,還幫咱們作了cdev 的初始化以及cdev 的註冊;
 

 

三、註銷設備號:
void unregister_chrdev_region(dev_t from, unsigned count);
 
四、建立設備文件:
     利用cat /proc/devices查看申請到的設備名,設備號。
1)使用mknod手工建立:mknod filename type major minor
2)自動建立設備節點:
    利用udev(mdev)來實現設備文件的自動建立,首先應保證支持udev(mdev),由busybox配置。在驅動初始化代碼裏調用class_create爲該設備建立一個class,再爲每一個設備調用device_create建立對應的設備。
 
下面看一個實例,練習一下上面的操做:
hello.c
  1. #include <linux/module.h>  
  2. #include <linux/fs.h>  
  3. #include <linux/cdev.h>  
  4. static int major = 250;  
  5. static int minor = 0;  
  6. static dev_t devno;  
  7. static struct cdev cdev;  
  8. static int hello_open (struct inode *inode, struct file *filep)  
  9. {  
  10.     printk("hello_open \n");  
  11.     return 0;  
  12. }  
  13. static struct file_operations hello_ops=  
  14. {  
  15.     .open = hello_open,           
  16. };  
  17.   
  18. static int hello_init(void)  
  19. {  
  20.     int ret;      
  21.     printk("hello_init");  
  22.     devno = MKDEV(major,minor);  
  23.     ret = register_chrdev_region(devno, 1, "hello");  
  24.     if(ret < 0)  
  25.     {  
  26.         printk("register_chrdev_region fail \n");  
  27.         return ret;  
  28.     }  
  29.     cdev_init(&cdev,&hello_ops);  
  30.     ret = cdev_add(&cdev,devno,1);  
  31.     if(ret < 0)  
  32.     {  
  33.         printk("cdev_add fail \n");  
  34.         return ret;  
  35.     }     
  36.     return 0;  
  37. }  
  38. static void hello_exit(void)  
  39. {  
  40.     cdev_del(&cdev);  
  41.     unregister_chrdev_region(devno,1);  
  42.     printk("hello_exit \n");  
  43. }  
  44. MODULE_LICENSE("GPL");                 //GPL許可聲明
  45. module_init(hello_init);               //初始化函數聲明
  46. module_exit(hello_exit);               //註銷函數聲明
測試程序 test.c
[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. #include <sys/types.h>  
  2. #include <sys/stat.h>  
  3. #include <fcntl.h>  
  4. #include <stdio.h>  
  5.   
  6. main()  
  7. {  
  8.     int fd;  
  9.   
  10.     fd = open("/dev/hello",O_RDWR);  
  11.     if(fd<0)  
  12.     {  
  13.         perror("open fail \n");  
  14.         return ;  
  15.     }  
  16.   
  17.     close(fd);  
  18. }  
makefile:
[cpp] view plain copy 在CODE上查看代碼片派生到個人代碼片
  1. ifneq  ($(KERNELRELEASE),)  
  2. obj-m:=hello.o  
  3. $(info "2nd")  
  4. else  
  5. KDIR := /lib/modules/$(shell uname -r)/build  
  6. PWD:=$(shell pwd)  
  7. all:  
  8.     $(info "1st")  
  9.     make -C $(KDIR) M=$(PWD) modules  
  10. clean:  
  11.     rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order  
  12. endif  

 

編譯成功後,使用 insmod 命令加載:
而後用cat /proc/devices 查看,會發現設備號已經申請成功;
 

 

 

 

----------------------------------------------------------------------------------------------------------------------------------------------------------

 

linux字符設備驅動結構:

 

一、cdev結構體:

 

cdev結構體用來描述字符設備,這個結構體中的一個重要的成員,這個結構體中的一個重要成員:file_operations 定義了字符設備驅動提供給虛擬文件系統的接口函數。

 

struct cdev {

 

struct kobject kobj;          // 每一個 cdev都是一個 kobject

 

struct module *owner;       //指 向實現驅動的模塊

 

const struct file_operations *ops;   // 操縱這個字符設備文件的方法

 

struct list_head list;       // 與 cdev對應的字符設備文件的inode->i_devices的鏈表頭

 

dev_t dev;                  // 起始設備編號

 

unsigned int count;       // 設備範圍號大小

 

};

 

一個 cdev通常它有兩種定義初始化方式:靜態的和動態的。

 

靜態內存定義初始化:

 

struct cdev my_cdev;

 

cdev_init(&my_cdev, &fops);

 

my_cdev.owner = THIS_MODULE;

 

動態內存定義初始化:

 

struct cdev *my_cdev = cdev_alloc();

 

my_cdev->ops = &fops;

 

my_cdev->owner = THIS_MODULE;

 

兩種使用方式的功能是同樣的,只是使用的內存區不同,通常視實際的數據結構需求而定。

 

linux內核向系統提供的操做函數API:

 

(1)void cdev_init(struct cdev *, struct  file_operations  *);//這個函數用於初始化cdev成員,並創建cdev和file_operations之間的鏈接。

 

(2)struct cdev *cdev_alloc(void);                                      //動態申請一個cdev內存,返回一個結構體指針類型

 

(3)void cdev_put(struct cdev *p);

 

(4)int cdev_add(struct cdev *, devt_t,unsigned);             //向系統添加一個cdev,完成字符設備的註冊,發生在字符設備驅動模塊加載函數中

 

(5)void cdev_del (struct cdev *);                                      //向系統刪除一個cdev,完成字符設備的註銷,發生在字符設備驅動模塊卸載函數中

 

 

 

二、分配和釋放設備號:

 

 

 

 創建一個字符設備以前,驅動程序首先要作的事情就是得到設備編號。其這主要函數在<linux/fs.h>中聲明:

 

 

 

//指定設備編號在調用cdev_add()函數以前
int  register_chrdev_region(dev_t from, unsigned int count,const char  *name); 
//動態生成設備編號,向系統動態申請未被佔用的設備號的狀況,在調用cdev_add()函數以前
int alloc_chrdev_region(dev_t  *dev, unsigned int baseminor,unsigned int count, const char *name); 
//釋放設備編號,在調用cdev_del()函數以後
void unregister_chrdev_region(dev_t from, unsigned int count);

 

 

 

分配之設備號的最佳方式是:默認採用動態分配,同時保留在加載甚至是編譯時指定主設備號的餘地。

 

 

 

 

 

三、file_operations結構體

 

 

 

結構體file_operations在頭文件 linux/fs.h中定義,用來存儲驅動內核模塊提供的對設備進行各類操做的函數的指針。該結構體的每一個域都對應着驅動內核模塊用來處理某個被請求的 事務的函數的地址。

 

舉個例子,每一個字符設備須要定義一個用來讀取設備數據的函數。結構體 file_operations中存儲着內核模塊中執行這項操做的函數的地址。一下是該結構體 在內核2.6.5中看起來的樣子:

 

struct file_operations {

 

  struct module *owner;

 

  loff_t(*llseek) (struct file *, loff_t, int);

 

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

 

  ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);

 

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

 

  ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);

 

  int (*readdir) (struct file *, void *, filldir_t);

 

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

 

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

 

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

 

  int (*open) (struct inode *, struct file *);

 

  int (*flush) (struct file *);

 

  int (*release) (struct inode *, struct file *);

 

  int (*fsync) (struct file *, struct dentry *, int datasync);

 

  int (*aio_fsync) (struct kiocb *, int datasync);

 

  int (*fasync) (int, struct file *, int);

 

  int (*lock) (struct file *, int, struct file_lock *);

 

  ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

 

  ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

 

  ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);

 

  ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

 

  unsigned long (*get_unmapped_area) (struct file *, unsigned long,

 

         unsigned long, unsigned long,

 

         unsigned long);

 

};

 

驅動內核模塊是不須要實現每一個函數的。像視頻卡的驅動就不須要從目錄的結構 中讀取數據。那麼,相對應的file_operations重的項就爲 NULL。

 

gcc還有一個方便使用這種結構體的擴展。你會在較現代的驅動內核模塊中見到。新的使用這種結構體的方式以下:

 

struct file_operations fops = {

 

 read: device_read,

 

 write: device_write,

 

 open: device_open,

 

 release: device_release

 

};

 

一樣也有C99語法的使用該結構體的方法,而且它比GNU擴展更受推薦。我使用的版本爲 2.95爲了方便那些想移植你的代碼的人,你最好使用這種語法。它將提升代碼的兼容性:

 

struct file_operations fops = {

 

 .read = device_read,

 

 .write = device_write,

 

 .open = device_open,

 

 .release = device_release

 

};

 

這種語法很清晰,你也必須清楚的意識到沒有顯示聲明的結構體成員都被gcc初始化爲NULL。

 

指向結構體struct file_operations的指針一般命名爲fops。

 

 

 

關於file結構體

 

每個設備文件都表明着內核中的一個file結構體。該結構體在頭文件linux/fs.h定義。注意,file結構體是內核空間的結構體, 這意味着它不會在用戶程序的代碼中出現。它絕對不是在glibc中定義的FILE。 FILE本身也從不在內核空間的函數中出現。它的名字確實挺讓人迷惑的。它表明着一個抽象的打開的文件,但不是那種在磁盤上用結構體inode表示的文件。

 

指向結構體struct file的指針一般命名爲filp。你一樣能夠看到struct file file的表達方式,但不要被它誘惑。

 

去看看結構體file的定義。大部分的函數入口,像結構體struct dentry沒有被設備驅動模塊使用,你大可忽略它們。這是由於設備驅動模塊並不本身直接填充結構體file:它們只是使用在別處創建的結構體file中的數據。

 

 

 

註冊一個設備

 

如同先前討論的,字符設備一般經過在路徑/dev下的設備文件進行訪問。主設備號告訴你哪些驅動模塊是用來操縱哪些硬件設備的。從設備號是驅動模塊本身使用來區別它操縱的不一樣設備,當此驅動模塊操縱不僅一個設備時。

 

將內核驅動模塊加載入內核意味着要向內核註冊本身。這個工做是和驅動模塊得到主設備號時初始化一同進行的。你可使用頭文件linux/fs.h中的函數register_chrdev來實現。

 

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

 

其中unsigned int major是你申請的主設備號,const char *name是將要在文件/proc/devices中顯示的名稱,struct file_operations *fops是指向你的驅動模塊的file_operations表的指針。負的返回值意味着註冊失敗。注意註冊並不須要提供從設備號。內核自己並不在乎從設備號。

 

如今的問題是你如何申請到一個沒有被使用的主設備號?最簡單的方法是查看文件 Documentation/devices.txt從中挑選一個沒有被使用的。這不是一勞永逸的方法由於你沒法得知該主設備號在未來會被佔用。最終的方法是讓內核爲你動態分配一個。

 

若是你向函數register_chrdev傳遞爲0的主設備號,那麼返回的就是動態分配的主設備號。反作用就是既然你沒法得知主設備號,你就沒法預先創建一個設備文件。有多種解決方法:第一種方法是新註冊的驅動模塊會輸出本身新分配到的主設備號,因此咱們能夠手工創建須要的設備文件。第二種是利用文件/proc/devices新註冊的驅動模塊的入口,要麼手工創建設備文件,要麼編一個腳本去自動讀取該文件而且生成設備文件。第三種是在咱們的模塊中,當註冊成功時,使用mknod系統調用創建設備文件而且在驅動模塊調用函數cleanup_module前,調用rm刪除該設備文件。

 

 

 

 

 

---------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                               linux字符設備驅動的組成

 

一、字符設備驅動模塊的加載與卸載函數:

 

在 字符設備驅動模塊的加載函數中應該實現設備號的申請和cdev的註冊,而在卸載函數中應該實現設備號的釋放和cdev的註銷。

 

 

 

 

 

 

 

 

 

 

 

---------------------------------------------------------------------------------------------------------------------------------------------

 

                                                                             linux設備驅動中的併發控制

 

 

 

一、訪問共享資源的代碼區域稱爲臨界區,臨界區須要以某種互斥機制加以保護。eg:中斷屏蔽、原子操做、自旋鎖和信號量是linux設備驅動中能夠採用預防併發的有效方法。

 

(1)中斷屏蔽:該方法使中斷與進程之間的併發再也不發生。

 

local_irq_disable()    //屏蔽中斷

 

。。。

 

critical section()       //臨界區

 

。。。

 

local_irq_enable()     //開中斷

 

(2)原子操做:指的是在執行過程當中不會被別的代碼路徑所中斷的操做。

 

整型原子操做:

 

 

 

位原子操做:

 

(3)自旋鎖:指對臨界資源進行互斥訪問的一種方式。
相關文章
相關標籤/搜索