内核空间与用户空间

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

减少用户/内核切换

优化策略

  1. 批量操作:一次系统调用处理多个请求
  2. 异步 I/O:使用 io_uring
  3. 内存映射:使用 mmap 避免 read/write
  4. 用户空间驱动:DPDK、SPDK

下一步:学习 文件系统深度解析 章节。