Chinaunix首页 | 论坛 | 博客
  • 博客访问: 135284
  • 博文数量: 38
  • 博客积分: 2510
  • 博客等级: 少校
  • 技术积分: 376
  • 用 户 组: 普通用户
  • 注册时间: 2010-05-07 22:44
文章分类
文章存档

2010年(38)

我的朋友

分类: LINUX

2010-05-08 16:28:56

scull:Simple Character Utility for Loading Localities,区域装载的简单字符工具。

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

主设备号和次设备号

主设备号标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的设备。可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当做设备本地数组的索引。

设备编号的内部使用 dev_t类型()用来保存设备编号,包括主设备号和次设备号。在内核2.6.0版本中,dev_t是一个32为32位,其中12位表示主设备号,其余20位表示次设备号。

获得主设备号和次设备号

MAJOR(dev_t dev);
MINOR(dev_t dev);

将主设备号和次设备号转换成dev_t类型

MKDEV(int major, int minor);


分配和释放设备编号建立一个字符设备之前,首先要获得一个或多个设备编号。

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

first是要分配的设备编号范围的起始值,常设置为0.

count是所请求的连续设备编号的个数.

name是和该编号范围关联的设备名称,将出现在/proc/devices和sysfs中.

动态分配设备编号

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

dev仅用于输出,成功完成调用后将保存已分配范围的第一个编号.

firstminor是要使用的被请求的第一个次设备号,通常是0.

设备编号释放(通常在清除函数中调用)

void unregister_chrdev_region(dev_t first, unsigned int count);

scull.c中获取主设备号的代码片段

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_operations()

file_operations结构用来将驱动程序连接到上面获得的设备编号。每个打开的文件(在内部由一个file结构表示)和一组函数关联(通过包含指向一个file_operations结构的f_op字段)。

scull设备驱动程序所实现的方法

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,
};

struct file结构()

struct file结构和用户空间的FILE没有任何关联,FILE在C库中定义不会出现在内核,struct file是一个内核结构,不会出现在用户空间。 file代表一个打开的文件,其重要的成员如下:

mode_t f_mode;

    文件模式。通过FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写。

loff_t f_pos;

    当前读写位置。loff_t是64位,gcc术语来说是long long.

unsigned int f_flags;

    文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC。

struct file_operations *f_op;

    与文件相关的操作。内核在open的时候对这个指针赋值,以后需要处理这些操作时就读取这个指针。

void *private_date;

    open系统调用在调用驱动程序的open前将这个指针置为NULL。驱动程序可以将这个字段用于任何目的或者忽略这个字段。也可以用这个字段指向已分配的数据,但一定要在内核销毁file结构的前在release方法中释放内存。

struct dentry *f_dentry;

    文件对应的目录项结构。

inode结构

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

inode结构中与驱动程序代码有关的成员:

dev_t i_rdev;

    对表示设备文件的inode结构,该字段包含了真正的设备标号。

struct cdev *i_cdev;

    struct cdev表示字符设备的内核内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。

从inode结构中获得主设备号和次设备号的宏

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

字符设备的注册

内核内部使用struct cdev结构表示字符设备。在内核调用设备操作前,必须分配一个或多个上述结构,因此代码要包含,其中定义了这个结构及其辅助函数。

字符设备的分配

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

字符设备的初始化

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


另外,struct cdev的所有者字段也应该设置为THIS_MODULES.

通知内核该结构的信息

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

dev是cdev结构,num是该设备对应的第一个设备编号,count是应该和该设备关联的设备编号的数量。

移除一个字符设备 

void cdev_del(struct cdev *dev);


Scull设备中的设备注册

scull内部,struct scull_dev结构表示每个设备

struct scull_dev {
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 */
};

scull设备的注册

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);
}


早期方法

注册,移除字符设备驱动程序的经典方法

int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);


major是设备的主设备号,name是驱动程序名称,fops是默认的file_operations结构。

register_chrdev()将为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构。不能使用大于255的主设备号和次设备号。

open和release方法

open方法提供给驱动程序初始化能力。

  • 检查设备特定的错误
  • 如果设备首次打开,则对其初始化
  • 如有必要,更新f_op指针
  • 分配并填写置于file->private_data里的数据结构

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

inode参数的i_cdev字段包含了cdev结构,可以从cdev结构得到scull_dev结构。

#include <linux/kernel.h>

struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */


将scull_dev结构指针保存到file结构的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相反。

  • 释放由open分配的、保存在filp->private_data中的所有内容
  • 在最后一次关闭操作时关闭设备

scull_release代码片段

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


并不是每个close系统调用都会引起对release方法的调用。只有真正释放设备数据结构的close调用才会调用这个方法。内核对每个file结构维护其被使用次数的计数器。只有在file结构的计数归0时,close系统调用才会执行release方法。

scull设备的内存使用

scull使用的内存区域也成为设备,长度可变。

scull驱动程序引入Linux内核中用于内存管理的2个核心函数。

#include <linux/slab.h>

void *kmalloc(size_t size, int flags);
void kfree(void *ptr);


前者试图分配size个字节的内存,其返回值指向该内存,失败返回NULL。flags表示分配内存的方法。

scull设备和它的内存区

 

scull结构的定义

/*
 * Representation of scull quantum sets.
 */

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

struct scull_dev {
    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 */
};

scull_trim函数负责释放数据区,并且在文件以写入方式打开时由scull_open调用。

int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset; /* "dev" is not-null */
    int i;
    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) {
            for (i = 0; i < qset; i++)
                kfree(dptr->data[i]);
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}

模块的清楚函数也调用scull_trim函数,以便将由scull所使用的内存返回给系统。

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类型。

用于在用户地址空间和内核地址空间进行整段数据拷贝的函数

#include <asm/uaccess.h>

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);

这2个函数在进行数据拷贝之前先检查用户空间指针是否有效,如果无效则不拷贝,如果在拷贝的过程中遇到无效指针,则拷贝部分数据。

调用程序对read的返回值解释如下

  • 如果返回值等于传递给read系统调用的coun参数,说明请求的数据成功传输
  • 如果返回值为正,但小于count,说明只有部分数据成功传输,程序可能会重新读取数据
  • 如果返回值为0,表示到达了文件尾
  • 负值意味着发生了错误,该值指明了发生了什么错误,错误码在中定义

scull设备的read代码片段

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
        loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr; /* the first listitem */
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset; /* how many bytes in the listitem */
    int item, s_pos, q_pos, rest;
    ssize_t retval = 0;
    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    if (*f_pos >= dev->size)
        goto out;
    if (*f_pos + count > dev->size)
        count = dev->size - *f_pos;
    /* find listitem, qset index, and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;
    /* follow the list up to the right position (defined elsewhere) */
    dptr = scull_follow(dev, item);
    if (dptr = = NULL || !dptr->data || ! dptr->data[s_pos])
        goto out; /* don't fill holes */
    /* read only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
out:
    up(&dev->sem);
    return retval;
}


调用程序对write方法的返回值解释

  • 返回值等于count,则完成了所请求数据的传输
  • 返回值为正,但小于count,则只传输了部分数据,程序可能再次试图写入余下数据
  • 返回值为0,意味着什么也没写入。这个结果不是错误。标准库会重复调用write
  • 负值意味着发生错误,与read同解

scull设备的write代码

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
        loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr;
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset;
    int item, s_pos, q_pos, rest;
    ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    /* find listitem, qset index and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;
    /* follow the list up to the right position */
    dptr = scull_follow(dev, item);
    if (dptr = = NULL)
        goto out;
    if (!dptr->data) {
        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
        if (!dptr->data)
            goto out;
        memset(dptr->data, 0, qset * sizeof(char *));
    }
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
    /* write only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
    /* update the size */
    if (dev->size < *f_pos)
        dev->size = *f_pos;
out:
    up(&dev->sem);
    return retval;
}



readv和writev

向量型函数,具有一个结构数组,每个结构包含一个指向缓冲区的指针和长度值。

readv调用可用于将指定数量的数据依次读入每个缓冲区。

writev调用是把每个缓冲区内容收集起来,并将他们在一次写入操作中进行输出。

如果驱动程序没有提供用于处理向量操作的方法,readv和writev会通过多次调用read和write来实现。

向量操作函数原型

ssize_t (*readv) (struct file *filp, const struct iovec *iov,
                  unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov,
                  unsigned long count, loff_t *ppos);


iovec结构的定义在

struct iovec
{
    void _ _user *iov_base;
    __kernel_size_t iov_len;
};


iovec结构描述了一个用于传输的数据块--这个数据块起始位置在iov_base,长度为iov_len个字节。

count参数指明要操作多少个iovec结构。

scull设备驱动程序源码及测试

源码:

文件: scull.zip
大小: 10KB
下载: 下载

装载脚本:

文件: scull_load.zip
大小: 0KB
下载: 下载

测试代码:

文件: scull-test.zip
大小: 0KB
下载: 下载

 

阅读(1427) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~