面试题:什么是用户态和内核态,如何进入内核态?——分叉智能科技一面

旅行   2024-11-19 08:08   广东  

欢迎关注本公众号,专注面试题拆解

分享一套视频课程《C++百万并发服务器开发》,有需要的加我微信获取:fb964919126


面试题:什么是用户态和内核态,如何进入内核态?

这道面试题属于难者不会,会者不难类型的,而且今年的校招不止这一家公司出现了,也算一道小高频的面试题了。


1

基本概念


1.1、什么是用户态和内核态?

在现代操作系统中,为了保护系统的稳定性和安全性,CPU的执行级别被划分为不同的权限等级。最常见的是将CPU的运行空间划分为用户空间和内核空间。


用户态(User Mode):应用程序运行在用户空间,权限受限,不能直接访问硬件资源和执行特权指令。

内核态(Kernel Mode):操作系统内核运行在内核空间,具有最高权限,可以直接访问所有硬件资源。


假设所有程序都可以直接访问硬件和内存,会发生什么?

// 危险的代码示例 - 如果在用户态允许直接访问内存void dangerous_operation() {    // 直接写入任意内存地址    int* ptr = (int*)0x12345678;    *ptr = 0;  // 这可能会导致系统崩溃
// 直接操作硬件端口 outb(0x378, 0xff); // 直接向并口写数据}

为了防止这种情况,操作系统引入了特权级别的概念。


1.2、x86架构的特权级别

在x86架构中,CPU提供了4个特权级别(Ring 0-3):

// CPU特权级别的符号定义(Linux内核源码片段)#define RING0_PRIVILEGE_LEVEL 0#define RING1_PRIVILEGE_LEVEL 1#define RING2_PRIVILEGE_LEVEL 2#define RING3_PRIVILEGE_LEVEL 3

Ring 0:最高特权级,内核态运行在这个级别

Ring 3:最低特权级,用户态程序运行在这个级别

Ring 1和Ring 2:很少使用



2

如何进入内核态


有三种主要方式进入内核态:系统调用、异常、硬件中断。下面我们分别来看一下。

2.1、系统调用

系统调用是最常见的进入内核态的方式,是用户程序和操作系统内核之间的接口。它的主要目的是:

a、为用户程序提供受控的内核访问方式

b、保护系统资源和硬件不被直接访问

c、提供统一的抽象接口


以打开文件为例:当你的应用程序需要打开文件时,它不能直接访问硬盘,而是通过open()系统调用,让内核来完成实际的硬盘读取操作。

// 用户态程序#include <fcntl.h>#include <unistd.h>int main() {    // open()是一个系统调用包装函数    int fd = open("test.txt", O_RDONLY);    if (fd < 0) {        return -1;    }    // 使用文件描述符    close(fd);    return 0;}// 内核中的系统调用实现(简化版)SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode){    return do_sys_open(AT_FDCWD, filename, flags, mode);}


系统调用的执行流程,一个完整的系统调用经历以下步骤:


1、用户态准备:
准备系统调用号
按照约定将参数放入指定寄存器
触发系统调用指令(syscall)
2、特权级切换:
CPU从用户态(Ring3)切换到内核态(Ring0)
切换到内核栈
保存用户态上下文
3、内核态处理:
验证系统调用号
检查参数有效性
执行实际的系统调用函数
准备返回值
4、返回用户态:
恢复用户态上下文
切换回用户栈
返回用户程序继续执行

系统调用的分类

Linux系统调用大致可以分为以下几类:进程管理:fork(): 创建新进程exec(): 执行新程序exit(): 终止进程wait(): 等待子进程文件操作:open(): 打开文件read(): 读取文件write(): 写入文件close(): 关闭文件设备操作:ioctl(): 设备控制mmap(): 内存映射系统控制:getpid(): 获取进程IDkill(): 发送信号sysinfo(): 获取系统信息内存管理:brk(): 改变数据段大小mmap(): 映射内存区域munmap(): 取消内存映射


2.2、硬件中断

硬件中断是由硬件设备发出的异步信号,用于通知CPU发生了需要处理的硬件事件。硬件中断也会触发进入内核态。

当硬件中断发生时,处理过程如下:

a、硬件级别:

设备 -> 产生中断信号 -> 中断控制器 -> CPU

b、CPU响应:

完成当前指令执行保存当前执行上下文切换到内核态跳转到中断处理程序

c、软件级别:

中断处理程序:-> 保存额外上下文-> 执行具体中断处理代码-> 恢复上下文-> 返回之前的执行


键盘中断处理示例:

// drivers/input/keyboard/atkbd.cstatic irqreturn_t atkbd_interrupt(struct serio *serio, unsigned char data,                                 unsigned int flags){    struct atkbd *atkbd = serio_get_drvdata(serio);
if (unlikely(atkbd->ps2dev.flags & PS2_FLAG_ACK)) if (ps2_handle_ack(&atkbd->ps2dev, data)) goto out;
if (unlikely(atkbd->ps2dev.flags & PS2_FLAG_CMD)) if (ps2_handle_response(&atkbd->ps2dev, data)) goto out;
atkbd_process_byte(atkbd, data, flags);
out: return IRQ_HANDLED;}


2.3、异常

当程序出现异常时会触发进入内核态:

// 页故障处理程序示例void page_fault_handler(struct pt_regs *regs, unsigned long error_code){    unsigned long address = read_cr2();  // 获取故障地址
// 检查是否是合法访问 if (!(error_code & PF_PROT)) { // 处理缺页异常 handle_mm_fault(current->mm, address, error_code); } else { // 处理访问违例 send_sigsegv(regs, error_code, address); }}



3

内核态切换具体过程


1. 用户态程序发起系统调用

用户态程序通过调用特定的库函数(如 read、write、open 等)发起系统调用。这些库函数最终会调用特定的指令来触发中断,从而进入内核态。


2. 触发中断

用户态程序通过特定的指令(如 syscall、int 0x80)触发中断,进入内核态。不同的架构有不同的指令来触发系统调用:

x86 架构:使用 int 0x80 或 syscall 指令。

x86_64 架构:通常使用 syscall 指令。


3. 硬件自动保存上下文

当触发中断时,硬件会自动保存当前的寄存器状态和程序计数器(PC),以确保在返回用户态时能够恢复到中断前的状态。


4. 切换到内核态

硬件将 CPU 切换到内核态,这意味着 CPU 现在可以执行任何指令,访问任何内存地址,直接与硬件交互。


5. 内核处理中断

内核的中断处理程序接管控制,查找并执行相应的系统调用处理函数。这个过程通常包括以下步骤:

保存用户态上下文:内核进一步保存用户态的寄存器状态和栈信息,以确保在返回用户态时能够正确恢复。

设置内核栈:内核切换到自己的栈,以便在内核态中执行系统调用处理函数。

查找系统调用号:内核从寄存器中读取系统调用号,确定要执行的系统调用。

调用系统调用处理函数:内核调用相应的系统调用处理函数,执行用户请求的操作。


6. 执行系统调用处理函数

系统调用处理函数执行用户请求的操作,如读写文件、创建进程等。这些操作通常涉及内核的数据结构和硬件资源。


7. 恢复用户态上下文

系统调用处理完成后,内核恢复用户的上下文信息,包括寄存器状态和栈信息。


8. 返回用户态

硬件将 CPU 切换回用户态,继续执行用户态程序。用户态程序从系统调用返回点继续执行。


内核态切换是一个复杂的操作,涉及硬件和软件的协同工作。通过触发中断,硬件自动保存上下文信息,内核处理中断并执行系统调用处理函数,最后恢复上下文信息并返回用户态。

4

用户态内核态数据交换


copy_to_user 和 copy_from_user

这两个函数用于在内核态和用户态之间安全地复制数据。它们可以处理内存边界检查,确保不会访问无效的内存地址。


copy_to_user:从内核态复制数据到用户态。

copy_from_user:从用户态复制数据到内核态。

// 用户空间和内核空间的数据拷贝int copy_from_user(void *to, const void __user *from, unsigned long n){    if (access_ok(VERIFY_READ, from, n)) {        kasan_check_write(to, n);        return raw_copy_from_user(to, from, n);    }    return n;}
int copy_to_user(void __user *to, const void *from, unsigned long n){ if (access_ok(VERIFY_WRITE, to, n)) { kasan_check_read(from, n); return raw_copy_to_user(to, from, n); } return n;}


5

总结


1、用户态和内核态的划分是现代操作系统的基础安全机制。通过特权级别的控制,保证了系统的稳定性和安全性。

2、主要进入内核态的方式有:系统调用、硬件中断、异常。

3、切换过程涉及:上下文保存和恢复、栈切换、特权级别切换。

end



CppPlayer 



关注,回复【电子书】珍藏CPP电子书资料赠送

精彩文章合集

专题推荐

【专辑】计算机网络真题拆解
【专辑】大厂最新真题
【专辑】C/C++面试真题拆解

CppPlayer
一个专注面试题拆解的公众号
 最新文章