Linux 虚拟字符设备globalmem
在虚拟设备驱动中,分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,用于实现自定义的虚拟字符设备globalmem实例。
globalmem 没有任何实用价值,仅用于讲解问题。
globalmem设备驱动
头文件、宏、设备结构体
定义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 GLOBALMEM_SIZE 0x1000 // 4KB
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230
static int globalmem_major = GLOBALMEM_MAJOR; // 主设备号
module_param(globalmem_major, int, S_IURGO); // 向当前模块传入的参数
/* globalmem设备结构 */
struct globalmem_dev {
struct cdev cdev; // 对应globalmem字符设备的dev
unsinged char mem[GLOBALMEM_SIZE]; // 使用的内容mem[]
};
struct globalmem_dev *globalmem_devp;
加载与卸载设备驱动
// 完成cdev的初始化与添加
// dev: 指向globalmem dev设备结构
// index: globalmem_dev结构指针数组的索引, 代表设备索引
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
int err, devno = MKDEV(globalmem_major, index);
cdev_init(&dev->cdev, &globalmem_fops); // 初始化cdev, 并建立与file_operations联系
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: // kzalloc异常处理
unregister_chrdev_region(devno, 1);
return ret;
}
module_init(globalmem_init);
globalmem_setup_cdev完成cdev的初始化和添加,其中在初始化cdev时,需要一个file_operations 结构对象globalmem_fops。globalmem_fops是与globalmem设备驱动的文件操作结构体对象:
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,
};
下面实现file_operations绑定的文件操作。
读写函数
globalmem的读写操作,主要是让设备结构体中的mem[] 与用户空间交互,随着访问的字节数变更,更新文件读写偏移位置。
读函数
// filp: 要读取的文件指针
// buf: 用户空间缓冲区地址
// size: 用户空间缓冲区大小(bytes)
// ppos [in-out]: 指向要读的位置相对于文件开头的偏移
static ssize_t globalmem_read(struct file* filp, 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; // EOF
if (count > GLOBALMEM_SIZE - p) // 要读的数据超过mem[]边界
count = GLOBALMEM_SIZE - p;
if (copy_to_user(buf, dev->mem + p, count)) { // 将内核空间mem[p..p+count)拷贝到用户空间buf
ret = -EFAULT;
} else {
*ppos += count; // 更新偏移位置
ret = count; // 已读取字节数
printk(KERN_INFO "read %u bytes(s) from %lu\n", count ,p);
}
return ret;
}
写函数
// filp: 要写的文件指针
// buf: 用户空间缓冲区地址
// size: 用户空间缓冲区大小(bytes)
// ppos [in-out]: 指向要写的位置相对于文件开头的偏移
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) // 要写的位置超出mem[]边界
return 0;
if (count > GLOBALMEM_SIZE - p) // 要写的用户缓冲区大小超过mem[]大小, 截断
count = GLOBALMEM_SIZE - p;
if (copy_from_user(dev->mem + p, buf, count)) {
ret= -EFAULT;
} else {
*ppos += count; // 更新偏移位置
ret = count; // 已读取字节数
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
seek函数
seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET, 0),当前位置(SEEK_CUR, 1)和文件末尾(SEEK_END, 2)。假设globalmem支持从文件开头和当前位置的相对偏移,不支持文件末尾的相对偏移。
定位时,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时更新文件的当前位置并返回该位置。
// 重新定位文件偏移
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch(orig) {
case 0: // SEEK_SET
if (offset < 0) {
ret = -EINVAL; // 参数错误
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsinged int)offset;
ret = filp->f_pos;
break;
case 1: // SEEK_CUR
if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { // 超出mem[]上边界
ret = -EINVAL;
break;
}
if ((filep->f_pos + offset) < 0) { // 超出mem[]下边界
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
ioctl函数
1)globalmem设备驱动的ioctl()函数
globalmem设备驱动的ioctl() 接受MEM_CLEAR命令(前面已定义为0x01),该命令将全局内存的有效数据长度清0;对于不支持的命令,该函数返回-EINVAL。
// 应用程序中, 通过ioctl(fd, cmd, ...)来调用
// cmd: 命令
// arg: 命令参数, 由驱动程序根据不同命令解释
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;
}
globalmem的读写操作,主要是让设备结构体中的mem[] 与用户空间交互,随着访问的字节数变更,更新文件读写偏移位置。
2)ioctl() 命令的命名规则
前面提到,MEM_CLEAR被定义为0x01,实际上这不是一种推荐的方法。因为不同设备驱动很可能拥有相同命令号,导致命令码污染。
Linux 内核推荐采用一套统一的ioctl() 命令生成方式。命令码组成:
设备类型 | 序列号 | 方向 | 数据尺寸 |
---|---|---|---|
8bit | 8bit | 2bit | 13/14bit |
- 设备类型字段,是一个“幻数”,可以是0~0xFF,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”。新设备驱动定义的“幻数”应避免与其冲突。
- 方向字段,表示数据传送的方向,可能值_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ | _IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
- 数据尺寸,表示涉及到用户数据的大小,成员的宽度依赖于体系结构,通常是13或14bit。
内核还定义了几个宏_IO(), _IOR(), _IOW(), _IOWR(),用于辅助生成命令。
#include <asm-generic/ioctl.h>
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
/* _IO, _IOR 等使用的_IOC宏 */
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
宏的作用是根据传入的type: 设备类型、nr: 序列号、size: 数据长度、宏名隐含的方向,这几个参数移位组合生成的命令码。
因为globalmem的MEM_CLEAR命令不涉及数据传输,所以它可以定义为:
// 可以将MEM_CLEAR 宏定义修改为以下方式
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)
使用文件私有数据
大多数Linux驱动遵循“潜规则”:将文件的私有数据private_data,指向设备结构体,再用read/write/ioctl/llseek 等文件操作函数通过private_data 访问设备结构体。
/*
* Linux 驱动遵循潜规则: 将文件的私有数据private_data 指向设备结构体
* 供read/write/ioctl/llseek 等文件操作函数通过private_data 访问设备结构体
*/
static int globalmem_open (struct inode *inode, struct file *filp)
{
filp->private_data = globalmem_devp;
return 0;
}
用户空间验证globalmem驱动
加载驱动
make命令编译globalmem驱动后,得到globalmem.ko。
通过insmod命令加载驱动程序:
# insmod globalmem.ko
[ 909.744023] globalmem_drv: loading out-of-tree module taints kernel.
# lsmod
Module Size Used by
globalmem_drv 2599 0
evbug 2078 0
inv_mpu6050_spi 2052 0
inv_mpu6050 10948 2 inv_mpu6050_spi
注意到insmod加载globalmem驱动时,提示“loading out-of-tree module taints kernel”,这是由于自行编写的驱动没有加入Kconfig树,但仍能正常加载,内核给出的提示。参考:https://blog.csdn.net/y24283648/article/details/108608239
已经注册的字符设备,可以通过"cat /proc/devices" 命令查看到
# cat /proc/devices
...
230 globalmem
...
globalmem的主设备号为230,这个正是我们在驱动程序中指定的主设备号。
如果驱动程序中,没有通过调用class_create和device_create 为globalmem创建逻辑设备,那么也可以用mknod创建之,这样APP就可以通过 "/dev/globalmem" 访问设备驱动了。
# mknod /dev/glbalmem c 230 0
这里"/dev/glbalmem" 是要创建的逻辑设备名称,c表示char device(字符设备),230是主设备号,0是次设备号(如果只有1个设备,次设备号通常为0)。
命令行验证读写功能
echo命令可以调用驱动程序的write,因此可用于验证设备的写;
cat命令可以调用驱动程序的read,因此可以用于验证设备的读。
# echo "hello world" > /dev/globalmem
# cat /dev/globalmem
hello world
如果启用了sysfs文件系统,还会发现多出了 /sys/module/globalmem 目录。
APP测试程序验证读写功能
以write, read 的顺序,对文件"/dev/globalmem" 分别进行写、读操作。
int main()
{
int fd;
int nread, nwrite;
char buf[1024];
fd = open("/dev/globalmem", O_RDWR);
if (fd < 0) {
perror("open /dev/globalmem error");
return -1;
}
strcpy(buf, "hello, this is globalmem device driver");
nwrite = write(fd, buf, strlen(buf));
if (nwrite < 0) {
perror("write /dev/globalmem error");
return -1;
}
printf("write %d(bytes) to globalmem: %s\n", nwrite, buf);
nread = read(fd, buf, sizeof(buf));
if (nread < 0) {
perror("read /dev/globalmem error");
return -1;
}
buf[nread] = '\0';
printf("read %d(bytes) from globalmem: %s\n", nread, buf);
close(fd);
return 0;
}
将APP编译成目标globalmem_test,运行之
# ./globalmem_test
[ 105.426376] written 38 bytes(s) from 0
write 38(bytes) to globalmem: hel[ 105.431113] read 1024 bytes(s) from 38
lo, this is globalmem device driver
read 1024(bytes) from globalmem:
发现APP与驱动程序能正常打印。
完整源码:参见ch6_chardev | gitee
参考
[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.