Linux字符设备驱动学习

注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。

书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。对于用户而言,使用文件系统的操作接口open()、close()、read()、write()等进行访问。

1 cdev结构体

cdev结构体定义如下,路径:include/linux/cdev.h:

struct cdev {
	struct kobject kobj;   //内嵌的kobject对象
	struct module *owner;  //所属模块
	const struct file_operations *ops;  //文件操作结构体
	struct list_head list;
	dev_t dev;  //设备号
	unsigned int count;
};

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

file_operations成员:定义了字符设备驱动提供给虚拟文件系统的接口函数,后面会详细说明。

内核提供了一组操作cdev结构体的函数,代码路径:fs\char_dev.c:

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

cdev_init()函数:用于初始化cdev的成员,并建立cdev和file_operations之间的连接:

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

cdev_alloc()函数:用于动态申请一个cdev的内存:

/**
 * cdev_alloc() - allocate a cdev structure
 *
 * Allocates and returns a cdev structure, or NULL on failure.
 */
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

cdev_add()函数和cdev_del()函数:用于分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。

/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}
/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 */
void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

2 分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name);

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

register_chrdev_region()函数用于已知起始设备的设备号的情况。

alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。

alloc_chrdev_region()相比于register_chrdev_region()的优点在于它会自动避开设备号重复的冲突。

相应地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:

/**
 * unregister_chrdev_region() - return a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 *
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
 */
void unregister_chrdev_region(dev_t from, unsigned count);

3 file_operations结构体

file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用,定义在:include\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 *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	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);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*mremap)(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 *, loff_t, loff_t, 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 (*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 **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

llseek()函数:用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。

read()函数:用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。它与用户空间中的下列应用程序中对应:

/* fd:读取文件的文件描述符指针
 * buf:存放读取数据的缓存
 * count:要求读取一次数据的字节数
*/
ssize_t read(int fd,void *buf,size_t count);
/* 从流(stream)中读取nmemb个字段,每个字段为size字节,并将读取的字段放入ptr所指的字符数组中,返回实际已读取的字段数。*/
size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

write()函数:向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。它与用户空间应用程序中的下列函数对应:

/* fd:写入文件的文件描述符;
 * buf:存放待写数据的缓存;
 * count:要求写入一次数据的字节数;
 */
ssize_t write(int fd,const void *buf,size_t count);
/* 实现从缓冲区ptr所指的数组中把nmemb个字段写到流(stream)中,每个字段长为size个字节,返回实际写入的字段数。 */
size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);

read()和write()如果返回0,则暗示end-of-file(EOF)。

unlocked_ioctl()函数:提供设备相关控制命令的实现(既不是读操作,也不是写操作),当调用成功时,返回给调用程序一个非负值。它与用户空间应用程序调用的下列函数对应:

int fcntl(int fd, int cmd, ... /* arg */ );
int ioctl(int d, int request, ...);

mmap()函数:将设备内存映射到进程的虚拟地址空间中,如果设备驱动未实现此函数,用户进行mmap()系统调用时将获得-ENODEV返回值。这个函数对于帧缓冲等设备特别有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问它而无须在内核和应用间进行内存复制。它与用户空间应用程序中的下面函数对应:

/* addr:存储映射区的起始地址,通常设为0,让系统分配。
 * length:需要映射的字节数
 * offset:映射字节在文件中的偏移量
 * prot:PROT_READ映射区可读、PROT_WRITE映射区可写、PROT_EXEC映射区可执行、PROT_NONE:映射区不可访问
 * flags:MAP_FIXD返回地址必须等于addr,不推荐使用、MAP_SHARED存储操作立刻修改映射文件内容、MAP_PRIVATE存储操作导致创建映射文件的副本,并对副本读写。
*/
void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset);

当用户空间调用Linux API函数open()打开设备文件时,设备驱动的open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。

与open()函数对应的是release()函数。

poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。

aio_read()和aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符执行sys_io_setup、sys_io_submit、sys_io_getevents、sys_io_destroy等系统调用进行读写。

4 Linux设备驱动程序的组成

字符设备驱动由以下几部分组成:模块加载函数和卸载函数、file_operations结构体中的成员函数。

4.1 字符驱动模块加载和卸载函数

加载函数中实现设备号的申请和cdev的注册;卸载模块实现设备号的释放和cdev的注销。

Linux编码习惯:为设备定义一个设备相关的结构体,结构体涉及cdev、私有数据及锁等信息。

常见的加载和卸载模块函数的实现如下:

//设备结构体
struct xxx_dev_t {
	struct cdev cdev;
	...;
}xxx_dev;

//设备驱动模块加载函数
static int __init xxx__init(void)
{
	...;
	cdev_init(&xxx_dev.cdev, &xxx_fops);  //初始化cdev
	xxx_dev.cdev.owner = THIS_MODULE;
	//获取设备号
	if (xxx_major) {
		register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
	} else {
		alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
	}
	ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1);  //注册设备
	...;
}

//设备驱动模块卸载函数
static void __exit xxx_exit(void)
{
	unregister_chrdev_region(xxx_dev_no, 1);  //释放设备号
	cdev_del(&xxx_dev.cdev);
	...;
}

4.2 file_operations结构体中的成员函数

file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。

大多数字符设备驱动会实现read()、write()和ioctl()函数,常见的字符设备驱动的这3个函数的形式如下:

/* 读设备
 * filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不宜直接读写,
 * count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。
 */
ssize_t xxx_read(struct file *filep, char __user *buf, size_t count, loff_t *f_pos)
{
	...;
	copy_to_user(buf, ..., ...); //内核空间到用户空间缓冲区的复制
	...;
}

/* 写设备
 * filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不宜直接读写,
 * count是要写的字节数,f_pos是写的位置相对于文件开头的偏移。
 */
ssize_t xxx_write(struct file *filep, const char __user *buf, size_t count, loff_t *f_pos)
{
	...;
	copy_from_user(..., buf, ...); //用户空间缓冲区到内核空间的复制
	...;
}

//ioctl函数
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	...;
	switch (cmd) {
	case XXX_CMD1:
		...;
		break;
	case XXX_CMD2:
		...;
		break;
	default:  //不支持的命令
		return -ENOTTY;
	}
	return 0;
}
//如果完全复制成功,返回值为0。如果复制失败,则返回负值。
static inline long copy_from_user(void *to,
		const void __user * from, unsigned long n);
static inline long copy_to_user(void __user *to,
		const void *from, unsigned long n);

如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user()。

/* 用户-->内核,addr是用户空间的地址 */
#define GET_USER(error,value,addr) error = get_user(value,addr)
/* 内核-->用户,addr是用户空间的地址 */
#define PUT_USER(error,value,addr) error = put_user(value,addr)

读和写函数中的_user是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释的功能。

# define __user		__attribute__((noderef, address_space(1)))

内核空间虽然可以访问用户空间的缓冲区,但是在访问之前,一般需要先检查其合法性,通过access_ok(type,addr,size)进行判断,以确定传入的缓冲区的确属于用户空间。

copy_from_user()、copy_to_user()内部也进行了这样的检查。

在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员,代码如下:

struct file_operations xxx_fops = {
	.owner = THIS_MODULE,
	.read = xxx_read,
	.write = xxx_write,
	.unlocked_ioctl = xxx_ioctl,
	...,
};

下图所示为字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。

image-20220108131013972

5 globalmem虚拟设备实例描述

实验目的:基于虚拟的globalmem设备进行字符设备驱动的讲解。

实验说明:在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。

5.1 支持单设备的globalmem驱动

实例代码:

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

/* 直接使用立即数当作命令不合理,暂定 */
#define MEM_CLEAR           0x1
#define GLOBALMEM_MAJOR     230
#define GLOBALMEM_SIZE      0x1000

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

/* 设备结构体 */
struct  globalmem_dev {
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];
};

struct globalmem_dev *globalmem_devp;

static int globalmem_open(struct inode *inode, struct file *filp)
{
    /* 使用文件的私有数据作为获取globalmem_dev的实例指针 */
    filp->private_data = globalmem_devp;
    return 0;
}

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

/**
 * 设备ioctl函数
 * @param[in] filp:文件结构体指针
 * @param[in] cmd: 命令,当前仅支持MEM_CLEAR
 * @param[in] arg: 命令参数
 * @return  若成功返回0,若出错返回错误码
 */
static long globalmem_ioctl(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;
}

/**
 * 读设备
 * @param[in] filp:文件结构体指针
 * @param[out] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 读取的字节数
 * @param[in/out] ppos: 读的位置相当于文件头的偏移
 * @return  若成功返回实际读的字节数,若出错返回错误码
 */
static ssize_t globalmem_read(struct file *filp,
    char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long 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_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "read %lu bytes(s) from %lu\n", count, p);
    }
    return ret;
}

/**
 * 写设备
 * @param[in] filp:文件结构体指针
 * @param[in] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 写入的字节数
 * @param[in/out] ppos: 写的位置相当于文件头的偏移
 * @return  若成功返回实际写的字节数,若出错返回错误码
 */
static ssize_t globalmem_write(struct file *filp,
    const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long 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 %lu bytes(s) from %lu\n", count, p);
    }
    return ret;
}

/**
 * 文件偏移设置
 * @param[in] filp:文件结构体指针
 * @param[in] offset: 偏移值大小
 * @param[in] orig: 起始偏移位置
 * @return  若成功返回文件当前位置,若出错返回错误码
 */
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,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err, devno = MKDEV(globalmem_major, index);

    /* 初始化cdev */
    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    /* 注册设备 */
    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

/* 驱动模块加载函数 */
static int __init globalmem_init(void)
{
    int ret;
    dev_t devno = MKDEV(globalmem_major, 0);

    /* 获取设备号 */
    if (globalmem_major)
        ret = register_chrdev_region(devno, 1, "globalmem");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    
    if (ret < 0)
        return ret;
    
    /* 申请内存 */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
    }
    globalmem_setup_cdev(globalmem_devp, 0);
    return 0;

fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(globalmem_init);

/* 驱动模块卸载函数 */
static void __exit globalmem_exit(void)
{
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    /* 释放设备号 */
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

MODULE_AUTHOR("Mr Layfolk");
MODULE_LICENSE("GPL v2");

Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += globalmem.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

查看模块是否已经加载:

$ lsmod | grep globalmem
globalmem              12888  0 

查看驱动信息,主设备号为230:

$ cat /proc/devices | grep globalmem
230 globalmem

创建一个设备节点,主设备号为230,次设备号为0:

$ sudo mknod /dev/globalmem c 230 0 
$ ls -al /dev/globalmem 
crw-r--r-- 1 root root 230, 0 Jan  9 11:10 /dev/globalmem

验证设备的读写功能:

$ echo "hello world" > /dev/globalmem 
$ dmesg
[89981.450404] written 12 bytes(s) from 0
$ cat /dev/globalmem 
hello world
$ dmesg
[90003.550525] read 4096 bytes(s) from 0

查看文件系统下globalmem模块的信息:

$ tree /sys/module/globalmem/
/sys/module/globalmem/
├── coresize
├── holders
├── initsize
├── initstate
├── notes
├── parameters
│   └── globalmem_major
├── refcnt  # 模块引用计数
├── rhelversion
├── sections #模块BSS、数据段、代码段等地址信息
│   ├── __mcount_loc
│   └── __param
├── srcversion
├── taint
└── uevent

4 directories, 11 files

5.2 支持多设备的globalmem驱动

实验目的:把globalmem改造为支持多个设备,让globalmem驱动中包含N个同样的设备(次设备号分为0~N)。

不需要改造:globalmem_read()、globalmem_write()、globalmem_ioctl()等函数及globalmem_fops结构体等数据结构。

需要改造:修改globalmem_init()、globalmem_exit()和globalmem_open()。

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

/* 直接使用立即数当作命令不合理,暂定 */
#define MEM_CLEAR           0x1
#define GLOBALMEM_MAJOR     230
#define DEVICE_NUM          10
#define GLOBALMEM_SIZE      0x1000

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

/* 设备结构体 */
struct  globalmem_dev {
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];
};

struct globalmem_dev *globalmem_devp;

static int globalmem_open(struct inode *inode, struct file *filp)
{
    struct globalmem_dev *dev = container_of(inode->i_cdev, struct globalmem_dev, cdev);

    /* 使用文件的私有数据作为获取globalmem_dev的实例指针 */
    filp->private_data = dev;
    return 0;
}

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

/**
 * 设备ioctl函数
 * @param[in] filp:文件结构体指针
 * @param[in] cmd: 命令,当前仅支持MEM_CLEAR
 * @param[in] arg: 命令参数
 * @return  若成功返回0,若出错返回错误码
 */
static long globalmem_ioctl(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;
}

/**
 * 读设备
 * @param[in] filp:文件结构体指针
 * @param[out] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 读取的字节数
 * @param[in/out] ppos: 读的位置相当于文件头的偏移
 * @return  若成功返回实际读的字节数,若出错返回错误码
 */
static ssize_t globalmem_read(struct file *filp,
    char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long 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_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "read %lu bytes(s) from %lu\n", count, p);
    }
    return ret;
}

/**
 * 写设备
 * @param[in] filp:文件结构体指针
 * @param[in] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 写入的字节数
 * @param[in/out] ppos: 写的位置相当于文件头的偏移
 * @return  若成功返回实际写的字节数,若出错返回错误码
 */
static ssize_t globalmem_write(struct file *filp,
    const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long 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 %lu bytes(s) from %lu\n", count, p);
    }
    return ret;
}

/**
 * 文件偏移设置
 * @param[in] filp:文件结构体指针
 * @param[in] offset: 偏移值大小
 * @param[in] orig: 起始偏移位置
 * @return  若成功返回文件当前位置,若出错返回错误码
 */
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,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err, devno = MKDEV(globalmem_major, index);

    /* 初始化cdev */
    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    /* 注册设备 */
    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

/* 驱动模块加载函数 */
static int __init globalmem_init(void)
{
    int i;
    int ret;
    dev_t devno = MKDEV(globalmem_major, 0);

    /* 获取设备号 */
    if (globalmem_major)
        ret = register_chrdev_region(devno, DEVICE_NUM, "globalmem");
    else {
        ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    
    if (ret < 0)
        return ret;
    
    /* 申请内存 */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev) * DEVICE_NUM, GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
    }
    
    for (i = 0; i < DEVICE_NUM; i++)
        globalmem_setup_cdev(globalmem_devp + i, i);

    return 0;

fail_malloc:
    unregister_chrdev_region(devno, DEVICE_NUM);
    return ret;
}
module_init(globalmem_init);

/* 驱动模块卸载函数 */
static void __exit globalmem_exit(void)
{
    int i;
    for (i = 0; i < DEVICE_NUM; i++)
        cdev_del(&(globalmem_devp + i)->cdev);
    kfree(globalmem_devp);
    /* 释放设备号 */
    unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);
}
module_exit(globalmem_exit);

MODULE_AUTHOR("MrLayfolk");
MODULE_LICENSE("GPL v2");

Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += multi_globalmem.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

上述的代码中用到了container_of(),其作用是:通过结构体成员的指针找到对应数据结构的指针。

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

编译、运行、测试:

$ make
$ sudo insmod multi_globalmem.ko 
$ lsmod | grep multi_globalmem
multi_globalmem        12888  0 

创建两个设备节点,并分别验证设备的读写功能:

$ mknod /dev/multiglobalmem0 c 230 0  # 主设备号为230,次设备号为0
$ mknod /dev/multiglobalmem1 c 230 1  # 主设备号为230,次设备号为1
$ echo "hi! I am num0" > /dev/multiglobalmem0
$ echo "hi! I am num1" > /dev/multiglobalmem1
$ cat /dev/multiglobalmem0
hi! I am num0
$ cat /dev/multiglobalmem1
hi! I am num0
$ rmmod multi_globalmem
$ cat /dev/multiglobalmem0
cat: /dev/multiglobalmem0: No such device or address
posted @ 2022-01-09 17:35  zhengcixi  阅读(340)  评论(0编辑  收藏  举报
回到顶部