精华内容
下载资源
问答
  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。块设备:是指可以...
    Linux设备驱动之字符设备驱动
    
    展开全文
  • Linux设备驱动之字符设备驱动

    万次阅读 多人点赞 2016-07-01 19:36:15
    字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。 块设备:是指可以...

    一、linux系统将设备分为3类:字符设备、块设备、网络设备。

    应用程序调用的流程框图:



    三种设备的定义分别如下,

    字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。

    块设备:是指可以从设备的任意位置读取一定长度的数据设备。块设备如硬盘、磁盘、U盘和SD卡等存储设备。

    网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。



    对于字符设备和块设备来说,在/dev目录下都有对应的设备文件。linux用户程序通过设备文件或叫做设备节点来使用驱动程序操作字符设备和块设备。


    二、字符设备和快设备启动与用户控件访问该设备的程序 ,三者之间的关系。



    如上图,在linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号,以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

    在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operations之间的连接,通过cdev_add( )向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。

    用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。


    三、字符设备驱动模板



    这张图基本表示了字符驱动所需要的模板,只是缺少class的相关内容,class主要是用来自动创建设备节点的,还有就是一个比较常用的ioctl()函数没有列在上边。

    下面具体说一下每个步骤的详细内容。

    1.驱动初始化

    1.1分配cdev

    在2.6的内核中使用cdev结构体来描述字符设备,在驱动中分配cdev,主要是分配一个cdev结构体与申请设备号。

    例如:

    /*……*/
    /* 分配cdev*/
    struct cdev btn_cdev;
    /*……*/
    /* 1.1 申请设备号*/
    if(major){
    	//静态
    	dev_id = MKDEV(major, 0);
    	register_chrdev_region(dev_id, 1, "button");
    } else {
    	//动态
    	alloc_chardev_region(&dev_id, 0, 1, "button");
    	major = MAJOR(dev_id);
    }
    /*……*/
    从上面的代码可以看出,申请设备号有动静之分,其实设备号还有主次之分。


    在Linux中以主设备号用来标识与设备文件相连的驱动程序。次编号被驱动程序用来辨别操作的是哪个设备。cdev 结构体的 dev_t 成员定义了设备号,为 32 位,其中高 12 位为主设备号,低20 位为次设备号。

    设备号的获得与生成:
    获得:主设备号:MAJOR(dev_t dev);
    次设备号:MINOR(dev_t dev);
    生成:MKDEV(int major,int minor);

    设备号申请的动静之分:
    静态:

    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    /*功能:申请使用从from开始的count 个设备号(主设备号不变,次设备号增加)*/
    静态申请相对较简单,但是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

    动态:

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
    /*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/
    动态申请简单,易于驱动推广,但是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。


    1.2.初始化cdev

    1 void cdev_init(struct cdev *, struct file_operations *); 
    2 cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。


    1.3.注册cdev

    1 int cdev_add(struct cdev *, dev_t, unsigned);
    2      cdev_add()函数向系统添加一个 cdev,完成字符设备的注册。

    1.4.硬件初始化

    硬件初始化主要是硬件资源的申请与配置,主要涉及地址映射,寄存器读写等相关操作,每一个开发板不同都有不同的实现方式,这里以FS4412为例:

    	/* 地址映射 */
    	gpx2con = ioremap(GPX2CON, 4);
    	if (gpx2con == NULL)
    	{
    		printk("gpx2con ioremap err\n");
    		goto err3;
    	}
    	gpx2dat = ioremap(GPX2DAT, 4);
    	if (gpx2dat == NULL)
    	{
    		printk("gpx2dat ioremap err\n");
    		goto err4;
    	}
    
    	gpx1con = ioremap(GPX1CON, 4);
    	gpx1dat = ioremap(GPX1DAT, 4);
    
    	gpf3con = ioremap(GPF3CON, 4);
    	gpf3dat = ioremap(GPF3DAT, 4);
    
    	writel(readl(gpx2con)&(~(0xf<<28))|(0x1<<28), gpx2con);
    	writel((0x1<<7), gpx2dat);
    	writel(readl(gpx1con)&(~(0xf<<0))|(0x1<<0), gpx1con);
    	writel((0x1<<0), gpx1dat);
    	writel(readl(gpf3con)&(~(0xf<<16))|(0x1<<16), gpf3con);
    	writel(readl(gpf3dat)|(0x1<<4), gpf3dat);
    	writel(readl(gpf3con)&(~(0xf<<20))|(0x1<<20), gpf3con);
    	writel(readl(gpf3dat)|(0x1<<5), gpf3dat);

    2.实现设备操作

    用户空间的程序以访问文件的形式访问字符设备,通常进行open、read、write、close等系统调用。而这些系统调用的最终落实则是file_operations结构体中成员函数,它们是字符设备驱动与内核的接口。以FS4412的LED驱动为例:

    struct file_operations hello_fops = {
    	.owner = THIS_MODULE,
    	.open = hello_open,
    	.release = hello_release,
    	.read = hello_read,
    	.write = hello_write,
    };

    上面代码中的hello_open、hello_write、hello_read是要在驱动中自己实现的。file_operations结构体成员函数有很多个,下面就选几个常见的来展示:

    2.1. open()函数

    原型:

    1 int(*open)(struct inode *, struct file*); 
    2 /*打开*/

    2.2. read( )函数

    原型:

    ssize_t(*read)(struct file *, char __user*, size_t, loff_t*); 
    /*用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值*/

    2.3. write( )函数

    原型:

    1 ssize_t(*write)(struct file *, const char__user *, size_t, loff_t*);
    2 /*向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,
    3 当用户进行write()系统调用时,将得到-EINVAL返回值*/
    2.4. close( )函数

    原型:

    1 int(*release)(struct inode *, struct file*); 
    2 /*关闭*/

    2.5 ioctl( )函数

    原型:

    int ioctl(int fd, ind cmd, …);

    这里主要说一下ioctl函数。ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。
    其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。
    ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。

    如果不用ioctl的话,也可以实现对设备I/O通道的控制,但那就是蛮拧了。例如,我们可以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。

    所以,我们就使用ioctl来实现控制的功能。要记住,用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。

    在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情,因为设备都是特定的,这里也没法说。关键在于怎么样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径。

    命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。这些错误都会导致不可预料的事情发生,而当程序员发现了这些奇怪的事情的时候,再来调试程序查找错误,那将是非常困难的事情。

    所以在Linux核心中是这样定义一个命令码的:

          MAGIC: 幻数  8位的数  0 - 255    'L' 
          nr: 序数,用来区分同一类设备的不同命令
          #define   CMD     _IO(MAGIC, nr)

     hello.h
          #define   LED_MAGIC  'L'
          #define   LED_ON    _IO(LED_MAGIC, 1)
          #define   LED_OFF   _IO(LED_MAGIC, 2)

    通过这种方式来定义cmd,并在ioctl程序中区分cmd。


    2.5. 补充说明

    1. 在Linux字符设备驱动程序设计中,有3种非常重要的数据结构:struct file、struct inode、struct file_operations。

    struct file 代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建, 在文件关闭后释放。其成员loff_t f_pos 表示文件读写位置。

    struct inode 用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。其成员dev_t i_rdev表示设备号。

    struct file_operations 一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作, 对于不支持的操作保留为NULL。

    2. 在read( )和write( )中的buff 参数是用户空间指针。因此,它不能被内核代码直接引用,因为用户空间指针在内核空间时可能根本是无效的——没有那个地址的映射。因此,内核提供了专门的函数用于访问用户空间的指针:

    1 unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
    2 unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

    3. 驱动注销

    3.1. 删除cdev

    在字符设备驱动模块卸载函数中通过cdev_del()函数向系统删除一个cdev,完成字符设备的注销。
    /*原型:*/
    void cdev_del(struct cdev *);
    /*例:*/
    cdev_del(&btn_cdev);

    3.2. 释放设备号

    在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号。
    /*原型:*/
    void unregister_chrdev_region(dev_t from, unsigned count);
    /*例:*/
    unregister_chrdev_region(MKDEV(major, 0), 1);

    四、字符设备驱动程序基础:

    4.1 cdev结构体

    前面写到如何向系统申请一个设备号,设备号就像我们的身份证号一样,号本身并没有什么特殊的意义,只有把这个号和人对应才有意义,通用设备号也需要和一个特殊的东西对于,这就是cdev, cdev是linux下抽象出来的一个用来描述一个字符设备的结构体,在linux下定义如下:

    struct cdev {
                    struct kobject kobj;
                    struct module *owner;
                    const struct file_operations *ops;
                    struct list_head list;
                    dev_t dev;
                    unsigned int count;
            };

    结构体中有几个成员事我们写驱动的时候必须关心的:
    dev 类型是dev_t,也就是我们的设备号
    ops是一个同样也是一个结构体并且是一个字符驱动实现的主体,字符驱动通常需要和应用程序交互,在学linux系统编程的时候,都会讲到linux 应用程序通过系统调用陷入到内核空间,从而执行内核代码,而驱动作为内核的一部分同样也是需要在内核空间执行的,ops也就是file_operations这个结构体就是我们的驱动为应用程序调用驱动而实现的一个操作的集合。后面会详细讲解。

    cdev 结构体的dev_t 成员定义了设备号,为32位,其中12位是主设备号,20位是次设备号,我们只需使用二个简单的宏就可以从dev_t 中获取主设备号和次设备号:

    MAJOR(dev_t dev)
    MINOR(dev_t dev)

    相反地,可以通过主次设备号来生成dev_t:

    MKDEV(int major,int minor)


    4.2 Linux 2.6内核提供一组函数用于操作cdev 结构体

    1:void cdev_init(struct cdev*,struct file_operations *);
    2:struct cdev *cdev_alloc(void);
    3:int cdev_add(struct cdev *,dev_t,unsigned);
    4:void cdev_del(struct cdev *);

    其中(1)用于初始化cdev结构体,并建立cdev与file_operations 之间的连接。(2)用于动态分配一个cdev结构,(3)向内核注册一个cdev结构,(4)向内核注销一个cdev结构。

    cdev_add实现cdev的注册,linux内核里维护了一个cdev_map的表,所谓cdev的注册就是把我们的cdev注册到cdev_map表上,cdev_map表结构如图:



    4.3 Linux 2.6内核分配和释放设备号

    在调用cdev_add()函数向系统注册字符设备之前,首先应向系统申请设备号,有二种方法申请设备号,一种是静态申请设备号:

    5:int register_chrdev_region(dev_t from,unsigned count,const char *name)

    另一种是动态申请设备号:

    6:int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);

    其中,静态申请是已知起始设备号的情况,如先使用cat /proc/devices 命令查得哪个设备号未事先使用(不推荐使用静态申请);动态申请是由系统自动分配,只需设置major = 0即可。

    相反地,在调用cdev_del()函数从系统中注销字符设备之后,应该向系统申请释放原先申请的设备号,使用:

    7:void unregister_chrdev_region(dev_t from,unsigned count);

    4.4 cdev结构的file_operations结构体

    这个结构体是字符设备当中最重要的结构体之一,file_operations 结构体中的成员函数指针是字符设备驱动程序设计的主体内容,这些函数实际上在应用程序进行Linux 的 open()、read()、write()、close()、seek()、ioctl()等系统调用时最终被调用。

    1 struct file_operations {
     2 
     3 /*拥有该结构的模块计数,一般为THIS_MODULE*/
     4 struct module *owner;
     5 
     6 /*用于修改文件当前的读写位置*/
     7 loff_t (*llseek) (struct file *, loff_t, int);
     8 
     9 /*从设备中同步读取数据*/
    10 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    11 
    12 /*向设备中写数据*/
    13 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    14 
    15 
    16 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    17 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    18 int (*readdir) (struct file *, void *, filldir_t);
    19 
    20 /*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
    21 unsigned int (*poll) (struct file *, struct poll_table_struct *);
    22 
    23 /*执行设备的I/O命令*/
    24 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    25 
    26 
    27 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    28 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    29 
    30 /*用于请求将设备内存映射到进程地址空间*/
    31 int (*mmap) (struct file *, struct vm_area_struct *);
    32 
    33 /*打开设备文件*/
    34 int (*open) (struct inode *, struct file *);
    35 int (*flush) (struct file *, fl_owner_t id);
    36 
    37 /*关闭设备文件*/
    38 int (*release) (struct inode *, struct file *);
    39 
    40 
    41 int (*fsync) (struct file *, struct dentry *, int datasync);
    42 int (*aio_fsync) (struct kiocb *, int datasync);
    43 int (*fasync) (int, struct file *, int);
    44 int (*lock) (struct file *, int, struct file_lock *);
    45 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    46 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    47 int (*check_flags)(int);
    48 int (*flock) (struct file *, int, struct file_lock *);
    49 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    50 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    51 int (*setlease)(struct file *, long, struct file_lock **);
    52 };

    4.5 file结构

    file 结构代表一个打开的文件,它的特点是一个文件可以对应多个file结构。它由内核再open时创建,并传递给在该文件上操作的所有函数,直到最后close函数,在文件的所有实例都被关闭之后,内核才释放这个数据结构。

    在内核源代码中,指向 struct file 的指针通常比称为filp,file结构有以下几个重要的成员:

    1 struct file{
     2 
     3 mode_t fmode; /*文件模式,如FMODE_READ,FMODE_WRITE*/
     4 
     5 ......
     6 
     7 loff_t f_pos; /*loff_t 是一个64位的数,需要时,须强制转换为32位*/
     8 
     9 unsigned int f_flags; /*文件标志,如:O_NONBLOCK*/
    10 
    11 struct file_operations *f_op;
    12 
    13 void *private_data; /*非常重要,用于存放转换后的设备描述结构指针*/
    14 
    15 .......
    16 
    17 };

    4.6 inode 结构

    内核用inode 结构在内部表示文件,它是实实在在的表示物理硬件上的某一个文件,且一个文件仅有一个inode与之对应,同样它有二个比较重要的成员:

    1 struct inode{
     2 
     3 dev_t i_rdev; /*设备编号*/
     4 
     5 struct cdev *i_cdev; /*cdev 是表示字符设备的内核的内部结构*/
     6 
     7 };
     8 
     9 可以从inode中获取主次设备号,使用下面二个宏:
    10 
    11 /*驱动工程师一般不关心这二个宏*/
    12 
    13 unsigned int imajor(struct inode *inode);
    14 
    15 unsigned int iminor(struct inode *inode);

    4.7字符设备驱动模块加载与卸载函数

    在字符设备驱动模块加载函数中应该实现设备号的申请和cdev 结构的注册,而在卸载函数中应该实现设备号的释放与cdev结构的注销。

    我们一般习惯将cdev内嵌到另外一个设备相关的结构体里面,该设备包含所涉及的cdev、私有数据及信号量等等信息。常见的设备结构体、模块加载函数、模块卸载函数形式如下:

    1/*设备结构体*/
     2 
     3 struct xxx_dev{
     4 
     5 struct cdev cdev;
     6 
     7 char *data;
     8 
     9 struct semaphore sem;
    10 
    11 ......
    12 
    13 };
    14 
    15 
    16 
    17 /*模块加载函数*/
    18 
    19 static int __init xxx_init(void)
    20 
    21 {
    22 
    23 .......
    24 
    25 初始化cdev结构;
    26 
    27 申请设备号;
    28 
    29 注册设备号;
    30 
    31 
    32 
    33 申请分配设备结构体的内存; /*非必须*/
    34 
    35 }
    36 
    37 
    38 
    39 /*模块卸载函数*/
    40 
    41 static void __exit xxx_exit(void)
    42 
    43 {
    44 
    45 .......
    46 
    47 释放原先申请的设备号;
    48 
    49 释放原先申请的内存;
    50 
    51 注销cdev设备;
    52 
    53 }
    54 
    55

    4.8字符设备驱动的 file_operations 结构体重成员函数

    1 /*读设备*/
     2 
     3 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
     4 
     5 {
     6 
     7 ......
     8 
     9 使用filp->private_data获取设备结构体指针;
    10 
    11 分析和获取有效的长度;
    12 
    13 /*内核空间到用户空间的数据传递*/
    14 
    15 copy_to_user(void __user *to, const void *from, unsigned long count);
    16 
    17 ......
    18 
    19 }
    20 
    21 /*写设备*/
    22 
    23 ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
    24 
    25 {
    26 
    27 ......
    28 
    29 使用filp->private_data获取设备结构体指针;
    30 
    31 分析和获取有效的长度;
    32 
    33 /*用户空间到内核空间的数据传递*/
    34 
    35 copy_from_user(void *to, const void __user *from, unsigned long count);
    36 
    37 ......
    38 
    39 }
    40 
    41 /*ioctl函数*/
    42 
    43 static int xxx_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
    44 
    45 {
    46 
    47 ......
    48 
    49 switch(cmd){
    50 
    51 case xxx_CMD1:
    52 
    53 ......
    54 
    55 break;
    56 
    57 case xxx_CMD2:
    58 
    59 .......
    60 
    61 break;
    62 
    63 default:
    64 
    65 return -ENOTTY; /*不能支持的命令*/
    66 
    67 }
    68 
    69 return 0;
    70 
    71 }

    4.9、字符设备驱动文件操作结构体模板

    1 struct file_operations xxx_fops = {
     2 
     3 .owner = THIS_MODULE,
     4 
     5 .open = xxx_open,
     6 
     7 .read = xxx_read,
     8 
     9 .write = xxx_write,
    10 
    11 .close = xxx_release,
    12 
    13 .ioctl = xxx_ioctl,
    14 
    15 .lseek = xxx_llseek,
    16 
    17 };
    18 
    19 上面的写法需要注意二点,一:结构体成员之间是以逗号分开的而不是分号,结构体字段结束时最后应加上分号。

    5.自动创建设备节点

    在刚开始写Linux设备驱动程序的时候,很多时候都是利用mknod命令手动创建设备节点,实际上Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点。

    在2.6.17以前,在/dev目录下生成设备文件很容易,
    devfs_mk_bdev
    devfs_mk_cdev
    devfs_mk_symlink
    devfs_mk_dir
    devfs_remove
    这几个是纯devfs的api,2.6.17以前可用,但后来devfs被sysfs+udev的形式取代,同时期sysfs文件系统可以用的api:
    class_device_create_file,在2.6.26以后也不行了,现在,使用的是device_create ,从2.6.18开始可用
    struct device *device_create(struct class *class, struct device *parent, dev_t devt, const char *fmt, ...)
    从2.6.26起又多了一个参数drvdata: the data to be added to the device for callbacks
    不会用可以给此参数赋NULL
    struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

    在驱动用加入对udev的支持主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)( 在2.6较早的内核中用class_device_create)创建对应的设备。
    内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应 device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
    struct class和class_create(…) 以及device_create(…)都包含在在/include/linux/device.h中,使用的时候一定要包含这个头文件,否则编译器会报错。


    struct class定义在头文件include/linux/device.h中

    //device classes
    
    struct class {
          const char       *name;
          struct module     *owner;
                             nbsp;
          struct kset        subsys;
          struct list_head        devices;
          struct list_head        interfaces;
          struct kset             class_dirs;
          struct semaphore sem;    //lockschildren, devices, interfaces 
          struct class_attribute   *class_attrs;
          struct device_attribute     *dev_attrs;
    
          int (*dev_uevent)(structdevice *dev, struct kobj_uevent_env *env);
          void (*class_release)(structclass *class);
          void (*dev_release)(struct device *dev);
          int (*suspend)(struct device*dev, pm_message_t state);
          int (*resume)(struct device *dev);
    };
    class_create(…)在/drivers/base/class.c中实现

    // class_create - create a struct class structure
    // @owner: pointer to the module that is to "own"this struct class
    // @name: pointer to a string for the name of this class.
    
    // This is used to create a struct class pointer that canthen be used
    // in calls to device_create().
    
    //Note, the pointer created here is to be destroyed whenfinished by
    // making a call to class_destroy().
    
    struct class *class_create(struct module *owner, const char *name)
    {
          struct class *cls;
          int retval;
    
          cls = kzalloc(sizeof(*cls), GFP_KERNEL);
          if (!cls) {
               retval = -ENOMEM;
               goto error;
          }
    
          cls->name = name;
          cls->owner = owner;
          cls->class_release =class_create_release;
    
          retval =class_register(cls);
          if (retval)
               goto error;
    
          return cls;
    error:
          kfree(cls);
          return ERR_PTR(retval);
    }

    class_destroy(...)函数

    // class_destroy - destroys a struct class structure
    //@cs: pointer to the struct class that is to be destroyed
    
    //Note, the pointer to be destroyed must have been created with a call
    //to class_create().
    
    void class_destroy(struct class *cls)
    {
        if((cls == NULL) || (IS_ERR(cls)))
            return;
    
        class_unregister(cls);
    }

    device_create(…)函数在/drivers/base/core.c中实现

    // device_create - creates a device and registersit with sysfs
    // @class: pointer to the struct class that thisdevice should be registered to
    // @parent: pointer to the parent struct device ofthis new device, if any
    // @devt: the dev_t for the char device to beadded
    // @fmt: string for the device's name
         
    // This function can be used by char deviceclasses. A struct device
    // will be created in sysfs, registered to thespecified class.
    
    // A "dev" file will be created, showingthe dev_t for the device, if
    // the dev_t is not 0,0.
    // If a pointer to a parent struct device ispassed in, the newly created
    // struct device will be a child of that device insysfs.
    // The pointer to the struct device will bereturned from the call.
    // Any further sysfs files that might be requiredcan be created using this
    // pointer.
    
    // Note: the struct class passed to this functionmust have previously
    // been created with a call to class_create().
    
    struct device *device_create(struct class *class,struct device *parent,
                            dev_tdevt, const char *fmt, ...)
    {
             va_list vargs;
             struct device *dev;
    
             va_start(vargs,fmt);
             dev =device_create_vargs(class, parent, devt, NULL, fmt, vargs);
             va_end(vargs);
    
             return dev;
    }
    第一个参数指定所要创建的设备所从属的类,
    第二个参数是这个设备的父设备,如果没有就指定为NULL,
    第三个参数是设备号,
    第四个参数是设备名称,
    第五个参数是从设备号。


    class_destroy(...),device_destroy(...)也在/drivers/base/core.c中实现

    // device_destroy - removes a device that was created with device_create()
    // @class: pointer to the struct class that this device was registered with
    // @devt: the dev_t of the device that was previously registered
    
    // This call unregisters and cleans up a device that was created with a
    // call to device_create().
    
    void device_destroy(struct class *class,dev_t devt)
    {
        structdevice *dev = NULL;
        structdevice *dev_tmp;
     
        down(&class->sem);
        list_for_each_entry(dev_tmp,&class->devices, node) {
            if(dev_tmp->devt == devt) {
                dev= dev_tmp;
                break;
            }
        }
        up(&class->sem);
     
        if(dev)
            device_unregister(dev);
    }
    

    相比devfs,udev有很多优势,在此就不罗嗦了,提醒一点,udev是应用层的东东,不要试图在内核的配置选项里找到它;加入对udev的支持很简单,以字符设备驱动为例,在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用 class_device_create创建对应的设备。大致用法如下:

    struct class *myclass = class_create(THIS_MODULE, “my_device_driver”);
    class_device_create(myclass, NULL, MKDEV(major_num, 0), NULL, “my_device”);
    这样的module被加载时,udev daemon就会自动在/dev下创建my_device设备文件。

    下面以一个简单字符设备驱动来展示如何使用这几个函数:

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
     
    int HELLO_MAJOR = 0;
    int HELLO_MINOR = 0;
    int NUMBER_OF_DEVICES = 2;
     
    struct class *my_class;
    //struct cdev cdev;
    //dev_t devno;
     
    struct hello_dev {
    struct device *dev;
    dev_t chrdev;
    struct cdev cdev;
    };
    
    static struct hello_dev *my_hello_dev = NULL;
    
    struct file_operations hello_fops = {
     .owner = THIS_MODULE
    };
     
    static int __init hello_init (void)
    {
    int err = 0;
    struct device *dev;
    
    my_hello_dev = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
    if (NULL == my_hello_dev) {
    printk("%s kzalloc failed!\n",__func__);
    return -ENOMEM;
    }
    
    devno = MKDEV(HELLO_MAJOR, HELLO_MINOR);
    if (HELLO_MAJOR)
    err= register_chrdev_region(my_hello_dev->chrdev, 2, "memdev");
    else
    {
    err = alloc_chrdev_region(&my_hello_dev->chrdev, 0, 2, "memdev");
    HELLO_MAJOR = MAJOR(devno);
    }  
    if (err) {
    printk("%s alloc_chrdev_region failed!\n",__func__);
    goto alloc_chrdev_err;
    }
    printk("MAJOR IS %d\n",HELLO_MAJOR);
    
    cdev_init(&(my_hello_dev->cdev), &hello_fops);
    my_hello_dev->cdev.owner = THIS_MODULE;
    err = cdev_add(&(my_hello_dev->cdev), my_hello_dev->chrdev, 1);
    if (err) {
    printk("%s cdev_add failed!\n",__func__);
    goto cdev_add_err;
    }
    printk (KERN_INFO "Character driver Registered\n");
    
    my_class =class_create(THIS_MODULE,"hello_char_class");  //类名为hello_char_class
    if(IS_ERR(my_class)) 
    {
    err = PTR_ERR(my_class);
    printk("%s class_create failed!\n",__func__);
    goto class_err;
    }
    
    dev = device_create(my_class,NULL,my_hello_dev->chrdev,NULL,"memdev%d",0);      //设备名为memdev
    if (IS_ERR(dev)) {
    err = PTR_ERR(dev);
    gyro_err("%s device_create failed!\n",__func__);
    goto device_err;
    }
    
    printk("hello module initialization\n");
    return 0;
     
    device_err:
    device_destroy(my_class, my_hello_dev->chrdev);
    class_err:
    cdev_del(my_hello_dev->chrdev);
    cdev_add_err:
    unregister_chrdev_region(my_hello_dev->chrdev, 1);
    alloc_chrdev_err:
    kfree(my_hello_dev);
    return err;
    }
     
    static void __exit hello_exit (void)
    {
    cdev_del (&(my_hello_dev->cdev));
    unregister_chrdev_region (my_hello_dev->chrdev,1);
    device_destroy(my_class, devno);         //delete device node under /dev//必须先删除设备,再删除class类
    class_destroy(my_class);                 //delete class created by us
    printk (KERN_INFO "char driver cleaned up\n");
    }
     
    module_init (hello_init);
    module_exit (hello_exit);
     
    MODULE_LICENSE ("GPL");

    加载结果:

    [root@localhost node]# insmod node.ko  
    [root@localhost node]# dmesg | tail -3  
    [23503.365316] create node success:  
    [23503.365319]   ls -l /dev/noddev*  
    [23503.365321]   ls -l /sys/class/noddev  
    [root@localhost node]# ls -l /dev/noddev*  
    crw------- 1 root root 66,  0 11月 26 15:02 /dev/noddev0  
    crw------- 1 root root 66, 20 11月 26 15:02 /dev/noddev20  
    [root@localhost node]# ls -l /sys/class/noddev  
    总用量 0  
    lrwxrwxrwx 1 root root 0 11月 26 15:02 noddev0 -> ../../devices/virtual/noddev/noddev0  
    lrwxrwxrwx 1 root root 0 11月 26 15:02 noddev20 -> ../../devices/virtual/noddev/noddev20  
















    展开全文
  • 字符设备驱动

    千次阅读 2014-01-28 22:39:50
    一、设备驱动概述 linux内核的驱动模型为编写驱动提供...基本上设备可以分为两类,一类适合于面向字符的交换,一类适合于处理包含固定数目字节的数据块,这两类分别称为字符设备和块设备(网络设备是一种特殊的设备,它

    一、设备驱动概述

    linux内核的驱动模型为编写驱动提供了抽象,将驱动公共的部分提取了出去简化了驱动的编写工作,但是那并不是实际的驱动,如果要实现真正的驱动,还要给予驱动模型做一些其它的工作。根据外设和内核交互数据的方式,内核将驱动分成了几种类别。基本上设备可以分为两类,一类适合于面向字符的交换,一类适合于处理包含固定数目字节的数据块,这两类分别称为字符设备和块设备(网络设备是一种特殊的设备,它以一种完全不同的方式来实现)。
    设备文件都位于/dev下,通过ls命令可以查看系统中设备的信息,设备文件没有长度,而多了主次设备号。在ls中可以看到访问权限之前的那个位如果是c表示是字符设备,如果是b则表示是块设备。比如
    hwang7@hwang7$ ls /dev/sda -l
    brw-rw---- 1 root disk 8, 0  1月 26 09:41 /dev/sda
    hwang7@hwang7$ ls /dev/tty -l
    crw-rw-rw- 1 root tty 5, 0  1月 26 09:42 /dev/tty

    1.1 主次设备号

    linux内核使用主次设备号来识别设备驱动程序以及所驱动的设备,具体的来说主设备号用来查找驱动程序,而次设备号则可以提供使用了那个设备的信息。因为一个驱动可以管理同种类型的多个设备,比如一个uart的驱动可以管理芯片上的多个uart硬件。
    主设备号的分配不是随意的,其分配来自于一个组织,设备号的当前列表可以从http://lanana.org中获取。内核源码中的文档Documents/device.txt中也给出了该版本内核发布时的最新主设备号的分配结果。另外major.h头文件定义了主次设备号的相关宏,但是不是所有的主设备号都有自己的宏。从该文件也可以看出有的类型的设备可能会分配多个主设备,比如SCSI设备。

    1.2 动态创建设备文件

    设备是给使用的,如果无法使用,那么假如这个设备就没什么实际的意义,linux通过/dev中的设备文件向用户空间提供对设备的访问功能。通常/dev中的文件应该在系统启动时就被创建出来,但是由于各个系统支持硬件变化较大,所支持的硬件越来越多,而又不是所有的硬件都是用户所要的,因而linux提供了uevent机制来动态的创建/dev下的设备文件,根据驱动模型,uevent事件在硬件加载的时候才会被创建,因而这就可以使得只加载需要的硬件。而当今的计算机系统中,外设的种类又千变万化,种类极其繁多,不可能在内核中将所有的硬件驱动都加载起来并提供接口给用户空间。
    uevent的基本工作过程是:
    当设备的硬件状态发生变化时,就会发送uevent消息(即热插拔消息)到用户空间的udevd守护进程,该进程会根据消息内容创建、删除dev设备文件或者进行其它处理。所以在热插拔消息中,非常重要的一点就是要提供设备的主次设备号信息,这是通过devices_kset的device_uevent_ops中的uevent函数完成的,该函数会往热插拔消息中添加设备主次设备号的环境变量。由于devices_kset是设备模型的kset,这就保证了所有设备在添加和删除的uevent处理中都会包括设备主次设备号的环境变量。
    另外在引入uevent机制后,/dev不再是一个持久文件系统,其内容是动态生成的,一旦关机其信息就消失,系统启动时,再由udevd守护进程生成。
    另外需要注意的是字符设备和块设备的主次设备号可以相同,因为它们对应不同类型的设备,因而内核是可以区分它们的。

    1.3 主次设备号的表示

    在内核中, dev_t 类型(在 <linux/types.h>中定义)用来表示设备编号--即包括主设备号也包括次设备号。在使用设备号时,不要对其组成方式做任何假设(即使你知道旧的系统使用16位表示设备号,新的系统使用32位表示设备号),这会使得你写的代码具有最好的可移植性,内核提供了如下宏来帮助使用设备号:
    • MAJOR(dev_t dev)用于从设备号中得到其主设备号
    • MINOR(dev_t dev)用于从设备号中得到其次设备号
    • MKDEV(int major, int minor)用于从主次设备号得到一个设备号。
    由于dev_t是一个只由内核使用的数据结构,外部是看不到的(即用户空间程序),因此内核也为设备号提供了外部可见的数据类型的表示,在新的系统中是u32,在旧的系统中是u16,对应的内核提供了api用于在dev_t和外部可见格式之间的转换,相关API定义如下:
    u32 new_encode_dev(dev_t dev)
    dev_t new_decode_dev(u32 dev)
    u16 old_encode_dev(dev_t dev)
    dev_t old_decode_dev(u16 val)
    前两个用于在dev_t和新的用于外部表示的u32之间进行转换,后两个用于dev_t和旧的用于外部表示的u16之间进行转换。

    1.4 和文件系统的关联

    内核使用struct file来表示一个打开的文件,而struct inode用于在内核内部表示一个文件。对于一个文件来说,可能有多个file结构,但是只会有一个inode结构。
    struct file,这是内核中常见的一个数据结构,对于每一个打开的文件就有一个它的实例,我们关心的是其中的f_op成员,它是一个类型为file_operations的结构,该结构包括了操作文件的函数集合,在打开文件时会给它赋值,可以在任何需要的时候修改它,修改完后新的函数就立即生效。由于每个文件都对应于一个file结构,因而我们甚至可以给同一个主设备下的各个次设备分配不同的操作函数集,从而实现对设备的多种操作。
    struct inode也是一个内核常见的数据结构,我们关心其中的如下几个域:
    umode_ti_mode; 文件模式
    dev_t i_rdev; 设备号
    union {
    struct pipe_inode_info*i_pipe;
    struct block_device*i_bdev;
    struct cdev *i_cdev;
    }; 设备文件是字符设备、块设备还是管道设备
    const struct file_operations*i_fop; 文件的操作函数集
    这里最关键的一步就是打开文件的操作,在打开文件时,各种文件系统的实现者会调用init_special_inode(各种文件系统在这点上是统一的,如果不是常规文件,不是目录文件也不是链接文件就会调用该函数)。该函数的实现很简单:
    void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
    {
    	inode->i_mode = mode;
    	if (S_ISCHR(mode)) {
    		inode->i_fop = &def_chr_fops;
    		inode->i_rdev = rdev;
    	} else if (S_ISBLK(mode)) {
    		inode->i_fop = &def_blk_fops;
    		inode->i_rdev = rdev;
    	} else if (S_ISFIFO(mode))
    		inode->i_fop = &def_fifo_fops;
    	else if (S_ISSOCK(mode))
    		inode->i_fop = &bad_sock_fops;
    	else
    		printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
    				  " inode %s:%lu\n", mode, inode->i_sb->s_id,
    				  inode->i_ino);
    }
    这就给特殊文件的inode的文件操作赋值了,而对应的文件操作中的open又会进一步对打开文件进行初始化,比如字符设备中就会给file的f_op赋值。
    从总体上来说,文件系统的操作大致流程都是首先根据名字获取inode,如果还没有相应的inode就创建相应的inode,然后打开文件,在这个过程中会初始化打开文件的的各个域,包括文件操作指针。

    1.5 IOCTL

    在文件操作函数集中包括了一个ioctl函数,它用于不能用常规方式处理的文件操作,一般是控制文件的操作。/proc,/sys、以及文件的IOCTL都能帮助我们实现一些特殊的功能,可以根据自己的需要选择。

    1.5.1 IOCTL概述

    ioctl被用来实现一些比较难用读写文件的方式来完成的功能,在设备驱动中,通常这涉及到对硬件的控制能力。ioctl文件操作被包括在文件操作函数集中,并且通过同名系统调用提供给了用户空间程序。用户使用ioctl时只需要打开文件,然后向文件发出ioctl调用即可,用户空间的ioctl函数原型如下:
    int ioctl(int fd, unsigned long cmd, ...);
    • fd:文件描述符
    • cmd:命令常数
    • ...:表示一个变参列表,但是实际上只使用了一个参数,之所以用该形式,是因为该参数的类型和意义取决于第二个参数,它可能是一个常数值,也可能是一个指针。ioctl的这种形式使得无法用统一的方式对它进行处理,由于无法确定参数的确切类型、含义,因而每个ioctl都相当于一个私有的系统调用,如果没有相关的文档,你无法知道一个文件支持哪些ioctl,它支持什么功能,因而在内核开发中不推荐使用该功能。
    在内核中ioctl的原型如下:
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    
    它的三个参数分别对应到用户发出的系统调用的三个参数,其中发出的调用的第三个参数被以unsigned long的形式传递。

    1.5.2 选择ioctl命令

    如果要实现ioctl,首先第一步要为自己的ioctl定义全局唯一的命令编码,否则可能得不到期望的结果。比如假如用户错误的将一个IOCTL命令发给了一个错误的设备,如果每个IOCTL在全局都是唯一的,则这就会导致一个错误被返回,但是如果命令不是唯一的,则可能该错误的设备也实现了该IOCTL命令,那么用户就会得到自己不期望的结果。
    为了方便定义IOCTL命令,内核将命令号分成了几个字段:类型、序数、传送方向(即是读还是写)和参数大小等等,可以参考文件include/asm/ioctl.h和相关内核文档ioctl-number.txt文件。同时内核定义了一些列宏来方便定义一个全局唯一的I0CTL命令:
    #define _IO(type,nr)		_IOC(_IOC_NONE,(type),(nr),0)                        定义无参数的命令
    #define _IOR(type,nr,size)	_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))   定义用于读的命令
    #define _IOW(type,nr,size)	_IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))  定义用于写的命令
    #define _IOWR(type,nr,size)	_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) 定义用于读写的命令
    其中的type为类型字段参数,nr为序号字段参数,size为参数大小
    另外内核还定义了从cmd中获取序号,类型,是读还是写以及大小的宏
    #define _IOC_DIR(nr)		(((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
    #define _IOC_TYPE(nr)		(((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
    #define _IOC_NR(nr)		(((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
    #define _IOC_SIZE(nr)		(((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

    1.5.3 预定义IOCTL命令

    系统预定义了一些IOCTL命令,这些命令对于任何文件都是成立的,即任何文件都支持这些命令(可以参考函数compat_sys_ioctl,这就是ioctl系统调用的内核入口):
    • FIOCLEX:设置 close-on-exec 标志(File IOctl Close on EXec)。
    • FIONCLEX:清除 close-no-exec 标志
    • FIOASYNC:设置或者清除文件的异步通知标记。
    • FIOQSIZE:返回一个文件或者目录的大小,对于设备文件,它返回一个ENOTTY错误。
    • FIONBIO:"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一节中描述)。用于修改文件的标记中的O_NONBLOCK标志。

    1.5.4 ioctl参数

    ioctl的第三个参数可能是整数,这个时候可以直接使用,但是它也可能是指针。这时候就要要小心了, 因为必须保证它指向的内存地址是有效的。
    如果第三个参数是指针,则可以使用copy_from_user和copy_to_user函数来完成内核和用户空间的数据交互。但是由于ioctl的第三个参数包含的数据往往较小,因而还有其它选择,可以调用access_ok函数来进行检查,其原型如下:
    #define access_ok(type, addr, size)	(__chk_user_ptr(addr),	__access_ok((__force unsigned long)(addr), (size), get_fs()))
    • type:应该是VERIFY_READ或VERIFY_WRITE,表示要进行的是读还是写
    • addr:用户空间地址
    • size:字节大小
    如果access_ok检查通过,则就可安全地进行真正的数据读写了。此时可以使用内核提供的另外一组API:
    put_user(datum, ptr)
    __put_user(datum, ptr)
    get_user(local, ptr)
    __get_user(local, ptr)
    来完成内核和用户空间的数据交互。它们会根据ptr参数的大小来完成传输。

    1.6 定位设备

    默认情况下,系统认为文件都是支持定位的及lseek文件操作,如果一个设备不支持该操作,则应该在open时调用nonseekable_open来通知文件系统该设备不支持定位操作。

    二、字符设备

    2.1 数据结构

    内核使用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;
    };
    • kobj:用于将设备链接到kobject层次结构中,进而出现在sysfs中
    • owner:拥有者
    • ops:字符设备的操作函数集
    • list:用于将该设备上打开的文件的inode链接起来。一个文件只会有一个inode节点,但是我们需要注意的是一个字符设备可能包括多个连续的次设备号,只要次设备号不同,在设备文件系统中即/dev下就会表示为一个单独的文件,但是在内核中具有相同主设备号的多个连续的设备可能以同一个cdev结构表示。具体的说就是调用cdev_add一次只会创建一个cdev结构用于表示字符设备,这个cdev结构会保存这些连续的字符设备的起始设备号和连续设备的数目。因而内核中的一个cdev可能对应多个设备文件,进而就对应了多个inode。
    • dev:设备号
    • count:该设备所拥有的次设备号数目
    字符设备相关数据结构之间的关系如图:


    2.2 分配和释放设备编号

    在编写驱字符驱动时,第一件事就是要获取一个设备号来使用。内核提供了api来获取字符设备号:
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    • from:要分配的起始设备编号。必须包含主设备号
    • count:要为其分配设备号的连续的设备数目
    • name:驱动的名字
    如果分配失败会返回负的出错码。该函数适用于使用者确切的直到应该使用哪个主设备号的场合。如果使用者不知道应该使用哪个主设备号,可以使用如下API,由内核来分配主次设备号:
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,	const char *name);
    • dev:分配的第一个设备号
    • baseminor:期望分配的第一个次设备号,通常为0
    • count:期望为多少个连续的设备分配设备号
    • name:驱动的名字
    不管是如何得到设备号的,在不适用的时候都应该将它释放掉。相关的api如下:
    void unregister_chrdev_region(dev_t from, unsigned count);
    • from:要释放的起始设备号
    • count:释放多少个连续设备的设备号
    内核使用了类型为char_device_struct的一个数据库chrdevs来跟踪管理字符设备号。设备的设备号可以从/proc/devices中查看,也可以通过/sys文件系统查看。/proc/devices在proc_devices_init中被创建,其实现很简单就是遍历相关的设备号数据库然后输出。

    2.3 添加字符设备到系统中、从系统中删除字符设备

    在获取设备号后需要将设备添加到系统中,这需要两步:
    1. 调用cdev_init初始化cdev数据结构,这一步会将字符设备的kobject的kobj_type设置为ktype_cdev_default,它指定了该类型的字符设备的释放函数
    2. 调用cdev_add将设备添加到系统中。
    要从系统中删除一个字符设备,需要首先调用cdev_del,然后再释放其设备号。
    系统还提供了一个api cdev_alloc可以用于创建并初始化字符设备数据结构struct cdev,但是如果使用该api,需要自己设置字符设备的操作函数集。cdev_alloc会把字符设备的kobject的kobj_type设置为ktype_cdev_dynamic。
    除了以上两个API之外,内核还提供有一对API用于向系统添加或者删除字符设备:它们分别为:
    int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
    void unregister_chrdev(unsigned int major, const char *name);
    这是旧式的API。
    register_chrdev最终会调用cdev_alloc和cdev_add将设备添加到系统中。所谓的添加到系统中并没有添加到sysfs中,也没有创建/dev下的设备文件,cdev_add完成的工作是将字符设备添加到一个全局数组cdev_map中,它是一个kobj_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;
    	} *probes[255];
    	struct mutex *lock;
    };
    • next:散列到同一个哈希槽位的下一个设备probe结构
    • dev:设备号
    • range:该设备的主设备号下的连续的次设备数目
    • owner:所有者
    • get:用于由设备号获取设备的kobject
    • lock:用于对获取的设备上锁
    • data:get的参数。
    该结构用于维护一个对象数据库,该数据库采用哈希表来实现,并用链式法来解决冲突。系统中当前定义了两个该类型的数据库,一个为cdev_map,一个bdev_map,分别对应字符设备和块设备。在调用cdev_add时,字符设备被添加到cdev_map数据库中,同时添加到数据库中的还有一个用于上锁的函数exact_lock和一个用于获取设备的函数exact_match,在打开字符设备时(chrdev_open函数中)会首先查找该数据库,在获得设备后才会继续进行操作。
    但是完成cdev_add并不会创建sysfs下的文件,也不会为设备创建/dev下的设备文件,这要通过调用device_create或者其它设备子系统提供的类似API来实现。在/dev下创建文件以及在/sys下创建文件最终都是由device_add完成的。
    /dev文件系统的内容由内核线程devtmpfsd维护,相关代码路径在drivers/base/devtmpfs.c中

    2.4 打开设备

    正如前边所说,所有文件系统的实现都会对不是常规文件、不是目录文件也不是连接文件的特殊文件调用init_special_inode,对于设备文件,这会将文件inode的文件操作函数集设置为def_chr_fops,该操作集主要的是提供了打开文件的操作chrdev_open,chrdev_open主要完成:
    • 判断inode的i_cdev
      • 如果是第一次打开,即如果inode的i_cdev为空,则调用kobj_lookup从字符设备数据库中查找字符设备,并在成功时获取它,同时还会将inode添加到字符设备的链表中(由字符设备的list元所指向的链表)
      • 如果已经打开过了,则只是增加设备的引用计数
    • 为该设备的本次打开准备操作函数集,即给file结构的f_op赋值
    • 如果该字符设备提供了打开的操作函数,就调用它。
    由此也可以看出所有对字符设备的操作,必须经过的一步就是打开,因此如果要对设备文件的使用添加限制,open函数里是一个绝佳的时机,驱动的实现者可以在这里添加所想要的限制。

    2.5 字符文件的引用计数

    字符文件使用了kobject,因而其引用计数也是使用了kobject提供的接口,这也看到了kobject的威力了,它的一套机制贯穿了设备驱动模型的所有部分。相关的API为:
    struct kobject *cdev_get(struct cdev *p)
    void cdev_put(struct cdev *p)
    字符设备的put也没什么特别之处,它遵循kobject提供的机制,在引用计数为0时会进行释放,在释放时会调用在初始化字符设备时提供的释放函数,这里唯一需要注意的是通过cdev_init初始化的字符设备的kobj_type为ktype_cdev_default而通过cdev_alloc初始化的字符设备的kobj_type为ktype_cdev_dynamic,二者的释放函数不同。

    2.6 读写字符文件

    在打开文件之后,读写文件就没有什么特别之处了,打开文件时已经设置好了文件的操作函数集,因而直接调用即可。当然字符驱动的实现必须遵循读写文件应该遵循的规则,比如怎么和用户空间交互数据,如何处理出错、返回何值。

    在文件读写IO中,由于各种原因,读写请求可能会被阻塞,这时候读写操作就应该休眠以等待操作可进行执行的时刻,则会时候就要用到内核提供的一些同步互斥机制。通常的做法是声明一个等待队列,然后等待被唤醒。

    2.7 poll和select支持

    有时候应用程序会想使用poll或者select以完成一些高级的读写功能,比如确定接下来的I/O操作是否会阻塞,一次等待多个数据流等。无论是poll还是select还是epoll在最底层都对应到文件操作函数中的poll。其原型为:
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    • file:打开的文件
    • poll_table:该结构对驱动是透明的,驱动可以不关心它的内容。它用于在内核中实现poll,select和epoll系统调用。驱动只需要知道它可以通过poll_wait向poll_table中添加一个等待队列即可(当你调用wakeup家族来唤醒等待队列上的等待任务时添加到poll_table中的等待队列就会被唤醒)。
    poll_wait的原型如下:
    void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    参数分别为:
    • filp:打开文件指针
    • wait_address:等待队列头
    • p:poll_table
    poll需要的返回值中包含了哪些操作可以进行的信息,或者是出错信息。常见的一些返回值及其意义如下:
    1. POLLIN:设备可以无阻塞读
    2. POLLRDNORM:设备可以被用来读取"正常"的数据。因此一个可读的设备返回( POLLIN|POLLRDNORM ).
    3. POLLRDBAND:设备可以被用来读取带外数据。常用于socket。
    4. POLLPRI:高优先级数据(带外)可不阻塞地被读取。
    5. POLLHUP:读取设备的进程读到了文件尾。
    6. POLLERR:发生了错误
    7. POLLOUT:设备可以无阻塞的写入
    8. POLLWRNORM:它和POLLOUT的关系类似于POLLRDNORM和POLLIN的关系,一个可写的设备返回( POLLOUT|POLLWRNORM).
    9. POLLWRBAND:类似于POLLRDBAND,带外数据可写。常用于socket。
    在驱动程序的poll被调用时,poll_table参数有时候被设置为NULL,这可能有两个原因:
    1. 应用程序调用poll时超时值被设置为0,此时内核知道没有任何必要等待,因而就不要创建等待队列。
    2. 当被指定的多个驱动中有一个表明可以进行IO操作时,poll_table也会被设置为NULL,因为根据poll的语义,只要有任何一个指定的文件描述符可以IO就可以了,因而当一个设备表明可以进行IO后,就没必要再创建等待队列进行等待了。
    poll的数据结构关系图:

    2.8 poll、read、write的一些原则

    这是最基本的三个API,为了保证应用程序正确工作,这三个API必须正确的被实现,正确实现这三个API是保证所有文件的读写语义一致的基础。正确实现这三者需要保证以下几点:

    2.8.1 从设备读数据

    • 如果在输入缓冲中有数据, 则即使缓冲区额数据少于请求的数目,并且驱动可以保证后续数据可以很快到达,read也应该以尽可能小的延时立即返回。read甚至可以一直返回少于请求数目的字节数,当然至少要返回一个字节。在缓冲区有数据的情况下,poll 应当返回 POLLIN|POLLRDNORM。
    • 如果输入缓冲中没有数据,默认情况下read必须阻塞等待,直到输入缓冲区中至少有一个字节。但是如果设置了O_NONBLOCK标记,则read应该立即返回-EAGIN。在缓冲区中没有数据时,poll应该报告设备是不可读的。
    • 如果到达了文件尾,  不管是否阻塞read都应立即返回一个0。这种情况下poll应该报告POLLHUP。

    2.8.2 向设备写入数据

    • 如果输出缓冲区有空闲区域,则即使空闲区域小于请求的大小,write也应该立即返回,唯一的要求时空闲区域至少要能够容纳一个字节。这种情况下,poll应该报告该设备是可写的POLLOUT|POLLWRNORM。
    • 如果输出缓冲已满,则默认情况下write要阻塞直到有一些空间被释放了。但是如果设置了O_NOBLOCK,则write应该立即返回一个-EAGAIN的错误。这种情况下,poll应当报告文件是不可写的。另外,如果设备不能接受任何多余数据,则无论是否设置了 O_NONBLOCK,write都应该返回-ENOSPC。
    • 不要让write在返回之前等待数据传输结束,即便没有设置O_NONBLOCK标记。因为许多应用程序使用select来检查write是否会阻塞。如果返回的结果是设备可写,那么调用就不能阻塞(否则就是说话不算话了:))。如果应用程序想要确保数据报真正写到设备上,则应该调用fsync,相应的驱动必须支持该接口。

    2.8.3 刷新待处理输出

    fsync函数,用于保证缓冲区中的数据被发送到了设备上。不过大多数情况下只有块设备实现了该函数,字符设备是不支持该调用的。

    2.9 异步通知

    在有的场景下,设备可能在将来不确定的时间点才能准备好用于读取数据,为了应对这种场景,用户可以使用poll或者select,但是这显然不是最优的,很显然这里如果有异步通知机制就好了,事实上也确实有异步通知机制。对于用户程序来说,如果要接收异步通知,它需要完成:
    • 使用fcntl系统调用发出F_SETOWN 命令,这样该进行的ID就被保存到了filp->f_owner中。
    • 使用fcntl系统调用发出F_SETFL命令来设置设备的FASYNC标记
    完成这两步之后,当有新数据可读时,相应的进程就会收到SIGIO信号。不过信号并不能表明是哪个文件有数据可读,如果进程订阅了多个文件的异步通知事件,则它应该在收到信号时再调用以下poll或者select等函数以确定发生了什么。
    对于驱动来说,要支持异步通知需要做:
    • 当用户进程发出F_SETOWN 调用时,做的时间很简单,就是保存用户进程PID即可。
    • 当用户进程发出设置FASYNC的调用时,驱动程序的fasync函数会被调用,事实上只要文件标记中的FASYNC标记发生了变化,相应驱动的fasync函数就会被调用。
    • 当有数据到达时,给所有注册了异步通知的进程发送SIGIO。
    由于可能有多个进程向设备注册异步通知事件,因而设备需要维护注册异步消息的进程的信息,对于这一点内核已经提供了支持,内核提供了如下数据结构用于保存异步通知的注册者的信息:
    struct fasync_struct {
    spinlock_t	 fa_lock;
    int	 magic;
    int	 fa_fd;
    struct fasync_struct	*fa_next; /* singly linked list */
    struct file	 *fa_file;
    struct rcu_head	 fa_rcu;
    };
    支持异步通知的设备需要包含该结构,每次应用注册异步通知事件时内核都会创建一个新的该类型的结构,然后添加到设备中,这些结构被以链表的形式管理。不过驱动不必关心,对于驱动来说很简单,只需要在自己的fasync实现中调用fasync_helper即可,fasync_helper会完成创建该结构并添加到设备的异步注册者链表中的工作,其原型如下:
    int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp);
    在数据到达的时候,驱动需要发送异步通知,在这一点上内核也提供了支持,驱动只需要调用kill_async即可,其原型如下:
    void kill_fasync(struct fasync_struct **fp, int sig, int band);
    最后需要注意的是在关闭文件时,一定要注意调用文件的fasync方法将异步通知的注册者删除。

    展开全文
  • 编写字符设备驱动的基本步骤为: 1、编写对该设备的各种操作函数(open、write、ioctl) 2、定义一个file_operation结构体,该结构体用来存储驱动内核模块提供的对设备进行各种操作的函数的指针即open()、

    注册字符设备有三种方法:chardev、cdev、misc注册,本文介绍用misc_dev注册方法注册设备,编写简单字符设备驱动程序,实现字符设备驱动程序的基本框架。
    编写字符设备驱动的基本步骤为:
    1、编写对该设备的各种操作函数(open、write、ioctl)
    2、定义一个file_operation结构体,该结构体用来存储驱动内核模块提供的对设备进行各种操作的函数的指针即open()、write()、ioctl()
    3、编写入口函数,该函数中进行设备注册
    4、编写出口函数,该函数进行设备的注销
    随着字符设备种类和数量的增加,设备号越来越紧张,为此Linux系统提出misc设备模型以解决此问题。所有misc设备其主设备号都是10,不同设备使用不同的次设备号区分。另外misc设备驱动会为设备自动创建设备文件,不需要想cdev设备那样,需要自己手动创建,所以使用起来更为方便。
    以下是一个简单的字符设备驱动程序:

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/delay.h>
    #include <asm/uaccess.h>
    #include <asm/irq.h>
    #include <asm/io.h>
    #include <linux/major.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/backing-dev.h>
    #include <linux/raw.h>
    #include <linux/capability.h>
    #include <linux/uio.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/mutex.h>
    #include <linux/gfp.h>
    #include <linux/compat.h>
    #include <linux/vmalloc.h>
    #include <linux/miscdevice.h>
    #define NXP_ERR(fmt, args...)    pr_err("[TFA98XX] %s %d : "fmt, __func__, __LINE__, ##args)
    
    #define RED 1
    #define GREEN 0
    
    static int Hello_Misc_open(struct inode *inode, struct file *fp) 
    {
        printk("neo: Hello_Misc_open \n");
        return 0;
    }
    
    
    static ssize_t Hello_Misc_write(struct file *fp, const char __user *data, size_t count, loff_t *offset)  
    {
        printk("neo: Hello_Misc_write \n");
        return 0;   
    }
    static ssize_t Hello_Misc_read(struct file *fp, char __user *data, size_t count, loff_t *offset)
    {
        printk("neo: Hello_Misc_read\n");
        return 0;   
    }
    
    static long Hello_Misc_ioctl(struct file *fp,unsigned int cmd, unsigned long arg) //
    {
        printk("neo: Hello_Misc_ioctl\n");
        switch(cmd)
        {
            case RED:
                printk("neo: turn on the red light\n");
                break;
            case GREEN:
                printk("neo: turn on the green light\n");
                break;
            default:
                return -1;
        }
        return 0;   
    }
    //手机是64位的,Android.mk 编译的是32 位的,底层需要使用 compat_ioctl ,不是使用unlocked_ioctl
    static long Misc_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
    {
        printk("neo: Misc_compat_ioctl\n");
        switch(cmd)
        {
            case RED:
                printk("neo: turn on the red light\n");
                break;
            case GREEN:
                printk("neo: turn on the green light\n");
                break;
            default:
                return -1;
        }
        return 0;
    }
    //用来存储驱动内核模块提供的对设备进行各种操作的函数的指针
    static struct file_operations Hello_Misc_fops =
    {
        .owner   = THIS_MODULE,
        .open    = Hello_Misc_open,
        .read    = Hello_Misc_read,
        .unlocked_ioctl   = Hello_Misc_ioctl,
        .write   = Hello_Misc_write,
        .compat_ioctl = Misc_compat_ioctl,
    };
    //misc设备结构体
    static struct miscdevice Hello_Misc =
    {
        .minor = MISC_DYNAMIC_MINOR,
        .name = "hello_misc",
        .fops = &Hello_Misc_fops,
    };
    //入口函数
    static int hello_init(void)
    {
        dev_t devid;
        int ret = 0;
        printk("hello_init \n");
        /* register MISC device */
        printk("neo: misc_register\n");
        if ((ret = misc_register(&Hello_Misc)))
        {
            NXP_ERR("AudDrv_nxpspk_mod_init misc_register Fail:%d\n", ret);
            return ret;
        }
        return 0;
    }
    
    //出口函数
    static void hello_exit(void)
    {
        // unregister misc 设备
        misc_deregister(&Hello_Misc);
    }
    
    module_init(hello_init);
    module_exit(hello_exit);
    展开全文
  • 第一,字符设备是指在I/O传输过程以字符为单位进行传输的设备,例如键盘,打印机等。请注意,以字符为单位并不一定意味着是以字节为单位,因为有的编码规则规定,1个字符占16比特,合2个字节。 在UNIX系统,...
  • 字符设备驱动实验

    千次阅读 2019-06-11 08:38:54
    字符设备:是指只能一个字节一个字节进行读写操作的设备,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。 一般每个字符设备...
  • LinuxI/O设备分为两类:字符设备和块设备。两种设备本身没有严格限制,但是,基于不同的功能进行了分类。 (1)字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/...
  • Linux字符设备驱动剖析

    千次阅读 2015-05-23 23:09:13
    忠于源码,讲述linux字符设备驱动的那些事儿,重点讲述字符设备的创建和访问过程。
  • 本文简述在linux字符设备驱动编程自动创建设备节点。至于手动创建设备节点的编程
  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。 块设备:是指可以...
  • 绝对干货~~学习Linux设备驱动开发的过程自然会遇到字符设备驱动、平台设备驱动、设备驱动模型和sysfs等相关概念和技术。对于初学者来说会非常困惑,甚至对Linux有一定基础的工程师而言,能够较好理解这些相关技术...
  • 深入理解Linux字符设备驱动

    千次阅读 2016-03-20 11:09:13
    文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次、组成框架和交互、如何编写驱动、设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解。
  • 来自:http://man.linuxde.net/mknodmknod命令用于创建Linux字符设备文件和块设备文件。参数文件名:要创建的设备文件名;类型:指定要创建的设备文件的类型;主设备号:指定设备文件的主设备号;次设备号:指定...
  • 横向比较关联各个驱动相关的知识点(字符设备驱动、平台设备驱动、设备驱动模型、sysfs)和纵向剖析Linux整个驱动软件层次,对于Linux驱动的理解和开发很有帮助,绝对干货!
  • 第一,字符设备是指在I/O传输过程以字符为单位进行传输的设备,例如键盘,打印机等。请注意,以字符为单位并不一定意味着是以字节为单位,因为有的编码规则规定,1个字符占16比特,合2个字节。 在UNIX...
  • 本文主要是针对Linux的字符设备驱动,Linux设备驱动中字符设备...字符设备是Linux驱动最简单的驱动,也是最常的驱动! 字符设备驱动除了注册为字符设备类还可以注册为混杂设备类 字符设备可以动态注册也可以静态注册
  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。块设备:是指可以...
  • 如何编写字符设备驱动

    千次阅读 2018-11-15 17:43:43
    0.包含必须的头文件 #include &lt;linux/fs.h&gt; #include &lt;linux/cdev.h&gt; #include &lt;linux/slab.h&...1.编写字符设备函数 1.1、分配核心结构 struct cdev *cd...
  • 在Linux内核里面,设备(device)主要分为字符设备,块设备,网络设备,字符设备驱动是Linux驱动基础,在看《Linux 设备驱动开发详解》这本书的过程,把字符设备相知识记录整理如下。 字符设备驱动的组成 字符设备...
  • ...混杂设备、字符设备、平台设备三者的注册方式比较。...今天没事干,顺便总结一下设备注册的几种方式,有的方式已经不提倡使用了,所以大家可以...首先说已经不提倡使用的字符设备注册方法:register_chrdev()函
  • 实验内容实现一个虚拟的字符设备及其相关的驱动程序,支持以下功能:用户可以向设备写入字符串,并覆盖设备中原有的字符串;用户可以从设备读出写入的字符串;用户通过系统调用ioctl清除设备中写入的字符串;设备关闭...
  •  学习Linux设备驱动开发的过程自然会遇到字符设备驱动、平台设备驱动、设备驱动模型和sysfs等相关概念和技术。  对于初学者来说会非常困惑,甚至对Linux有一定基础的工程师而言,能够较好理解这些相关技术也相对...
  • 字符设备通过文件系统的名字来读取。这些名字就是文件系统 的特殊文件或者称为设备文件、文件系统的简单结点,一般位 于/dev/目录下使用ls进行查看会显示以C开头证明这是字符设备文 件crw--w----
  • Linux字符设备驱动注册三种方法以及内核分析

    千次阅读 多人点赞 2018-10-07 15:59:38
    Linux驱动是用户访问底层硬件的桥梁,驱动有可以简单分成三类:字符设备、块设备、网络设备。其中最多的是字符设备,其中字符设备的注册方法主要有三种:杂项设备注册、早期字符设备注册、标准字符设备注册。以及...
  • linuxc表示字符设备文件,b表示块设备文件,l表示符号链接文件,r表示可读权限,w表示可写权限。linux文件属性解读:文件类型:-:普通文件 (f)d:目录文件b:块设备文件 (block)c:字符设备文件 (character)l:...
  • Linux字符设备驱动程序框架

    千次阅读 2014-11-17 10:18:56
    对于每一个驱动函数来说,都有一些和该设备密切相关的功能函数,拿最常用的字符设备来来说,存在着诸如open() read() write() ioctl()这一类的操作,当系统使用这些系统调用的时候,将自动使用驱动模块的特定的...
  • 转】字符设备、块设备、裸设备、RAW设备2008年12月01日 星期一 14:43第一,字符设备是指在I/O传输过程以字符为单位进行传输的设备,例如键盘,打印机等。请注意,以字符为单位并不一定意味着是以字节为单位,因为...
  • 字符设备驱动程序: 主要有以下知识点:设备号、创建设备文件、重要数据结构、设备注册、设备操作。 设备号分主设备号和次设备号,可以进入/dev/ 进行查看(ll)已经存在的设备文件。 主设备号是将 字符设备...
  • 对于Linux系统,一般字符设备和驱动之间的函数调用关系如下图所示 上图描述了用户空间应用程序通过系统调用来调用程序的过程。一般而言在驱动程序的设计,会关系 struct file 和 struct inode 这两个结构体...
  • linux字符设备驱动解析

    千次阅读 2015-12-14 21:49:10
    字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。块设备:是指可以...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 240,846
精华内容 96,338
关键字:

以下设备中属于字符设备的是