8、Linux驱动开发:驱动-读写接口实现(read&write)

目录

🍅点击这里查看所有博文

随着自己工作的进行,接触到的技术栈也越来越多。给我一个很直观的感受就是,某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了,只有经常会用到的东西才有可能真正记下来。存在很多在特殊情况下有一点用处的技巧,用的不多的技巧可能一个星期就忘了。

想了很久想通过一些手段把这些事情记录下来。也尝试过在书上记笔记,这也只是一时的,书不在手边的时候那些笔记就和没记一样,不是很方便。

很多时候我们遇到了问题,一般情况下都是选择在搜索引擎检索相关内容,这样来的也更快一点,除非真的找不到才会去选择翻书。后来就想到了写博客,博客作为自己的一个笔记平台倒是挺合适的。随时可以查阅,不用随身携带。

同时由于写博客是对外的,既然是对外的就不能随便写,任何人都可以看到。经验对于我来说那就只是经验而已,公布出来说不一定我的一些经验可以帮助到其他的人。遇到和我相同问题时可以少走一些弯路。

既然决定了要写博客,那就只能认真去写。不管写的好不好,尽力就行。千里之行始于足下,一步一个脚印,慢慢来 ,写的多了慢慢也会变好的。权当是记录自己的成长的一个过程,等到以后再往回看时,就会发现自己以前原来这么菜😂。

本系列博客所述资料均来自互联网,并不是本人原创(只有博客是自己写的)。出于热心,本人将自己的所学笔记整理并推出相对应的使用教程,方面其他人学习。为国内的物联网事业发展尽自己的一份绵薄之力,没有为自己谋取私利的想法。若出现侵权现象,请告知本人,本人会立即停止更新,并删除相应的文章和代码。

系统调用

在前面讲到的基础上,我们继续学习如何实现读写接口。

在Linux中,应用程序在打开字符设备文件之后。可通过readwrite 对字符设备进行读写。应用程序的readwrite通常定义为系统调用函数。

对于read 操作,应用程序通常会调用read()函数来读取数据。该函数的原型如下:

c 复制代码
ssize_t read(int fd, void *buf, size_t count);

其中,fd是文件描述符,buf是用户空间缓冲区指针,count是要读取的字节数。该函数会将数据从文件中读取到缓冲区中,并返回实际读取的字节数。如果读取失败,则会返回-1,并设置相应的错误码。

对于write操作,应用程序通常会调用write()函数来写入数据。该函数的原型如下:

c 复制代码
ssize_t write(int fd, const void *buf, size_t count);

其中,fd是文件描述符,buf是用户空间缓冲区指针,count是要写入的字节数。该函数会将数据从用户空间缓冲区中写入到文件中,并返回实际写入的字节数。如果写入失败,则会返回-1,并设置相应的错误码。

除了read()write()函数外,还有其他一些系统调用函数可以用于文件读写操作,例如pread()pwrite()等。这些函数与read()write()函数类似,但提供了更多的选项和控制。

这些系统调用函数,最终会通过对应驱动的file_operations 结构体调用。这在5.设备-设备注册 章节有详细的描述。file_operations 结构体如下,本章节的内容就是实现其中的readwrite方法。

c 复制代码
struct file_operations {
	struct module *owner;
	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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (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 *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	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 **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

数据隔离

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。因为Linux使用的虚拟内存机制,用户空间的数据可能被换出。当内核空间使用用户空间指针时,对应的数据可能不在内存中。

正确的做法是在内核空间开辟自己可以操作的内存,将用户空间的数据拷贝到内核空间之后,内核操作自己的这块内存即可,即使进程意外结束,内核也不会崩溃,这种做法的安全性是要比前者的高的。

我们在驱动中常用copy_from_usercopy_to_user来完成内核空间和用户空间之间的数据交换。

copy_from_user&write

函数copy_from_user的定义如下,其作用是将数据从用户空间拷贝到内核空间。

c 复制代码
/*
功能:将数据从用户空间拷贝到内核空间 (write)
参数:
    @to:内核空间的首地址
 @from:用户空间的首地址
    @n:大小(单位是字节)
返回值:成功返回0,失败返回未拷贝字节的个数
*/
int copy_from_user(void *to, const void __user volatile *from,unsigned long n);

对应的操作也就是系统调用的的write 接口。用户程序调用write 接口时,通过系统调用,驱动模块中会根据文件的fd 数据,最后调用到file_operations 中的*write函数指针。

c 复制代码
/*
参数:
	filp:待操作的设备文件file结构体指针
	buf:待写入所读取数据的用户空间缓冲区指针
	count:待读取数据字节数
	f_pos:待读取数据文件位置,写入完成后根据实际写入字节数重新定位
返回:
	成功实际写入的字节数,失败返回负值
*/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

copy_to_user&read

函数copy_to_user的定义如下,其作用是将数据从内核空间拷贝到用户空间。

c 复制代码
/*
功能:将数据从内核空间拷贝到用户空间 (read)
参数:
    @to:用户空间的首地址
 @from:内核空间的首地址
    @n:大小(单位是字节)
返回值:成功返回0,失败返回未拷贝字节的个数     
*/   
int copy_to_user(void __user volatile *to, const void *from, unsigned long n);

对应的操作也就是系统调用的的read 接口。用户程序调用read 接口时,通过系统调用,驱动模块中会根据文件的fd 数据,最后调用到file_operations 中的*read函数指针。

c 复制代码
/*
参数:
	filp: 待操作的设备文件file结构体指针
	buf: 待写入所读取数据的用户空间缓冲区指针
	count:待读取数据字节数
	f_pos:待读取数据文件位置,读取完成后根据实际读取字节数重新定位
	__user :是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址。
返回值:
	成功实际读取的字节数,失败返回负值
*/   
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

驱动编写

节省篇幅,这里的代码是在上一篇文章设计内容的基础上修改的。驱动代码中首先补充file_operationsreadwrite函数。

c 复制代码
static struct file_operations hello_ops = 
{
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
};

读取函数的实现就只是读取一个全局变量的内容,该全局变量的默认值为kernel 。使用copy_to_user将内核空间的数据拷贝到用户空间的内存中。

c 复制代码
#define KMAX_LEN 32
char kbuf[KMAX_LEN + 1] = "kernel";
//read(fd,buff,40);
static ssize_t hello_read (struct file *filep, char __user *buf, size_t size, loff_t *pos)
{
	int error;
	if(size > KMAX_LEN)
		size = KMAX_LEN;
	if(copy_to_user(buf, kbuf, size)){
		error = -EFAULT;
		return error;
	}
	return size;
}

写入函数的实现修改全局变量的内容。使用copy_from_user将用户空间的数据拷贝到内核空间的内存中。

c 复制代码
//write(fd,buff,40);
static ssize_t hello_write (struct file *filep, const char __user *buf, size_t size, loff_t *pos)
{
	int error;
	if(size > KMAX_LEN)
		size = KMAX_LEN;
	memset(kbuf,0,sizeof(kbuf));
	if(copy_from_user(kbuf, buf, size)){
		error = -EFAULT;
		return error;
	}
	printk("%s\n",kbuf);
	return size;
}

实验结果

编写用户空间的读写程序。打开驱动文件后,首先读取一遍驱动中的默认数据。然后写入数据device write test,最后在读取一遍驱动的数据。

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> //close
void main(void)
{
	int len = 0;
	int fd = open("/dev/hellodev",O_RDWR);
	if(fd < 0) {
		perror("open fail\n");
		return;
	}
	char buf[64 + 1] = {0};
	len = read(fd, buf, sizeof(buf) - 1);
	buf[len] = '\0';
	printf("read:%s,len = %d\n", buf, len);

	char buf2[64 + 1] = "device write test";
	len = write(fd, buf2, strlen(buf2));
	printf("write ok,len=%d\n", len);

	len = read(fd, buf, sizeof(buf) - 1);
	buf[len] = '\0';
	printf("read:%s,len=%d\n", buf, len);

	close(fd);
	return;
}

从日志中可以看到,读写的数据都是符合预期的。

shell 复制代码
root@ubuntu:# insmod ./hello.ko 
root@ubuntu:# gcc ./test.c 
root@ubuntu:# ./a.out 
read:kernel,len = 32
write ok,len=17
read:device write test,len=32
root@ubuntu:# dmesg
[236852.445548] hello_init 
[236866.611393] hello_open()
[236866.611482] device write test
[236866.611495] hello_release()

那么本篇博客就到此结束了,这里只是记录了一些我个人的学习笔记,其中存在大量我自己的理解。文中所述不一定是完全正确的,可能有的地方我自己也理解错了。如果有些错的地方,欢迎大家批评指正。如有问题直接在对应的博客评论区指出即可,不需要私聊我。我们交流的内容留下来也有助于其他人查看,说不一定也有其他人遇到了同样的问题呢😂。

相关推荐
ac.char6 分钟前
在 Ubuntu 上安装 Yarn 环境
linux·运维·服务器·ubuntu
敲上瘾6 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
长弓聊编程24 分钟前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
cherub.31 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
梅见十柒1 小时前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热1 小时前
路由基础(全)
linux·网络·网络协议·安全
传而习乎1 小时前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
soulteary1 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
IT果果日记2 小时前
ubuntu 安装 conda
linux·ubuntu·conda