此文是 ChatGPT 生成的文章,帮助理解 : 用户态什么时候会切换到内核态

在 Linux 中,程序平时运行在用户态,而内核代码运行在内核态。很多人会把“切换到内核态”理解成“用户程序一调用函数就进内核了”,其实不是。真正会触发用户态到内核态切换的,是系统调用、中断和异常

一、什么是切换到内核态

CPU 运行程序时,会处在不同的权限级别。
用户程序运行在低权限的用户态,不能直接操作硬件,也不能直接执行内核函数。
当程序需要操作文件、申请内存、访问设备、等待输入输出时,就必须通过受保护的入口进入内核,由内核代为完成。

这个过程就是:

用户态 → 内核态 → 返回用户态

二、不是“调用函数就切换”,而是“发生特定事件才切换”

很多常见函数名看起来像是在做系统级事情,比如:

  • printf()
  • malloc()
  • memcpy()

但它们本身不一定会切换到内核态

它们通常先在用户态完成自己的工作,只有在内部确实需要内核帮助时,才会发起系统调用,进入内核态。

例如:

  • malloc() 先尝试在用户堆里分配
  • 不够时才通过 brk()mmap() 向内核申请更多空间
  • 这时才发生用户态到内核态切换

三、最常见的切换方式:系统调用

用户程序不能直接调用内核内部函数,必须通过系统调用进入内核。

例如:

  • read()
  • write()
  • open()
  • ioctl()
  • fork()
  • mmap()

这些函数的最终路径,都会经过系统调用入口。

系统调用的过程大致是:

  1. 用户程序调用 libc 接口(这里的 libc ,一般指 C 运行时库,也就是常说的 C 标准库 / 标准库实现,在 Linux 上最常见的是 glibc,也可能是 uClibc、musl。它不只是 C 语言标准里的那些函数,还包括很多 POSIX 接口的封装

比如:

  • printf()
  • malloc()
  • free()
  • strlen()
  • strcpy()
  • fopen()
  • fread()
  • fwrite()
  • open()
  • read()
  • write()

其中有些是纯库函数,有些是对系统调用的封装。

        2. libc 发现需要进入内核

        3. 通过 syscallint 0x80svc 等指令触发切换

        4. CPU 进入内核态

        5. 内核执行对应服务函数

        6. 返回用户态

四、除了系统调用,中断和异常也会切换到内核态

1. 中断

比如:

  • 定时器中断
  • 键盘中断
  • 网卡中断
  • 设备中断

硬件一旦发出中断,CPU 会暂停当前用户程序,转去执行内核中的中断处理代码。

2. 异常

比如:

  • 缺页异常(page fault)
  • 除零异常
  • 非法指令异常

当程序访问了尚未映射的内存页,CPU 也会进入内核,由内核处理缺页,再返回用户态继续执行。

五、以 malloc() 为例理解切换过程

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *p = malloc(100);
    printf("%p\n", p);
    free(p);
    return 0;
}

这个程序里,malloc(100) 表面上只是一个普通函数,但它内部可能分两种情况:

情况一:用户堆里还有空闲内存

malloc() 直接从用户态的内存管理器中分配,不进入内核态

情况二:用户堆不够了

malloc() 会通过系统调用向内核申请更多空间,例如:

  • brk()
  • mmap()

这时 CPU 才会:

  • 从用户态切换到内核态
  • 内核扩展进程虚拟地址空间
  • 返回用户态
  • malloc() 再把结果交给程序

所以,malloc 本身不一定切换内核态,只有在需要向内核要资源时才会切换

六、用户态与内核态各自使用自己的栈

用户栈

  • 每个线程有自己的用户栈
  • 保存函数调用、局部变量、返回地址
  • 程序结束时由内核自动回收

内核栈

  • 每个线程进入内核态时使用自己的内核栈
  • 空间较小,通常只有几 KB 到十几 KB
  • 不能随便放大数组或大对象

也就是说,切换到内核态时,不只是权限变了,连使用的栈也变了

七、 可以这样记忆

用户程序并不是“想进内核就能进内核”,而是必须通过正规入口:

  • 系统调用:最常见
  • 中断:硬件触发
  • 异常:错误或缺页触发

而普通的用户态函数调用,大多数仍然是在用户空间内部完成。

八、一句话总结

用户态切换到内核态,发生在系统调用、中断和异常这些受保护事件中;像 malloc()printf() 这类函数本身不一定切换,只有它们内部需要内核服务时,才会真正进入内核态。

--------------------------------------------------------------------------------------------------------------------

下面是补充的知识点:

1. libc,一般指 C 运行时库,也就是常说的 C 标准库 / 标准库实现,在 Linux 上最常见的是 glibc,也可能是 uClibc、musl。

它不只是 C 语言标准里的那些函数,还包括很多 POSIX 接口的封装

比如:

  • printf()
  • malloc()
  • free()
  • strlen()
  • strcpy()
  • fopen()
  • fread()
  • fwrite()
  • open()
  • read()
  • write()

其中有些是纯库函数,有些是对系统调用的封装。

2.  libc 接口和系统调用是什么关系?

可以简单理解成这样:

用户程序 → libc 接口 → 系统调用 → 内核

但要注意:

  • 不是所有 libc 函数都会进入内核
  • 只有需要内核服务时,才会触发系统调用

例子

printf()

大多数时候只是,用户态库函数,先把内容放到缓冲区里。
真正写到终端或文件时,可能才会调用 write(),进而进入内核。

malloc()

先由 libc 自己管理堆内存。
如果不够了,才会调用 brk()mmap() 之类的系统调用。

3.  系统调用指的是什么?

系统调用是 用户态 请求 内核服务 的入口
用户程序不能直接执行内核代码,必须通过系统调用。

常见系统调用有很多,按功能大致可以分成几类:

文件和目录相关

  • open
  • close
  • read
  • write
  • lseek
  • stat
  • unlink
  • rename

进程管理相关

  • fork
  • vfork
  • execve
  • exit
  • waitpid
  • getpid
  • kill

内存管理相关

  • brk
  • mmap
  • munmap
  • mprotect

设备和控制相关

  • ioctl

时间相关

  • nanosleep
  • clock_gettime
  • gettimeofday

网络相关

  • socket
  • bind
  • listen
  • accept
  • connect
  • send
  • recv

事件等待相关

  • select
  • poll
  • epoll_create
  • epoll_ctl
  • epoll_wait

4.  一个很重要的区别:库函数不等于系统调用

例如:

  • printf()库函数
  • write() 才是更接近内核的接口
  • write() 最终会触发 系统调用

再比如:

  • malloc() 是库函数
  • brk() / mmap() 才会进入内核

所以可以记成:

libc 是“用户态工具箱”,系统调用是“进入内核的门”。

5.  最容易混淆的一点

有些函数名字看起来像系统调用,但其实你平时调用的往往是 libc 封装后的版本

比如:

int fd = open("a.txt", O_RDONLY);

你写的是 open(),但它通常是 libc 提供的包装,内部再调用系统调用进入内核。

open() 是 sys_open 的用户态薄封装,只负责传参和触发系统调用,真正打开文件的是内核里的 sys_open

-------------------------------------------------------------------------------------------------------------

Linux 内核和应用层这一块,可以把它理解成下面这座"三层楼":

对于 应用开发和驱动开发 来说,可以这么理解:

用户程序
    ↓
库函数(libc)
    ↓
系统调用
    ↓
内核函数
    ↓
驱动函数
    ↓
硬件

但如果再往下追:

内核函数
    ↓
驱动函数
    ↓
寄存器读写
    ↓
CPU总线
    ↓
硬件设备

真正最底层的是:

readl();
writel();

或者:

gpio_set_value();

最终变成:

CPU访问寄存器

驱动开发本质上就是在做这件事。

用户程序不能直接调用内核函数,必须通过系统调用进入内核;系统调用只是入口,真正完成工作的仍然是内核中的各种函数和驱动函数。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐