天天看點

Linux字元裝置驅動程式開發(2)-字元裝置驅動模型分析1、裝置描述結構cdev2、字元裝置驅動模型3、範例驅動分析4、自己編寫驅動代碼

1、裝置描述結構cdev

在Linux系統中,裝置的類型非常繁多,如:字元裝置,塊裝置,網絡接口裝置,USB裝置,PCI裝置,平台裝置,混雜裝置……,而裝置類型不同,也意味着其對應的驅動程式模型不同,這樣就導緻了我們需要去掌握衆多的驅動程式模型。那麼能不能從這些衆多的驅動模型中提煉出一些具有共性的規則,則是我們能不能學好Linux驅動的關鍵。這些規則可以參考第二節的裝置驅動模型,在任何一種驅動模型中,裝置都會用核心中的一種結構來描述。我們的字元裝置在核心中使用structcdev來描述。

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops; //裝置操作集
	struct list_head list;
	dev_t dev; //裝置号
	unsigned int count; //裝置數
};
           

在這些成中有一些成員是開發者不需要關注的,有一些則要關注,比如說count是裝置數,表示目前擁有的硬體裝置個數,還有裝置号,裝置操作集。

1.1裝置号

裝置号包含主裝置号和次裝置号,使用:

#ls  -l  /dev/

來檢視目前驅動檔案的裝置号,在中間有2列用逗号隔開的數字,前面的是主裝置号,後面的是次裝置号。

主裝置号用來将字元裝置檔案與字元驅動程式建立起對應關系。

次裝置号用來區分同一種裝置下的不同裝置,比如說有3個序列槽,它們隻需要一個驅動程式即可,那麼怎麼區分這幾個序列槽呢?答案就是使用次裝置号。

裝置号這個類型使用的是dev_t,那麼主裝置号和次裝置号是怎麼構成dev_t的呢?又怎麼樣從dev_t中得到主次裝置号呢?

dev_t這種類型其實質為32位的unsigned int,其中高12位為主裝置号,低20位為次裝置号.

問1:如果知道主裝置号,次裝置号,怎麼組合成dev_t類型

答:dev_t dev = MKDEV(主裝置号,次裝置号)

問2: 如何從dev_t中分解出主裝置号?

答: 主裝置号 = MAJOR(dev_t dev)

問3: 如何從dev_t中分解出次裝置号?

答: 次裝置号=MINOR(dev_t dev)

對程式員來說次裝置号可以自己定義,但是這裝置号卻很敏感,容易發生沖突,如何為裝置配置設定一個主裝置号呢?

靜态申請:

開發者自己選擇一個數字作為主裝置号,然後通過函數register_chrdev_region向核心申請使用。缺點:如果申請使用的裝置号已經被核心中的其他驅動使用了,則申請失敗。

動态配置設定:

使用alloc_chrdev_region由核心配置設定一個可用的主裝置号。優點:因為核心知道哪些号已經被使用了,是以不會導緻配置設定到已經被使用的号。

登出裝置号:

為了避免裝置号的浪費,不論使用何種方法配置設定裝置号,都應該在驅動退出時,使用unregister_chrdev_region函數釋放這些裝置号。

1.2操作函數集

操作集函數ops,它是struct file_operations *ops,在核心代碼中打開這個結構:

/*
 * NOTE:
 * all file operations except setlease can be called without
 * the big kernel lock held in all filesystems.
 */
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 (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, 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 (*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);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
};
           

可以發現在這個結構裡面全部成員都是函數指針,這些名字都是系統調用的函數。其實這個結構就是實作應用程式系統調用時的函數到核心驅動函數映射的功能。

Struct file_operations是一個函數指針的集合,定義能在裝置上進行的操作。結構中的函數指針指向驅動中的函數,這些函數實作一個針對裝置的操作, 對于不支援的操作則設定函數指針為 NULL。例如:

struct file_operations dev_fops = {

.llseek = NULL,

.read = dev_read,

.write = dev_write,

.ioctl = dev_ioctl,

.open = dev_open,

.release = dev_release,

};

2、字元裝置驅動模型

裝置驅動模型需遵循下面3個流程:

2.1、驅動初始化

驅動初始化一般在子產品初始化函數中完成,這裡的工作主要和裝置描述結構和硬體初始化相關。流程一般為

配置設定裝置描述結構cdev

cdev變量的定義可以采用靜态和動态兩種辦法

• 靜态配置設定,就是直接自己定義一個

struct cdev mdev;

• 動态配置設定,就是由核心動态配置設定

struct cdev *pdev = cdev_alloc();

初始化裝置描述結構cdev

struct cdev的初始化使用cdev_init函數來完成。

cdev_init(struct cdev *cdev, const struct file_operations *fops)

參數:

cdev: 待初始化的cdev結構

fops: 裝置對應的操作函數集

注冊裝置描述結構cdev

字元裝置的注冊使用cdev_add函數來完成。

cdev_add(struct cdev *p, dev_t dev, unsigned count)

參數:

p: 待添加到核心的字元裝置結構

dev: 裝置号

count: 該類裝置的裝置個數

硬體初始化

根據相應硬體的晶片手冊,完成初始化。

2.2、實作裝置操作

裝置操作又稱裝置方法,這裡實作幾個函數: int (*open) (struct inode *, struct file *);//打開裝置,響應open系統

int (*release) (struct inode *, struct file *);//關閉裝置,響應close系統調用 loff_t (*llseek) (struct file *, loff_t, int);//重定位讀寫指針,響應lseek系統調用

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//從裝置讀取資料,響應read系統調用

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向裝置寫入資料,響應write系統調用

這些裝置函數在應用程式調用open、read、write、close時将會通過file_operations 的映射,調用上面幾個裝置檔案。 那麼這些函數改怎麼實作呢?這裡先介紹幾個重要的知識點。

Struct file:

struct file { 
  struct file *f_next,**f_pprev; 
  struct dentry *f_dentry; 
  struct file_operations *f_op; 
  mode_t f_mode; 
  loff_t f_pos; 
  unsigned int f_count,f_flags; 
  unsigned long f_reada,f_ramax,f_raend,f_ralen,f_rawin; 
  struct fown_struct f_owner; 
  unsigned long f_version; 
  void *private_data; 
  };
           

在Linux系統中,每一個打開的檔案,在核心中都會關聯一個struct file,它由核心在打開檔案時建立, 在檔案關閉後釋放。

重要成員:

loff_t f_pos

struct file_operations *f_op

Struct inode: 每一個存在于檔案系統裡面的檔案都會關聯一個inode 結構,該結構主要用來記錄檔案實體上的資訊。是以, 它和代表打開檔案的file結構是不同的。一個檔案沒有被打開時不會關聯file結構,但是卻會關聯一個inode 結構。

重要成員:

dev_t i_rdev:裝置号

open

int mem_open(struct inode *inode, struct file *filep)

open裝置方法是驅動程式用來為以後的操作完成初始化準備工作的。在大部分驅動程式中,open完成如下工作:

v标明次裝置号

v啟動裝置

release

int mem_release(struct inode *inode, struct file *filep)

release方法的作用正好與open相反。這個裝置方法有時也稱為close,它應該:

v關閉裝置。

read

static ssize_t mem_read(struct file *filep, char __user *buf, size_t size, loff_t *ppos)

read裝置方法通常完成2件事情:

v從裝置中讀取資料(屬于硬體通路類操作)

v将讀取到的資料傳回給應用程式

ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *offp)

參數分析:

filp:與字元裝置檔案關聯的file結構指針, 由核心建立。

buff : 從裝置讀取到的資料,需要儲存到的位置。由read系統調用提供該參數。

count: 請求傳輸的資料量,由read系統調用提供該參數。

offp: 檔案的讀寫位置,由核心從file結構中取出後,傳遞進來。

buff參數是來源于使用者空間的指針,這類指針都不能被核心代碼直接引用,必須使用專門的函數,read一般使用copy_to_user

int copy_to_user(void __user *to, const void *from, int n)

int copy_from_user(void *to, const void __user *from, int n)

write

static ssize_t mem_write(struct file *filep, const char __user *buf, size_t size, loff_t *ppos)

write裝置方法通常完成2件事情:

v從應用程式提供的位址中取出資料

v将資料寫入裝置(屬于硬體通路類操作)一般使用v int copy_from_user(void *to, const void __user *from, int n)

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//其參數類似于read

llseek函數(檔案定位函數):

目前讀寫位置儲存在filp->ops儲存了目前讀寫位置,然後根據whence和偏移來确定需要讀寫的位置,最後修改fpos即可。

如果是SEEK_SET,則是從起始位置開始偏置

如果是SEEK_CUR,則是從目前位置開始偏移

如果是SEEK_END,則是從檔案尾部開始偏置

判斷新的位置是否合法,如果合法寫入f_ops中

2.3、登出驅動裝置

當我們從核心中解除安裝驅動程式的時候,需要使用cdev_del函數來完成字元裝置的登出。

3、範例驅動分析

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>


int dev1_registers[5];
int dev2_registers[5];

struct cdev cdev; 
dev_t devno;

/*檔案打開函數*/
int mem_open(struct inode *inode, struct file *filp)
{
    
    /*擷取次裝置号*/
    int num = MINOR(inode->i_rdev);
    
    if (num==0)
        filp->private_data = dev1_registers;
    else if(num == 1)
        filp->private_data = dev2_registers;
    else
        return -ENODEV;  //無效的次裝置号
    
    return 0; 
}

/*檔案釋放函數*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/*讀函數*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*擷取裝置的寄存器基位址*/

  /*判斷讀位置是否有效*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;

  /*讀資料到使用者空間*/
  if (copy_to_user(buf, register_addr+p, count))
  {
    ret = -EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/*寫函數*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*擷取裝置的寄存器位址*/
  
  /*分析和擷取有效的寫長度*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;
    
  /*從使用者空間寫入資料*/
  if (copy_from_user(register_addr + p, buf, count))
    ret = -EFAULT;
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/* seek檔案定位函數 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{ 
    loff_t newpos;

    switch(whence) {
      case SEEK_SET: 
        newpos = offset;
        break;

      case SEEK_CUR: 
        newpos = filp->f_pos + offset;
        break;

      case SEEK_END: 
        newpos = 5*sizeof(int)-1 + offset;
        break;

      default: 
        return -EINVAL;
    }
    if ((newpos<0) || (newpos>5*sizeof(int)))
    	return -EINVAL;
    	
    filp->f_pos = newpos;
    return newpos;

}

/*檔案操作結構體*/
static const struct file_operations mem_fops =
{
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
};

/*裝置驅動子產品加載函數*/
static int memdev_init(void)
{
  /*初始化cdev結構*/
  cdev_init(&cdev, &mem_fops);
  
  /* 注冊字元裝置 */
  alloc_chrdev_region(&devno, 0, 2, "memdev");
  cdev_add(&cdev, devno, 2);
}

/*子產品解除安裝函數*/
static void memdev_exit(void)
{
  cdev_del(&cdev);   /*登出裝置*/
  unregister_chrdev_region(devno, 2); /*釋放裝置号*/
}

MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);
           

首先看子產品初始化函數,先完成cdev的配置設定,這裡采用靜态配置設定的方法,然後是初始化cdev,參數一是cdev指針,參數二是mem_fops,即支援的操作函數。 注冊使用cdev_add()函數,參數有3個,參數一是cdev指針,參數二是devno裝置号,裝置号通過alloc_chrdev_region()函數動态配置設定,它的參數有4個,裝置号填入的地方,後面2個參數是次裝置号開始的數字和次裝置号的個數,最後使用裝置号的驅動的名字。參數三是同類型裝置的個數,這裡有2個。

接下來就要實作裝置操作方法,即實作mem_fops裡面定義的函數。 open函數: 提取次裝置号,然後根據次裝置号擷取寄存器的基位址。由于這裡是模拟的硬體,這裡沒有裝置啟動函數。

read函數: read函數首先需要根據Struct file檔案提取出寄存器的基位址,才能對正确的裝置進行讀寫。 然後判斷讀寫位置是否有效,如果目前讀寫位置p比裝置總位元組數都大,顯然是不能讀取的,如果需要讀寫位元組數count大于可讀寫位元組數(總位元組減去目前讀寫位置),那麼隻能令count = 總位元組數-目前讀寫位置,即還有多少讀多少。 讀寫位置合法後就可以把資料從裝置發送給使用者了,使用copy_to_user函數,參數分别為應用程式存放資料的位址,裝置存放資料的位址(基位址加上讀寫位置),讀寫直接個數 最後根據讀寫情況修改目前讀寫位置和其他參數即可。

write函數: 這個函數和read函數類似,這裡不在分析

llseek函數(檔案定位函數):

目前讀寫位置儲存在filp->ops儲存了目前讀寫位置,然後根據whence和偏移來确定需要讀寫的位置,最後修改fpos即可。 如果是SEEK_SET,則是從起始位置開始偏置 如果是SEEK_CUR,則是從目前位置開始偏移 如果是SEEK_END,則是從檔案尾部開始偏置

判斷新的位置是否合法,如果合法寫入f_ops中

release函數,裝置關閉函數: 由于這裡是模拟的硬體裝置,是以不需要做什麼,直接傳回即可。

裝置登出: 這一步需要登出裝置并釋放裝置号。   cdev_del(&cdev);  

  unregister_chrdev_region(devno, 2);

4、自己編寫驅動代碼

根據上面分析的模型,就可以編寫出自己需要的驅動代碼了。在編寫代碼過程中,最好不參照範例代碼,而是根據總結出來的架構,一點點往裡面填充。

繼續閱讀