手把手教Linux驅動3-之字符設備架構詳解,有這篇就夠了


一、Linux設備分類

Linux系統為了管理方便,將設備分成三種基本類型:

  1. 字符設備

  2. 塊設備

  3. 網絡設備

字符設備:

  字符(char)設備是個能夠像字節流(類似文件)一樣被訪問的設備,由字符設備驅動程序來實現這種特性。字符設備驅動程序通常至少要實現open、close、read和write的系統調用。

  字符終端(/dev/console)和串口(/dev/ttyS0以及類似設備)就是兩個字符設備,它們能很好的說明“流”這種抽象概念。

  字符設備可以通過文件節點來訪問,比如/dev/tty1和/dev/lp0等。這些設備文件和普通文件之間的唯一差別在於對普通文件的訪問可以前后移動訪問位置,而大多數字符設備是一個只能順序訪問的數據通道。然而,也存在具有數據區特性的字符設備,訪問它們時可前后移動訪問位置。例如framebuffer就是這樣的一個設備,app可以用mmap或lseek訪問抓取的整個圖像。

 

 在/dev下執行ls -l ,可以看到很多創建好的設備節點:

 

 

 

字符設備文件(類型為c),設備文件是沒有文件大小的,取而代之的是兩個號碼:主設備號5 +次設備號1 。

塊設備:

  和字符設備類似,塊設備也是通過/dev目錄下的文件系統節點來訪問。塊設備(例如磁盤)上能夠容納filesystem。在大多數的Unix系統中,進行I/O操作時塊設備每次只能傳輸一個或多個完整的塊,而每塊包含512字節(或2的更高次冪字節的數據)。

  Linux可以讓app像字符設備一樣地讀寫塊設備,允許一次傳遞任意多字節的數據。因此,塊設備和字符設備的區別僅僅在於內核內部管理數據的方式,也就是內核及驅動程序之間的軟件接口,而這些不同對用戶來講是透明的。在內核中,和字符驅動程序相比,塊驅動程序具有完全不同的接口。

 

  塊設備文件(類型為b):

 

 

 

網絡設備:

  任何網絡事物都需要經過一個網絡接口形成,網絡接口是一個能夠和其他主機交換數據的設備。接口通常是一個硬件設備,但也可能是個純軟件設備,比如回環(loopback)接口。

  網絡接口由內核中的網絡子系統驅動,負責發送和接收數據包。許多網絡連接(尤其是使用TCP協議的連接)是面向流的,但網絡設備卻圍繞數據包的傳送和接收而設計。網絡驅動程序不需要知道各個連接的相關信息,它只要處理數據包即可。

  由於不是面向流的設備,因此將網絡接口映射到filesystem中的節點(比如/dev/tty1)比較困難。

  Unix訪問網絡接口的方法仍然是給它們分配一個唯一的名字(比如eth0),但這個名字在filesystem中不存在對應的節點。內核和網絡設備驅動程序間的通信,完全不同於內核和字符以及塊驅動程序之間的通信,內核調用一套和數據包相關的函數socket,也叫套接字。

   查看網絡設備使用命令ifconfig:

 

 

 

 

 

 

二、字符設備架構是如何實現的?

在Linux的世界里面一切皆文件,所有的硬件設備操作到應用層都會被抽象成文件的操作。我們知道如果應用層要訪問硬件設備,它必定要調用到硬件對應的驅動程序。Linux內核中有那么多驅動程序,應用層怎么才能精確的調用到底層的驅動程序呢?

  在這里我們字符設備為例,來看一下應用程序是如何和底層驅動程序關聯起來的。必須知道的基礎知識:

  1. 在Linux文件系統中,每個文件都用一個struct inode結構體來描述,這個結構體里面記錄了這個文件的所有信息,例如:文件類型,訪問權限等。

  2. 在Linux操作系統中,每個驅動程序在應用層的/dev目錄下都會有一個設備文件和它對應,並且該文件會有對應的主設備號和次設備號。

  3. 在Linux操作系統中,每個驅動程序都要分配一個主設備號,字符設備的設備號保存在struct cdev結構體中。

     
    struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;//接口函數集合
        struct list_head list;//內核鏈表
        dev_t dev;    //設備號
        unsigned int count;//次設備號個數
    };

     

  4. 在Linux操作系統中,每打開一次文件,Linux操作系統在VFS層都會分配一個struct file結構體來描述打開的這個文件。該結構體用於維護文件打開權限、文件指針偏移值、私有內存地址等信息。

     

注意:

  常常我們認為struct inode描述的是文件的靜態信息,即這些信息很少會改變。而struct file描述的是動態信息,即在對文件的操作的時候,struct file里面的信息經常會發生變化。典型的是struct file結構體里面的f_pos(記錄當前文件的位移量),每次讀寫一個普通文件時f_ops的值都會發生改變。

 

 這幾個結構體關系如下圖所示:

 

 

 

通過上圖我們可以知道,如果想訪問底層設備,就必須打開對應的設備文件。也就是在這個打開的過程中,Linux內核將應用層和對應的驅動程序關聯起來。

  1. 當open函數打開設備文件時,可以根據設備文件對應的struct inode結構體描述的信息,可以知道接下來要操作的設備類型(字符設備還是塊設備)。還會分配一個struct file結構體。

  2. 根據struct inode結構體里面記錄的設備號,可以找到對應的驅動程序。這里以字符設備為例。在Linux操作系統中每個字符設備有一個struct cdev結構體。此結構體描述了字符設備所有的信息,其中最重要一項的就是字符設備的操作函數接口。

  3. 找到struct cdev結構體后,Linux內核就會將struct cdev結構體所在的內存空間首地記錄在struct inode結構體的i_cdev成員中。將struct cdev結構體的中記錄的函數操作接口地址記錄在struct file結構體的f_op成員中。

  4. 任務完成,VFS層會給應用層返回一個文件描述符(fd)。這個fd是和struct file結構體對應的。接下來上層的應用程序就可以通過fd來找到strut file,然后在由struct file找到操作字符設備的函數接口了。

三、字符驅動相關函數分析

 

 1 /**
 2  * cdev_init() - initialize a cdev structure
 3  * @cdev: the structure to initialize
 4  * @fops: the file_operations for this device
 5  *
 6  * Initializes @cdev, remembering @fops, making it ready to add to the
 7  * system with cdev_add().
 8  */
 9 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
10 功能:
11   初始化cdev結構體
12 參數:
13   @cdev cdev結構體地址
14   @fops 操作字符設備的函數接口地址
15 返回值:
16
 1 /**
 2  * register_chrdev_region() - register a range of device numbers
 3  * @from: the first in the desired range of device numbers; must include
 4  *        the major number.
 5  * @count: the number of consecutive device numbers required
 6  * @name: the name of the device or driver.
 7  *
 8  * Return value is zero on success, a negative error code on failure.
 9  */                                              
10 int register_chrdev_region(dev_t from, unsigned count, const char *name)
11 功能:
12   注冊一個范圍()的設備號
13 參數:
14   @from 設備號
15   @count 注冊的設備個數
16   @name 設備的名字
17 返回值:
18   成功返回0,失敗返回錯誤碼(負數)
 1 /**
 2  * cdev_add() - add a char device to the system
 3  * @p: the cdev structure for the device
 4  * @dev: the first device number for which this device is responsible
 5  * @count: the number of consecutive minor numbers corresponding to this
 6  *         device
 7  *
 8  * cdev_add() adds the device represented by @p to the system, making it
 9  * live immediately.  A negative error code is returned on failure.
10  */
11 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
12 功能:
13   添加一個字符設備到操作系統
14 參數:
15   @p cdev結構體地址
16   @dev 設備號
17   @count 次設備號個數
18 返回值:
19   成功返回0,失敗返回錯誤碼(負數)
 1 /**
 2  * cdev_del() - remove a cdev from the system
 3  * @p: the cdev structure to be removed
 4  *
 5  * cdev_del() removes @p from the system, possibly freeing the structure
 6  * itself.
 7  */
 8 void cdev_del(struct cdev *p)
 9 功能:
10   從系統中刪除一個字符設備
11 參數:
12   @p cdev結構體地址
13 返回值:
14
 1 static inline int register_chrdev(unsigned int major, const char *name,
 2           const struct file_operations *fops)
 3 
 4 功能:
 5   注冊或者分配設備號,並注冊fops到cdev結構體,
 6   如果major>0,功能為注冊該主設備號,
 7   如果major=0,功能為動態分配主設備號。
 8 參數:
 9   @major : 主設備號
10   @name : 設備名稱,執行 cat /proc/devices顯示的名稱
11   @fops  : 文件系統的接口指針
12 返回值
13   如果major>0   成功返回0,失敗返回負的錯誤碼
14   如果major=0  成功返回主設備號,失敗返回負的錯誤碼

該函數實現了對cdev的初始化和注冊的封裝,所以調用該函數之后就不需要自己操作cdev了。

 

 相對的注銷函數為unregister_chrdev

 

1 static inline void unregister_chrdev(unsigned int major, const char *name)

 

四、如何編寫字符設備驅動

 

 

 

 

參考上圖,編寫字符設備驅動步驟如下:

 
        


1 實現模塊加載和卸載入口函數

module_init (hello_init);
module_exit (hello_exit);

2 申請主設備號

  申請主設備號  (內核中用於區分和管理不同字符設備)
1 register_chrdev_region (devno, number_of_devices, "hello");
3 創建設備節點

創建設備節點文件 (為用戶提供一個可操作到文件接口--open())
創建設備節點有兩種方式:手動方式創建,函數自動創建。
    手動創建:
1 mknod /dev/hello c 250 0
    自動創建設備節點

除了使用mknod命令手動創建設備節點,還可以利用linux的udev、mdev機制,而我們的ARM開發板上移植的busybox有mdev機制,那么就使用mdev機制來自動創建設備節點。


在etc/init.d/rcS文件里有一句:

1 echo /sbin/mdev > /proc/sys/kernel/hotplug

 

   該名命令就是用來自動創建設備節點。

 

 udev 是一個工作在用戶空間的工具,它能根據系統中硬件設備的狀態動態的更新設備文件,包括設備文件的創建,刪除,權限等。這些文件通常都定義在/dev 目錄下,但也可以在配置文件中指定。udev 必須有內核中的sysfs和tmpfs支持,sysfs 為udev 提供設備入口和uevent 通道,tmpfs 為udev 設備文件提供存放空間。 

 udev 運行在用戶模式,而非內核中。udev 的初始化腳本在系統啟動時創建設備節點,並且當插入新設備——加入驅動模塊——在sysfs上注冊新的數據后,udev會創新新的設備節點。

   注意,udev 是通過對內核產生的設備文件修改,或增加別名的方式來達到自定義設備文件的目的。但是,udev 是用戶模式程序,其不會更改內核行為。也就是說,內核仍然會創建sda,sdb等設備文件,而udev可根據設備的唯一信息來區分不同的設備,並產生新的設備文件(或鏈接)。

 

例如:

如果驅動模塊可以將自己的設備號作為內核參數導出,在sysfs文件中就有一個叫做uevent文件記錄它的值。

 

 

 

由上圖可知,uevent中包含了主設備號和次設備號的值以及設備名字。

 

在Linux應用層啟動一個udev程序,這個程序的第一次運行的時候,會遍歷/sys目錄,尋找每個子目錄的uevent文件,從這些uevent文件中獲取創建設備節點的信息,然后調用mknod程序在/dev目錄下創建設備節點。結束之后,udev就開始等待內核空間的event。這個設備模型的東西,我們在后面再詳細說。這里大就可以這樣理解,在Linux內核中提供了一些函數接口,通過這些函數接口,我們可在sysfs文件系統中導出我們的設備號的值,導出值之后,內核還會向應用層上報event。此時udev就知道有活可以干了,它收到這個event后,就讀取event對應的信息,接下來就開始創建設備節點啦。

 

如何創建一個設備類?

第一步 :通過宏class_create() 創建一個class類型的對象;

 

 1 /* This is a #define to keep the compiler from merging different
 2  * instances of the __key variable */
 3 #define class_create(owner, name)    \
 4 ({            \
 5   static struct lock_class_key __key;  \
 6   __class_create(owner, name, &__key);  \
 7 })
 8 參數:
 9   @owner  THIS_MODULE
10   @name   類名字
11 返回值
12   可以定義一個struct class的指針變量cls接受返回值,然后通過IS_ERR(cls)判斷
13   是否失敗,如果成功這個宏返回0,失敗返回非9值(可以通過PTR_ERR(cls)來獲得
14   失敗返回的錯誤碼)

 

在Linux內核中,把設備進行了分類,同一類設備可以放在同一個目錄下,該函數啟示就是創建了一個類,例如:

 

 

 

 

第二步:導出我們的設備信息到用戶空間

 

 1 /**
 2  * device_create - creates a device and registers it with sysfs
 3  * @class: pointer to the struct class that this device should be registered to
 4  * @parent: pointer to the parent struct device of this new device, if any
 5  * @devt: the dev_t for the char device to be added
 6  * @drvdata: the data to be added to the device for callbacks
 7  * @fmt: string for the device's name
 8  *
 9  * This function can be used by char device classes.  A struct device
10  * will be created in sysfs, registered to the specified class.
11  *
12  * A "dev" file will be created, showing the dev_t for the device, if
13  * the dev_t is not 0,0.
14  * If a pointer to a parent struct device is passed in, the newly created
15  * struct device will be a child of that device in sysfs.
16  * The pointer to the struct device will be returned from the call.
17  * Any further sysfs files that might be required can be created using this
18  * pointer.
19  *
20  * Returns &struct device pointer on success, or ERR_PTR() on error.
21  *
22  * Note: the struct class passed to this function must have previously
23  * been created with a call to class_create().
24  */
25 struct device *device_create(struct class *class, struct device *parent,
26            dev_t devt, void *drvdata, const char *fmt, ...)

 

自動創建設備節點使用實例:

 

 1 static struct class *cls;
 2 static struct device *test_device;
 3 
 4   devno = MKDEV(major,minor);
 5   cls = class_create(THIS_MODULE,"helloclass");
 6   if(IS_ERR(cls))
 7   {
 8     unregister_chrdev(major,"hello");
 9     return result;
10   }
11   test_device = device_create(cls,NULL,devno,NULL,"hellodevice");
12   if(IS_ERR(test_device ))
13   {
14     class_destroy(cls);
15     unregister_chrdev(major,"hello");
16     return result;
17   }

 


4 實現file_operations

1 static const struct file_operations fifo_operations = {
2     .owner =   THIS_MODULE,
3     .open =   dev_fifo_open,
4     .read =   dev_fifo_read,
5     .write =   dev_fifo_write,
6     .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
7 };

  open、release對應應用層的open()、close()函數。實現比較簡單,

直接返回0即可。
其中read、write、unloched_ioctrl 函數的實現需要涉及到用戶空間
和內存空間的數據拷貝。

  在Linux操作系統中,用戶空間和內核空間是相互獨立的。也就是說內核空間是不能直接訪問用戶空間內存地址,同理用戶空間也不能直接訪問內核空間內存地址。

如果想實現,將用戶空間的數據拷貝到內核空間或將內核空間數據拷貝到用戶空間,就必須借助內核給我們提供的接口來完成。

 

read接口實現

用戶空間-->內核空間

字符設備的write接口定義如下:

 

1 ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
2 參數:
3   filp:待操作的設備文件file結構體指針
4   buf:待寫入所讀取數據的用戶空間緩沖區指針
5   count:待讀取數據字節數
6   f_pos:待讀取數據文件位置,寫入完成后根據實際寫入字節數重新定位
7 返回:
8   成功實際寫入的字節數,失敗返回負值

 

如果該操作為空,將使得write系統調用返回負EINVAL失敗,正常返回實際寫入的字節數。

 

用戶空間向內核空間拷貝數據需要使用copy_from_user函數,該函數定義在arch/arm/include/asm/uaccess.h中。

 

1 static inline int copy_from_user(void *to, const void __user volatile *from,unsigned long n)
2 參數:
3   to:目標地址(內核空間)
4   from:源地址(用戶空間)
5   n:將要拷貝數據的字節數
6 返回:
7   成功返回0,失敗返回沒有拷貝成功的數據字節數

 

 

 

 

還可以使用get_user宏:

1 int get_user(data, ptr);
2 參數:
3   data:可以是字節、半字、字、雙字類型的內核變量
4   ptr:用戶空間內存指針
5 返回:
6   成功返回0,失敗返回非0

 

 

write接口實現

內核空間-->用戶空間

字符設備的read接口定義如下:

 1 ssize_t (*read)(struct file *filp, char __user *buf, size_t  count, lofft *f_pos);
 2 參數:
 3   filp: 待操作的設備文件file結構體指針
 4   buf:  待寫入所讀取數據的用戶空間緩沖區指針
 5   count:待讀取數據字節數
 6   f_pos:待讀取數據文件位置,讀取完成后根據實際讀取字節數重新定位
 7   __user :是一個空的宏,主要用來顯示的告訴程序員它修飾的指針變量存放的是用戶空間的地址。
 8 
 9 返回值:
10   成功實際讀取的字節數,失敗返回負值

 

注意:如果該操作為空,將使得read系統調用返回負EINVAL失敗,正常返回實際讀取的字節數。

 

用戶空間從內核空間讀取數據需要使用copy_to_user函數:

 

1 static inline int copy_to_user(void __user volatile *to, const void *from,unsigned long n)
2 參數:
3   to:目標地址(用戶空間)
4   from:源地址(內核空間)
5   n:將要拷貝數據的字節數
6 返回:
7   成功返回0,失敗返回沒有拷貝成功的數據字節數

 

 

 

 

還可以使用put_user宏:

 

1 int put_user(data, prt)
2 參數:
3   data:可以是字節、半字、字、雙字類型的內核變量
4   ptr:用戶空間內存指針
5 返回:
6   成功返回0, 失敗返回非0

 

這樣我們就可以實現read、write函數了,實例如下:

 

 1 ssize_t hello_read (struct file *filp, char *buff,   size_t count, loff_t *offp)
 2 {
 3   ssize_t   result = 0;
 4 
 5   if (count   > 127) 
 6     count = 127;
 7 
 8   if   (copy_to_user (buff, data, count))
 9   {
10     result =   -EFAULT;
11   }
12   else
13   {
14     printk   (KERN_INFO "wrote %d bytes\n", count);
15     result =   count;
16   } 
17   return   result;
18 }
19 ssize_t hello_write (struct file *filp,const char *buf, size_t count, loff_t *f_pos)
20 {
21   ssize_t ret   = 0;
22   //printk   (KERN_INFO "Writing %d bytes\n", count);
23   if (count   > 127) return -ENOMEM;
24 
25   if   (copy_from_user (data, buf, count)) {
26     ret =   -EFAULT;
27   }
28   else {
29     data[count] = '\0';
30     printk   (KERN_INFO"Received: %s\n", data);
31     ret =   count;
32   }
33   return ret;
34 }

 

unlocked_ioctl接口實現

(1)為什么要實現xxx_ioctl ? 

    前面我們在驅動中已經實現了讀寫接口,通過這些接口我們可以完成對設備的讀寫。但是很多時候我們的應用層工程師除了要對設備進行讀寫數據之外,還希望可以對設備進行控制。例如:針對串口設備,驅動層除了需要提供對串口的讀寫之外,還需提供對串口波特率、奇偶校驗位、終止位的設置,這些配置信息需要從應用層傳遞一些基本數據,僅僅是數據類型不同。

 

    通過xxx_ioctl函數接口,可以提供對設備的控制能力,增加驅動程序的靈活性。

 

(2)如何實現xxx_ioctl函數接口?

增加xxx_ioctl函數接口,應用層可以通過ioctl系統調用,根據不同的命令來操作dev_fifo。

 

   kernel 2.6.35 及之前的版本中struct file_operations 一共有3個ioctl :ioctl,unlocked_ioctl和compat_ioctl 現在只有unlocked_ioctl和compat_ioctl 了

在kernel 2.6.36 中已經完全刪除了struct file_operations 中的ioctl 函數指針,取而代之的是unlocked_ioctl 。

 

·         2.6.36 之前的內核

1 long (ioctl) (struct inode node ,struct file* filp, unsigned int cmd,unsigned long arg)

·         2.6.36之后的內核

1 long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)

參數cmd: 通過應用函數ioctl傳遞下來的命令

 

 

先來看看應用層的ioctl和驅動層的xxx_ioctl對應關系:

    

 

 

 

 

<1>應用層ioctl參數分析

 

1 int ioctl(int fd, int cmd, ...);
2 參數:
3 @fd:打開設備文件的時候獲得文件描述符 
4 @ cmd:第二個參數:給驅動層傳遞的命令,需要注意的時候,驅動層的命令和應用層的命令一定要統一
5 @第三個參數: "..."在C語言中,很多時候都被理解成可變參數。
6 返回值
7        成功:0
8        失敗:-1,同時設置errno

 

小貼士:

當我們通過ioctl調用驅動層xxx_ioctl的時候,有三種情況可供選擇:

1: 不傳遞數據給xxx_ioctl 

2: 傳遞數據給xxx_ioctl,希望它最終能把數據寫入設備(例如:設置串口的波特率)

3: 調用xxxx_ioctl希望獲取設備的硬件參數(例如:獲取當前串口設備的波特率)

這三種情況中,有些時候需要傳遞數據,有些時候不需要傳遞數據。在C語言中,是無法實現函數重載的。那怎么辦?用"..."來欺騙編譯器了,"..."本來的意思是傳遞多參數。在這里的意思是帶一個參數還是不帶參數。

 

參數可以傳遞整型值,也可以傳遞某塊內存的地址,內核接口函數必須根據實際情況提取對應的信息。

 

 

 

 

 

 

 

 

 

<2>驅動層xxx_ioctl參數分析 

 

1 long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
2 參數:
3 @file:   vfs層為打開字符設備文件的進程創建的結構體,用於存放文件的動態信息 
4 @ cmd: 用戶空間傳遞的命令,可以根據不同的命令做不同的事情
5 @第三個參數: 用戶空間的數據,主要這個數據可能是一個地址值(用戶空間傳遞的是一個地址),也可能是一個數值,也可能沒值
6 返回值
7        成功:0
8        失敗:帶錯誤碼的負值

 

<3>如何確定cmd 的值。

該值主要用於區分命令的類型,雖然我只需要傳遞任意一個整型值即可,但是我們盡量按照內核規范要求,充分利用這32bite的空間,如果大家都沒有規矩,又如何能成方圓?

現在我就來看看,在Linux 內核中這個cmd是如何設計的吧!


 

 

 

 

具體含義如下:

設備類型 類型或叫幻數,代表一類設備,一般用一個字母或者1個8bit的數字
序列號 代表這個設備的第幾個命令
方向 表示是由內核空間到用戶空間,或是用戶空間到內核空間,入:只讀,只寫,讀寫,其他
數據尺寸 表示需要讀寫的參數大小

 

 

 

 

 

由上可以一個命令由4個部分組成,每個部分需要的bite都不完全一樣,制作一個命令需要在不同的位域寫不同的數字,Linux 系統已經給我們封裝好了宏,我們只需要直接調用宏來設計命令即可。 

 

 

 

   通過Linux 系統給我們提供的宏,我們在設計命令的時候,只需要指定設備類型、命令序號,數據類型三個字段就可以了。

Linux 系統中已經設計了一場用的命令,可以通過查閱Linux 源碼中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已經被使用過了。

 

 

 

 


<4> 如何檢查命令?

可以通過宏_IOC_TYPE(nr)來判斷應用程序傳下來的命令type是否正確;

可以通過宏_IOC_DIR(nr)來得到命令是讀還是寫,然后再通過宏access_ok(type,addr,size)來判斷用戶層傳遞的內存地址是否合法。

使用方法如下:

 

 1  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
 2     pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
 3     return-ENOTTY;
 4   }
 5   if(_IOC_DIR(cmd)&_IOC_READ)
 6     ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
 7   else if( _IOC_DIR(cmd)&_IOC_WRITE )
 8     ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
 9   if(ret){
10     pr_err("bad   access %ld.\n",ret);
11     return-EFAULT;
12   }

 


5 注冊cdev

 

定義好file_operations結構體,就可以通過函數cdev_init()、cdev_add()注冊字符設備驅動了。

實例如下:

1 static struct cdev cdev;
2 
3 cdev_init(&cdev,&hello_ops);
4 error = cdev_add(&cdev,devno,1);

 

注意如果使用了函數register_chrdev(),就不用了執行上述操作,因為該函數已經實現了對cdev的封裝。

五、實例

好了,現在我們可以來實現一個完整的字符設備框架的實例,包括打開、關閉、讀寫、ioctrl、自動創建設備節點等功能。

 

  1 #include <linux/init.h>
  2 #include <linux/module.h>
  3 #include <linux/cdev.h>
  4 #include <linux/fs.h>
  5 #include <linux/device.h>
  6 #include <linux/slab.h>
  7 #include <asm/uaccess.h>
  8 #include "dev_fifo_head.h"
  9 
 10 //指定的主設備號
 11 #define   MAJOR_NUM 250
 12 
 13 //自己的字符設備
 14 struct mycdev
 15 {
 16     int len;
 17     unsigned   char buffer[50];
 18     struct   cdev cdev;
 19 };
 20 
 21 MODULE_LICENSE("GPL");
 22 //設備號
 23 static dev_t   dev_num = {0};
 24 //全局gcd
 25 struct mycdev *gcd;
 26 //設備類
 27 struct class *cls;
 28 //獲得用戶傳遞的數據,根據它來決定注冊的設備個數
 29 static int ndevices = 1;
 30 module_param(ndevices, int, 0644);
 31 MODULE_PARM_DESC(ndevices, "The number of devices for register.\n");
 32 
 33 //打開設備
 34 static int dev_fifo_open(struct   inode *inode,   struct file *file)
 35 {
 36     struct   mycdev *cd;  
 37 
 38     printk("dev_fifo_open   success!\n");  
 39     //用struct file的文件私有數據指針保存struct mycdev結構體指針
 40     cd   = container_of(inode->i_cdev,struct   mycdev,cdev);
 41     file->private_data =   cd;  
 42     return   0;
 43 }
 44 
 45 //讀設備
 46 static ssize_t   dev_fifo_read(struct file *file, char   __user *ubuf,   size_t
 47 size, loff_t *ppos)
 48 {
 49     int n;
 50     int ret;
 51     char   *kbuf;
 52     struct   mycdev *mycd =   file->private_data;
 53 
 54     printk("read *ppos :   %lld\n",*ppos); 
 55 
 56     if(*ppos == mycd->len)
 57         return   0;
 58 
 59     //請求大大小 > buffer剩余的字節數   :讀取實際記得字節數
 60     if(size > mycd->len - *ppos)
 61         n = mycd->len - *ppos;
 62     else
 63         n = size;
 64 
 65     printk("n =   %d\n",n);
 66     //從上一次文件位置指針的位置開始讀取數據
 67     kbuf   = mycd->buffer   + *ppos;
 68     //拷貝數據到用戶空間
 69     ret   = copy_to_user(ubuf,kbuf, n);
 70     if(ret != 0)
 71         return   -EFAULT;
 72 
 73     //更新文件位置指針的值
 74     *ppos += n;
 75     printk("dev_fifo_read   success!\n");
 76     return   n;
 77 }
 78 //寫設備
 79 static ssize_t   dev_fifo_write(struct file *file, const char __user *ubuf,size_t size, loff_t *ppos)
 80 {
 81     int n;
 82     int ret;
 83     char   *kbuf;
 84     struct   mycdev *mycd =   file->private_data;
 85 
 86     printk("write *ppos :   %lld\n",*ppos);
 87     //已經到達buffer尾部了
 88     if(*ppos == sizeof(mycd->buffer))
 89        return   -1;
 90     //請求大大小 > buffer剩余的字節數(有多少空間就寫多少數據)
 91     if(size > sizeof(mycd->buffer) - *ppos)
 92         n = sizeof(mycd->buffer) - *ppos;
 93     else
 94         n = size;
 95     //從上一次文件位置指針的位置開始寫入數據
 96 
 97     kbuf   = mycd->buffer   + *ppos;
 98     //拷貝數據到內核空間
 99     ret   = copy_from_user(kbuf, ubuf, n);
100     if(ret != 0)
101         return   -EFAULT;
102 
103     //更新文件位置指針的值
104     *ppos += n;
105     //更新dev_fifo.len
106     mycd->len += n;
107     printk("dev_fifo_write   success!\n");
108     return   n;
109 }
110 
111 //linux 內核在2.6以后,已經廢棄了ioctl函數指針結構,取而代之的是
112 
113 long   dev_fifo_unlocked_ioctl(struct file *file,   unsigned int cmd,
114     unsigned   long arg)
115 {
116   int ret = 0;
117   struct mycdev *mycd   = file->private_data;
118 
119   if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
120     pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
121     return-ENOTTY;
122   }
123   if(_IOC_DIR(cmd)&_IOC_READ)
124     ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
125   else if( _IOC_DIR(cmd)&_IOC_WRITE )
126     ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
127   if(ret){
128     pr_err("bad   access %ld.\n",ret);
129     return-EFAULT;
130   } 
131     switch(cmd)
132     {
133       case DEV_FIFO_CLEAN:
134          printk("CMD:CLEAN\n");
135       memset(mycd->buffer, 0, sizeof(mycd->buffer));
136          break;
137       case DEV_FIFO_SETVALUE:
138          printk("CMD:SETVALUE\n");
139          mycd->len = arg;
140          break;
141       case DEV_FIFO_GETVALUE:
142          printk("CMD:GETVALUE\n");
143          ret   = put_user(mycd->len, (int *)arg);
144          break;
145       default:
146          return   -EFAULT;
147     }
148     return   ret;
149 }
150 
151 //設備操作函數接口
152 
153 static const struct file_operations fifo_operations = {
154     .owner =   THIS_MODULE,
155     .open =   dev_fifo_open,
156     .read =   dev_fifo_read,
157     .write =   dev_fifo_write,
158     .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
159 };
160 //模塊入口
161 int __init dev_fifo_init(void)
162 {
163     int i = 0;
164     int n = 0;
165     int ret;
166 
167     struct   device *device;
168   gcd   = kzalloc(ndevices   * sizeof(struct   mycdev), GFP_KERNEL);
169 
170     if(!gcd){
171         return   -ENOMEM;
172     }
173 
174     //設備號 : 主設備號(12bit) | 次設備號(20bit)
175     dev_num   = MKDEV(MAJOR_NUM, 0);
176     //靜態注冊設備號
177     ret   = register_chrdev_region(dev_num,ndevices,"dev_fifo");
178     if(ret < 0){
179     //靜態注冊失敗,進行動態注冊設備號
180      ret   =alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
181       if(ret < 0){
182         printk("Fail to register_chrdev_region\n");
183         goto   err_register_chrdev_region;
184       }
185     }
186     //創建設備類
187     cls   = class_create(THIS_MODULE, "dev_fifo");
188     if(IS_ERR(cls)){
189         ret   = PTR_ERR(cls);
190         goto   err_class_create;
191     }
192     printk("ndevices :   %d\n",ndevices);
193     for(n = 0;n < ndevices;n   ++)
194     {
195       //初始化字符設備
196       cdev_init(&gcd[n].cdev,&fifo_operations);
197       //添加設備到操作系統
198       ret   = cdev_add(&gcd[n].cdev,dev_num + n,1);
199       if (ret < 0)
200       {
201          goto   err_cdev_add;
202       }
203      //導出設備信息到用戶空間(/sys/class/類名/設備名)
204       device   = device_create(cls,NULL,dev_num +n,NULL,"dev_fifo%d",n);
205       if(IS_ERR(device)){
206          ret   = PTR_ERR(device);
207          printk("Fail to device_create\n");
208          goto   err_device_create;    
209       }
210     }
211     printk("Register   dev_fito to system,ok!\n");
212     return   0;
213 err_device_create:
214 
215     //將已經導出的設備信息除去
216     for(i = 0;i < n;i ++)
217     {
218        device_destroy(cls,dev_num + i);    
219     }
220 err_cdev_add:
221     //將已經添加的全部除去
222     for(i = 0;i < n;i ++)
223     {
224        cdev_del(&gcd[i].cdev);
225     }
226 err_class_create:
227     unregister_chrdev_region(dev_num,   ndevices);
228 err_register_chrdev_region:
229     return   ret;
230 }
231 void __exit dev_fifo_exit(void)
232 {
233     int i;
234     //刪除sysfs文件系統中的設備
235     for(i = 0;i < ndevices;i   ++)
236     {
237         device_destroy(cls,dev_num + i);    
238     }
239     //刪除系統中的設備類
240     class_destroy(cls);
241     //從系統中刪除添加的字符設備
242     for(i = 0;i < ndevices;i   ++)
243     {
244        cdev_del(&gcd[i].cdev);
245     } 
246     //釋放申請的設備號
247     unregister_chrdev_region(dev_num,   ndevices);
248     return;
249 }
250 module_init(dev_fifo_init);
251 module_exit(dev_fifo_exit);   

 

頭文件內容:

dev_fifo_head.h

 

1 #ifndef _DEV_FIFO_HEAD_H
2 #define _DEV_FIFO_HEAD_H
3 #define DEV_FIFO_TYPE 'k'
4 #define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0x10)
5 #define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,0x11,int)
6 #define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,0x12,int)
7 #endif

 

Makefile :

 

 1 ifeq ($(KERNELRELEASE),)
 2 KERNEL_DIR ?=/lib/modules/$(shell uname -r)/build  
 3 PWD :=$(shell pwd)
 4 modules:
 5     $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) modules
 6 .PHONY:modules clean
 7 clean:
 8     $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) clean
 9 else
10     obj-m := dev_fifo.o  
11 endif

 

應用程序:

 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/types.h>
 4 #include <string.h>
 5 #include <sys/stat.h>
 6 #include <fcntl.h>
 7 
 8 int main(int argc, const char *argv[])
 9 {
10     int fd ;
11     int n;
12     char buf[1024] = "hello   word";
13     
14     fd = open("/dev/dev_fifo0",O_RDWR);
15     if(fd < 0){
16         perror("Fail   ot open");
17         return   -1;
18     }
19     printf("open   successful ,fd = %d\n",fd);
20     n = write(fd,buf,strlen(buf));
21     if(n < 0){
22         perror("Fail   to write");
23         return   -1;
24     }
25     printf("write   %d bytes!\n",n);
26     n = write(fd,buf,strlen(buf));
27     if(n < 0){
28         perror("Fail   to write");
29         return   -1;
30     }
31     printf("write   %d bytes!\n",n);
32     return 0;
33 }

 

測試步驟:

(1)   加載模塊

1 sudo insmod hello.ko

(2)   創建設備節點

1 sudo mknod /dev/hello c 250 0

如果代碼中增加了自動創建設備節點的功能,這個步驟不要執行。

(3)   測試字符設備

1 gcc test.c -o runsudo 
2 ./run

 


更多嵌入式資料,請關注公眾號: 一口Linux

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM