在Linux
字符设备驱动中,用户程序使用read()
、write()
相关函数时,内核会调用驱动程序中的的file_operations
结构体中对应的read()
、write()
函数。
file_operations
,其是一个函数指针的集合,用于存放我们定义的用于操作设备的函数的指针,如果我们不定义,它默认保留为NULL
。其中有最重要的几个函数,分别是open()
、read()
、write()
、ioctl()
,下面分别对其进行解析。
1. 打开和关闭设备函数
(1)打开设备
int (*open) (struct inode *, struct file *);
在操作设备前必须先调用open
函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open
返回0。
(2)关闭设备
int (*release) (struct inode *, struct file *);
当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL
。关闭设备永远成功。
2. read()、write() 函数
2.1 read() 函数
函数原型:
ssize_t (*read) (struct file *filp, char __user *buffer, size_t size, loff_t *p);
参数含义:
- filp:待操作的设备文件
file
结构体指针; - buffer:为对应放置所读数据的缓冲区指针(即用户空间内存地址);
- size:为要读取的数据长度;
- p:为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值;
- __user:是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址。
返回值:
成功实际读取的字节数,失败返回负值。
如果该操作为空,将使得read系统调用返回负EINVAL失败,正常返回实际读取的字节数。
2.2 write() 函数
函数原型:
ssize_t (*write) (struct file *filp, const char __user *buffer, size_t count, loff_t *ppos);
参数含义:
- filp:待操作的设备文件
file
结构体指针; - buffer:待写入数据的用户空间缓冲区指针;
- count:为要写入信息的长度;
- ppos:为当前的偏移位置,这个值通常是用来判断写文件是否越界
返回值:
成功实际写入的字节数,失败返回负值。
如果该操作为空,将使得write系统调用返回负EINVAL失败,正常返回实际写入的字节数。
两个函数的作用分别是 从设备中获取数据及发送数据给设备,应用程序中与之对应的也有 write()
函数及 read()
函数:
len = read(fd,buf,len )
static ssize_t hello_read(struct file *filep, char __user *buf, size_t len, loff_t *pos)
len = write(fd,buf,size)
static ssize_t hello_write(struct file *filep, const char __user *buf, size_t len, loff_t *pos)
我们知道,应用程序工作在用户空间,而驱动工作在内核空间,二者不能直接通信的,那我们用何种方法进行通信呢?下面介绍一下内核中的 memcpy—copy_from_user和copy_to_user,虽然说内核中不能使用C
库提供的函数,但是内核也有一个memcpy
函数,用法跟C
库中的一样。
2.3 copy_from_user函数与copy_to_user函数
从字面意义上理解:user是指用户,即用户空间。
在file_operations
结构体中实现的write
函数,即2.2中的write()
函数中,用户空间向内核空间拷贝(写)数据需要使用copy_from_user
函数。而用户空间从内核空间读取数据需要使用copy_to_user
函数。两个函数定义在arch/arm/include/asm/uaccess.h
中。
两个函数定义:
static inline int copy_from_user(void *to, const void __user volatile *from, unsigned long n){
__chk_user_ptr(from, n);
volatile_memcpy(to, from, n);
return 0;
}
static inline int copy_to_user(void __user volatile *to, const void *from, unsigned long n){
__chk_user_ptr(to, n);
volatile_memcpy(to, from, n);
return 0;
}
可以看到两个函数均是调用了 _memcpy() 函数:
static void volatile_memcpy(volatile char *to, const volatile char *from, unsigned long n){
while (n--)
*(to++) = *(from++);
}
其实在这里,我们可以思考,既然拷贝的功能上面的_memcpy()
函数就可以实现,为什么还要封装成 copy_to_user()
和copy_from_user()
呢?
答案是_memcpy()
函数是有缺陷的,譬如我们在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃。
出于上面的原因,内核和用户态之间交互的数据时必须要先对数据进行检测,如果数据是安全的,才可以进行数据交互。上面的函数就是memcpy
的改进版,在memcpy
功能的基础上加上的检查传入参数的功能,防止有些人有意或者无意的传入无效的参数。
现在我们可以审视一下这两个函数了:
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
参数:
- to:目标地址(内核空间)
- from:源地址(用户空间)
- n:将要拷贝数据的字节数
返回值:
成功返回0,失败返回没有拷贝成功的数据字节数
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
参数:
- to:目标地址(用户空间)
- from:源地址(内核空间)
- n:将要拷贝数据的字节数
返回值:
成功返回0,失败返回没有拷贝成功的数据字节数
3.示例
部分代码:
static char kernel_buf[1024];
static ssize_t hello_read(struct file *filep, char __user *buf, size_t len, loff_t *pos){
if(len>1024){
len =1024;
}
if(copy_to_user(buf,kernel_buf,len)){
return -EFAULT;
}
return len;
}
static ssize_t hello_write(struct file *filep, const char __user *buf, size_t len, loff_t *pos){
if(len>1024){
len = 1024;
}
if(copy_from_user(kernel_buf,buf,len)){
return -EFAULT;
}
printk("write %s\n",temp);
return len;
}
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
char buf[]="111232342342342";
char temp[64]={0};
int main(){
int fd,len;
fd = open("/dev/hello",O_RDWR);
if(fd<0){
perror("open fail \n");
return ;
}
write(fd,buf,strlen(buf));
len=read(fd,temp,sizeof(temp));
printf("len=%d,%s \n",len,temp);
close(fd);
}