一文帶你掌握Linux字符設備架構

1、Linux設備分類

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

  • 字符設備
  • 塊設備
  • 網絡設備

字符設備:

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

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

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

在/dev下執行ls -l ,能夠看到不少建立好的設備節點:shell

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

塊設備:

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

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

塊設備文件(類型爲b):架構

網絡設備:

任何網絡事物都須要通過一個網絡接口造成,網絡接口是一個可以和其餘主機交換數據的設備。接口一般是一個硬件設備,但也多是個純軟件設備,好比迴環(loopback)接口。app

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

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

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

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

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

在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找到操做字符設備的函數接口了。

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

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:
  初始化cdev結構體
參數:
  @cdev cdev結構體地址
  @fops 操做字符設備的函數接口地址
返回值:
  無
/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
                                              
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:
  註冊一個範圍()的設備號
參數:
  @from 設備號
  @count 註冊的設備個數
  @name 設備的名字
返回值:
  成功返回0,失敗返回錯誤碼(負數)
/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:
  添加一個字符設備到操做系統
參數:
  @p cdev結構體地址
  @dev 設備號
  @count 次設備號個數
返回值:
  成功返回0,失敗返回錯誤碼(負數)
/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 */

void cdev_del(struct cdev *p)
功能:
  從系統中刪除一個字符設備
參數:
  @p cdev結構體地址
返回值:
  無
static inline int register_chrdev(unsigned int major, const char *name,
          const struct file_operations *fops)


功能:
  註冊或者分配設備號,並註冊fops到cdev結構體,
  若是major>0,功能爲註冊該主設備號,
  若是major
=0,功能爲動態分配主設備號。
參數:
  @major : 主設備號
  @name : 設備名稱,執行 cat /proc/devices顯示的名稱
  @fops  : 文件系統的接口指針
返回值
  若是major>0   成功返回0,失敗返回負的錯誤碼
  若是major=0  成功返回主設備號,失敗返回負的錯誤碼

該函數實現了對cdev的初始化和註冊的封裝,因此調用該函數以後就不須要本身操做cdev了。

相對的註銷函數爲unregister_chrdev

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

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

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

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

module_init (hello_init);
module_exit (hello_exit);

2. 申請主設備號

申請主設備號  (內核中用於區分和管理不一樣字符設備)

register_chrdev_region (devno, number_of_devices, "hello");

3. 建立設備節點

建立設備節點文件 (爲用戶提供一個可操做到文件接口--open()) 建立設備節點有兩種方式:手動方式建立,函數自動建立。手動建立:

mknod /dev/hello c 250 0

自動建立設備節點

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

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

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類型的對象;

/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */

#define class_create(owner, name)    \
({            \
  static struct lock_class_key __key;  \
  __class_create(owner, name, &__key);  \
})


參數:
  @owner  THIS_MODULE
  @name   類名字
返回值
  能夠定義一個struct class的指針變量cls接受返回值,而後經過IS_ERR(cls)判斷
  是否失敗,若是成功這個宏返回0,失敗返回非9值(能夠經過PTR_ERR(cls)來得到
  失敗返回的錯誤碼)

在Linux內核中,把設備進行了分類,同一類設備能夠放在同一個目錄下,該函數啓示就是建立了一個類,例如:

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

/**
 * device_create - creates a device and registers it with sysfs
 * @class: pointer to the struct class that this device should be registered to
 * @parent: pointer to the parent struct device of this new device, if any
 * @devt: the dev_t for the char device to be added
 * @drvdata: the data to be added to the device for callbacks
 * @fmt: string for the device's name
 *
 * This function can be used by char device classes.  A struct device
 * will be created in sysfs, registered to the specified class.
 *
 * A "dev" file will be created, showing the dev_t for the device, if
 * the dev_t is not 0,0.
 * If a pointer to a parent struct device is passed in, the newly created
 * struct device will be a child of that device in sysfs.
 * The pointer to the struct device will be returned from the call.
 * Any further sysfs files that might be required can be created using this
 * pointer.
 *
 * Returns &struct device pointer on success, or ERR_PTR() on error.
 *
 * Note: the struct class passed to this function must have previously
 * been created with a call to class_create().
 */

struct device *device_create(struct class *class, struct device *parent,
           dev_t devt, void *drvdata, const char *fmt, ...)

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

static struct class *cls;
static struct device *test_device;

  devno = MKDEV(major,minor);
  cls = class_create(THIS_MODULE,"helloclass");
  if(IS_ERR(cls))
  {
    unregister_chrdev(major,"hello");
    return result;
  }
  test_device = device_create(cls,NULL,devno,NULL,"hellodevice");
  if(IS_ERR(test_device ))
  {
    class_destroy(cls);
    unregister_chrdev(major,"hello");
    return result;
  }

4 實現file_operations

static const struct file_operations fifo_operations = {
    .owner =   THIS_MODULE,
    .open =   dev_fifo_open,
    .read =   dev_fifo_read,
    .write =   dev_fifo_write,
    .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
};

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

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

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

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

1. read接口實現

用戶空間-->內核空間

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

ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
參數:
  filp:待操做的設備文件file結構體指針
  buf:待寫入所讀取數據的用戶空間緩衝區指針
  count:待讀取數據字節數
  f_pos:待讀取數據文件位置,寫入完成後根據實際寫入字節數從新定位
返回:
  成功實際寫入的字節數,失敗返回負值

若是該操做爲空,將使得write系統調用返回負EINVAL失敗,正常返回實際寫入的字節數。

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

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

還可使用get_user宏:

int get_user(data, ptr);
參數:
  data:能夠是字節、半字、字、雙字類型的內核變量
  ptr:用戶空間內存指針
返回:
  成功返回0,失敗返回非0

2. write接口實現

內核空間-->用戶空間

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

ssize_t (*read)(struct file *filp, char __user *buf, size_t  count, lofft *f_pos);
參數:
  filp: 待操做的設備文件file結構體指針
  buf:  待寫入所讀取數據的用戶空間緩衝區指針
  count:待讀取數據字節數
  f_pos:待讀取數據文件位置,讀取完成後根據實際讀取字節數從新定位
  __user :是一個空的宏,主要用來顯示的告訴程序員它修飾的指針變量存放的是用戶空間的地址。

返回值:
  成功實際讀取的字節數,失敗返回負值

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

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

 static inline int copy_to_user(void __user volatile *to, const void *from,unsigned long n)
參數:
  to:目標地址(用戶空間)
  from:源地址(內核空間)
  n:將要拷貝數據的字節數
返回:
  成功返回0,失敗返回沒有拷貝成功的數據字節數
在這裏插入圖片描述

還可使用put_user宏:

int put_user(data, prt)
參數:
  data:能夠是字節、半字、字、雙字類型的內核變量
  ptr:用戶空間內存指針
返回:
  成功返回0, 失敗返回非0

這樣咱們就能夠實現read、write函數了,實例以下:

ssize_t hello_read (struct file *filp, char *buff,   size_t count, loff_t *offp)
{
  ssize_t   result = 0;

  if (count   > 127
    count = 127;

  if   (copy_to_user (buff, data, count))
  {
    result =   -EFAULT;
  }
  else
  {
    printk   (KERN_INFO "wrote %d bytes\n", count);
    result =   count;
  } 
  return   result;
}
ssize_t hello_write (struct file *filp,const char *buf, size_t count, loff_t *f_pos)
{
  ssize_t ret   = 0;
  //printk   (KERN_INFO "Writing %d bytes\n", count);
  if (count   > 127return -ENOMEM;

  if   (copy_from_user (data, buf, count)) {
    ret =   -EFAULT;
  }
  else {
    data[count] = '\0';
    printk   (KERN_INFO"Received: %s\n", data);
    ret =   count;
  }
  return ret;
}

3. 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 以前的內核

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

·         2.6.36以後的內核

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

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

先來看看應用層的ioctl和驅動層的xxx_ioctl對應關係:<1>應用層ioctl參數分析

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

小貼士:

當咱們經過ioctl調用驅動層xxx_ioctl的時候,有三種狀況可供選擇:

1: 不傳遞數據給xxx_ioctl 
2: 傳遞數據給xxx_ioctl,但願它最終能把數據寫入設備(例如:設置串口的波特率)
3: 調用xxxx_ioctl但願獲取設備的硬件參數(例如:獲取當前串口設備的波特率)
這三種狀況中,有些時候須要傳遞數據,有些時候不須要傳遞數據。在C語言中,是
沒法實現函數重載的。那怎麼辦?用"..."來欺騙編譯器了,"..."原本的意思是傳
遞多參數。在這裏的意思是帶一個參數仍是不帶參數。

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

<2>驅動層xxx_ioctl參數分析

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

<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)來判斷用戶層傳遞的內存地址是否合法。

使用方法以下:

  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
    pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
    return-ENOTTY;
  }
  if(_IOC_DIR(cmd)&_IOC_READ)
    ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if( _IOC_DIR(cmd)&_IOC_WRITE )
    ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
  if(ret){
    pr_err("bad   access %ld.\n",ret);
    return-EFAULT;
  }

5 註冊cdev

定義好file_operations結構體,就能夠經過函數cdev_init()、cdev_add()註冊字符設備驅動了。

實例以下:

static struct cdev cdev;

cdev_init(&cdev,&hello_ops);
error = cdev_add(&cdev,devno,1);

注意若是使用了函數register_chrdev(),就不用了執行上述操做,由於該函數已經實現了對cdev的封裝。

5、實例

千言萬語,所有彙總在這一個圖裏,你們能夠對照相應的層次來學習。

6、實例

好了,如今咱們能夠來實現一個完整的字符設備框架的實例,包括打開、關閉、讀寫、ioctrl、自動建立設備節點等功能。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include "dev_fifo_head.h"

//指定的主設備號
#define   MAJOR_NUM 250

//本身的字符設備
struct mycdev
{

    int len;
    unsigned   char buffer[50];
    struct   cdev cdev;
};

MODULE_LICENSE("GPL");
//設備號
static dev_t   dev_num = {0};
//全局gcd
struct mycdev *gcd;
//設備類
struct class *cls;
//得到用戶傳遞的數據,根據它來決定註冊的設備個數
static int ndevices = 1;
module_param(ndevices, int0644);
MODULE_PARM_DESC(ndevices, "The number of devices for register.\n");

//打開設備
static int dev_fifo_open(struct   inode *inode,   struct file *file)
{
    struct   mycdev *cd;  

    printk("dev_fifo_open   success!\n");  
    //用struct file的文件私有數據指針保存struct mycdev結構體指針
    cd   = container_of(inode->i_cdev,struct   mycdev,cdev);
    file->private_data =   cd;  
    return   0;
}

//讀設備
static ssize_t   dev_fifo_read(struct file *file, char   __user *ubuf,   size_t
size, loff_t *ppos)

{
    int n;
    int ret;
    char   *kbuf;
    struct   mycdev *mycd =   file->private_data;

    printk("read *ppos :   %lld\n",*ppos); 

    if(*ppos == mycd->len)
        return   0;

    //請求大大小 > buffer剩餘的字節數   :讀取實際記得字節數
    if(size > mycd->len - *ppos)
        n = mycd->len - *ppos;
    else
        n = size;

    printk("n =   %d\n",n);
    //從上一次文件位置指針的位置開始讀取數據
    kbuf   = mycd->buffer   + *ppos;
    //拷貝數據到用戶空間
    ret   = copy_to_user(ubuf,kbuf, n);
    if(ret != 0)
        return   -EFAULT;

    //更新文件位置指針的值
    *ppos += n;
    printk("dev_fifo_read   success!\n");
    return   n;
}
//寫設備
static ssize_t   dev_fifo_write(struct file *file, const char __user *ubuf,size_t size, loff_t *ppos)
{
    int n;
    int ret;
    char   *kbuf;
    struct   mycdev *mycd =   file->private_data;

    printk("write *ppos :   %lld\n",*ppos);
    //已經到達buffer尾部了
    if(*ppos == sizeof(mycd->buffer))
       return   -1;
    //請求大大小 > buffer剩餘的字節數(有多少空間就寫多少數據)
    if(size > sizeof(mycd->buffer) - *ppos)
        n = sizeof(mycd->buffer) - *ppos;
    else
        n = size;
    //從上一次文件位置指針的位置開始寫入數據

    kbuf   = mycd->buffer   + *ppos;
    //拷貝數據到內核空間
    ret   = copy_from_user(kbuf, ubuf, n);
    if(ret != 0)
        return   -EFAULT;

    //更新文件位置指針的值
    *ppos += n;
    //更新dev_fifo.len
    mycd->len += n;
    printk("dev_fifo_write   success!\n");
    return   n;
}

//linux 內核在2.6之後,已經廢棄了ioctl函數指針結構,取而代之的是

long   dev_fifo_unlocked_ioctl(struct file *file,   unsigned int cmd,
    unsigned   long arg)

{
  int ret = 0;
  struct mycdev *mycd   = file->private_data;

  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
    pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
    return-ENOTTY;
  }
  if(_IOC_DIR(cmd)&_IOC_READ)
    ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if( _IOC_DIR(cmd)&_IOC_WRITE )
    ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
  if(ret){
    pr_err("bad   access %ld.\n",ret);
    return-EFAULT;
  } 
    switch(cmd)
    {
      case DEV_FIFO_CLEAN:
         printk("CMD:CLEAN\n");
      memset(mycd->buffer, 0sizeof(mycd->buffer));
         break;
      case DEV_FIFO_SETVALUE:
         printk("CMD:SETVALUE\n");
         mycd->len = arg;
         break;
      case DEV_FIFO_GETVALUE:
         printk("CMD:GETVALUE\n");
         ret   = put_user(mycd->len, (int *)arg);
         break;
      default:
         return   -EFAULT;
    }
    return   ret;
}

//設備操做函數接口

static const struct file_operations fifo_operations = {
    .owner =   THIS_MODULE,
    .open =   dev_fifo_open,
    .read =   dev_fifo_read,
    .write =   dev_fifo_write,
    .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
};
//模塊入口
int __init dev_fifo_init(void)
{
    int i = 0;
    int n = 0;
    int ret;

    struct   device *device;
  gcd   = kzalloc(ndevices   * sizeof(struct   mycdev), GFP_KERNEL);

    if(!gcd){
        return   -ENOMEM;
    }

    //設備號 : 主設備號(12bit) | 次設備號(20bit)
    dev_num   = MKDEV(MAJOR_NUM, 0);
    //靜態註冊設備號
    ret   = register_chrdev_region(dev_num,ndevices,"dev_fifo");
    if(ret < 0){
    //靜態註冊失敗,進行動態註冊設備號
     ret   =alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
      if(ret < 0){
        printk("Fail to register_chrdev_region\n");
        goto   err_register_chrdev_region;
      }
    }
    //建立設備類
    cls   = class_create(THIS_MODULE, "dev_fifo");
    if(IS_ERR(cls)){
        ret   = PTR_ERR(cls);
        goto   err_class_create;
    }
    printk("ndevices :   %d\n",ndevices);
    for(n = 0;n < ndevices;n   ++)
    {
      //初始化字符設備
      cdev_init(&gcd[n].cdev,&fifo_operations);
      //添加設備到操做系統
      ret   = cdev_add(&gcd[n].cdev,dev_num + n,1);
      if (ret < 0)
      {
         goto   err_cdev_add;
      }
     //導出設備信息到用戶空間(/sys/class/類名/設備名)
      device   = device_create(cls,NULL,dev_num +n,NULL,"dev_fifo%d",n);
      if(IS_ERR(device)){
         ret   = PTR_ERR(device);
         printk("Fail to device_create\n");
         goto   err_device_create;    
      }
    }
    printk("Register   dev_fito to system,ok!\n");
    return   0;
err_device_create:

    //將已經導出的設備信息除去
    for(i = 0;i < n;i ++)
    {
       device_destroy(cls,dev_num + i);    
    }
err_cdev_add:
    //將已經添加的所有除去
    for(i = 0;i < n;i ++)
    {
       cdev_del(&gcd[i].cdev);
    }
err_class_create:
    unregister_chrdev_region(dev_num,   ndevices);
err_register_chrdev_region:
    return   ret;
}
void __exit dev_fifo_exit(void)
{
    int i;
    //刪除sysfs文件系統中的設備
    for(i = 0;i < ndevices;i   ++)
    {
        device_destroy(cls,dev_num + i);    
    }
    //刪除系統中的設備類
    class_destroy(cls);
    //從系統中刪除添加的字符設備
    for(i = 0;i < ndevices;i   ++)
    {
       cdev_del(&gcd[i].cdev);
    } 
    //釋放申請的設備號
    unregister_chrdev_region(dev_num,   ndevices);
    return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);   

頭文件內容:

dev_fifo_head.h

#ifndef _DEV_FIFO_HEAD_H
#define _DEV_FIFO_HEAD_H
#define DEV_FIFO_TYPE 'k'
#define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0x10)
#define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,0x11,int)
#define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,0x12,int)
#endif

Makefile :

ifeq ($(KERNELRELEASE),)
KERNEL_DIR ?=/lib/modules/$(shell uname -r)/build  
PWD :=$(shell pwd)
modules:
    $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) modules
.PHONY:modules clean
clean:
    $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) clean
else
    obj-m := dev_fifo.o  
endif

應用程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, const char *argv[])
{
    int fd ;
    int n;
    char buf[1024] = "hello   word";
    
    fd = open("/dev/dev_fifo0",O_RDWR);
    if(fd < 0){
        perror("Fail   ot open");
        return   -1;
    }
    printf("open   successful ,fd = %d\n",fd);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!\n",n);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!\n",n);
    return 0;
}

測試步驟:

(1)   加載模塊

sudo insmod hello.ko

(2)   建立設備節點

sudo mknod /dev/hello c 250 0

若是代碼中增長了自動建立設備節點的功能,這個步驟不要執行。

(3)   測試字符設備

gcc test.c -o run
sudo ./run
 


 


其餘網友提問彙總


 1. 兩個線程,兩個互斥鎖,怎麼造成一個死循環?


 2. 一個端口號能夠同時被兩個進程綁定嗎?


 3. 一個多線程的簡單例子讓你看清線程調度的隨機性

4. 粉絲提問|c語言:如何定義一個和庫函數名同樣的函數,並在函數中調用該庫函數

5.  [網友問答5]i2c的設備樹和驅動是如何匹配以及什麼時候調用probe的?

6. [粉絲問答6]子進程進程的父進程關係

7. 【粉絲問答7】局域網內終端是如何訪問外網?答案在最後




推薦閱讀


【1】嵌入式工程師到底要不要學習ARM彙編指令?必讀
【2】 Modbus協議概念最詳細介紹 必讀
【3】嵌入式工程師到底要不要學習ARM彙編指令?
【4】【從0學ARM】你不瞭解的ARM處理異常之道
【5】 4. 從0開始學ARM-ARM彙編指令其實很簡單
【6】 爲何使用結構體效率比較高? 必讀

 

進羣,請加一口君我的微信,帶你嵌入式入門進階。

 


本文分享自微信公衆號 - 一口Linux(yikoulinux)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索