LDD字符驱动学习

    xiaoxiao2024-12-24  17

    字符驱动程序

    scull ,即“simple character utility for loading localities, 区域装载的简单字符工具’”。

    是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个字符设备。

    字符设备的执行流程

    获取设备号 -> 注册设备 -> 关联File operations结构 -> open(打开设备) -> write ->read -> release资源 -> close(关闭设备)

    主设备号与次设备号

    主设备号标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的设备。

    设备编号

    在内核中,dev_t类型用来保存设备编号,在

    #include <linux/kedv_t.h> MAJOR(dev_t dev); MINOR(dev_t dev);

    如果需要将主设备号和次设备号转换成dev_t类型,则使用:

    MKDEV(int major, int minor);

    分配和释放设备编号

    在建立字符设备之前,驱动程序首先要做的是获得一个或多个设备的编号。完成该工作的必要函数是register_chrdev_region,该函数在

    int register_chrdev_region(dev_t first, unsigned int count, char *name);

    first是要分配的设备编号范围起始值 ,其次设备号经常被置为0。count是所有请求的连续设备编号的个数,name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。该函数分配成功时返回0,错误返回负的错误码。

    以上的固定分配,有时不明确所需要的设备编号,故通常采用动态分配,函数如下:

    int alloc_chrdev_region(dev_t, unsigned int firstminor, unsigned int count, char *name);

    dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数。 fisetminor 应当是请求的第一个要用的次编号;它常常是 0。count 和 name 参数如同给 request_chrdev_region 的一样。

    为了防止选定的设备号出现冲突和麻烦,驱动程序应尽量使用动态分配而不是静态分配。

    不管哪种方法,不使用时需要释放设备号,函数如下:

    void unregister_chrdev_region(dev_t first, unsigned int count);

    通常我们会在模块的清除函数中调用unregister_chrdev_region函数。

    由于动态分配的主设备号无法预先创建设备节点,所以需要从/proc/devices中读取得到。可以将对insmod的调用替换成一个脚本,该脚本在调用insmod之后读取/proc/devices以获得分配的主设备号,然后创建对应的设备文件。其脚本为:

    #!/bin/sh module="scull" device="scull" mode="664" # 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定具体模块位置 # 这是因为新的modutiles默认不会在当前目录中查找模块 /sbin/insmod ./$module.ko $* || exit 1 # 删除原有节点 rm -f /dev/${device}[0-3] major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices) mknod /dev/${device}0 c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2 mknod /dev/${device}3 c $major 3 # 给予适当的组属性许可,并修改组 # 并非所有的发行版都具有staff组,有些有wheel组 group="staff" grep -q '^staff:' /etc/group || group="wheel" chgrp $group /dev/${device}[0-3] chmod $mode /dev/${device}[0-3]

    分配主设备号的最佳方式是:默认采用动态分配, 同时保留在加载甚至是编译时批定设备号的余地。常用的获取主设备号的代码为:

    if (scull_major) { dev = MKDEV(scull_major, scull_minor); result = register_chrdev_region(dev, scull_nr_devs, "scull"); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); scull_major = MAJOR(dev); } if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; }

    数据结构

    file_operation结构

    struct file_operations scull_fops = { .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl, .open = scull_open, .release = scull_release, };

    这种结构体初始化定义是C语言最新C99标准,称为指定初始化(designated initializer)。采用这种方式的优势就在于由此初始化不必严格按照定义时的顺序。这带来了极大的灵活性。

    file结构

    此file结构不是用户空间中的FILE,两者无关系,FILE是在C库中定义的,不会出现在内核代码中,而struct file是一个内核结构,它不会出现在用户程序中。file结构代表一个打开的文件,同open创建,直到最后的close之后才释放这个结构,指向struct file的指针为file和filp,其中file指的是结构本身,filp指向该结构的指针。

    inode结构

    内核用inode结构在内部表示文件,和file结构不同,file表示打开的文件描述符,对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。

    字符设备注册

    struct cdev结构表示字符设备,获取一个独立的cdev结构并嵌入到自己的设备特定结构中,其代码如下:

    struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;

    之后初始化已分配到的结构:

    void cdev_init(struct cdev *cdev, struct file_operations *fops);

    cdev的字段需有初始化,它有一个所有者字段,应该设置成THIS_MODULE。

    cdev设置好后,通过下面代码告诉内核该结构的消息:

    int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

    dev 是 cdev 结构,num 是这个设备对应的第一个设备号,count 是应当关联到设备的设备号的数目.通常是1, 但在某些情况下,会有多个设备号对应于一个特定的设备。

    如果要移除一个字符设备,做如下调用:

    void cdev_del(struct cdev *dev);

    注册实例,scull:

    struct scull_dev { //quantum,量子,一个内存区称为一个量子,而这个指针数组称为量子集 struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */ };

    内核和设备间的接口struct cdev必须如上所述的被初始化并添加到系统中,其代码如下:

    static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; err = cdev_add (&dev->cdev, devno, 1); /* Fail gracefully if need be */ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); }

    open和release

    open原型:

    int (*open)(struct inode *inode, struct file *filp);

    其作用为:

    检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误如果它第一次打开, 初始化设备如果需要, 更新 f_op 指针.分配并填充要放进 filp->private_data 的任何数据结构

    scull_open代码:

    int scull_open(struct inode *inode, struct file *filp) { struct scull_dev *dev; /* device information */ dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev; /* for other methods */ /* now trim to 0 the length of the device if open was write-only */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) { scull_trim(dev); /* ignore errors */ } return 0; /* success */ }

    release方法作用与open相反,该方法实现被称为device_close或device_release,作用如下:

    释放 open 分配在 filp->private_data 中的任何东西在最后的 close 关闭设备

    scull中的release代码:

    int scull_release(struct inode *inode, struct file *filp) { return 0; }

    并不是每个close系统调用都会引起release方法的调用,只有真正释放设备数据的close调用才会调用这个方法,所以scull中的release代码这么少。

    read和write

    read和write原型相似,如下 :

    ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);

    filp 是文件指针,count 是请求的传输数据大小, buff 参数指向用户空间的缓存区, 最后, offp 是一个指针指向一个”long offset type,长偏移类型”对象, 它指出用户正在存取的文件位置. 返回值是一个”signed size type,有符号的尺寸类型”;

    大多数read和write方法实现的核心部分如下,用于拷贝任意的一段字节序列。

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

    这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。

    快速参考

    #include <linux/types.h> dev_t dev_t 是用来在内核里代表设备号的类型. int MAJOR(dev_t dev); int MINOR(dev_t dev); 从设备编号中抽取主次编号的宏. dev_t MKDEV(unsigned int major, unsigned int minor); 从主次编号来建立 dev_t 数据项的宏定义. #include <linux/fs.h> "文件系统"头文件是编写设备驱动需要的头文件. 许多重要的函数和数据结构在此定义. int register_chrdev_region(dev_t first, unsigned int count, char *name) int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name) void unregister_chrdev_region(dev_t first, unsigned int count); 允许驱动分配和释放设备编号的范围的函数. register_chrdev_region 应当用在事先知道需要的主编号时; 对于动态分配, 使用alloc_chrdev_region 代替. int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); 老的( 2.6 之前) 字符设备注册函数. 它在 2.6 内核中被模拟, 但是不应当给新代码使用. 如果主编号不是 0, 可以不变地用它; 否则一个动态编号被分配给这个设备. int unregister_chrdev(unsigned int major, const char *name); 恢复一个由 register_chrdev 所作的注册的函数. major 和 name 字符串必须包含之前用来注册设备时同样的值. struct file_operations; struct file; struct inode; 大部分设备驱动使用的 3 个重要数据结构. file_operations 结构持有一个字符驱动的方法; struct file 代表一个打开的文件, struct inode 代表磁盘上的一个文件. #include <linux/cdev.h> struct cdev *cdev_alloc(void); void cdev_init(struct cdev *dev, struct file_operations *fops); int cdev_add(struct cdev *dev, dev_t num, unsigned int count); void cdev_del(struct cdev *dev); cdev 结构管理的函数, 它代表内核中的字符设备. #include <linux/kernel.h> container_of(pointer, type, field); 一个传统宏定义, 可用来获取一个结构指针, 从它里面包含的某个其他结构的指针. #include <asm/uaccess.h> 这个包含文件声明内核代码使用的函数来移动数据到和从用户空间. unsigned long copy_from_user (void *to, const void *from, unsigned long count); unsigned long copy_to_user (void *to, const void *from, unsigned long count); 在用户空间和内核空间拷贝数据
    转载请注明原文地址: https://ju.6miu.com/read-1294937.html
    最新回复(0)