字符设备驱动模型

<背景>

在linux系统驱动程序中,因为要面临各种各样的硬件,字符设备,快设备,网络接口设备,USB设备,PCI设备,平台设备,混在设备 ,设备不同则所对应的驱动模型不同,这就导致我们要掌握众多的驱动模型,能从这些众多的驱动模型中找到共性,则是学号linux驱动的关键

 

<linux 驱动程序的编写流程>

创建设备驱动文件:

mknode  /dev/文件名  类型(c,b...)  主设备号(当一个设备被挂载就会有相应的设备号) 次设备号(用于区分同类型的设备)

 

设备号:

静态创建设备号:

mkdev(主设备号,次设备号) //就是将主设备号和次设备号连接起来

分离设备号:

主:major(设备号)

次:minor(设备号)

 

申请分配设备号:

静态分配:

register_chrdev_region()

动态分配:

alloc_chrdev_region()

 

注销设备号:

当一个设备使用完就需要将占有的设备号释放

unregister_chrdev_region()

 

linux 系统通过chardev[] 组织256 个device_struct 结构,主要就是记录设备的名称和函数。

 

linux 设备描述结构

 

1)字符cdev结构体

 

      在Linux2.6 内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

 

struct cdev {

 

      struct kobject kobj;

 

      struct module *owner;  /*通常为THIS_MODULE*/

 

      struct file_operations *ops; /*在cdev_init()这个函数里面与cdev结构            联系起来*/

 

      struct  list_head list; 

 

      dev_t  dev;  /*设备号可以动态分配/或静态分配*/

 

      unsigned int count;

};

 

设备描述结构的分配:

静态分配:

struct cdev  名称 //直接定义一个设备描述结构

 

动态分配

struct cdev  *pdev= cdev_alloc();

 

 

初始化设备驱动程序:

1)定义一个设备描述结构

2)实现file_operation以便于应用程序的调用

llseek()

read()

write()

ioctl()/unlocked_ioctl() //该函数用于控制设备

open()

releade()

注意:在Linux系统中每打开一个文件便会在Linux内核中关联一个struct file。

 

来源于用户空间的指针不能被被内核空间直接使用,这时需要调用函数:

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

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

 

3)调用函数操作设备描述结构的函数对对该结构进行操作(放在设备初始化函数中,最后用moudle_init() 函数进行加载)

(1)将设备描述结构和设备操作集关系起来

cdev_init(stuct cdev *,const struct file_operation *)

(2)将设备描述结构注册到Linux系统中,让系统知道有这么个东西

    int cdev_add(struct cdev *,dev_t,unsigned);向linux系统注册一个cdev结构。

 

卸载设备驱动程序:

1)调用设备描述结构函数对该结构进行操作

(1)调用函数void cdev_del(struct cdev *);来将之前注册的设备描述结构注销掉。

 

实现对设备的控制:

1)用于操作设备描述结构的函数:

linux 2.6.36 以前:

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

 

Linux2.6.36 以后:

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

 

注意:这两个函数是使用在应用程序中的,和read(),write()一样。

2)定义命令

命令的实质就是一个整数,但是又不是一般的整数,该整数分为几个段:类型(8位),序号,参数传递方向,参数长度

Type (类型/幻术):表明这是属于哪个设备的命令

Number (序号):用来区分同一设备的不同命令

Direction:参数传递的方向(IOC_NONE(没有数据传送,IOC_READ(读取参数),IOC_WRITE(向设备写入参数))

Size:参数长度

 

《《设备驱动模型》》

 注:几乎所有的设备结构体都包含"strcut kobject kobj"和"srtuct list_head list"该结构体。

struct kobject kobj:

   该结构体用于构建Linux设备驱动模型的模型建立

struct list_head

{

    struct list_head *prev,*next;

};

   该结构体用于建立设备文件和设备结构体建立联系,以找到对应的设备操作函数。

 假设应用程序大开设备文件A,那么设备会产生一个inode节点,这样就可以通过高inode节点的i_devices找到设备结构体,进而找到设备操作函数。 

《驱动初始化》

 

<分配设备描述结构>

 

在linux的任何一种驱动模型中,都会有内核的一种结构来描述,字符设备在内核中使用结构体数组:
struct cdev

{

struct kobject kobj;

struct module *owner;

const struct file operations *ops;  //设备操作集

struct list_head list;

dev_t dev;  //设备号

unsigned int count;   //设备数

}

数据分析:

(1)设备数:

表示某种同类设备总共有几个

 

(2)设备号:
在目录/dev/ 中会有详细的描述,分为主设备号和次设备号。

主设备号:通过主设备号和设备文件和设备驱动联系起来。

次设备号:用于区分同类设备中的不同设备

在linux系统中,dev_t 这种数据类型是32位,其中高12位主设备号,低20位是次设备号。

 

使用宏:MKDEV(主设备号,次设备号) 将设备号组合起来

使用宏:MAJOR(设备号) 将主设备号分离出来

使用宏:MINOR(设备号) 将次设备号分离出来

 

(3)分配设备号:

1)静态分配:
使用函数register_chrdev_region()向内核申请,缺点,可能申请的设备号已经被使用,这样就会导致申请失败。

该函数会到该指针数组里面查找是否已经将该设备号注册进入内核。

static struct char_device_struct {

struct char_device_struct *next;

unsigned int major;

unsigned int baseminor;

int minorct;

const char *name;

struct file_operations *fops;

struct cdev *cdev; /* will die */

} *chrdevs[MAX_PROBE_HASH];

搜索方法:

搜索以哈希列表的方式,首先需要有个一个索引值,在这里使用设备号。

                       i = major_to_index(major);

调用:

                        i = major%255;

利用i对chrdevs[i]进行扫描,如果i已经在节点上面了,将会在进行对之前的内存分配进行释放后,然后返回错误,反之就会将该设备注册到内核链表中。

 

 

2)动态分配:

使用函数alloc_chrdev_region() 由内核分配一个主设备号。优点是内核知道哪些设备号没有被使用,所以不会出现分配已使用的设备号导致分配失败。

该内核函数会调用函数:

95__register_chrdev_region(unsigned int major, unsigned int baseminor,

  96                           int minorct, const char *name)

  97{

  98        struct char_device_struct *cd, **cp;

  99        int ret = 0;

 100        int i;

 101

 102        cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);

 103        if (cd == NULL)

 104                return ERR_PTR(-ENOMEM);

 105

 106        mutex_lock(&chrdevs_lock);

 107

 108        /* temporary */

 109        if (major == 0) {

 110                for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {

 111                        if (chrdevs[i] == NULL)

 112                                break;

 113                }

 114

 115                if (i == 0) {

 116                        ret = -EBUSY;

 117                        goto out;

 118                }

 119                major = i;

 120                ret = major;

 121        }

   }

该内核函数会扫描内核数组chrdevs[],知道找到没有被使用的设备号。

 

将字符设备加入到系统中去调用函数:

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

{

   p->dev = dev;

   p->count = count;

   return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);

}

该函数会操作操作一个全局变量cdev_map来将dev加入到哈希链表中(注:这里的cdev_map也是一个指针数组,该结构体如下。)

struct kobj_map {

    struct probe {

        struct probe *next;      /* 这样形成了链表结构 */

        dev_t dev;               /* 设备号 */

        unsigned long range;     /* 设备号的范围 */

        struct module *owner;

        kobj_probe_t *get;

        int (*lock) (dev_t, void *);

        void *data;              /* 指向struct cdev对象 */

    } *probes[255];

    struct mutex *lock;

}

kobj_map()和注册设备号一样,通过扫描probe[]数组来将字符设备加入到内核链表中。

(4)注销设备号:

无论是用那种方法分配设备的设备号,都应该在驱动退出时,使用unregister_chrdev_region()来释放设备号。

调用函数:

void cdev_del(struct cdev *p)

{

    cdev_unmap(p->dev,p->count);

    kobject_put(&p->kobj);

}

(5)设备操作集:

 

当应用程序调用系统函数,该系统函数同时是需要访问硬件的,则系统首先会根据函数去找一个表,根据这个表找到相应的驱动函数,然后再根据这些驱动完成相应的操作。——这个表就是设备操作集。

 

struct file_operations:

struct file_operations是一个函数指针的集合集合,定义能在设备进行的操作,若果设备不支持某项操作,就将其赋值为空指针

struct file_operations dev_fops

{

.llseek = NULL;

.read = dev_read;

.write = dev_write;

.ioctl = dev_ioctl;

.open = dev_open;

.release = dev_release;

}

 

<初始设备描述结构>

<注册设备描述结构>

<硬件初始化>

例:

《字符设备驱动》

<分配cdev>

首先设备描述结构是一个结构体数组,即是一个变量,凡是变量的分配有两种方式,一种是静态分配,另一种对动态分配。

1)静态分配

struct cdev mdev

2)动态分配

使用函数cdev_alloc()

struct dev *pdev = cdev_alloc();

 

<初始化cdev>

设备描述结构的初始化使用函数 cdev_init()来完成。

函数原型:

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

参数分析:

cdev :待初始化的设备描述结构

fops:设备对应的操作函数

 

<注册cdev>

字符设备的注册使用函数cdev_add()来完成。

函数原型:
cdev_add(struct cdev*p,dev_t dev,unsigned count)

参数分析:

p:待添加到内核中的设备描述结构

dev:设备号

count:该类设备的个数

 

<硬件初始化>

如果是一个实际的硬件,就去完成相应硬件设备的初始化。

 

 

《实现设备操作》(针对设备结构描述里面的 struct file_operations )

<打开设备文件>

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

操作open 是用来为以后的操作完成相应的第一步初始化工作,左右工作有:

1)表明设备号

2)启动设备

 

<关闭设备>

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

该操作的作用和open刚好相反,主要作用:

1)关闭设备

 

<重新定位指针>

loff_t(*llseek)(srtuct file*,loff_t ,int )

<读文设备件>

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

主要完成两件事:

1)从设备中读取数据(属于硬件访问操作)

2)将读取到数据返回给应用数据

注意:

凡是来源于用户空间的指针,不能被内核直接应用,必须使用专门的函数。一下两个函数会在read()调用。

 

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

int copy_to _user(void __user *to ,const ,const void *from ,int n)

 

<写设备文件>

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

 

注意:

struct file

在linux系统中,每一打开的 文件都会关联一个struct file,他由内核在打开文件时创建,在关闭文件时释放。

重要成员:
loff_t f_pos /文件读写位置指针/

struct file_operations *f_op /该文件所对应的操作/

 

struct inode

每一在文件系统中的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息,因此他和打开的 文件file 不同,一个文件没有被打开时是不会关联file结构的,但是却会关联一个inode 结构。

重要成员:

dev_t i_rdev  /设备号/

 

例:

struct file_operations dev_fops

{

.llseek = NULL;

.read = dev_read;

.write = dev_write;

.ioctl = dev_ioctl;

.open = dev_open;

.release = dev_release;

}

 

《驱动注销》

使用函数cdev_del() 来将字符设备从内核中注销掉。

原文地址:https://www.cnblogs.com/big-devil/p/8589494.html