前言
在 Linux 内核开发中,用户空间和内核空间的数据传输是至关重要的。由于两者有不同的访问权限和地址空间,直接使用 memcpy
进行传输是不安全的。为解决这个问题,内核提供了专门的函数 copy_to_user
和 copy_from_user
。本博客将详细介绍这些函数的使用场景、参数和返回值,并通过实际代码示例展示如何在设备驱动开发中实现安全的数据传输。此外,还会讨论 put_user
和 get_user
函数,帮助读者更好地理解和掌握 Linux 内核中用户空间和内核空间之间的数据传输方法。
用户空间和内核空间的数据传输
在 Linux 内核开发中,用户空间和内核空间的数据传输是一个重要的概念。由于用户空间和内核空间具有不同的访问权限和地址空间,直接使用 memcpy
函数进行数据传输是不安全的。为了确保数据传输的安全性和合法性,内核提供了专门的函数:copy_to_user
和 copy_from_user
。
用户空间数据传输函数 copy_to_user
copy_to_user
函数用于将数据从内核空间复制到用户空间,确保在复制过程中不会违反内存访问权限。
使用场景
- 设备驱动程序:通常在读操作中使用,将设备数据从内核空间复制到用户空间缓冲区。
- 系统调用实现:在实现系统调用时,用于将数据从内核传递给用户进程。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
- 参数:
void __user *to
:指向用户空间目标缓冲区的指针。const void *from
:指向内核空间源缓冲区的指针。unsigned long n
:要复制的字节数。
- 返回值:
- 成功时返回未能成功复制的字节数,通常为0。
- 失败时返回未能成功复制的字节数。如果整个复制操作失败,返回值等于参数
n
。
用户空间数据传输函数 copy_from_user
copy_from_user
函数用于将数据从用户空间缓冲区复制到内核空间,确保在数据传输过程中不会违反内存访问权限。
使用场景
- 设备驱动程序:通常在写操作或 ioctl 操作中使用,将用户提供的数据从用户空间复制到内核空间缓冲区。
- 系统调用实现:在实现系统调用时,用于接收从用户进程传递到内核的数据。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
- 参数:
void *to
:指向内核空间目标缓冲区的指针。const void __user *from
:指向用户空间源缓冲区的指针。unsigned long n
:要复制的字节数。
- 返回值:
- 成功时返回未能成功复制的字节数,通常为0。
- 失败时返回未能成功复制的字节数。如果整个复制操作失败,返回值等于参数
n
。
简单变量的数据传输
在 Linux 内核中,put_user
和 get_user
函数用于在内核空间和用户空间之间传输简单变量的数据。这些函数用于确保安全的数据传输,并防止内核因非法内存访问而崩溃。
函数 put_user
put_user
函数用于将一个简单变量从内核空间复制到用户空间。
int put_user(type x, type __user *ptr);
- 参数:
type x
:要复制到用户空间的变量值。type __user *ptr
:指向用户空间目标地址的指针。
- 返回值:
- 成功时返回0。
- 失败时返回一个负的错误码,例如 -EFAULT,表示发生了非法内存访问。
函数 get_user
get_user
函数用于从用户空间复制一个简单变量到内核空间。
int get_user(type x, const type __user *ptr);
- 参数:
type x
:用于存储从用户空间复制的变量值。const type __user *ptr
:指向用户空间源地址的指针。
- 返回值:
- 成功时返回0,并将变量值复制到
x
。 - 失败时返回一个负的错误码,例如 -EFAULT,表示发生了非法内存访问。
- 成功时返回0,并将变量值复制到
读写操作实现
在设备驱动开发中,实现读写操作是一个关键任务。下面将详细介绍读写操作的实现方法。
读操作实现
read 系统调用
ssize_t read(int fd, void *buf, size_t count);
- 功能:用于从文件或设备中读取数据到用户空间的缓冲区中。
- 参数:
int fd
:文件描述符,标识要读取的文件或设备。void *buf
:指向用户空间的缓冲区,用于存储从文件或设备中读取的数据。size_t count
:要读取的字节数。
- 返回值:
- 成功时返回实际读取的字节数。
- 返回值为0表示已到达文件末尾(EOF)。
- 出错时返回 -1,并设置 errno 以指示具体的错误码。
文件操作的 read
方法
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
- 功能:读取设备产生的数据。
- 参数:
struct file *filp
:指向 open 产生的 struct file 类型的对象。char __user *pbuf
:指向用户空间一块内存,用来保存读到的数据。size_t count
:用户期望读取的字节数。loff_t *ppos
:指示读取的起始位置。
- 返回值:成功返回本次成功读取的字节数,失败返回 -1。
数据读取的过程
-
用户空间到内核空间的请求传递:
-
当应用程序调用
read
系统调用时,数据参数(fd, buf, count
)从用户空间的缓冲区通过系统调用接口传递到内核。 -
内核根据文件描述符找到对应的文件对象和文件操作方法。
-
-
内核空间数据准备:
-
内核中的文件操作方法
read
会从设备或内核缓冲区中读取数据。 -
在
mychar_read
函数中,首先确定要读取的字节数,确保不会超过当前缓冲区中的数据量。 -
然后,调用
copy_to_user
函数,将数据从内核缓冲区mydev_buf
复制到用户空间缓冲区buf
。
-
-
内核空间到用户空间的数据传输:
-
使用
copy_to_user
函数将数据从内核缓冲区复制到用户空间缓冲区,并返回读取的字节数。确保数据传输的安全性和合法性,防止非法内存访问。 -
最后,将剩余的缓冲区数据前移,更新缓冲区长度
curlen
。
-
写操作实现
在进行写入操作时,从应用程序写入到内核的数据通常存放在内核的缓冲区或设备驱动程序中为该设备分配的内存区域中。
write 系统调用
ssize_t write(int fd, const void *buf, size_t count);
- 功能:用于将数据从用户空间缓冲区写入到指定文件或设备。
- 参数:
int fd
:文件描述符,标识要写入的文件或设备。const void *buf
:指向用户空间缓冲区的指针。size_t count
:要写入的字节数。
- 返回值:
- 成功时返回实际写入的字节数。
- 失败时返回 -1,并设置 errno 以指示具体的错误码。
文件操作的 write
方法
ssize_t xxx_write(struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);
- 功能:向设备写入数据。
- 参数:
struct file *filp
:指向 open 系统调用产生的 struct file 类型的对象。const char __user *pbuf
:指向用户空间的一块内存。size_t count
:用户期望写入的字节数。loff_t *ppos
:指示写入的起始位置。
- 返回值:成功返回实际写入的字节数,失败返回 -1。
数据写入的过程
-
用户空间到内核空间的请求传递:
-
当应用程序调用
write
系统调用时,数据参数(fd, buf, count
)从用户空间的缓冲区通过系统调用接口传递到内核。 -
内核根据文件描述符找到对应的文件对象和文件操作方法。
-
-
内核空间数据接收:
-
内核中的文件操作方法
write
会为数据写入做准备。 -
在
mychar_write
函数中,首先确定要写入的字节数,确保不会超过内核缓冲区的剩余空间。
-
-
用户空间到内核空间的数据传输:
-
mychar_write
函数调用copy_from_user
函数将数据从用户空间缓冲区buf
复制到内核缓冲区mydev_buf
,并返回成功复制的字节数。 -
确保数据传输的安全性和合法性,防止非法内存访问。
-
-
内核空间数据存储:
-
更新内核缓冲区长度
curlen
,记录当前存储的数据量。 -
如果有需要,执行其他设备相关操作(如触发硬件事件或更新设备状态)。
-
-
内核数据存放位置:
-
内核缓冲区:对于普通文件,数据会被放入文件系统的内核缓冲区(如页缓存)。这些缓冲区临时存储数据,最终会被写入磁盘。
-
设备驱动程序的缓冲区:对于字符设备或块设备,数据会被存放在设备驱动程序中分配的缓冲区或内存区域中。例如,在字符设备驱动程序中,可以是一个全局的内核缓冲区。
-
网络设备:对于网络设备,数据会被存放在网络协议栈的缓冲区中,然后通过网络接口发送出去。
-
示例
驱动示例
下面是一个完整的字符设备驱动示例,展示了如何实现读写操作。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> // 包含copy_to_user和copy_from_user函数
#define BUF_LEN 100
int major = 11;
int minor = 0;
int mychar_num = 1;
struct cdev mydev; // 字符设备结构体
char mydev_buf[BUF_LEN]; // 设备驱动缓冲区
int curlen = 0; // 当前缓冲区长度
// 打开设备文件的函数
int mychar_open(struct inode *pnode, struct file *pfile)
{
printk("mychar_open is called\n");
return 0;
}
// 关闭设备文件的函数
int mychar_close(struct inode *pnode, struct file *pfile)
{
printk("mychar_close is called\n");
return 0;
}
// 读取设备数据的函数
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
int size = 0;
int ret = 0;
// 判断要读取的字节数是否大于当前缓冲区中的数据量
if(count > curlen)
{
size = curlen;
}
else
{
size = count;
}
// 将数据从内核空间复制到用户空间
ret = copy_to_user(puser, mydev_buf, size);
if(ret)
{
printk("copy_to_user failed\n");
return -1;
}
// 将缓冲区剩余数据前移
memcpy(mydev_buf, mydev_buf + size, curlen - size);
// 更新当前缓冲区长度
curlen -= size;
return size;
}
// 写入设备数据的函数
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
int size = 0;
int ret = 0;
// 判断缓冲区是否有足够的空间容纳要写入的数据
if(count > (BUF_LEN - curlen))
{
size = BUF_LEN - curlen;
}
else
{
size = count;
}
// 将数据从用户空间复制到内核空间
ret = copy_from_user(mydev_buf + curlen, puser, size);
if(ret)
{
printk("copy_from_user failed\n");
return -1;
}
// 更新当前缓冲区长度
curlen += size;
return size;
}
// 文件操作函数集
struct file_operations myops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_close,
.read = mychar_read,
.write = mychar_write,
};
// 模块初始化函数
int __init mychar_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major, minor);
// 申请设备号
ret = register_chrdev_region(devno, mychar_num, "mychar");
if(ret)
{
ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar");
if(ret)
{
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno); // 设置主设备号
}
// 给struct cdev对象指定操作函数集
cdev_init(&mydev, &myops);
// 将struct cdev对象添加到内核对应的数据结构里
mydev.owner = THIS_MODULE;
cdev_add(&mydev, devno, mychar_num);
return 0;
}
// 模块退出函数
void __exit mychar_exit(void)
{
dev_t devno = MKDEV(major, minor);
cdev_del(&mydev);
unregister_chrdev_region(devno, mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);
测试程序
为了验证上述字符设备驱动程序的功能,可以编写一个简单的用户空间测试程序 testmychar_app.c
。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd = -1; // 文件描述符
char buf[8] = ""; // 读取缓冲区
// 检查命令行参数是否足够
if(argc < 2)
{
printf("the argument is too few\n"); // 提示参数过少
return 1; // 返回错误码1
}
// 打开指定的文件或设备
fd = open(argv[1], O_RDWR);
// 检查文件是否成功打开
if(fd < 0)
{
printf("open %s failed\n", argv[1]); // 提示打开文件失败
return 2; // 返回错误码2
}
// 向文件或设备写入数据
write(fd, "hello", 6);
// 从文件或设备读取数据
read(fd, buf, 8);
printf("read buf: %s\n", buf); // 打印读取到的数据
// 关闭文件描述符
close(fd);
fd = -1;
return 0;
}
运行测试程序
在运行测试程序之前,需要确保设备文件存在并具有正确的权限。
# 创建设备文件 sudo mknod /dev/mychar c 11 0 # 修改设备文件权限 sudo chmod 666 /dev/mychar
然后编译并运行测试程序:
gcc testmychar_app.c -o testmychar_app ./testmychar_app /dev/mychar
结论
通过上述代码和测试程序,我们实现了一个简单的字符设备驱动程序,并验证了读写操作的正确性。以下是一些关键点:
- 安全的数据传输:使用
copy_to_user
和copy_from_user
确保内核和用户空间之间的数据传输是安全的。 - 读写操作实现:通过实现字符设备的
read
和write
方法,实现设备数据的读取和写入。 - 测试验证:通过编写用户空间测试程序,验证字符设备驱动程序的功能和稳定性。