Linux 字符设备驱动实例笔记

Posted Welljia

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 字符设备驱动实例笔记相关的知识,希望对你有一定的参考价值。

概述

Linux中,一切皆文件,设备也不例外,每一个字符设备都在/dev目录下对应一个设备文件。所以对于设备的所有操作基本上都可以简化成open、close、read、write、ioctrl这几个函数。
在Linux下一个字符设备驱动与用户调用该设备的程序之间的关系如下图所示,Linux内核使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

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

设计框架


程序的目的是编写一个简单的实现读写接口的虚拟驱动,其整体程序框架图如上图所示,主要分为三部分:
• 驱动的初始化
主要完成动态获取设备号并解析,注册cdev,分配设备体结构实间和建立设备节点等功能。
• 驱动操作的具体实现
完成open, release, write, read, lseek, close等功能。
• 注销驱动
释放硬件资源,避免占用过多造成浪费。

代码实现

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>

#ifndef _MYCHAR_android_H_
#define _MYCHAR_ANDROID_H_

#include <linux/cdev.h>
#include <linux/semaphore.h>

//以下四个变量为硬件设备在文件系统的名称
#define MYCHAR_DEVICE_NODE_NAME "mychar"
#define MYCHAR_DEVICE_FILE_NAME "mychar"
#define MYCHAR_DEVICE_PROG_NAME "mychar"
#define MYCHAR_DEVICE_CLASS_NAME "mychar"

//该结构体用来描述硬件设备
struct mychar_android_dev

    int val; //描述一个虚拟的寄存器

    //信号量,主要用来访问同步的
    // 其结构为:
    // struct semaphore 
    //      spinlock_t lock;
    //      unsigned int count;
    //      struct list_head wait_list; 
    struct semaphore sem; 
    struct cdev dev; //标准的字符设备结构变量,用来标识该设备为字符设备
;

#endif

//主设备号和从设备号
static int mychar_major = 0;
static int mychar_minor = 0;

//设备类别和设备变量
/*内核中定义了struct class结构体,一个struct class 结构体类型变量对应一个类,内核同时提供了class_create()函数,
可以用它来创建一个类,这个类存放于sysfs下面,一旦创建了这个类,再调用device_create()函数在/dev目录下创建相应的设备节点。
这样,加载模块的时候,用户空间中的udev会自动响应device_create()函数,去/sysfs下寻找对应的类而创建设备节点*/
static struct class* mychar_class = NULL;
static struct mychar_android_dev* mychar_dev = NULL;

//ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t 就是无符号型的ssize_t
static int mychar_open(struct inode* inode, struct file* flip);
static int mychar_release(struct inode* inode, struct file* flip);
static ssize_t mychar_read(struct file* flip, char __user* buf, size_t count, loff_t* f_pos);
static ssize_t mychar_write(struct file* flip, const char __user* buf, size_t count, loff_t* f_pos);

//初始化的时候通过调用 cdev_init 函数将该结构体告知系统
static struct file_operations mychar_fops = 
    .owner = THIS_MODULE,
    .open = mychar_open,
    .release = mychar_release,
    .read = mychar_read,
    .write = mychar_write,
;

static int mychar_open(struct inode* inode, struct file* flip)

    struct mychar_android_dev* dev;

    //将结构体变量保存到私有区域   //container_of作用是根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针
    dev = container_of(inode->i_cdev, struct mychar_android_dev, dev);
    flip->private_data = dev;

    /*pr_info 打印调试信息,包含有:
    1. 打印调用函数名  
    2. 打印调用的行号  
    3. 打印判断语句或者返回值  
    4. 如果有错误,应该输出错误的值及我们期望的值 */
    pr_info("mychar_open\\n");

    return 0;


static int mychar_release(struct inode* inode, struct file* filp)

    pr_info("mychar_release\\n");

    return 0;


static ssize_t mychar_read(struct file* filp, char __user* buf, size_t count, loff_t* f_pos)

    ssize_t err = 0;
    struct mychar_android_dev* dev = filp->private_data;
    //down_interruptible用来获取信号量,如果信号量大于或等于0,获取信号量,否则进入睡眠状态,等待信号量被释放后,激活该程。
    if(down_interruptible( &(dev->sem) ))//锁住信号量,以保证同步访问
    
        return -ERESTARTSYS;
    

    if(count < sizeof(dev->val) )
    
        goto out;
    

    // 将数据拷贝到用户提供的缓存区中
    if(copy_to_user(buf, &(dev->val), sizeof(dev->val)))
    
        err = -EFAULT;
        goto out;
    

    err = sizeof(dev->val);

out:
//void up()该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,
// 表明有任务等待该信号量,因此唤醒这些等待者
    up(&(dev->sem)); //释放同步信号

    pr_info("mychar_read\\n");

    return err;


static ssize_t mychar_write(struct file* filp, const char __user* buf, size_t count, loff_t* f_pos)

    struct mychar_android_dev* dev = filp->private_data;
    ssize_t err = 0;

    //锁住信号量,以便进行同步访问
    if(down_interruptible( &(dev->sem) ))
    
        return -ERESTARTSYS;
    

    if(count != sizeof(dev->val) )
    
        goto out;
    

    //从用户提供的缓存中获取相关数据
    if( copy_from_user( &(dev->val), buf, count) )
    
        err = -EFAULT;
        goto out;
    

    err = sizeof(dev->val);

out:
    up(&(dev->sem));
    pr_info("mychar_write\\n");

    return err;


/*
static ssize_t __mychar_get_val(struct mychar_android_dev* dev, char* buf)

    int val = 0;

    if(down_interruptible( &(dev->sem) ))
    
        return -ERESTARTSYS;
    

    val = dev->val;
    up( &(dev->sem) );

    return snprintf(buf, PAGE_SIZE, "%d\\n", val);


static ssize_t __mychar_set_val(struct mychar_android_dev* dev, const char* buf, size_t count)

    int val = 0;

    val = simple_strtol(buf, NULL, 10);
    
        return -ERESTARTSYS;
    

    dev->val = val;
    up( &(dev->sem));

    return count;

*/

//初始化设备
static int __mychar_setup_dev(struct mychar_android_dev* dev)

    int err;

    //将主设备号和次设备号转换成dev_t类型
    dev_t devno = MKDEV(mychar_major, mychar_minor);

    memset(dev, 0, sizeof(struct mychar_android_dev) );

    //将file_operations告知系统
    cdev_init( &(dev->dev), &mychar_fops);

    //dev->dev.owner = THIS_MODULE;
    dev->dev.ops = &mychar_fops;

    //注册字符设备
    err = cdev_add( &(dev->dev), devno, 1);

    if(err)
    
        return err;
    

    //初始化信号量
    //Init_MUTEX()函数初始化信号量为互斥量。 互斥量为信号量的特例,它可以防止数据被两个不同系统调用读写。 
    //不过2.6.25及以后的linux内核版本废除了init_MUTEX函数
    //新版本使用sema_init(sem, 1)函数,所以废除 init_MUTEX后
// 将原来使用 init_MUTEX(sem)的地方统统替换为sema_init(sem, 1); 即可
    init_MUTEX(&(dev->sem));
    dev->val = 0;

    return 0;


//模块初始化
static int __init mychar_init(void)

    int err = -1;
    dev_t dev = 0;
    struct device* temp = NULL;

    printk(KERN_ALERT"Initializing mychar device.\\n");

    //动态分配设备号码
    err = alloc_chrdev_region( &dev, 0, 1, MYCHAR_DEVICE_NODE_NAME);
    if(err < 0)
    
        printk(KERN_ALERT"Failed to alloc char dev region.\\n");
        goto fail;
    

    // 获得dev_t的主设备号和次设备号
    mychar_major = MAJOR(dev);
    mychar_minor = MINOR(dev);

    // 分配设备结构体的空间,kmalloc对其分配的内存不进初始化(清零),
    // 分配的区仍然持有它原来的内容, 分配的区也是在物理内存中连续
    //GFP_KERNEL宏是分配内核空间的内存时的一个标志位,无内存可用时可引起休眠
    mychar_dev = kmalloc( sizeof(struct mychar_android_dev), GFP_KERNEL);
    if(!mychar_dev)
    
        err = -ENOMEM;
        printk(KERN_ALERT"Failed to alloc mychar_dev.\\n");
        goto unregister;
    

    //初始化设备
    err = __mychar_setup_dev(mychar_dev);
    if(err)
    
        printk(KERN_ALERT"Failed to setup dev:%d.\\n", err);
        goto cleanup;
    

// 内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,
// 可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,
// 再调用device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,
// 用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。



    //创建传统设备文件系统的接口设备
    //建立了逻辑设备类,在 /sys/class/ 下新建了my_char目录
    mychar_class = class_create(THIS_MODULE, MYCHAR_DEVICE_CLASS_NAME);
    if(IS_ERR(mychar_class))
    
        err = PTR_ERR(mychar_class);
        printk(KERN_ALERT"Failed to create mychar class.\\n");
        goto destroy_cdev;
    

    // create device type directory mychar on /dev/ and /sys/class/mychar
    // 函数能够自动在/sys/devices/virtual目录下创建新的逻辑设备目录,在/dev目录下创建于逻辑类对应的设备文件
    temp = device_create(mychar_class, NULL, dev, "%s", MYCHAR_DEVICE_FILE_NAME);
    if(IS_ERR(temp))
    s
        err = PTR_ERR(temp);
        printk(KERN_ALERT"Failed to create mychar device.\\n");
        goto destroy_class;
    

/*
destroy_device:
    device_destroy(mychar_class, dev);
    */

destroy_class:
    class_destroy(mychar_class);

destroy_cdev:
    cdev_del( &mychar_dev->dev);

cleanup:
    kfree(mychar_dev);

unregister:
    unregister_chrdev_region(MKDEV(mychar_major, mychar_minor), 1);

fail:
    return err;



//卸载模块
static void __exit mychar_exit(void)

    dev_t devno = MKDEV(mychar_major, mychar_minor);

    printk(KERN_ALERT"Destroy mychar device.\\n");

    //传统设备文件系统的接口设备
    if(mychar_dev)
    
        cdev_del(&(mychar_dev->dev) );
        kfree(mychar_dev);
    

    // destroy device number
    unregister_chrdev_region(devno, 1);


MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("First Android Device");

module_init(mychar_init);
module_exit(mychar_exit);

以上是关于Linux 字符设备驱动实例笔记的主要内容,如果未能解决你的问题,请参考以下文章

Linux驱动开发-混杂字符设备驱动模型笔记 4

Linux驱动开发-混杂字符设备驱动模型笔记 4

LINUX设备驱动程序笔记字符设备驱动程序

Linux字符设备驱动实例—globalmem驱动

Linux学习笔记:存储管理

linux字符设备学习笔记原创