Linux 字符设备驱动

这部分主要讲Linux字符设备驱动程序的结构,解释主要组成部分的编程方法。

字符设备

字符设备:指只能一个byte一个byte读写的设备,不能随机读写数据,要按先后顺序。字符设备是面向流的设备,常见字符设备有鼠标、键盘、串口、终端、LED灯。

块设备:指可以从设备的任意位置读取一定长度数据的设备。常见块设备有磁盘、硬盘、U盘、SD卡等。

每个字符设备或块设备,都在/dev目录下有一个对应的设备文件。Linux APP可以通过这些设备文件(又称设备节点),来使用驱动程序操作字符设备和块设备。

字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系:

Linux 字符设备驱动
from Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析 | CSDN

Linux内核中,

  • 使用cdev结构体描述字符设备;
  • 通过成员dev_t定义设备号(主、次设备号),确定字符设备的唯一性;
  • 通过成员file_operations定义字符设备驱动,为VFS(虚拟文件系统)提供接口函数,如open/close/read/write等。

Linux字符设备驱动中,

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

TIPS: register_chrdev 与 register_chrdev_region, alloc_chrdev_region有何区别?

register_chrdev 设备注册 + 设备号申请。register_chrdev_region和alloc_chrdev_region 设备号申请,设备注册由cdev_init + cdev_add完成。
register_chrdev() 支持一次注册一个设备,而且需要传入参数file_operations。默认写死注册的设备号范围0~255。释放字符设备时,使用unregister_chrdev()。但不必使用cdev_xxx系列操作。
register_chrdev_region() 支持一次注册多个设备号,不需要传入参数file_operations,在cdev_init()中绑定cdev与file_operations。释放设备号时,使用unregister_chrdev_region。register_chrdev_region需要搭配cdev_xxx系列操作使用。
alloc_chrdev_region() 与register_chrdev_region()的区别在于前者申请的设备号由系统决定,后者由调用者指定。

APP中访问设备驱动程序,

  • 通过Linux系统调用,如open/close/read/write,调用file_operations中定义的接口函数。

Linux字符设备驱动结构

cdev结构体

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

#include <linux/cdev.h>

struct cdev {
    struct kobject kobj;           /* 内嵌的kobject对象 */
    struct module *owner;          /* 所属模块 */
    struct file_operations *ops;   /* 文件操作结构体 */
    struct list_head list;
    dev_t dev;                     /* 设备号 */
    unsigned int count;            /* 该设备关联的设备编号的数量 */
};

cdev结构体的dev_t成员定义设备号(32bit),其中高12bit为主设备号,低20bit为次设备号。

如何获取主次设备号,或dev_t?

  • 从dev_t获得主设备号和次设备号
MAJOR(dev_t dev); // 主设备号
MINOR(dev_t dev); // 次设备号
  • 通过主设备号、次设备号生成dev_t
MKDEV(int major, int minor); // 生成dev_t, 包含主次设备号信息

这几个宏定义如下:

#include <linux/kdev_t.h>

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)

#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS)) // 高12bit为主设备号
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  // 低20bit为次设备号
#define MKDEV(ma,mi)  (((ma) << MINORBITS) | (mi))

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

void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int  cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

1)cdev_init 初始化cdev成员,最重要的是建立cdev和file_operations之间的连接

源码:

/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev); /* 将整个结构体清零 */
    INIT_LIST_HEAD(&cdev->list);   /* 初始化list成员, 指向自身 */
    kobject_init(&cdev->kobj, &ktype_cdev_default); /* 初始化kobj成员 */
    cdev->ops = fops; /* 建立cdev和file_operations之间的连接 */
}

2)cdev_alloc 动态申请一个cdev内存

源码:

/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
    struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); /* 动态申请一个cdev内存, GFP_KERNEL: 无内存可用时可休眠 */
    if (p) {
        INIT_LIST_HEAD(&p->list);  /* 初始化list成员, 指向自身 */
        kobject_init(&p->kobj, &ktype_cdev_dynamic);  /* 初始化kobj成员 */
    }
    return p;
}

上面两个初始化函数,为何都没看到owner、dev、count 这3个成员的初始化?

对于owner成员,struct module类型对象,是内核对于一个模块的抽象。该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式初始化.owner = THIS_MODULE

对于dev和count成员,在cdev_add中才会赋值。

3)cdev_add 向内核添加一个cdev,完成字符设备的注册

这里需要提供参数dev(设备号)和count(该设备关联的设备编号的数量),直接赋值给cdev结构的dev和count成员。

/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
*         device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately.  A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;

    p->dev = dev;
    p->count = count;

    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p); /* 将cdev放入cdev_map中 */
    if (error)
        return error;

    kobject_get(p->kobj.parent); /* 增加引用计数 */

    return 0;
}

4)cdev_del 从内核删除一个cdev

/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)
{
    cdev_unmap(p->dev, p->count);      /* 将dev从cdev_map中擦除 */
    kobject_put(&p->kobj);             /* 减少引用计数 */
}

static void cdev_unmap(dev_t dev, unsigned count)
{
    kobj_unmap(cdev_map, dev, count); /* 将dev从cdev_map中擦除 */
}

分配、释放设备号

分配设备号

调用cdev_add()向系统注册字符设备前,应先申请设备号。分配设备号有2种方法:

1)静态申请:register_chrdev_region

register_chrdev_region() 用于已知起始设备号的情况,向系统静态申请设备号(范围)。

要申请的设备号范围:[from, from + count)。

有些设备号已被Linux内核开发者分配掉了,具体分配内容可查看Documentation/devices.txt。

/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
*        the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
    struct char_device_struct *cd;
    dev_t to = from + count;
    dev_t n, next;

    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        if (next > to)
            next = to;
        cd = __register_chrdev_region(MAJOR(n), MINOR(n),
                   next - n, name);
        if (IS_ERR(cd))
            goto fail;
    }
    return 0;
fail: /* 出错回滚 */
    to = n;
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
    }
    return PTR_ERR(cd);
}

2)动态申请:alloc_chrdev_region
alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。
得到的设备号会放入第一个参数dev中。alloc_chrdev_region相比register_chrdev_region,优点:alloc_chrdev_region会自动避开设备号重复的冲突。

/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers.  The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev.  Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;             /* 字符设备结构指针 */
    cd = __register_chrdev_region(0, baseminor, count, name); /* 注册单个指定主设备号、次设备号 */
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}

检查:注册设备成功后,会在/proc/devices 添加字符设备名称。
因此,可以利用insmod命令加载设备驱动后,观察/proc/devices值,判断是否注册了设备。

# cat /proc/devices

释放设备号

在调用cdev_del()从系统注销字符设备后,unregister_chrdev_region()应该被调用以释放原先申请的设备号。

从系统反注册设备号,范围:[from, from + count)

/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from.  The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
    dev_t to = from + count;
    dev_t n, next;

    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0); /* 下一个设备号dev_t */
        if (next > to)
            next = to;
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); /* 反注册单个设备号, 并释放空间 */
    }
}

file_operations结构体

file_operations 是设备驱动程序与APP交互的接口,其成员函数是字符设备驱动程序设计的主体,实际会在APP调用open/write/read/close等系统调用时被内核调用。

file_operations结构体定义:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
            loff_t, size_t, unsigned int);
    int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
            u64);
    ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
            u64);
};

主要成员:

  • llseek() 用来修改一个文件的当前读写位置,并将新位置返回,出错时,函数返回一个负值。
  • read() 用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。对应用户空间read(2)。
  • write() 向设备发送数据,成功时函数返回写入的字节数。如果未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。对应用户空间write(2)。

read和write返回0,暗示end-of-line(EOF)。

  • unlocked_ioctl() 提供设备相关控制命令的实现(不是读,也不是写),成功时返回一个非负值。对应用户空间fcntl(2)应。
  • mmap() 将设备内存映射到进程的虚拟地址空间,如果设备驱动未实现该函数,用户调用mmap()系统调用时返回-ENODEV。对应用户空间mmap(2)。与mmap对应的是unmap。
  • open() 打开设备,用于初始化设备状态。用户空间调用open(2)时,设备驱动的open()被调用。驱动程序可以不实现该函数,设备打开操作永远成功。与open对应的是release。
  • release() 释放设备资源。如果open()中有申请系统资源,则可以在release()中释放。对应用户空间close(2)。
  • poll() 用于询问设备是否可以被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程阻塞。
  • aio_read()/aio_write() 分别对与文件描述符对应的设备进行异步读、写操作。设备实现这2个函数后,用户空间可以对该设备文件描述符执行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系统调用进行读写。

字符设备驱动的组成

Linux中,字符设备驱动组成:字符设备驱动模块加载、卸载函数,字符设备驱动的file_operations结构体的成员函数。

字符设备驱动模块的加载、卸载函数

加载函数应该实现:1)设备号的申请;2)cdev的注册。

卸载函数应该实现:1)设备号的释放;2)cdev的注销。

典型的设备结构体、模块加载函数、卸载函数代码形式:

/* 设备结构体
struct xxx_dev_t = {
    struct cdev cdev;
    ...
} xxx_dev;
 */
/* 设备驱动模块加载函数 */
static init __init xxx_init(void)
{
    ...
    cdev_init(&xxx_dev.cdev, &xxx_fops);          /* 初始化cdev */
    xxx_dev.cdev.owner = THIS_MODULE;
    /* 获得字符设备号 */
    if (xxx_major) {
        register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
    } else {
        alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
    }

    ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */
    ...
}

/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
    unregister_chrdev_region(xxx_dev_no, 1);     /* 释放占用的设备号 */
    cdev_del(&xxx_dev.cdev);                     /* 注销设备 */
}

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

file_operations的成员函数是字符设备驱动跟内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()/write/ioctl()。

典型字符设备驱动代码形式:

/* 读设备
 * filp: 文件结构指针
 * buf: 用户空间内存地址, 在内核空间不能直接读写
 * count: 要读的字节数
 * f_pos: 读的位置相对于文件开头的偏移
 */
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    ...
    copy_to_user(buf, ..., ...); /* 将数据从内核空间拷贝到用户空间 */
    ...
}

/* 写设备 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    ...
    copy_from_user(.... buf, ...); /* 将数据从用户空间拷贝到内核空间 */
    ...
}

/* ioctl函数 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    ...
    switch(cmd) {
    case XXX_CMD1:
        ...
        break;
    case XXX_CMD2:
        ...
        break;
    default:
        /* 不支持的命令 */
        return -ENOTTY;
    }
    return 0;
}

copy_to_user和copy_from_user

注:用户空间不能直接访问内核空间的内存,所以要借助copy_to_user()将数据从内核空间拷贝到用户空间;
同样地,内核空间不能直接访问用户空间的内存,所以借助copy_from_user()将数据从用户空间拷贝到内核空间。

#include <linux/uaccess.h>

/* 用户 -> 内核 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
/* 内核 -> 用户 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
注:函数返回不能被复制的字节数。如果完全复制成功,返回0;如果失败,返回负值。

其源码如下:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
    if (likely(access_ok(VERIFY_READ, from, n))) /* 检查地址的合法性, from起始地址, 长度n */
        n = __copy_from_user(to, from, n);       /* 数据拷贝, 但不做地址合法性检查 */
    else
        memset(to, 0, n);
    return n;
}

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
    if (likely(access_ok(VERIFY_WRITE, to, n))) /* 检查地址的合法性, to起始地址, 长度n */
        n = __copy_to_user(to, from, n);        /* 数据拷贝, 但不做地址合法性检查 */
    return n;
}

likely:是宏定义,常用于编译器优化,告诉编译器分支大概率会发生。
access_ok(type, addr, size):内核空间可以访问用户空间的缓冲区,但访问之前需要用access_ok检查其合法性,以确定传入的缓冲区地址的确术语用户空间。

如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user()。

int val;                    /* 内核空间变量 */
...
get_user(val, (int* ) arg); /* 用户 -> 内核, arg 是用户空间地址 */
...
put_user(val, (int* ) arg); /* 内核 -> 用户, arg 是用户空间地址 */

copy_from_user函数中的__user宏是什么?
该宏表明背后的指针指向用户空间,实际上更多地充当了代码注释的功能。

#ifdef __CHECKER__
# define __user __attribute__((noderef, address_space(1)))
#else
# define __user
#endif

put_user和get_user

put_user(), get_user() 也有另外一个版本:__put_user(), __get_user()。区别在于__put_user()不用access_ok()检查地址的合法性,而put_user()会。通常,在调用__put_user()之前,会手动检查用户空间缓冲区。
get_user()和__get_user() 关系类似。

I/O控制函数unlocked_ioctl

I/O控制函数的cmd参数为事先定义的I/O控制命令,arg为对应于命令的参数。例如,对于串行设备,如果SET_BAUDRATE是设置波特率的命令,那arg就应该是波特率值。

字符设备驱动文件操作file_operations

字符设备驱动文件操作,通过定义file_operations实例,并将具体设备驱动函数赋值给file_operations成员来完成。

struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    .unlocked_ioctl = xxx_ioctl,
};

通过模块加载函数中调用cdev_init(&xxx_dev.cdev, &xxx_fops) 为cdev和fops建立连接。


参考

[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.
[2] https://blog.csdn.net/zqixiao_09/article/details/50839042

原文链接: https://www.cnblogs.com/fortunely/p/16396800.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Linux 字符设备驱动

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402525

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:06
下一篇 2023年4月21日 上午11:06

相关推荐