内核空间与用户空间
Linux 系统通过内核空间和用户空间的分离,实现了系统的安全性和稳定性。本章深入探讨两者的区别、通信机制和内存布局。
地址空间划分
32 位系统
4GB ┌──────────────────────┐
│ Kernel Space │ 1GB (0xC0000000 - 0xFFFFFFFF)
│ 内核空间 │
3GB ├──────────────────────┤
│ │
│ User Space │ 3GB (0x00000000 - 0xBFFFFFFF)
│ 用户空间 │
│ │
0GB └──────────────────────┘
64 位系统
高地址 ┌──────────────────────┐
│ Kernel Space │ 128TB
│ 0xFFFF800000000000 │
│ - 0xFFFFFFFFFFFFFFFF│
├──────────────────────┤
│ Non-canonical │ 空洞(无效地址)
│ Address Hole │
├──────────────────────┤
│ User Space │ 128TB
│ 0x0000000000000000 │
低地址 └──────────────────────┘
特权级别
CPU 保护环
Ring 0 - 内核模式
├─ 最高权限
├─ 可执行所有指令
├─ 可访问所有内存
└─ 运行内核代码
Ring 1, 2 - 系统服务(未使用)
Ring 3 - 用户模式
├─ 受限权限
├─ 禁止执行特权指令
├─ 只能访问用户空间内存
└─ 运行应用程序
权限检查:
// arch/x86/include/asm/processor.h
static inline unsigned long current_user_stack_pointer(void)
{
unsigned long sp;
asm volatile("mov %%rsp, %0" : "=r"(sp));
return sp;
}
// 检查当前权限级别
static inline int user_mode(struct pt_regs *regs)
{
return !!(regs->cs & 3); // CS 寄存器的低 2 位
}
内存保护
页表权限位
// arch/x86/include/asm/pgtable_types.h
#define _PAGE_PRESENT 0x001 // 页面在内存中
#define _PAGE_RW 0x002 // 可写
#define _PAGE_USER 0x004 // 用户可访问
#define _PAGE_PWT 0x008 // 写通
#define _PAGE_PCD 0x010 // 禁用缓存
#define _PAGE_ACCESSED 0x020 // 已访问
#define _PAGE_DIRTY 0x040 // 已修改
#define _PAGE_PSE 0x080 // 大页
#define _PAGE_GLOBAL 0x100 // 全局页
#define _PAGE_NX (1ULL << 63) // 不可执行
SMEP/SMAP
SMEP(Supervisor Mode Execution Prevention):
- 防止内核执行用户空间代码
- CR4.SMEP = 1 启用
SMAP(Supervisor Mode Access Prevention):
- 防止内核访问用户空间数据
- CR4.SMAP = 1 启用
查看是否启用:
# 查看 CPU 特性
grep -E 'smep|smap' /proc/cpuinfo
# 查看内核参数
dmesg | grep -E 'SMEP|SMAP'
数据交换机制
copy_to_user / copy_from_user
// include/linux/uaccess.h
// 从内核空间复制到用户空间
unsigned long copy_to_user(void __user *to,
const void *from,
unsigned long n);
// 从用户空间复制到内核空间
unsigned long copy_from_user(void *to,
const void __user *from,
unsigned long n);
// 示例:内核模块中的使用
static ssize_t dev_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
char kernel_data[100] = "Hello from kernel!";
if (count > sizeof(kernel_data))
count = sizeof(kernel_data);
// 安全地复制到用户空间
if (copy_to_user(buf, kernel_data, count)) {
return -EFAULT;
}
return count;
}
get_user / put_user
// 读取单个值
int get_user(x, ptr);
// 写入单个值
int put_user(x, ptr);
// 示例
static long dev_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
int value;
int __user *user_ptr = (int __user *)arg;
switch (cmd) {
case MY_IOCTL_GET:
value = 42;
if (put_user(value, user_ptr))
return -EFAULT;
break;
case MY_IOCTL_SET:
if (get_user(value, user_ptr))
return -EFAULT;
printk("Received value: %d\n", value);
break;
}
return 0;
}
上下文切换
进程上下文切换流程
1. 保存当前进程的 CPU 状态
├─ 通用寄存器
├─ 栈指针
├─ 程序计数器
└─ 标志位
2. 切换内存映射
├─ 更新 CR3 寄存器(页表基址)
└─ 刷新 TLB
3. 加载新进程的 CPU 状态
├─ 恢复寄存器
└─ 恢复执行
4. 继续新进程的执行
内核代码:
// kernel/sched/core.c
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
// 切换内存映射
if (!next->mm) {
// 内核线程
next->active_mm = prev->active_mm;
} else {
// 用户进程
switch_mm_irqs_off(prev->active_mm, next->mm, next);
}
// 切换 CPU 寄存器状态
switch_to(prev, next, prev);
return finish_task_switch(prev);
}
中断上下文
中断处理流程:
用户程序执行
↓
硬件中断发生
↓
保存当前状态(自动)
↓
跳转到中断处理程序(内核空间)
↓
执行中断处理
↓
iret 指令返回
↓
恢复之前的状态
↓
继续执行用户程序
中断栈:
// arch/x86/kernel/irq_64.c
DEFINE_PER_CPU_PAGE_ALIGNED(struct irq_stack, irq_stack_backing_store);
// 中断栈布局
struct irq_stack {
char stack[IRQ_STACK_SIZE];
} __aligned(IRQ_STACK_SIZE);
实践:用户空间与内核空间通信
示例1:字符设备驱动
// hello_device.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "hello"
#define BUF_LEN 256
static int major;
static char msg[BUF_LEN] = "Hello from kernel!\n";
static int msg_len = 19;
static int device_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "hello: device opened\n");
return 0;
}
static int device_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "hello: device closed\n");
return 0;
}
static ssize_t device_read(struct file *file, char __user *buffer,
size_t len, loff_t *offset)
{
int bytes_read = 0;
if (*offset >= msg_len)
return 0;
if (*offset + len > msg_len)
len = msg_len - *offset;
if (copy_to_user(buffer, msg + *offset, len))
return -EFAULT;
*offset += len;
bytes_read = len;
printk(KERN_INFO "hello: sent %d bytes to user\n", bytes_read);
return bytes_read;
}
static ssize_t device_write(struct file *file, const char __user *buffer,
size_t len, loff_t *offset)
{
if (len > BUF_LEN - 1)
len = BUF_LEN - 1;
if (copy_from_user(msg, buffer, len))
return -EFAULT;
msg[len] = '\0';
msg_len = len;
printk(KERN_INFO "hello: received %zu bytes from user\n", len);
return len;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
static int __init hello_init(void)
{
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
printk(KERN_ALERT "hello: failed to register device\n");
return major;
}
printk(KERN_INFO "hello: registered with major number %d\n", major);
printk(KERN_INFO "hello: run 'mknod /dev/hello c %d 0'\n", major);
return 0;
}
static void __exit hello_exit(void)
{
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "hello: unregistered\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple character device driver");
Makefile:
obj-m += hello_device.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
使用示例:
# 编译模块
make
# 加载模块
sudo insmod hello_device.ko
# 查看主设备号
dmesg | tail
# 创建设备文件
sudo mknod /dev/hello c <major> 0
sudo chmod 666 /dev/hello
# 测试读取
cat /dev/hello
# 测试写入
echo "New message" > /dev/hello
cat /dev/hello
# 卸载模块
sudo rmmod hello_device
sudo rm /dev/hello
示例2:procfs 接口
// procfs_example.c
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
static int show_info(struct seq_file *m, void *v)
{
seq_printf(m, "Current PID: %d\n", current->pid);
seq_printf(m, "Process name: %s\n", current->comm);
seq_printf(m, "User mode: %s\n",
user_mode(task_pt_regs(current)) ? "yes" : "no");
return 0;
}
static int proc_open(struct inode *inode, struct file *file)
{
return single_open(file, show_info, NULL);
}
static const struct proc_ops proc_fops = {
.proc_open = proc_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
static int __init procfs_init(void)
{
proc_create("myinfo", 0, NULL, &proc_fops);
printk(KERN_INFO "procfs: created /proc/myinfo\n");
return 0;
}
static void __exit procfs_exit(void)
{
remove_proc_entry("myinfo", NULL);
printk(KERN_INFO "procfs: removed /proc/myinfo\n");
}
module_init(procfs_init);
module_exit(procfs_exit);
MODULE_LICENSE("GPL");
使用:
sudo insmod procfs_example.ko
cat /proc/myinfo
sudo rmmod procfs_example
性能考量
上下文切换开销
测量工具:
# 使用 perf 测量上下文切换
perf stat -e context-switches,cpu-migrations sleep 10
# 查看进程上下文切换次数
cat /proc/<PID>/status | grep ctxt
# vmstat 监控
vmstat 1
减少用户/内核切换
优化策略:
- 批量操作:一次系统调用处理多个请求
- 异步 I/O:使用 io_uring
- 内存映射:使用 mmap 避免 read/write
- 用户空间驱动:DPDK、SPDK
下一步:学习 文件系统深度解析 章节。