一切皆文件之字符设备

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define BUFFER_SIZE 1024

static char device_buffer[BUFFER_SIZE];
static int open_count = 0;

static int device_open(struct inode *inode, struct file *file)
{
    if (open_count > 0)
        return -EBUSY;

    open_count++;
    return 0;
}

static int device_release(struct inode *inode, struct file *file)
{
    open_count--;
    return 0;
}

static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset)
{
    int bytes_to_read;

    if (*offset >= BUFFER_SIZE)
        return 0;

    bytes_to_read = min(length, (size_t)(BUFFER_SIZE - *offset));

    if (copy_to_user(buffer, device_buffer + *offset, bytes_to_read))
        return -EFAULT;

    *offset += bytes_to_read;
    return bytes_to_read;
}

static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset)
{
    int bytes_to_write;

    if (*offset >= BUFFER_SIZE)
        return 0;

    bytes_to_write = min(length, (size_t)(BUFFER_SIZE - *offset));

    if (copy_from_user(device_buffer + *offset, buffer, bytes_to_write))
        return -EFAULT;

    *offset += bytes_to_write;
    return bytes_to_write;
}

static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

static int __init chardev_init(void)
{
    int ret = register_chrdev(0, DEVICE_NAME, &fops);
    if (ret < 0) {
        printk(KERN_ALERT "Failed to register char device\\n");
        return ret;
    }

    printk(KERN_INFO "Char device driver registered,major:
    return 0;
}

static void __exit chardev_exit(void)
{
    unregister_chrdev(0, DEVICE_NAME);
    printk(KERN_INFO "Char device driver unregistered\\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

下面是一个加载字符设备驱动后的测试示例。

mknod /dev/mychardev c MAJOR_NUMBER 0 #MAJOR_NUMER为注册字符设备时获得的主设备号
echo "Hello, world!" > /dev/mychardev   # 写入数据到设备
cat /dev/mychardev                  # 从设备读取数据

注册字符设备驱动

(1)先调用__register_chrdev_region分配一个strcut char_device_strcut的实例,这个实例表示一个字符设备驱动,在函数中会填充cd数据结构。系统为了管理设备,为每个设备编了号,每个设备又分为主设备号和次设备号,主设备号用来区分不同类似的设备,而次设备号用来区分同一类型的多个设备,如IIC驱动的主设备号是100,IIC有3个设备IIC-0,IIC-1,IIC-2,这3个设备共用一套驱动。可以通过cat /proc/devices 查看已加载的驱动设备主设备号。

static struct char_device_struct {
    struct char_device_struct *next;  //指向下一个字符设备
    unsigned int major;            //主设备号
    unsigned int baseminor;        //次设备起始值
    int minorct;                  //次设备数量
    char name[64];               //设备或驱动的名称
    struct cdev *cdev;      /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

(2)调用cdev_alloc分配设备一个cdev实例,cdev描述了一个字符设备。

struct cdev {
    struct kobject kobj; //内核对象,通过他将设备统一加到设备驱动模型中管理
    struct module *owner;
    const struct file_operations *ops; //文件系统与设备直接的操作函数集合
    struct list_head list;//所有的字符设备驱动形成链表
    dev_t dev; //字符设备的设备号,由主设备和次设备构成
    unsigned int count;
} __randomize_layout;

(3)填充用户注册的驱动函数操作集合,用户注册的open/read/write等函数,这是文件系统与设备的沟通桥梁。
(4)将cdev添加到strutct kobj_map *cdev_map全局列表中,kobj_map中有255个probe,每个probe对应一个主设备,相当于字符设备的数量不能超过255。每个主设备下可以由多个次设备。

创建设备节点

insmod加载完成驱动后,通常需要使用mknod创建一个设备节点,这样用户就可以通过文件系统节点的方式访问设备,下面是mknod的使用示例。

mknod /dev/xxx 设备类型 主设备号 从设备号

从上图的流程图可知,/dev是挂载了tmpfs类型的文件系统。在系统启动初始化的时候会调用shmem_init注册一个tmpfs类型的文件系统。
在使用mknode在/dev下创建设备驱动节点,与前面文件系统创建一个新文件类似,关键点就是为文件创建一个dentry和inode,然后填充inode对应的数据,我们需要重点关注的是inode填充的i_fop的操作函数集合是def_chr_fops,这个file_operations对应的open是chardev_open,因此后续在用户打开文件时将会调用到该函数。

驱动系统调用

上一小节中,使用mknod创建设备节点时,inode->i_fop填充的是def_char_fops,在文件系统打开时就会调用到chrdev_open函数,打开的流程与前面分析的流程一致,这里就不再分析了,我们这里重点关注跟设备驱动相关的差异。chrdev_open函数如下:

static int chrdev_open(struct inode *inode, struct file *filp)
{
    const struct file_operations *fops;
    struct cdev *p;
    struct cdev *new = NULL;
    int ret = 0;

    spin_lock(&cdev_lock);
    p = inode->i_cdev; //获取文件对应的cdev,如果为空则需要进行查找。
    if (!p) {
        struct kobject *kobj;
        int idx;
        spin_unlock(&cdev_lock);
        kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); //从cdev_map中查找kobj
        if (!kobj)
            return -ENXIO;
        new = container_of(kobj, struct cdev, kobj);//获取到cdev
        spin_lock(&cdev_lock);
        /* Check i_cdev again in case somebody beat us to it while
           we dropped the lock. */
        p = inode->i_cdev; 
        if (!p) {
            inode->i_cdev = p = new; //将cdev赋值给inode
            list_add(&inode->i_devices, &p->list);//将inode添加到设备列表中
            new = NULL;
        } else if (!cdev_get(p))
            ret = -ENXIO;
    } else if (!cdev_get(p))
        ret = -ENXIO;
    spin_unlock(&cdev_lock);
    cdev_put(new);
    if (ret)
        return ret;

    ret = -ENXIO;
    fops = fops_get(p->ops); //获取驱动注册的file_operations
    if (!fops)
        goto out_cdev_put;

    replace_fops(filp, fops); //将struct file中的f_op更新跟驱动的注册的file_operations
    if (filp->f_op->open) {
        ret = filp->f_op->open(inode, filp); //调用驱动注册的open函数
        if (ret)
            goto out_cdev_put;
    }

    return 0;

 out_cdev_put:
    cdev_put(p);
    return ret;
}

chrdev_open函数关键的作用先从cdev_map中找到设备的cdev,然后填充到inode中,这样下次操作设备文件节点时就不用再查找了,接下来非常关键的作用是将strcut file中的f_op由原来inode->i_fop指向的file_operations(def_chr_fops)替换为驱动注册的file_operations,这样后续用户在进程中再操作该文件节点时,对文件的open/read/write等操作经过VFS后就直接调用到驱动注册的file_opearations操作集合了,不会再经过def_chr_fops。