LDD字符驱动学习

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013483704/article/details/52200046

字符驱动程序

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);
在用户空间和内核空间拷贝数据

猜你喜欢

转载自blog.csdn.net/u013483704/article/details/52200046
ldd