目录
file_operations结构体的使用,指定对应驱动函数
前言
本文主要介绍内核空间与用户空间通信的一种方式----ioctl,用户程序可以通过调用ioctl函数来实现将一个cmd传给内核,而内核驱动根据switch case来实现预先设定好cmd对应执行的相关程序。
学习ioctl是一个字符设备,所以在我们学习ioctl之前需要先了解一下linux设备驱动中的字符设备。
字符设备
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备按照字节流进行读写操作的设备(一个字节一个字节地读写)。
应用程序与驱动程序间的关系
我们常说的驱动实际上就是驱动程序。以字符设备为例,驱动函数在加载成功后,会在/dev目录下生成一个驱动名称的文件夹,即/dev/xx。应用程序通过系统调用open()打开这个文件夹,然后通过对这个文件的操作来调用相应的驱动程序去控制硬件。
通过下图可以更直观的理解整个调用的过程。首先应用程序通过系统调用如open、read、write、close等函数进入内核空间,之后驱动程序会根据用户传递进来的信息去完成相应的操作。
file_operations 结构体
每一个系统调用都会对应一个驱动程序中的函数。如系统调用的open会对应驱动程序中的一个open,用户使用的系统调用open在进入到内核空间后实际上执行的就是驱动程序中与open对应的函数。
驱动往往就是去实现驱动程序系统调用对应的这些函数,那么内核怎么知道用户在使用系统调用函数时该去执行驱动程序中的哪个函数呢?
答案就在于:Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体。通过这个结构体可以指定系统调用对应的驱动函数。
struct file_operations {
struct module *owner; // owner表示拥有该结构体的模块的指针,基本都是THIS_MODULE。
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*); /* poll轮询函数,用来查询设备是否可以进行非阻塞的读写 */
long (*unlocked_ioctl) (struct file *, unsigned int, unsignedlong); /* 对应ioctl函数,在32位操作系统上运行32位的程序会使用unlocked_ioctl,而64位操作系统上运行32位程序会对应compat_ioctl */
long (*compat_ioctl) (struct file *, unsigned int, unsignedlong);
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 *); // 对应close函数
int (*fsync) (struct file *, loff_t, loff_t, int datasync); /* 用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。 */
int (*aio_fsync) (struct kiocb *, int datasync); /*与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据 */
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
}
file_operations结构体的使用,指定对应驱动函数
/* :int ioctl_demo_open
* @description : 打开设备,对应open函数
* @param – inode : 传递给驱动的 inode
* @param - pfilp_t : 设备文件, file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_open(struct inode *pinode_t, struct file *pfilp_t)
{
// 用户实现具体功能
return 0;
}
/* :ioctl_demo_release
* @description : 关闭/释放设备,对应close函数
* @param - pfilp_t : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_release(struct inode *pinode_t, struct file *pfilp_t)
{
// 用户实现具体功能
return 0;
}
/* :ioctl_demo_write
* @description : 向设备写数据,对应write函数
* @param - pfilp_t : 设备文件,表示打开的文件描述符
* @param - pusr_buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - pofft_t : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t ioctl_demo_write(struct file *pfilp_t,
const char __user *pusr_buf,
size_t cnt, loff_t *pofft_t)
{
// 用户实现具体功能
return 0;
}
/* :ioctl_demo_read
* @description : 从设备读文件,对应read函数
* @param – filp : 要打开的设备文件(文件描述符)
* @param – buf : 返回给用户空间的数据缓冲区
* @param – cnt : 要读取的数据长度
* @param – offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ioctl_demo_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
// 用户实现具体功能
return 0;
}
#endif
static struct file_operations ioctl_demo_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = ioctl_demo_unlocked_ioctl, //指定驱动函数对应系统调用的ioctl
.open = ioctl_demo_open, //指定驱动函数对应系统调用的open
.release = ioctl_demo_release, //指定驱动函数对应系统调用的close
.write = ioctl_demo_write, //指定驱动函数对应系统调用的write
.read = ioctl_demo_read, //指定驱动函数对应系统调用的read
};
字符设备的注册与注销
设备号
为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件include/linux/types.h 里面,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
/*
* 根据上面的定义可以看出dev_t是__u32类型的
* 而在include/uapi/asm-generic/int-ll64.h给出了u__32的定义
* typedef unsigned int __u32;
* 由此看出dev_t实际上就是unsigned int类型的数据
*/
dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。在include/linux/kdev_t.h 中提供了几个关于设备号的操作函数,如下所示:
#define MINORBITS 20 // 表示次设备号位数,一共是 20 位。
#define MINORMASK ((1U << MINORBITS) - 1) // 表示次设备号掩码
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
静态分配设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
/* : register_chrdev
* @description : 字符设备注册函数,一般放在驱动模块的入口函数modlue_init
* @param – major : 主设备号,Linux 下每个设备都有一个主设备号和次设备号
* @param - name : 设备名字,指向一串字符串。即调用注册函数生成/dev/name
* @param - fops : 结构体 file_operations 类型指针,指向设备的操作函数集合变量
* @return : 0 成功;其他 失败
*/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
/* : unregister_chrdev
* @description : 字符设备注销函数,一般放在驱动模块的出口函数modlue_exit
* @param – major : 主设备号,Linux 下每个设备都有一个主设备号和次设备号
* @param - name : 设备名字,指向一串字符串。
* @return : 无
*/
static inline void unregister_chrdev(unsigned int major, const char *name)
主设备号查看命令:可以查看已经使用的主设备号
cat /proc/devices
动态分配主设备号
静态分配主设备号还需要手动查看主设备号是否被占用,容易冲突,比较麻烦,所以可以使用动态分配相关的函数如下:
/* : alloc_chrdev_region
* @description : 动态分配设备号
* @param – dev : 传出参数,用来保存申请到的设备号。
* @param - baseminor: 次设备号起始地址,一般 baseminor 为 0,也就是说次设备号从 0 开始。申请多
* 个设备号时用
* @param - count : 要申请的设备号数量。
* @param - name : 设备名字
* @return : 0 成功;其他 失败
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
/* : unregister_chrdev_regio
* @description : 释放设备号
* @param – from : 需要释放的设备号。
* @param - count : 表示从 from 开始,要释放的设备号数量。
* @return : 无
*/
void unregister_chrdev_region(dev_t from, unsigned count)
ioctl-基于字符设备的代码实现
驱动模块 kernel_ioctl_demo.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#define IOCTL_DEMO_MAJOR 200 /* 主设备号 */
#define IOCTL_DEMO_NAME "ioctl_demo" /* 设备名 */
#define HELLO_OPEN (0U)
#define HELLo_CLOSE (1U)
/* : ioctl_demo_unlocked_ioctl
* @description : 驱动实现ioctl,获取用户态发送的cmd
* @param – pfilp_t : 设备文件
* @param - cmd : 用户态发送给内核的cmd
* @param - arg : 用户态发给内核的额外的参数
* @return : 0 成功;其他 失败
*/
long ioctl_demo_unlocked_ioctl(struct file *pfilp_t, unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case HELLO_OPEN:
{
printk("ioctl_demo:get cmd:HELLO_OPEN from usr\n");
break;
}
case HELLo_CLOSE:
{
printk("ioctl_demo:get cmd:HELLO_CLOSE from usr\n");
break;
}
default:
{
printk("ioctl_demo:get cmd failure from usr\n");
return -1;
}
}
return 0;
}
/*
* @description : 打开设备
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件, file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_open(struct inode *inode, struct file *filp)
{
printk("ioctl_demo open!\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_release(struct inode *inode, struct file *filp)
{
printk("ioctl_demo close!\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t ioctl_demo_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 从设备读文件
* @param – filp : 要打开的设备文件(文件描述符)
* @param – buf : 返回给用户空间的数据缓冲区
* @param – cnt : 要读取的数据长度
* @param – offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ioctl_demo_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
static struct file_operations ioctl_demo_fops = {
.owner = THIS MODULE,
.open = ioctl_demo_open,
.release = ioctl_demo_release,
.write = ioctl_demo_write,
.read = ioctl_demo_read,
};
static int __init ioctl_demo_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(200, "ioctl_demo", &ioctl_demo_fops);
if(0 > retvalue)
{
printk("kernel ioctl_demo register_chrdev failure\n");
return -1;
}
printk("chrdev ioctl_demo is insmod, major_dev is 200\n");
return 0;
}
static void __exit ioctl_demo_exit(void)
{
unregister_chrdev(200, "ioctl_demo");
printk("chrdev ioctl_demo is rmmod and unregister success\n");
}
module_init(ioctl_demo_init);
module_exit(ioctl_demo_exit);
MODULE_DESCRIPTION("a ioctl demo");
MODULE_AUTHOR("lixuezhang");
MODULE_LICENSE("GPL");
用户程序 usr_ioctl_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#define MISC_NAME "/tmp/ioctl_demo"
#define HELLO_OPEN (0UL)
#define HELLO_CLOSE (1UL)
int main(int argc, char **argv)
{
int fd = open(MISC_NAME, O_RDWR);
if(fd < 0)
{
perror("open failure\n");
return -1;
}
while(1)
{
int retvalue = ioctl(fd, HELLO_OPEN);
if(0 != retvalue)
{
perror("ioctl failure\n");
break;
}
sleep(1);
retvalue = ioctl(fd, HELLO_CLOSE);
if(0 != retvalue)
{
perror("ioctl failure\n");
break;
}
}
close(fd);
return 0;
}
makefile
KERNEL_DIR := /XX/XX/linux-3.10 #指定内核的路径
CURRENT_PATH := $(shell pwd) #当前路径
obj-m += kernel_ioctl_demo.o #编译kernel_ioctl_demo.c生成.ko文件
build:ioctl_module #总目标
ioctl_module:
$(MAKE) -C $(KERNEL_DIR) ARCH=XXX CROSS_COMPILE=XXX M=$(CURRENT_PATH) modules
#-C [CPATH]表示make要跳转到CPATH路径执行make
#-M [MPATH]表示执行完CPATH路径下的make后再回到MPATH继续执行剩下的
#ARCH 指定处理器架构
#CROSS_COMPILE 指定交叉编译器
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURRENT_PATH) clean
MISC 杂项设备驱动
MISC 驱动也叫做杂项驱动,当某些外设无法进行分类的时候就可以使用 MISC 驱动。 MISC 驱动其实就是最简单的字符设备驱动,通常嵌套使用来实现复杂的驱动。
所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的次设备号。 MISC 设备会自动创建 cdev,不需要像我们以前那样手动创建,因此采用 MISC 设备驱动可以简化字符设备驱动的编写。我们需要向 Linux 注册一个 miscdevice 设备, miscdevice是一个结构体,定义在文件 include/linux/miscdevice.h 中,内容如下:
struct miscdevice {
int minor; // 次设备号
const char *name; // 设备名
const struct file_operations *fops; // 指向file_operations操作集
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
minor 表示子设备号,需要我们指定。在linux中已经预定义了一些 MISC 设备的子设备号,这些预定义的子设备号定义在include/linux/miscdevice.h文件中,当然也可以自己定义,只要这个子设备号没有被其他设备使用接口。
//include/linux/miscdevice.h
#define PSMOUSE_MINOR 1
#define MS_BUSMOUSE_MINOR 2 /* unused */
#define ATIXL_BUSMOUSE_MINOR 3 /* unused */
#define ATARIMOUSE_MINOR 5 /* unused */
#define SUN_MOUSE_MINOR 6 /* unused */
......
#define MISC_DYNAMIC_MINOR 255
name 就是此 MISC 设备名字,当此设备注册成功以后就会在/dev 目录下生成一个名为 name
的设备文件。
fops 就是字符设备的操作集合, MISC 设备驱动最终是需要使用用户提供的 fops操作集合。
MISC设备注册和注销
/* : misc_register
* @description : 注册MISC杂项设备
* @param – misc : 要注册的 MISC 设备。
* @return : 0,成功,其他失败。
*/
int misc_register(struct miscdevice * misc)
/* : misc_deregister
* @description : 注销MISC杂项设备
* @param – misc : 要注销的 MISC 设备。
* @return : 0,成功,其他失败。
*/
int misc_deregister(struct miscdevice *misc)
基于杂项设备的ioctl驱动
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/semaphore.h>
#include <linux/string.h>
#include <linux/miscdevice.h>
#define IOCTL_DEMO_NAME "ioctl_demo" /* 设备名 */
#define HELLO_OPEN (0U)
#define HELLo_CLOSE (1U)
/*
* @description : 驱动实现ioctl,获取用户态发送的cmd
* @param – pfilp_t : 设备文件
* @param - cmd : 用户态发送给内核的cmd
* @param - arg : 用户态发给内核的额外的参数
* @return : 0 成功;其他 失败
*/
long ioctl_demo_unlocked_ioctl(struct file *pfilp_t, unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case HELLO_OPEN:
{
printk("ioctl_demo:get cmd:HELLO_OPEN from usr\n");
break;
}
case HELLo_CLOSE:
{
printk("ioctl_demo:get cmd:HELLO_CLOSE from usr\n");
break;
}
default:
{
printk("ioctl_demo:get cmd failure from usr\n");
return -1;
}
}
return 0;
}
/*
* @description : 打开设备
* @param – inode : 传递给驱动的 inode
* @param - pfilp_t : 设备文件, file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_open(struct inode *pinode_t, struct file *pfilp_t)
{
printk("ioctl_demo open!\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - pfilp_t : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ioctl_demo_release(struct inode *pinode_t, struct file *pfilp_t)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - pfilp_t : 设备文件,表示打开的文件描述符
* @param - pusr_buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - pofft_t : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t ioctl_demo_write(struct file *pfilp_t,
const char __user *pusr_buf,
size_t cnt, loff_t *pofft_t)
{
return 0;
}
/*
* @description : 从设备读文件
* @param – filp : 要打开的设备文件(文件描述符)
* @param – buf : 返回给用户空间的数据缓冲区
* @param – cnt : 要读取的数据长度
* @param – offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ioctl_demo_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
static struct file_operations ioctl_demo_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = ioctl_demo_unlocked_ioctl,
.open = ioctl_demo_open,
.release = ioctl_demo_release,
.write = ioctl_demo_write,
.read = ioctl_demo_read,
};
static struct miscdevice ioctl_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = IOCTL_DEMO_NAME,
.fops = &ioctl_demo_fops,
};
static int __init ioctl_demo_init(void)
{
int retvalue = 0;
retvalue = misc_register(&ioctl_misc);
if(0 != retvalue)
{
printk("kernel ioctl_demo register_chrdev failure\n");
return -1;
}
printk("miscdevice ioctl_demo is insmod, \n");
return 0;
}
static void __exit ioctl_demo_exit(void)
{
misc_deregister(&ioctl_misc);
printk("miscdevice ioctl_demo is rmmod and deregister success\n");
}
module_init(ioctl_demo_init);
module_exit(ioctl_demo_exit);
MODULE_DESCRIPTION("a miscdevice ioctl");
MODULE_AUTHOR("lixuezhang");
MODULE_LICENSE("GPL");