在Linux Device Driver这本书中讲述了一个scull驱动的实现,虽然这个例子是一个很简单的字符驱动程序,但对于初学者者来说,还是有些复杂。我将其修改了一下:将该设备模型改为在内存中固定大小且连续的区域(去掉原书中复杂的数据结构),可以对其进行打开、写入数据、读出数据等操作。接下来将描述该字符设备驱动的实现过程,以及如何使用该设备。

       Linux对于设备是通过面向对象思想实现的。对于一个字符设备,通过数据结构struct cdev来描述,而我们实现的scull通过数据结构struct scull_dev表示,scull_dev类是继承cdev类。因C语言没有定义面向对象的语法,在这里使用嵌套实现。这样就得到我们的设备模型struct scull_dev,data表示我们的硬件设备的起始地址,而size则表示这个硬件设备最大的存储空间。

<linux/cdev.h>
struct scull_dev {
    int size;
    void *data;
    struct cdev cdev;
};

       scull_dev的属性已经定义好了,接下来要定义其方法,即如何操作该设备。对于字符设备Linux,在struct file_operations已经定义好了操作字符设备的接口,通过提供统一的接口可以实现Unix一切皆文件的思想(网络设备除外)。对于我们要实现的scull,需要提供open、write、read、lseek等操作。

<linux/fs.h>
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 *);
	int (*open) (struct inode *, struct file *);
	int (*release) (struct inode *, struct file *);
	...
};

       open函数有两个参数,一个指向inode的指针,一个指向file的指针。inode表示期望打开的设备节点,在其成员中有个一个指向字符设备cdev的指针i_cdev,我们可以通过该指针得到指向scull设备的指针(借助container_of宏实现)。而file用于表示将要打开的设备描述,包含期望打开文件的标志f_flags(读写标志)。我们需要使用指向设备的指针,初始化file成员private_data,该成员指向的是将要打开的设备。

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

       release函数参数与open相同,由于我们要实现的scull设备一直存在于内存中,无需任何释放动作,因此只需要返回0即可。

int (*release) (struct inode *, struct file *);

       wirte函数,用于向设备写入数据。第一个参数表示打开的文件(即open中的file)。第二个参数是指向用户空间,期望写入数据的初始地址。第三个指针是期望写入的数据长度。第四个指针是当前写的位置,write结束时,需要更新该指针指向的数据。write函数的返回值为实际写入数据的长度。由于驱动程序处在内核空间,内核空间是不能直接读取用户地址空间的数据(用户空间也不能读取内核地址空间的数据),我们需要借用函数copy_from_user(),从用户空间将数据拷贝的内核空间。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

       read函数,用于读取设备中的数据,参数与write相类似,同样需要借助copy_to_user()函数,将数据从内核空间拷贝的用户空间。

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

       llseek函数,用于设置当前读写的位置。第一个参数为file,第二个参数为偏移量,第三个参数是描述第二个参数偏移量的参考位置。第三个参数只有三种有效值:SEEK_SET(0,相对于文件起始位置的偏移量),SEEK_CUR(1,相对于当前所处位置的偏移量),SEEK_END(1,相对于文件末的偏移量)。

loff_t (*llseek) (struct file *, loff_t, int);

       scull_dev设备的类已经构造完成,接下来就需要使用该类构建设备驱动模块。首先我们需要定义设备驱动模块的初始化函数al_scull_init()。通过宏控module_init告诉内核指定模块的初始化函数。初始化函数需要完成如下操作:

  1. 为字符设备注册一个可用的设备号,内核设通过设备号dev_t来区分不同的设备,因此我们需要首先注册或者分配一个可用的设备号。当已经知道一个可用的设备号时,可以直接通过register_chrdev_region(),为设备注册该设备号。但一般情况是为设备动态的分配一个或多个连续的设备号,则需要通过alloc_chrdev_region()来实现。
  2. 定义一个scull_dev设备,并对其初始化;我们动态分配一个scull_dev对象,初始化其成员data与size,对于cdev成员,需要初始化其成员ops,该成员是指向struct file_operations,即包含了操作该设备方法的指针。
  3. 将该设备添加到设备列表中。我们通过cdev_add()函数,将scull_dev和它对应的设备号添加到设备列表中。

       当然有初始化,就应该有清除函数al_scull_exit(),类似的,通过宏控module_exit告诉内核模块清除函数。清除函数需要将设备从设备列表中移除,释放动态分配的内存以及注销使用的设备号。

       至此,我们的设备驱动程序已经设计完成,接下来通过如下makefile文件编译好驱动程序scull.ko(编译scull驱动程序需要首先在Linux下构建内核源码树)。由于Linux内核可以动态的加载和移除模块,因此可以通过下述指令加载和移除我们的驱动程序:

#Makefile
ifneq ($(KERNELRELEASE),)
	obj-m := scull.o
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif 



#加载scull模块
$sudo insmod scull.ko

#移除scull模块
$sudo rmmod scull

#在内核中需要使用printk函数输出,在Ubuntu下不会在控制台显示,需要使用下述命令查看打印的log
$dmesg

       当我们加载了scull模块驱动程序后,可以在/proc/devices文件中查找到我们的字符设备scull。加载驱动后系统不会自动创建一个设备,接下来我们需要使用如下命令创建一个scull设备。创建好设备后,就可以在用户空间对scull设备进行操作(test.c)。

#在/dev/scull 创建一个字符设备c,主设备号为major,从设备号为0
#scull的主设备号可以在/proc/devices中查找到
$sudo mknod /dev/scull c major 0

       最后需要注意的是该设备驱动程序并没有考虑多线程的安全性,也未实现对scull的一些高级操作,这些会考虑在之后的scull驱动程序版本中添加。

       


完整源码:

       scull.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

/*
 * scull device driver
 **/    
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alants");
    

struct scull_dev {
        int size;
        void *data;
        struct cdev cdev;
};


#define SCULL_SIZE 1024

static int major = 0;
static int minor = 0;
static dev_t dev;
static char *scull_name = "scull";
static struct scull_dev *scull_device;

static inline void clean_up_scull_data(struct scull_dev *sdev)
{
        kfree(sdev->data);
        sdev->data = NULL;
}
static int al_scull_open(struct inode *inode, struct file *fp)
{

        struct scull_dev *sdev = container_of(inode->i_cdev, struct scull_dev, cdev);

        if ((fp->f_flags &O_ACCMODE) == O_WRONLY) {
                clean_up_scull_data(sdev);
        }

        fp->private_data = sdev;

        printk(KERN_ALERT "scull open!\n");
        return 0;
}

static int al_scull_write(struct file *fp, const char __user *buffer, size_t st, loff_t * pos)
{
        struct scull_dev *sdev = fp->private_data;
        int result = -ENOMEM;
      
        if (!sdev->data) {
                sdev->data = kmalloc(SCULL_SIZE,GFP_KERNEL);
                if (!sdev->data) {
                        goto nomem;
                }
        }
        
        result = (sdev->size - *pos) > st ? st : (sdev->size - *pos);
        printk(KERN_ALERT "scull_write : result = %d st = %d pos = %d\n", result, st, *pos);
        copy_from_user((sdev->data + *pos), buffer, result);

        *pos += result; 
nomem:
        printk(KERN_ALERT "scull_write char: %d!\n", result);
        return result;
}

static int al_scull_read(struct file *fp, char __user *buffer, size_t st, loff_t *pos)
{
        struct scull_dev *sdev = fp->private_data;
        int result;

        if (!sdev->data) {
                result = 0;
                goto nodata;
        }


        result = (sdev->size - *pos) > st ? st : (sdev->size - *pos);
        printk(KERN_ALERT "scull_read : result = %d st = %d pos = %d\n", result, st, *pos);
        copy_to_user(buffer, (sdev->data + *pos), result);

        *pos += result;
         
nodata:
        printk(KERN_ALERT "scull_read char: %d!\n", result);
        return result;
}

static int al_scull_release(struct inode *inode, struct file *fp)
{
        printk(KERN_ALERT "scull release!\n");
        return 0;
}

static loff_t al_scull_llseek(struct file *fp, loff_t off, int whence)
{
        loff_t result;
        struct scull_dev *sdev = fp->private_data;

        switch(whence) {
        case 0:
                result = off;
                break;
        case 1:
                result = fp->f_pos + off;
                break;
        case 2:
                result = sdev->size + off;
                break;
        default:
                return -EINVAL;
        }

        if (result > SCULL_SIZE) {
                return -EINVAL;
        }

        fp->f_pos = result;

        printk(KERN_ALERT "scull llseek!\n");
        return result;
}

static struct file_operations fops = {
        .owner = THIS_MODULE,
        .open = al_scull_open,
        .write = al_scull_write,
        .read = al_scull_read,
        .release = al_scull_release,
        .llseek = al_scull_llseek,
};

static void scull_setup_dev(struct scull_dev *sdev, int index) 
{
        dev_t dt = MKDEV(major, index);

        cdev_init(&sdev->cdev, &fops);
        sdev->cdev.owner = THIS_MODULE;
        sdev->cdev.ops = &fops;
        if (cdev_add(&sdev->cdev, dt, 1)) {
                printk(KERN_ALERT "ERROR: cdev_add %d\n", index);
        }
}

static int al_scull_init(void)
{
        int result = 0;

        if (major) {
                dev = MKDEV(major, minor);
                result = register_chrdev_region(dev, 1, scull_name);
        } else {
                result = alloc_chrdev_region(&dev, minor, 1, scull_name);
                major = MAJOR(dev);
        }
        
        if (result < 0) {
                printk(KERN_ALERT "register dev error!\n");
                return result;
        }

        scull_device = kmalloc(sizeof(struct scull_dev), GFP_KERNEL);
        if (!scull_device) {
                printk(KERN_ALERT "no memory!\n");
                result = -ENOMEM;
                goto malloc_fail;
        }

        memset(scull_device, 0, sizeof(struct scull_dev));
        scull_device->size = SCULL_SIZE;
        scull_setup_dev(scull_device, 0);

        printk(KERN_ALERT "%s init, major = %d\n", scull_name, major);
        return result;

malloc_fail:
        unregister_chrdev_region(dev, 1);
        return result;

}


static void al_scull_exit(void)
{
        
        cdev_del(&scull_device->cdev);
        clean_up_scull_data(scull_device);
        kfree(scull_device);
        scull_device = NULL;
        unregister_chrdev_region(dev, 1);

        printk(KERN_ALERT "%s exit!\n", scull_name);
}


module_init(al_scull_init);
module_exit(al_scull_exit);

       

       test.c



#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

/*
 * 测试程序:打开/dev/scull设备,并写入和读取该字符设备中的数据。
 * 
 * 运行: sudo ./test
 *
 **/    

int main(void)
{
        int fd;
        int res;
        char buf[] = "This is a test code by alants!";
        char buf2[100];
        char buf3[100];
        fd = open("/dev/scull", O_RDWR);
        if (fd < 0) {
                printf("can't open\n");
        }

        res = write(fd, buf, 15);
        printf("write : %d\n", res);

        lseek(fd, 0, SEEK_SET);

        res = read(fd, buf2, 15);
        buf2[15] = '\0';
        printf("read : %d\n", res);
        printf("buf2 = %s\n", buf2);

        close(fd);
        fd = open("/dev/scull", O_RDWR);
        if (fd < 0) {
                printf("can't open\n");
        }

        res = read(fd, buf3, 15);
        buf3[15] = '\0';
        printf("read : %d\n", res);
        printf("buf3 = %s\n", buf3);
        
        close(fd);
        return 0;
}