本节概述
在globalmem(全局内存)字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。
1、ioctl()命令
Linux内核推荐采用一套统一的ioctl()命令生成方式,Linux建议以如图6.2所示的方式定义ioctl()的命令。
命令码的设备类型字段为一个“幻数”,可以是0~0xff的值,内核中的Documentation/ioctl/ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义“幻数”的时候要避免与其冲突。
命令码的序列号也是8位宽。
命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
命令码的数据长度字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或者14位。
内核还定义了_IO()、_IOR()、_IOW()和_IOWR()这4个宏来辅助生成命令,这4个宏的通用定义如代码清单6.15所示。
代码清单6.15 _IO()、_IOR()、_IOW()和_IOWR()宏定义在<asm-generic/ioctl.h>
/* _IO 、 _IOR 等使用的 _IOC 宏 */
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
#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)))
由此可见,这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)、size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。
由于globalmem的MEM_CLEAR命令不涉及数据传输,所以它可以定义为:
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)
2、预定义命令
内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会作为预定义命令被内核处理而不是被设备驱动处理,下面列举一些常用的预定义命令。
FIOCLEX:File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。
FIONCLEX:即File IOctl Not Close on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。
FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。
FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。
FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO这些宏定义在内核的include/asm-generic/ioctls.h文件中。
例子:字符设备驱动之globalmem(全局内存)虚拟设备实例
3、驱动源代码头文件
global_mem.h
#ifndef __GLOBAL_MEM_H
#define __GLOBAL_MEM_H
#include <linux/cdev.h>
#define DRIVER_AUTHOR "xz@vi-chip.com.cn"
#define DRIVER_DESC "A sample driver"
#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define DEV_NAME "globalmem"
#define GLOBALMEM_MAJOR 230 /* 主设备号 */
/**
*
* 定义全局内存字符设备的结构体:
* 借用面向对象程序设计中“封装”思想,体现良好的编程习惯。
*
*/
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
#endif /* __GLOBAL_MEM_H */
4、驱动源代码global_mem.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
/* include local head files */
#include "global_mem.h"
static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO); /* mode:S_IRUGO */
struct globalmem_dev *globalmem_devp = NULL;
/**
*
* 读写函数
* 让设备结构体globalmem_dev的mem[]数组与用户空间交互数据,
* 并随着访问的字节数变更更新文件读写偏移位置。
*/
// globalmem设备驱动的读函数,从设备中读取数据
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; // 要读的位置相对于文件开头的偏移,如果该偏移大于或等于GLOBALMEM_SIZE,表示已经到达文件末尾,所以返回0(EOF)。
unsigned int count = size; // 要读取的字节数
int ret = 0;
struct globalmem_dev *dev = filp->private_data; // 使用文件的私有数据访问设备结构体
if (p >= GLOBALMEM_SIZE)
return 0; // 表示已经到达文件末尾,所以返回0(EOF)
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 %u bytes(s) from %lu\n", count, p);
}
return ret; // 实际读取的字节数
}
// globalmem设备驱动的写函数,向设备发送数据
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 %u bytes(s) from %lu\n", count, p);
}
return ret;
}
// globalmem设备驱动的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 */
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: /* 从文件当前位置开始 seek */
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;
}
// ioctl函数
// globalmem设备驱动的ioctl函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl函数应该返回-EINVAL。
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;
}
static int globalmem_open(struct inode *inode, struct file *filep)
{
printk(KERN_INFO "globalmem_open\n");
filep->private_data = globalmem_devp;// 设置私有数据,将文件的私有数据private_data指向设备结构体的实例,体现Linux的面向对象的设计思想
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filep)
{
printk(KERN_INFO "globalmem_release\n");
return 0;
}
// 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,
};
void globalmem_setup_cdev(struct globalmem_dev *dev, int minor)
{
int err, devno = MKDEV(globalmem_major, minor);
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, minor);
}
static int __init global_mem_init(void)
{
int ret;
dev_t devno = MKDEV(globalmem_major, 0); /* 主设备号、次设备号合并为设备号 */
if (globalmem_major)
ret = register_chrdev_region(devno, 1, DEV_NAME); // 静态分配设备号
else {
ret = alloc_chrdev_region(&devno, 0, 1, DEV_NAME); // 动态分配设备号
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_kmalloc;
}
globalmem_setup_cdev(globalmem_devp, 0); // 注册字符设备
printk(KERN_INFO "----global_mem_init----\n");
return 0; // 表示成功
fail_kmalloc:
unregister_chrdev_region(devno, 1); // 注销分配设备号
return ret;
}
static void __exit global_mem_exit(void)
{
cdev_del(&globalmem_devp->cdev); // 注销字符设备
kfree(globalmem_devp); // 释放分配的内存空间
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);// 注销分配设备号
printk(KERN_INFO "----global_mem_exit----\n");
}
module_init(global_mem_init);
module_exit(global_mem_exit);
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_ALIAS(DRIVER_DESC);
5、Makefile
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
else
obj-m := global_mem.o
endif
6、编写测试文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#define MEM_CLEAR 0x1
#define DEV_NAME "/dev/globalmem"
int main(void)
{
int fd,ret;
char* name="Hello Linux";
char test[100];
fd= open(DEV_NAME,O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
write(fd,name,strlen(name));
close(fd);
fd=open("/dev/globalmem",O_RDWR);
ret=read(fd,test,20);
test[ret]='\0';
ioctl(fd,MEM_CLEAR,1);
close(fd);
printf("%s\n",test);
return 0;
}
7、验证
1)sudo insmod global_mem.ko
2)lsmod
Module Size Used by
global_mem 12934 0
autofs4 37024 1
global_mem模块已被加载j
3)cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
216 rfcomm
226 drm
230 globalmem
结论:主设备号为230的“globalmem”字符设备驱动
4、ls -l /dev
crw-rw---- 1 root video 10, 175 May 19 19:33 agpgart
crw------T 1 root root 10, 235 May 19 19:34 autofs
drwxr-xr-x 2 root root 680 May 20 2018 block
drwxr-xr-x 2 root root 60 May 20 2018 bsg
crw------T 1 root root 10, 234 May 19 19:33 btrfs-control
drwxr-xr-x 3 root root 60 May 20 2018 bus
drwxr-xr-x 2 root root 3920 May 19 19:34 char
crw------- 1 root root 5, 1 May 19 19:34 console
lrwxrwxrwx 1 root root 11 May 19 19:33 core -> /proc/kcore
drwxr-xr-x 2 root root 60 May 19 19:33 cpu
crw------- 1 root root 10, 60 May 19 19:33 cpu_dma_latency
drwxr-xr-x 5 root root 100 May 20 2018 disk
drwxr-xr-x 2 root root 80 May 20 2018 dri
crw------- 1 root root 10, 61 May 19 19:33 ecryptfs
crw-rw---- 1 root video 29, 0 May 19 19:33 fb0
lrwxrwxrwx 1 root root 13 May 19 19:33 fd -> /proc/self/fd
crw-rw-rw- 1 root root 1, 7 May 19 19:33 full
crw-rw-rwT 1 root fuse 10, 229 May 19 19:33 fuse
crw-rw-rw- 1 root root 230, 0 May 19 19:54 globalmem
结论:创建字符设备globalmem,主设备号20、次设备号0。
5、向设备驱动写入数据
echo "hello world" > /dev/globalmem
6、从设备中读取数据
cat /dev/globalmem
hello world
7、如果启用sysfs文件系统,将发现多出了/sys/module/global_mem(模块名称)目录,该目录下的树形结构为:
/sys/module/global_mem
├── holders
├── initstate
├── notes
├── parameters
│ └── globalmem_major
├── refcnt
├── sections
│ ├── __mcount_loc
│ └── __param
├── srcversion
├── uevent
└── version
结论:refcnt记录了global_mem模块的引用计数,sections下包含的几个文件则给出了global_mem所包含的BSS、数据段和代码段等的地址及其他信息。