注:本文是《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,
...,
};

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

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

Linux字符设备驱动学习的更多相关文章

  1. linux字符设备驱动学习笔记(一):简单的字符设备驱动

    最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...

  2. Smart210学习记录----beep linux字符设备驱动

    今天搞定了beep linux字符设备驱动,心里还是很开心的,哈哈...但在完成的过程中却遇到了一个非常棘手的问题,花费了我大量的时间,,,, 还是把问题描述一下吧,好像这个问题很普遍的,网上许多解决 ...

  3. Linux字符设备驱动实现

    Linux字符设备驱动实现 要求 编写一个字符设备驱动,并利用对字符设备的同步操作,设计实现一个聊天程序.可以有一个读,一个写进程共享该字符设备,进行聊天:也可以由多个读和多个写进程共享该字符设备,进 ...

  4. (57)Linux驱动开发之三Linux字符设备驱动

    1.一般情况下,对每一种设备驱动都会定义一个软件模块,这个工程模块包含.h和.c文件,前者定义该设备驱动的数据结构并声明外部函数,后者进行设备驱动的具体实现. 2.典型的无操作系统下的逻辑开发程序是: ...

  5. 深入理解Linux字符设备驱动

    文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次.组成框架和交互.如何编写驱动.设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解.本文整合之前发表的& ...

  6. Linux字符设备驱动结构(一)--cdev结构体、设备号相关知识机械【转】

    本文转载自:http://blog.csdn.net/zqixiao_09/article/details/50839042 一.字符设备基础知识 1.设备驱动分类 linux系统将设备分为3类:字符 ...

  7. Linux字符设备驱动基本结构

    1.Linux字符设备驱动的基本结构 Linux系统下具有三种设备,分别是字符设备.块设备和网络设备,Linux下的字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中某一数据,读取数据 ...

  8. 一步步理解linux字符设备驱动框架(转)

    /* *本文版权归于凌阳教育.如转载请注明 *原作者和原文链接 http://blog.csdn.net/edudriver/article/details/18354313* *特此说明并保留对其追 ...

  9. 谈谈Linux字符设备驱动的实现

    @ 目录 字符设备驱动基础 申请设备号 创建设备节点 在驱动中实现操作方法 文件IO调用驱动中的操作 应用程序与驱动的数据交互 内核驱动如何控制外设 控制LED的简单驱动实例 驱动程序的改进 框架复盘 ...

  10. Linux字符设备驱动框架

    字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标.键盘.显示器.串口等等,当我们执行ls -l ...

随机推荐

  1. python中的赋值、浅拷贝、深拷贝的区别

    赋值: 可变类型:赋值前后id不会变,赋值后的数据会随源数据变化: 不可变类型:赋值前后id不会变,赋值后的数据不会随源数据变化: 浅拷贝(copy): 可变类型:copy前后id会变,可变类型中存储 ...

  2. redis命令和lua实现分布式锁

    Redis分布式锁关键 SETNX 语法: SETNX key value 如果key不存在,则存储(key:value)值,返回1 如果key已经不存在,则不执行操作,返回0 因为这个命令的性质,多 ...

  3. 3.CSS三种基本选择器

    三种选择器的优先级: id选择器 > class选择器 > 标签选择器 1.标签选择器:会选择到页面上所有的该类标签的元素 格式: 标签{} 1 <!DOCTYPE html> ...

  4. Apsara Stack 同行者专刊 | 怀同行之心,筑信任之基,践数智之行

    简介: 政企云平台处在怎样的历史阶段?数智创新的同行者们面临着怎样的挑战与机遇?在时代巨幕下,政企期待云厂商扮演怎样的角色?阿里云智能研究员.混合云平台总经理刘国华认为,云厂商不仅需要有定力与实力,也 ...

  5. 走进RDS|说说关系型数据库与Serverless

    ​简介:看到如今Serverless在云计算行业喷薄欲出的态势,像极了<星星之火,可以燎原>中的描述:虽然不能预测未来的发展和变化,但对于云计算来说这是个相对确定的方向.本文将和大家说说关 ...

  6. 大模型 RAG 是什么

    大模型 RAG(Retrieval-Augmented Generation)是一种结合了检索(Retrieval)与生成(Generation)能力的先进人工智能技术,主要用于增强大型语言模型(LL ...

  7. [GPT] nodejs 有哪些类似 jquery 语法的 html 解析库

      在Node.js中,有一些类似jQuery语法的HTML解析库可供选择. 以下是其中几个常用的库: 1. Cheerio: Cheerio是一个快速.灵活且易于使用的HTML解析库,它提供了类似于 ...

  8. 2019-4-29-win10-uwp-使用-Border-布局

    title author date CreateTime categories win10 uwp 使用 Border 布局 lindexi 2019-04-29 12:29:45 +0800 201 ...

  9. c#胖东来小程序自动购物程序(接单,windows桌面程序、linux程序、网络应用等等)

    一.程序效果 自动打开胖东来小程序,自动购物 二.实现 先截屏,然后利用opencv库识别下一步按键所在位置,然后使用mouse_event控制鼠标,模拟人的动作 第一步,截取屏幕 static Bi ...

  10. 羽夏壳世界—— PE 解析的实现

    写在前面   此系列是本人一个字一个字码出来的,包括代码实现和效果截图. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后 ...