用户态和内核态的区别

用户态和内核态是CPU硬件层面的两种运行模式,核心区别在于“权限大小“和”资源访问范围“(CPU指令与内存读写)。

1、权限级别不同

  • 内核态拥有最高特权级别(Ring 0):可以运行所有CPU指令,访问所有的内存空间,直接控制所有硬件设备。
  • 用户态拥有最低的特权级别(Ring 3):禁止执行特权CPU指令(关机、挂起进程、修改页表),只能访问自己的进程内存空间,禁止直接操控硬件。

2、内存空间隔离

  • 内核态:拥有独立的内核空间,所有进程的内核空间都映射同一块物理内存空间,用户态进程不可见不可访问
  • 用户态:拥有独立的用户空间,用户态拥有的是虚拟地址空间,彼此隔离不一致,一个进程的崩溃不影响其他进程

3、容错性

  • 内核态崩溃:意味着整个操作系统宕机,可能出现蓝屏等…
  • 用户态崩溃:进程退出,操作系统回收资源,系统依旧可以运行

4、用户态->内核态

image-20260714193452788

用户态通过特定的门触发CPU中断,陷入内核:

  • 系统调用:主动请求内核服务,比如read()write()读写文件,send()recv()网络IO,malloc()申请空间等,本质上是CPU执行了某指令(int 或 syscall)让CPU自己触发中断

  • 硬件中断:网卡收到远端发送的数据,键盘敲击输入…,是硬件中断通过中断控制器通知CPU。

  • 运行异常:CPU运算时发现除0,CPU执行时发现页表还没分配物理空间(缺页异常)

用户态->内核态是有开销的:具体要做几件事,上下文切换(寄存器内容替换),CPU缓存(TLB快表)失效(最重要的一点)等。

减少系统调用能够提高程序运行的性能,比如使用内存池避免频繁申请内存,比如epoll进行批量操作,将IO操作集中。

5、栈空间

  • 内核态:有内核栈(Linux下是4KB或者8KB)
  • 用户态:使用用户栈(通常8MB)可以调整

操作系统本质上是一批描述资源的数据结构+中断处理函数。用户态和内核态本质上是CPU在执行进程的指令时的不同状态,当进程执行的指令只访问用户空间定义的数据,不涉及对操作系统管理的资源的操作时,就是用户态,当执行的指令访问操作系统空间定义的数据或者访问操作系统管理的资源(文件、硬件、内核数据结构等)时,就是内核态。

系统调用

理解了用户态和内核态的区别之后,对于系统调用的理解自然也就清晰了。

1、为什么需要系统调用?

第一:为了隔离,操作系统是一切资源的管理者,对于一些不应该由用户程序直接访问或者操作的资源,应该请操作系统代劳,这样能够保证系统的安全和防止恶意程序随意篡改系统数据和损坏硬件。

第二:为了统一抽象,Linux下一切皆文件,那么对于磁盘、显示器、键盘、网卡等等不同的硬件设备的读写,我们都有一个统一的接口read()ANDwrite(),它到底是怎么实现的呢?

struct file
{
	...
	const struct file_operations *f_op;//文件操作,用来关联系统调用和驱动程序
	...
};
struct file_operations
{
	//读取
	ssize_t (*read) (struct file* ,char __user*, size_t ,loff_t *);
	//写入
        ssize_t (*write) (struct file*,const char __user*,size_t, loff_t *);
};

image-20260714195820655

2、怎么实现的?

当CPU执行(int 0x80或者syscall)时,触发软中断,陷入内核态,拿着系统调用号去系统调用表中查找对应的系统调用方法,比如说read()系统调用,然后这个系统调用内部就会调用对应文件的操作比如file->f_op->read(...)。操作完成之后会返回用户态执行下一条指令。

3、常见的系统调用:

  • 进程控制:fork()创建子进程,exec()执行新程序、exit()结束进程
  • 文件操作:open()打开文件,read()文件读,write()文件写,close()文件关闭
  • 设备通信:socket()网络套接字,connect()连接网络
  • 信息维护:getpid()获取进程ID,time()获取系统时间

由于系统调用会触发中断陷入内核,涉及上下文切换和缓存失效等,开销比较大,所以高并发服务中尽量让IO处理更加集中,而不是频繁陷入内核导致性能降低。

Linux内核和Windows内核的区别?

从内核架构上来说:

Linux把进程调度、内存管理、文件系统、设备驱动都集成在同一层面,好处是这样不同服务之间的沟通效率高,不需要进程间通信。坏处是一旦某个模块出错容易导致系统全面崩溃。

Windows则是把不同的服务分到不同的层面,微内核核心在最顶层,各种系统服务隔离在不同的层次,优点正好对应Linux的缺点,也就是抗风险能力较强,不会因为某个服务的出错导致系统全面崩溃。缺点就是效率的损耗,不同的系统服务间需要通信,带来了额外开销。

从软件生态来说:

Linux是开源的系统内核,可以随意组合不同的系统服务,比如:图形化界面,其他新的硬件驱动等。代价就是开发学习成本高。

Windows是闭源的商业化系统,市面上外设可以自动识别,代价就是内核无法修改,对于嵌入式不友好(比如没法塞进路由器中)。

从日常的配置说:

Linux的设置都写在配置文件中,想要修改IP地址或者其他什么配置,只需要修改配置文件即可。备份后,适合写脚本批量自动化管理

Windows的设置汇总在数据库里,好处是图形化界面对新手友好,坏处是结构复杂,并且备份不直观。

如果是服务器追求极致性能,使用Linux,开源透明且可操控性强。

如果想省心、桌面游戏等,使用Windows,兼容性强,用户基数大。

物理地址,逻辑地址,虚拟地址/线性地址的关系

mov eax, ds:[0x1234][0x1234]是偏移量,CPU做几件事:

第一步:从ds寄存器中取出段选择符比如5

第二步:拿着段选择符(索引)去内存的GDT(全局描述符表)找到段描述符

第三步:段描述符中记载下列信息:

  • 基地址(假设是平坦模式的0x00000000)
  • 段界限(确保偏移量没超)
  • 检查类型
  • 检查特权级别(Ring 0/3)

第四步:计算地址——线性地址=段基地址(0)+偏移量(0x1234)

线性地址/虚拟地址经过页表映射找到物理地址。

线性/虚拟地址————物理地址:

struct task_struct//进程/线程描述
{
	struct mm_struct *mm;//用户进程-》用户空间,内核线程-》null
	struct mm_struct *active_mm;//内核线程可以使用任意进程的地址空间
};
struct mm_struct
{
    //页表
    pgd_t *pgd;//指向第一级页表,MMU进行物理地址转换时需要加载到CR3/TTBRx寄存器的物理地址
    
	struct vm_area_struct *mmap;//指向虚拟区间链表
	struct rb_root mm_rb;//红黑树,快速查找vm_area
	
	//代码段,数据段,堆栈段,共享区等起始和结束地址
	unsigned long start_code,end_code,start_data,end_data;//代码段数据段
	unsigned long start_brk,brk,start_stack;//栈和堆
	unsigned long arg_start,arg_end,env_start,env_end;//...
};
//除了程序主体和文件映射外,还用来分配内存,线程的私有栈,共享内存等都要分配vm_area_struct
struct vm_area_struct
{
    unsigned long vm_start;//起始
    unsigned long vm_end;//结束
    
    struct file* vm_file;//映射的文件————只有一个?没错,这就是为什么要红黑树。
};

MMU是集成在CPU中的硬件,它根据寄存器中的页表的地址,拿着虚拟地址去查询页表(先查询快表再查询多级页表),如果页表项中(不一定是叶子页表,也可能是上级、中间…)的物理地址为空(存在位p=0),就触发缺页中断,执行缺页中断处理函数。如果物理地址不为空(存在位p=1),判断权限位(读/写/执行)和当前CPU特权级(用户态/内核态)是否匹配本次访问请求。

  • 权限不足(比如试图写入只读内存),MMU触发保护性缺页中断(访问异常),交给操作系统处理。
  • 权限通过,取出物理页框号,与虚拟地址中的偏移量拼接,计算出完整的物理地址。

随后将该虚拟——物理映射关系缓存在快表中,更新页表项中的“访问位”(辅助操作系统进行页面置换),如果写入更新“脏位”(辅助操作系统判断该位置和磁盘是否一致,决定换出时是直接丢弃还是先刷新到磁盘)。最后CPU拿到物理地址进行操作。

image-20260715190535509

CPU使用率和Load平均值

image-20260715193351988

CPU使用率和Load平均值是衡量系统负载的两个指标,描述不同的维度。

CPU使用率:

单位时间内,CPU处于非空闲时间的百分比。反映CPU繁忙程度

Load平均值:

特定时间段(一整块时间)内,处于R(可运行状态)和D(不可中断睡眠,一般是磁盘IO等待,锁资源)状态平均进程数量

高CPU使用率意味着CPU在全力运行,高Load可能是CPU繁忙,也可能是大量进程等待IO。

Load/核心数 < 1.0,资源够用

Load/核心数 == 1.0,刚好满负荷

Load/核心数 > 1.0,过载

实际场景:

  1. CPU高(如90%),Load低(如1.0):CPU正在全力处理一个复杂计算任务,但没有额外的任务排队(比如只跑一个单线程程序)。状态:健康,算力吃满。
  2. CPU低(如10%),Load高(如10.0)异常信号! 说明CPU没怎么干活,但大量进程堵在D状态(不可中断睡眠)。这通常意味着磁盘I/O极度缓慢(如机械硬盘高负载)或内存严重不足导致频繁交换(Swap)状态:I/O瓶颈。
  3. CPU高,Load也高:说明CPU已经算不过来了,而且排队的人越来越多(比如跑了太多CPU密集型任务)。状态:CPU算力严重不足,需要扩容。
  4. CPU低,Load也低:系统完全空闲,非常健康。

补充:进程状态

ps aux/ajx

a:所有终端所有用户的进程(如果没带x,只显示前台/关联进程)

u:面向用户的形式输出,(用户名列/CPU内存占用比)

x:无控制终端,不依赖终端的守护进程

j:面向作业,输出包括(父进程PPID、进程组PGID、会话SID等)

Linux内核定义了几种进程状态:

  • R(running):不一定在运行中,也可能在运行队列中
  • S(sleeping):进程等待事件,也叫可中断睡眠————对应不可中断睡眠D
  • D(disk sleep):不可中断睡眠,等待IO结束,不被信号杀死
  • T(stopped):发送SIGSTOP停止进程,发送SIGCONT继续运行
  • X(dead):返回给父进程的状态
  • Z(zombie):僵尸进程,父进程没回收子进程,只能杀死父进程让僵尸进程变成孤儿进程然后init(PID=1)领养这个进程,Init一直wait(),等待退出,发现这个子进程状态是Z就获取退出码,将tast_struct从内核抹除掉。

CPU是怎么执行程序/任务的

1、程序————>进程
  1. 编译链接:(.c/.java/.cpp…)文件被编译器转换成机器指令,打包成ELF格式的可执行文件,存放在硬盘。
  2. 加载:./a.outshellfork()子进程然后execve()程序替换,将这个文件的指令和数据加载到虚拟地址空间中。(内核层面是创建新的task_struct和mm_struct,vm_area_struct等一系列的数据结构)
  3. 建立映射:MMU配合操作系统,建立页表映射
  4. 入口跳转:操作系统将CPU的程序计数器(PC寄存器)指向该程序入口地址(_strat),交出控制权给这个进程
2、硬件循环
  1. 取指令:CPU根据程序计数器(PC)的地址,从缓存或者主存中取指令
  2. 译码:控制单元(CU)将二进制指令拆成“操作码”和“操作数”
  3. 执行:算术逻辑单元(ALU)执行计算或者判断
  4. 访存:指令需要读写内存(mov)指令,MMU翻译物理地址,CPU访问缓存或者主存读写数据
  5. 写回:将ALU计算结果或者访存读取的数据写回寄存器组中
3、多进程调度

时钟中断到来时,执行对应的中断处理函数,操作系统把当前进程时间片-1,如果为0就设置调度标志位为1,返回用户态前检查标志位是否为1,为1就调用进程调度的处理函数,不为1就不发生切换。除了这种时间片耗尽的情况,还有阻塞时主动调用休眠,以及硬件中断唤醒高优先级进程会抢占CPU。

发生进程切换会保存上下文信息(PC程序计数器,栈顶栈底指针,通用寄存器组)到task_struct中,TLB失效,内核空间的数据任在物理内存中,用户空间数据缓存会被最近最常使用算法慢慢替换。

负数如何表示

正数的补码就是原码,负数的补码是正数的按位取反+1。正数50000 0101负数-51111 1011

为什么这么设计?

方便计算减法:

0000 0101 + 1111 1011 = 1 0000 0000丢弃溢出位

如何识别补码的值?

先看符号位,正数就直接用,负数就取反+1得到绝对值再带上-号。

为什么能表示的负数比正数多?

因为0被归类为正数,比如八位有符号整数表示范围-128~1271000 0000表示-128,它不能由正数取反+1得到,因为正数表示不了128——1000 0000

访问磁盘为什么比访问内存慢好几个数量级

寄存器——L1缓存——内存——固态硬盘——机械硬盘

<1ns 1ns 100ns 10us 10ms

固态硬盘访问时间是内存的100倍

1、物理运动和电子流动(内存VS机械硬盘)
  • 内存靠电子流动
  • 机械硬盘要磁头读取,要定位柱面,磁道,扇区,天然慢。
2、电荷和浮栅(内存VS固态硬盘)
  • 内存使用电容充放电,可以按字节读写
  • 固态硬盘使用浮栅晶体管,写入要按页写,擦除按块擦,写之前如果块里有旧数据还要先整块擦除(ms级别)
3、总线协议和软件栈开销
  • CPU可以直接通过内存控制器,通过总线访问内存,几乎不经过操作系统干预
  • 硬盘数据要通过中断和DMA(帮助CPU搬运磁盘数据进内存)通知CPU,文件读写翻译成扇区读写还要操作系统经过文件系统(EXT2/3/4)和块设备驱动的封装翻译。
4、寻址方式不同
  • 内存是按字节寻址
  • 磁盘是块寻址,哪怕只想读一个字节,也要加载一整个块进内存(实际上为了解耦合,每次都读取一页数据进内存),这种数据传输带来了更大的时间开销

键盘敲下一个键,操作系统发生了什么

硬件层:
  1. 产生扫描码
  2. 发送中断信号给中断控制器,通知CPU获取中断号
操作系统:
  1. 上下文切换:CPU收到中断请求,保护当前执行的上下文(PC,栈指针寄存器,通用寄存器组等),然后获取中断号,去中断向量表中查找对应的中断处理方法
  2. 执行中断处理:CPU执行操作系统预先注册好的中断处理方法。
具体操作:
  1. 读取数据:键盘中断处理程序,从键盘控制器的寄存器中读取扫描码
  2. 数据转换:键盘驱动将扫描码转换为操作系统使用的虚拟键码
  3. 存入缓冲区:转换好的虚拟键码(还有按下或者释放动作)存入一个环形缓冲区,等待上层应用使用
应用层面:
  1. 应用监听键盘事件:操作系统将按键事件打包好给上层应用
  2. 应用处理:程序收到事件,从内核读取数据,调用对应的程序(比如图形接口)处理绘制
  3. 显示:图形系统将像素数据写入显卡缓冲区,显卡持续扫描这个缓冲区,将最终结果显示在屏幕上

并行和并发

并发是逻辑上的同时,并行是物理上的同时。

并发:通过进程调度实现的多任务交替推进

操作系统在单个CPU核心上,通过时钟中断,不断减少一个进程的时间片,直到时间片耗完,标志位设为1,返回用户态前检查标志位,如果为1就触发软中断,执行进程调度中断处理。或者硬件触发,比如键盘触发,比如之前等待在IO事件的休眠进程被唤醒,抢占CPU。

并行:多核CPU,同一时刻执行不同的任务

系统负责并发调度,多核硬件负责并行执行。

进程调度:每调度一个活动队列的新进程,旧进程就放入过期队列中,直到一方空了,就交换这两个的值,每个CPU都应该有一个运行队列

image-20260716030101300

并发:通过进程调度实现的多任务交替推进

操作系统在单个CPU核心上,通过时钟中断,不断减少一个进程的时间片,直到时间片耗完,标志位设为1,返回用户态前检查标志位,如果为1就触发软中断,执行进程调度中断处理。或者硬件触发,比如键盘触发,比如之前等待在IO事件的休眠进程被唤醒,抢占CPU。

并行:多核CPU,同一时刻执行不同的任务

系统负责并发调度,多核硬件负责并行执行。

进程调度:每调度一个活动队列的新进程,旧进程就放入过期队列中,直到一方空了,就交换这两个的值,每个CPU都应该有一个运行队列

[外链图片转存中…(img-iV9pXMDe-1784142161399)]

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐