【Linux驱动】字符设备驱动

,本文假定读者具备一定linux基础以及对linux驱动基础有所了解。

➜  ~  cat /proc/version
Linux version 3.13.0-43-generic (buildd@akateko) (gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1) )
 #72-Ubuntu SMP Mon Dec 8 19:35:44 UTC 2014

globalmem.c

关键词:register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region()、cdev_init()、cdev_add()、cdev_del()

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>

MODULE_LICENSE("Dual BSD/GPL");

#define GLOBALMEM_SIZE 0X1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 240

static int globalmem_major = GLOBALMEM_MAJOR;

//globalmem设备结构体
struct globalmem_dev{
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];//全局内存
};

struct globalmem_dev *globalmem_devp;//设备结构指针

//文件打开函数
int globalmem_open(struct inode *inode, struct file *filp)
{
    //将设备结构体指针赋值给文件私有数据指针
    filp->private_data = globalmem_devp;
    printk(KERN_ALERT "globalmem open\n");
    return 0;
}

//文件释放函数
int globalmem_release(struct inode *inode, struct file *filp)
{
    printk(KERN_ALERT "globalmem release\n");
    return 0;
}

//ioctl设备控制函数,linux kernel高版本中ioctl已经发生了改变,用了其余两个替换
/*static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg) { struct globalmem_dev *dev = filp->private_data;//获得设备结构体指针 switch(cmd) { case MEM_CLEAR://清空全局内存 memset(dev->mem, 0, GLOBALMEM_SIZE); printk(KERN_INFO "globalmem is set to zero\n"); break; default: return -EINVAL; } return 0; }*/

//读函数
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if(p >= GLOBALMEM_SIZE)
        return 0;
    if(count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE -p;
    if(copy_to_user(buf, (void*)(dev->mem +p), count))
        ret = -EFAULT;
    else
    {
        *ppos += count;
        ret = count;
// printk(KERN_INFO "read %d byte(s) from %d", count, p);
    }
    return ret;
}

//写函数
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned int count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if(p >= GLOBALMEM_SIZE)
        return 0;   
    if(count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    if(copy_from_user(dev->mem+p,buf,count))
        ret = -EFAULT;
    else
    {
        *ppos += count;
        ret = count;
// printk(KERN_INFO "written %d byte(s) form %d\n", count, p);
    }
    return ret;
}

//seek文件定位函数
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
    loff_t ret = 0;
    switch(orig)
    {
        case 0:
            if(offset < 0)
            {
                ret = -EINVAL;
                break;
            }
            if((unsigned int)offset > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;
                break;
            }
            filp->f_pos = (unsigned int)offset;
            ret = filp->f_pos;
            break;
        case 1:
            if((filp->f_pos + offset) > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;;
                break;
            }
            if((filp->f_pos + offset) < 0)
            {
                ret = -EINVAL;
                break;
            }
            filp->f_pos += offset;
            ret = filp->f_pos;
            break;
        default:
            ret = -EINVAL;
            break;
    }   
    return ret;
}

//文件操作结构体
static const struct file_operations globalmem_fops = 
{
    .owner = THIS_MODULE,
    .llseek = globalmem_llseek,
    .read = globalmem_read,
    .write = globalmem_write,
// .ioctl = globalmem_ioctl,//Linux kernel高版本中ioctl已被两个XX_ioctl替代,参见下面注释
    .open = globalmem_open,
    .release = globalmem_release,
};

//初始化并注册cdev
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err,devno = MKDEV(globalmem_major, index);
    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &globalmem_fops;
    err = cdev_add(&dev->cdev, devno, 1);
    if(err)
        printk(KERN_NOTICE "Error %d adding globalmem %d", err, index);
}

//设备驱动模块加载函数
int globalmem_init(void)
{
    int result;
    dev_t devno = MKDEV(globalmem_major, 0);

    //申请设备号
    if(globalmem_major)
        result = register_chrdev_region(devno,1,"globalmem");
    else//动态申请设备号
    {
        result = alloc_chrdev_region(&devno,0,1,"globalmem");
        globalmem_major = MAJOR(devno);
    }
    if(result < 0)
        return result;
    printk(KERN_ALERT "T1\n");
    //动态申请设备结构体的内存
    globalmem_devp = (struct globalmem_dev *)kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
    printk(KERN_ALERT "T0\n");

    if(!globalmem_devp)
    {
        result = -ENOMEM;
        goto fail_malloc;
    }       
    memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
    globalmem_setup_cdev(globalmem_devp,0);
    return 0;

    fail_malloc:
        unregister_chrdev_region(devno,1);

    return result;
}

//模块卸载函数
void globalmem_exit(void)
{
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    unregister_chrdev_region(MKDEV(globalmem_major,0),1);
}

module_param(globalmem_major, int,S_IRUGO);

module_init(globalmem_init);
module_exit(globalmem_exit);

下面是Linux kernel 3.14的源码中的file_operations结构体(include/linux/fs.h),由于有时候内核版本的升级会给接口带来一定的变化,所以在写驱动时,应该同时具有该版本的源码,以编写出对应内核接口的函数。所以对于上面的ioctl函数,自行参考修改一下即可编译通过。

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 (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    ......
}

Makefile

ifneq ($(KERNELRELEASE),)
    obj-m := globalmem.o
else
    KERNELDIR := /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
all:
    $(MAKE) -C $(KERNELDIR) SUBDIRS=$(PWD) modules
endif
clean:
    rm -f *.o *.ko *.mod.c .globalmem*

用户态程序test.c

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

int main(int argc, char *argv[])
{
    int fd;
    fd = open("/dev/globalmem", O_RDWR);

    if(fd < 0)
    {
        perror("open");
    }
    return EXIT_SUCCESS;
}

运行测试程序

root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem#insmod globalmem.ko
root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem# cat /proc/devices | grep "globalmem"
240 globalmem
root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem# mknod /dev/globalmem c 240 0
root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem# ll /dev/globalmem 
crw-r--r-- 1 root root 240, 0 122 19:47 /dev/globalmem
root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem#./test
root@selfimpr-pc:/home/selfimpr/wenqian/lkp/globalmem#dmesg
[210299.896657] T1
[210299.896665] T0
[210393.405124] globalmem open
[210393.405201] globalmem release

上面一个简单的case,只是大致的概述下linux字符设备驱动的编写流程,但这也是字符驱动的一个最初雏形,类似于单片机的最小系统之类。
在linux内核中,使用cdev结构体描述一个字符设备

struct cdev
{
  struct kobject kobj;//内嵌的kobject对象
  struct module *owner;//所属模块
  struct file_operations *ops;//文件操作结构体
  struct list_head list;
  dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
  unsigned int count;
};

关于设备号请具体参考任何一本linux字符驱动书籍。
cdev结构体中一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数(file_operations请参考前面的博文文件系统系列)。而下层的具体实现,则根据用户及驱动编写。
linux内核提供了一组函数用于操作cdev结构体:

void cdev_init(struct cdev *, struct file_operations *);//初始化cdev成员
struct cdev *cdev_alloc(void);//动态申请一个cdev内存
int cdev_add(struct cdev *, dev_t, unsigned);//向系统添加一个cdev
void cdev_del(struct cdev *);//向系统删除一个cdev

分配和释放设备号
在调用cdev_add()函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或 alloc_chrdev_region()函数向系统申请设备号

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

其中register_chrdev_region函数用于已知起始设备的设备号的情况,而alloc_chrdev_region函数用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功以后,会把得到的设备放入第一个参数dev中。
相反的,在调用cdev_del函数从系统注销字符设备之后,unregiter_chrdev_region()应该用以释放原先申请的设备号:

void unregister_chrdev_region(dev_t first, unsigned count);

我们可以使用下列宏从dev_t获得主设备号和次设备号:

MAJOR(dev_t dev)
MINOR(dev_t dev)

使用下列宏(也是我们通常使用的)则可以通过主设备号和次设备号生成dev_t

MKDEV(int major, int minor)

你可能感兴趣的:(linux驱动)