0%

内核空间和用户空间的数据拷贝

内核空间和用户空间的数据拷贝

1 内核空间和用户空间

1.1 地址空间的划分

img

  • 32位地址总线的寻址范围是4GB,默认情况下,在32位系统中,处于低地址的3GB地址被分配给用户空间,处于高地址的1GB地址被分配给内核空间
  • 在 arch/x86/include/asm/page_32_types.h 中有如下定义,
1
2
3
4
5
6
7
8
9
10
11
12
/*
* This handles the memory map.
*
* A __PAGE_OFFSET of 0xC0000000 means that the kernel has
* a virtual address space of one gigabyte, which limits the
* amount of physical memory you can use to about 950MB.
*
* If you want more physical memory than this then see the CONFIG_HIGHMEM4G
* and CONFIG_HIGHMEM64G options in the kernel configuration.
*/
#define __PAGE_OFFSET_BASE _AC(CONFIG_PAGE_OFFSET, UL)
#define __PAGE_OFFSET __PAGE_OFFSET_BASE

1.2 用户空间和内核空间的区别

1.2.1 权限不同

img

  • 对于 x86 体系的 CPU,用户空间代码运行在 Ring3 模式,内核空间代码运行在 Ring0 模式
  • 对于 arm 体系的 CPU, 用户空间代码运行在 usr 模式,内核空间代码运行在 svc 模式

1.2.2 安全考量

  • 整个系统中有各种资源,比如计算资源、内存资源和外设资源,而 linux 是多用户、多进程系统,所以这些资源必须在受限的、被管理的状态下使用,否则就会陷入混乱。空间隔离可以保证即使是单个应用程序出现错误也不会影响到操作系统的稳定性

1.2.3 侧重点

  • 内核代码偏重于系统管理;而用户空间代码(也即应用程序)偏重于业务逻辑的实现

1.3 应用程序使用内核提供的服务

img

  • CPU、内存、外设、文件等资源是受保护的,用户进程无法直接操作,必须以系统调用的形式访问
  • 当应用程序要使用内核提供的服务时,要通过 “访管” 指令陷入到内核态,触发软中断,执行对应的系统调用
  • 当处于进程上下文的程序在运行时,如果发生定时器中断、外设中断,当前进程会被打断,CPU 进入内核态去执行对应的中断处理程序,处理结束后再回到用户进程

2 数据拷贝

2.1 用户空间向内核空间拷贝数据

1
2
3
4
5
6
7
8
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) {
printk(KERN_INFO "hello_write\r\n");
int write_len = BUFFER_MAX > s ? s : BUFFER_MAX;
if (copy_from_user(buffer, u, write_len)) {
return -EFAULT;
}
return write_len;
}
  • 指针 u 用 __user 宏修饰,表示这是用户空间指针,内核不能直接使用
  • 在内核空间定义缓冲区 buffer,存放 hello_dev 字符设备的数据
  • write_len 取缓冲区容量 BUFFER_MAX 和字符长度 s 的最小值,防止越界
  • 调用 copy_from_user 函数执行用户空间到内核空间的数据拷贝,传入内核空间地址、用户空间地址、数据长度,成功返回零,失败返回非零值

2.2 内核空间向用户空间拷贝数据

1
2
3
4
5
6
7
8
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) {
printk(KERN_INFO "hello_read\r\n");
int read_len = BUFFER_MAX > s ? s : BUFFER_MAX;
if (copy_to_user(u, buffer, read_len)) {
return -EFAULT;
}
return read_len;
}
  • 调用 copy_to_user 函数执行内核空间到用户空间的数据拷贝,传入用户空间地址、内核空间地址、数据长度,成功返回零,失败返回非零值

2.3 测试

2.3.1 创建字符设备

  • 重新编译内核,
1
2
3
4
5
6
$ make
............
Kernel: arch/x86/boot/bzImage is ready (#3)
Building modules, stage 2.
MODPOST 19 modules
LD [M] drivers/char/hello_dev.ko
  • 加载 hello_dev 驱动模块,
1
$ sudo insmod hello_dev.ko
  • 创建 hello_dev 字符设备,
1
$ sudo mknod /dev/hello c 232 0
  • 添加访问权限,
1
$ sudo chmod a+w /dev/hello

2.3.2 编译、运行测试程序

  • 测试程序,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>

#define DATA_LEN (32)

int main(int argc, char *argv[]) {
char buf[DATA_LEN] = "Hello, world!";
int fd = open("/dev/hello", O_RDWR);
if (-1 == fd) {
perror("open file error\r\n");
return -1;
}
printf("open success\r\n");

int w_len = write(fd, buf, DATA_LEN);
memset(buf, 0, DATA_LEN);
int r_len = read(fd, buf, DATA_LEN);
printf("%d %d\r\n", w_len, r_len);
printf("%s\r\n", buf);

return 0;
}
  • 编译,
1
$ gcc hello_dev_test.c -o hello_dev_test
  • 运行,
1
2
3
4
$ ./hello_dev_test 
open success
32 32
Hello, world!