Linux设备管理(二):内核中字符设备的管理

/************************************************************************************

*本文为个人学习记录,如有错误,欢迎指正。

*本文参考资料: 

*        https://www.cnblogs.com/embedded-tzp/p/4507240.html

*        http://www.169it.com/tech-qa-linux/article-5682294992603241339.html

*        https://blog.csdn.net/zhoujiaxq/article/details/7646013

*        http://www.cnblogs.com/xiaojiang1025/p/6196198.html

************************************************************************************/

1. 字符设备的管理框架

Linux内核对设备的管理是基于kobject来进行的,详见Linux设备管理:kobject, kset, ktype分析。Linux对字符设备的管理框架依赖于struct kobj_map、struct cdev、dev_t dev、struct file_operations等数据结构。如下图所示。

2. 字符设备数据结构

 Linux内核中关于字符设备的操作函数存放在 "/kernel/fs/char_dev.c" 文件中。

2.1 dev_t dev

一个字符设备或块设备都有一个主设备号(major)和一个次设备号(minor)。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

Linux内核中,使用dev_t来描述设备号。

typedef u_long dev_t;  // 在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

Linux内核中提供以下几个宏来操作dev_t。

#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))  // 从设备号中提取主设备号
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))   // 从设备号中提取次设备号
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))         // 将主、次设备号拼凑为设备号

2.2 struct cdev

Linux内核中,使用struct cdev结构体来描述一个字符设备。

<include/linux/cdev.h>  

struct cdev 
{   
  struct kobject kobj;              //内嵌的内核对象 
  struct module *owner;             //该字符设备所在的内核模块(所有者)的对象指针,一般为THIS_MODULE,主要用于模块计数  
  const struct file_operations *ops;//该结构描述了字符设备所能实现的操作集(打开、关闭、读/写、...),是极为关键的一个结构体
  struct list_head list;            //用来将已经向内核注册的所有字符设备形成链表
  dev_t dev;                        //字符设备的设备号,由主设备号和次设备号构成(如果是一次申请多个设备号,此设备号为第一个)
  unsigned int count;               //隶属于同一主设备号的次设备号的个数
};

2.3 struct file_operations

Linux内核中,使用file_operations结构来管理设备驱动程序的函数,这个结构的每一个成员的名字都对应着一个函数调用。

用户进程利用在对设备文件进行操作时(read/write等),系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取其file_operations结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

struct file_operations 
{
  struct module *owner;  
    /* 模块拥有者,一般为 THIS——MODULE */
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    /* 从设备中读取数据,成功时返回读取的字节数,出错返回负值(绝对值是错误码) */
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);   
    /* 向设备发送数据,成功时该函数返回写入字节数。若为被实现,用户调层用write()时系统将返回 -EINVAL*/
  int (*mmap) (struct file *, struct vm_area_struct *);  
    /* 将设备内存映射内核空间进程内存中,若未实现,用户层调用 mmap()系统将返回 -ENODEV */
  long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);  
    /* 提供设备相关控制命令(读写设备参数、状态,控制设备进行读写...)的实现,当调用成功时返回一个非负值 */
  int (*open) (struct inode *, struct file *);  
    /* 打开设备 */
  int (*release) (struct inode *, struct file *);  
    /* 关闭设备 */
  int (*flush) (struct file *, fl_owner_t id);  
    /* 刷新设备 */
  loff_t (*llseek) (struct file *, loff_t, int);  
    /* 用来修改文件读写位置,并将新位置返回,出错时返回一个负值 */
  int (*fasync) (int, struct file *, int);  
    /* 通知设备 FASYNC 标志发生变化 */
  unsigned int (*poll) (struct file *, struct poll_table_struct *);  
    /* POLL机制,用于询问设备是否可以被非阻塞地立即读写。当询问的条件未被触发时,用户空间进行select()和poll()系统调用将引起进程阻塞 */
};

2.4 struct kobj_map

Linux内核中,所有的字符设备都会记录在一个cdev_map 变量中。cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(cdev)。

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;
}
struct kobj_map

字符设备驱动程序通过调用cdev_add把它所管理的字符设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。对系统而言,当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统就可以找到它。对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口调用该设备的驱动程序(具体调用流程详见Linux字符设备:应用程序调用字符设备驱动程序的流程)。

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    p->dev = dev;
    p->count = count;

   /*申请并填充struct probe,再通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255),
     然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中*/ 
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

cdev_map对字符设备的管理方式有两种:

(1)一个cdev对象对应这一个/多个设备号的情况

在cdev_map中, 一个probes对象就对应一个主设备号;多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象。

(2)主设备号超过255的情况

当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200]可以表示设备号200,455...3895等所有对255取余是200的数字。

 

原文地址:https://www.cnblogs.com/linfeng-learning/p/9307646.html