Chinaunix首页 | 论坛 | 博客
  • 博客访问: 4730198
  • 博文数量: 930
  • 博客积分: 12070
  • 博客等级: 上将
  • 技术积分: 11448
  • 用 户 组: 普通用户
  • 注册时间: 2008-08-15 16:57
文章分类

全部博文(930)

文章存档

2011年(60)

2010年(220)

2009年(371)

2008年(279)

分类: LINUX

2009-12-27 20:31:39

序言
Linux下的大部分驱动程序都是字符设备驱动程序,在这一章我们就扩展我们的“Hello World”程序来支持用户应用程序的读写操作。我们也会了解到字符设备是如何注册到系统中的,应用程序是如何访问驱动程序的数据的,及字符驱动程序是如何工作的。

设备号
通过前面的学习我们知道应用程序是通过设备节点来访问驱动程序及设备的,其根本是通过设备节点的设备号(主设备号及从设备号)来关联驱动程序及设备的,字符设备也不例外(其实字符设备只能这样访问)。这里我们详细讨论Linux内部如何管理设备号的。
  • 设备号类型
Linux内核里用“dev_t”来表示设备号,它是一个32位的无符号数,其高12位用来表示主设备号,低20位用来表示从设备号。它被定义在头文件里。内核里提供了操作“dev_t”的函数,驱动程序中通过这些函数(其实是宏,定义在文件中)来操作设备号。

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))


MAJOR(dev)用于获取主设备号,MINOR(dev)用于获取从设备号,而MKDEV(ma,mi)用于通过主设备号和从设备号构造"dev_t"数据。
另一点需要说明的是,dev_t数据类型支持2^12个主设备号,每个主设备号(通常是一个设备驱动)可以支持2^20个设备,目前来说这已经足够大了,但谁又能说将来还能满足要求呢?一个良好的编程习惯是不要依赖dev_t这个数据类型,切记必须使用内核提供的操作设备号的函数。
  • 字符设备号注册
内核提供了字符设备号管理的函数接口,作为一个良好的编程习惯,字符设备驱动程序应该通过这些函数向系统注册或注销字符设备号。

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)


register_chrdev_region用于向内核注册已知可用的设备号(次设备号通常是0)范围。由于历史的原因一些设备的设备号是固定的,你可以在内核源代码树的Documentation/devices.txt文件中找到这些静态分配的设备号。
alloc_chrdev_region用于动态分配的设备号并注册到内核中,分配的设备号通过dev参数返回。作为一个良好的内核开发习惯,我们推荐你使用动态分配的方式来生成设备号。
unregister_chrdev_region用于注销一个不用的设备号区域,通常这个函数在驱动程序卸载时被调用。

字符设备
Linux2.6内核使用“struct cdev”来记录字符设备的信息,内核也提供了相关的函数来操作“struct cdev”对象,他们定义在头文件中。可见字符设备及其操作函数接口定义的很简单。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

void cdev_init(struct cdev *, const 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 *);


对于Linux 2.6内核来说,struct cdev是内核字符设备的基础结构,用来表示一个字符设备,包含了字符设备需要的全部信息。
  • kobj:struct kobject对象数据,用来描述设备的引用计数,是Linux设备模型的基础结构。我们在后面的“Linux设备模型”在做详细的介绍。
  • owner:struct module对象数据,描述了模块的属主,指向拥有这个结构的模块的指针,显然它只有对编译为模块方式的驱动才由意义。一般赋值位“THIS_MODULE”。
  • ops:struct file_operations对象数据,描述了字符设备的操作函数指针。对于设备驱动来说,这是一个很重要的数据成员,几乎所有的驱动都要用到这个对象,我们会在下面做详细介绍。
  • dev:dev_t对象数据,描述了字符设备的设备号。
内核提供了操作字符设备对象“struct cdev”的函数,我们只能通过这些函数来操作字符设备,例如:初始化、注册、添加、移除字符设备。
  • cdev_alloc:用于动态分配一个新的字符设备 cdev 对象,并对其进行初始化。采用cdev_alloc分配的cdev对象需要显示的初始化ownerops对象。

// 参考drivers/scsi/st.c:st_probe 函数
struct cdev *cdev = NULL;
cdev = cdev_alloc();
// Error Processing
cdev->owner = THIS_MODULE;
cdev->ops = &st_fops;


  • cdev_init:用于初始化一个静态分配的cdev对象,一般这个对象会嵌入到其他的对象中。cdev_init会自动初始化ops数据,因此应用程序只需要显示的给owner对象赋值。cdev_init的功能与cdev_alloc基本相同,唯一的区别是cdev_init初始化一个已经存在的cdev对象,并且这中初始化会影响到字符设备删除函数(cdev_del)的行为,请参考cdev_del函数。
  • cdev_add:向内核系统中添加一个新的字符设备cdev,并且使它立即可用。
  • cdev_del:从内核系统中移除cdev字符设备。如果字符设备是由cdev_alloc动态分配的,则会释放分配的内存。
  • cdev_put:减少模块的引用计数,一般很少会有驱动程序直接调用这个函数。
文件操作对象
Linux中的所有设备都是文件,内核中用“struct file”结构来表示一个文件。尽管我们的驱动不会直接使用这个结构中的大部分对象,其中的一些数据成员还是很重要的,我们有必要在这里做一些介绍,具体的内容请参考内核源代码树头文件。

// struct file 中的一些重要数据成员
const struct file_operations    *f_op;
unsigned int         f_flags;
mode_t            f_mode;
loff_t            f_pos;
struct address_space    *f_mapping;


这里我们不对struct file做过多的介绍,在后面的Linux虚拟文件系统中我们在做详细介绍。这个结构中的f_ops成员是我们的驱动所关心的,它是一个struct file_operations结构。Linux里的struct 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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    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 *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, 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 (*dir_notify)(struct file *filp, unsigned long arg);
    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 **);
};


这是一个很大的结构,包含了所有的设备操作函数指针。当然,对于一个驱动,不是所有的接口都需要来实现的。对于一个字符设备来说,一般实现open、release、read、write、mmap、ioctl这几个函数就足够了。
这里需要指出的是,open和release函数的第一个参数是一个struct inode对象。这个一个内核文件系统索引节点对象,它包含了内核在操作文件或目录是需要的全部信息。对于字符设备驱动来说,我们关心的是从struct inode对象中获取设备号(inode的i_rdev成员)内核提供了两个函数来做这件事。

static inline unsigned iminor(const struct inode *inode)
{
    return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
    return MAJOR(inode->i_rdev);
}


尽管我们可以直接从inode->i_rdev获取设备号,但是尽量不要这样做。我们推荐你调用内核提供的函数来获取设备号,这样即使将来inode->i_rdev有所变化,我们的程序也会工作的很好。

新的驱动
我们已经了解了Linux字符驱动程序的知识点,下面我们就用上面学到的知识来构建一个字符驱动程序。前面我们的“Hello World”程序仅仅是在驱动注册和注销的时候打印“Hello world!”消息,这里我们将通过用户应用程序来读到这个消息,甚至我们可以修改从驱动读到的信息。这个驱动的读写逻辑是很简单的,几乎没有什么逻辑,但它足以满主现在的需要了。首先还是来浏览我们新的驱动,并编译运行这个驱动看看运行效果,最后我们对这个驱动做个详细的介绍。

// hello.c
#include
#include
#include
#include
#include
#include

#define DEFAULT_MSG "Hello World!\n"
#define MAXBUF 20
static unsigned char hello_buf[MAXBUF];

static int hello_open (struct inode *inode, struct file *filp);
static int hello_release (struct inode *inode, struct file *filp);
static ssize_t hello_read (struct file *filp, char __user *buf,
                           size_t count, loff_t *pos);
static ssize_t hello_write (struct file *filp, const char __user *buf,
                            size_t count, loff_t *pos);

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

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

static ssize_t hello_read (struct file *filp, char __user *buf,
                           size_t count, loff_t *pos)
{
    int size = count < MAXBUF ? count : MAXBUF;
    printk("hello: Read Hello World !\n");
    if (copy_to_user(buf, hello_buf, size))
        return -ENOMEM;
    return size;
}
static ssize_t hello_write (struct file *filp, const char __user *buf,
                            size_t count, loff_t *pos)
{
    int size = count < MAXBUF ? count : MAXBUF;
    printk("hello: Write Hello World !\n");
    memset(hello_buf, 0, sizeof(hello_buf));
    if (copy_from_user(hello_buf, buf, size))
        return -ENOMEM;
    return size;
}

static struct file_operations hello_fops = {
        .read = hello_read,
        .write = hello_write,
        .open = hello_open,
        .release = hello_release,
};

static struct cdev *hello_cdev;
static int __init hello_init(void)
{
    dev_t dev;
    int error;

    error = alloc_chrdev_region(&dev, 0, 2, "hello");
    if (error)
    {
        printk("hello: alloc_chardev_region failed!\n");
        return error;
    }
    hello_cdev = cdev_alloc();
    if (hello_cdev == NULL)
    {
        printk("hello: alloc cdev failed!\n");
        unregister_chrdev_region(hello_cdev->dev, 2);
        return -ENOMEM;
    }

    hello_cdev->ops = &hello_fops;
    hello_cdev->owner = THIS_MODULE;

    error = cdev_add(hello_cdev, dev, 1);
    if (error)
    {
        printk("hello: cdev_add failed!\n");
        unregister_chrdev_region(hello_cdev->dev, 2);
        cdev_del(hello_cdev);
        return error;
    }

    memset (hello_buf, 0, sizeof(hello_buf));
    memcpy(hello_buf, DEFAULT_MSG, sizeof(DEFAULT_MSG));
    printk("hello: Hello World!\n");

    return 0;
}
static void __exit hello_exit(void)
{
    unregister_chrdev_region(hello_cdev->dev, 2);
    cdev_del(hello_cdev);
    printk("hello: Goodbye World\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");


编译并测试这个驱动程序:

##需要root权限来运行
#make
#insmod helloworld.ko

##获取设备的设备号并创建设备节点
#cat /proc/devices
...
254 hello
#mknod /dev/hello c 254 0

##读写测试这个驱动
#cat /dev/hello
Hello World!
......
#echo My World! >/dev/hello
#cat /dev/hello
My World!
......


  • 初始化函数
这个驱动程序会向系统添加一个新的字符设备hello:

  static struct cdev *hello_cdev;


首先,在我们向内核系统添加我们的设备之前,先向内核系统为我们的设备动态申请设备号,不要忘记这个设备号会显示在/proc/devices列表里:

    error = alloc_chrdev_region(&dev, 0, 2, "hello");
    if (error)
    {
        printk("hello: alloc_chardev_region failed!\n");
        return error;
    }


申请到设备号后,我们就可以向系统添加我们的字符设备hello了,这里我们动态分配了字符设备对象hello_cdev:

    hello_cdev = cdev_alloc();
    if (hello_cdev == NULL)
    {
        printk("hello: alloc cdev failed!\n");
        unregister_chrdev_region(hello_cdev->dev, 2);
        return -ENOMEM;
    }

    hello_cdev->ops = &hello_fops;
    hello_cdev->owner = THIS_MODULE;

    error = cdev_add(hello_cdev, dev, 1);
    if (error)
    {
        printk("hello: cdev_add failed!\n");
        unregister_chrdev_region(hello_cdev->dev, 2);
        cdev_del(hello_cdev);
        return error;
    }


  • 操作函数
我们的字符设备的操作函数对象是hello_fops,它的定义如下:

  static struct file_operations hello_fops = {
        .read = hello_read,
        .write = hello_write,
        .open = hello_open,
        .release = hello_release,
};


这里我们的openrelease函数什么也没有做,仅仅是返回成功(返回0)。readwrite函数会把设备内存数据拷贝到用户空间或根据用户空间的数据修改设备内存。我们的设备内存是一个固定大小的数组,共20个字节。
这个readwrite函数调用了两个新的接口copy_to_usercopy_from_user。这两个函数用同用户空间交换数据。Linux系统使用虚拟地址空间管理内存,分为用户空间和内核空间。一般是这样划分的:用户地址空间是0-3G,内核地址空间是3-4G。用户的应用程序只能运行在用户空间,它不能直接访问内核地址空间的数据,同样内核地址空间的程序也不能直接访问用户空间的数据。因此内核提供函数接口来同用户空间应用程序交换数据,他们定义在同文件中。

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

  • 注销函数
注销函数会向系统注销我们的字符设备,并释放在初始化函数中申请到的空间:

    unregister_chrdev_region(hello_cdev->dev, 2);
    cdev_del(hello_cdev);


后记
在这一章里,我们为我们的“Hello World”驱动添加字符驱动接口,并实现了读写操作,尽管它的读写逻辑还有些问题,不过并不影响我们对于字符驱动的理解(这完全是驱动实现逻辑上的问题了,也许我们的驱动就是这个设计的:) )。现在我们清楚的知道了一个字符设备是如何注册的以及是如何的工作的了,你已经可以设计自己的字符设备驱动了。不妨实现自己的驱动来看看,比如从内核获取一些数据。

也许你会问:为什么每次的读写操作都是从设备内存的第一个字节开始?简单的回答就是我们无法保证应用程序的设备内存的同步操作。因此我们把问题简单化了,这样就不会存在同步的问题了。那么到底内核是如何实现同步的呢?我们会在下一章做详细的介绍,同时扩展我们的“Hello World”驱动,并实现真正的按流的方式操作设备。
阅读(1726) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~