与设备文件对话
在上一篇博文中,我们编写了一个字符设备驱动,简单的回顾下我们编写的流程:
- 实现open/close/read/write四个操作设备文件的函数
- 填充file_operations结构体
- 注册设备和指明操作设备的file_operations
与设备之间可以通过上面提到的几个函数来进行通信,但是对于某些设备来说,设备的专有特性操作无法通过上面提到的几个函数来实现
ioctl的一些简介:
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的参数个数如下:int ioctl(int fd, int cmd, …);其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就能在用户程序中使用ioctl函数控制设备的I/O通道。
来自百度百科
ioctl的功能:
控制I/O设备 ,提供了一种获得设备信息和向设备发送控制参数的手段。用于向设备发控制和配置命令 ,有些命令需要控制参数,这些数据是不能用read/write 读写的,称为Out-of-band数据。也就是说,read/ write 读写的数据是in-band数据,是I/O操作的主体,而ioctl 命令传送的是控制信息,其中的数据是辅助的数据。
来自百度百科
要想全面的去控制一个设备,那么在内核中实现ioctl和标准的VFS文件操作接口,这都应该是必须的。那么本篇博文其实主要讲的就是如何通过ioctl来和设备文件通信。
开始去实现
ioctl
ioctl中的其实最重要的就是cmd和参数,分别对比下用户空间和内核空间关于ioctl相关的函数原型
用户空间:
#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...);
fd是设备文件的描述符
cmd是发送给设备的命令,如果这个命令带有参数,后面还会接参数。
内核空间:
int (struct file *file,unsigned int ioctl_num,unsigned long ioctl_param)
file对应打开的设备文件
ioctl_num 对应用户空间的cmd
ioctl_param则对应于cmd指定的参数
那么就存在一个问题了,怎么知道哪些cmd带有参数,哪写不带参数,以及参数的类型。
cmd可以随便定义吗?
其实cmd的定义是有规定的,cmd大小为32位,这32位分成了四个域:
bit31~bit30 2位为 “区别读写” 区,作用是区分是读取命令还是写入命令。
bit29~bit15 14位为 “数据大小” 区,表示 ioctl() 中的 arg 变量传送的内存大小。
bit20~bit08 8位为 “魔数”(也称为”幻数”)区,这个值用以与其它设备驱动程序的 ioctl 命令进行区别。
bit07~bit00 8位为 “区别序号” 区,是区分命令的命令顺序序号。
那么如果要去定义个cmd,那么就的按照这个规定来定义,需要自己去计算。幸好在内核空间提供了一系列的
函数帮我们生成cmd,还有一些列的函数可以帮我们解析出cmd的每一个域。
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)
#define _IOC_NRSHIFT 0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
//一系列用来定义cmd的函数
/* used to create numbers */
#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)))
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
//一系列用来解析cmd的函数
/* used to decode ioctl numbers.. */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
介绍完ioctl后,那就开始去定义一些cmd吧
chardev1.h
#ifndef CHARDEV_H
#define CHARDEV_H
#include <asm-generic/ioctl.h>
#define IO_MAGIC 'z' //这是魔数,参照上面的解释,通常是a-z A-Z范围内
#define IOCTL_SET_MSG _IOR(IO_MAGIC,0,char*) //定义了一个接受char*参数的命令码
#define IOCTL_GET_MSG _IOR(IO_MAGIC,1,char*)
#define IOCTL_GET_NTH_BYTE _IOWR(IO_MAGIC,2,int) //定义了一个接受int型参数的命令码
#define DEVICE_FILE_NAME "char_dev"
#endif
这个chardev1.h 不仅是给内核模块使用的,用户空间也要使用,因为要给设备发ioctl cmd的时候,需要
知道cmd的定义。所以这个文件很重要。
老生常谈的模块初始化和注销
和我上一篇博文,关于字符设备驱动的代码基本一样,只不过这里是通过ioctl来控制读写的。
#define SUCCESS 0
#define DEVICE_NAME "char_dev"
#define BUF_LEN 80
static int Device_Open = 0;
static int MAJOR_NUM;
static char Message[BUF_LEN];
static char *Message_Ptr;
int init_module(void)
{
//随机生成
MAJOR_NUM = register_chrdev(0,DEVICE_NAME,&Fops);
if(MAJOR_NUM < 0) {
printk("%s failed with %d\n",
"Sorry,registering the character device",MAJOR_NUM);
return MAJOR_NUM;
}
printk("%s The major device number is %d\n",
"Register is a success",MAJOR_NUM);
printk("If you want to talk to the device driver,\n");
printk("you'll have to create a device file\n");
printk("we suggest you use:\n");
printk("mknod %s c %d 0\n",DEVICE_NAME,MAJOR_NUM);
printk("the device file name is important,because\n");
printk("the ioctl program assumes that's the \n");
printk("file you'll use\n");
return 0;
}
void cleanup_module(void)
{
unregister_chrdev(MAJOR_NUM,DEVICE_NAME);
}
read/write/open/release的实现
static int device_open(struct inode *inode,struct file *file)
{
printk("device_open(%p)\n",file);
if (Device_Open)
return -EBUSY;
Device_Open++;
Message_Ptr = Message;
try_module_get(THIS_MODULE);
return SUCCESS;
}
static int device_release(struct inode *inode,struct file *file)
{
printk("device_release(%p,%p)\n",inode,file);
Device_Open--;
module_put(THIS_MODULE);
return SUCCESS;
}
static ssize_t device_read(struct file *file,
char __user *buffer,
size_t length,
loff_t *offset)
{
int bytes_read = 0;
printk("device_read(%p,%p,%d)\n",file,buffer,length);
if(*Message_Ptr == 0)
return 0;
while(length && *Message_Ptr) {
put_user(*(Message_Ptr++),buffer++);
length--;
bytes_read++;
}
return bytes_read;
}
static ssize_t device_write(struct file *file,
const char __user *buffer,
size_t length,
loff_t *offset)
{
int i;
for(i = 0;i < length && i < BUF_LEN;i++)
get_user(Message[i],buffer+i);
Message_Ptr = Message;
return i;
}
最重要的ioctl的实现
int device_ioctl(struct file *file,
unsigned int ioctl_num,
unsigned long ioctl_param)
{
int i;
char *temp;
char ch;
//典型的switch case分发命令
switch(ioctl_num) {
case IOCTL_SET_MSG:
temp = (char*)ioctl_param; //获取到参数
get_user(ch,temp); //不断读取数据到ch中,统计ioctl_param的长度用i表示
for(i = 0;ch && i < BUF_LEN;i++,temp++)
get_user(ch,temp);
//最终调用write来将用户态的数据写入到设备中
device_write(file,(char*)ioctl_param,i,0);
break;
case IOCTL_GET_MSG:
i = device_read(file,(char*)ioctl_param,99,0);//写入数据,最多写入99字节的数据
put_user('\0',(char*)ioctl_param+i); //设置用户空间bu中的第i个位置为'\0'用来结束
break;
case IOCTL_GET_NTH_BYTE:
return Message[ioctl_param];//获取指定位置处的字符
}
return SUCCESS;
}
注册VFS接口
struct file_operations Fops = {
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl, //这里需要注意,用的是unlocked_ioctl而不是ioctl
//因为在kernel 2.6.36 中已经完全删除了struct file_operations中的ioctl 函数指针,取而代之的是unlocked_ioctl
.open = device_open,
.release = device_release,
};
用户空间程序测试
首先需要编译,产生内核模块。然后载入内核模块,根据打印的信息获取分配的主设备号,然后创建该设备
insmod chardev1.ko
dmesg查看打印信息
Register is a success The major device number is 247
If you want to talk to the device driver,
you'll have to create a device file
we suggest you use:
mknod char_dev c 247 0
the device file name is important,because
the ioctl program assumes that's the
file you'll use
创建字符设备
mknod /dev/char_dev c 247 0
用户空间程序测试
#include "chardev1.h"
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void ioctl_set_msg(int file_desc,char *message)
{
int ret_val;
ret_val = ioctl(file_desc,IOCTL_SET_MSG,message);
if (ret_val < 0) {
printf("ioctl_set_msg failed:%d\n",ret_val);
exit(-1);
}
}
void ioctl_get_msg(int file_desc)
{
int ret_val;
char message[100];
ret_val = ioctl(file_desc,IOCTL_GET_MSG,message);
if (ret_val < 0) {
printf("ioctl_get_msg failed:%d\n",ret_val);
exit(-1);
}
printf("get_msg message:%s\n",message);
}
void ioctl_get_nth_byte(int file_desc)
{
int i;
char c='m'; //最好给个初值,在我的机器上居然好几次初值都是0.导致while循环跳出
printf("get_nth_byte message:");
i = 0;
while(c != 0) {
c = ioctl(file_desc,IOCTL_GET_NTH_BYTE,i++);
if(c < 0) {
printf("ioctl_get_nth_byte failed at the %d'th byte:\n",i);
exit(-1);
}
putchar(c);
}
putchar('\n');
}
int main()
{
int file_desc,ret_val;
char *msg = "Message passwd by ioctl\n";
file_desc = open("/dev/char_dev",0);
if(file_desc < 0) {
printf("can't open device file: %s\n",DEVICE_FILE_NAME);
exit(-1);
}
// ioctl_get_nth_byte(file_desc);
// ioctl_get_msg(file_desc);
// ioctl_set_msg(file_desc,msg);
close(file_desc);
}
对于上面注释的三条语句,可以依次打开 查看效果,ioctl_get_msg获取设备数据的,但是第一次获取是空
通过ioctl_set_msg设置后,就可以获取到msg指向的内容。ioctl_get_nth_byte则是一个字符字符的获取打印。