内核态和用户态是操作系统中的两种运行模式,其主要区别在于权限和可以执行的操作
- 内核态(Kernel Mode):在内核态下,CPU可以访问所有的指令和所有的硬件资源
- 用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。
内核态的底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等等
分为内核态和用户态,是出于安全性、稳定性、隔离性而考虑的
- 本质区别:进程是操作系统资源分配的最小单位,而线程是任务调度和执行的最小单位
一个进程通常包括如下几个内存区域:
- 代码段:存储进程执行的机器指令
- 数据段:存储全局变量和静态变量
- 堆空间:用于动态内存分配(例如 C/C++ 中的 malloc/new)
- 栈空间:用于存储局部变量、函数参数和函数调用返回地址
共享部分:
- 代码段、数据段和堆空间是同一个进程内所有线程共享的
- 这意味着一个线程可以访问和修改另一个线程创建的全局变量或者在堆上分配的内存。这也是在多线程编程中,要使用同步机制(比如互斥锁/信号量)去防止数据竞争的原因
非共享/私有部分:
- 栈空间:每个线程都有自己独立且私有的栈空间(也叫做调用栈)
- 这是为了保证每个线程的函数调用、局部变量和返回地址不会相互干扰
协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。
协程拥有自己的寄存器上下文和栈,但是和其他协程共享堆内存。协程的切换开销非常小,因此只需要保存和回复协程的上下文,而不需要进行内核级的上下文切换。
由此,协程在大量处理并发任务时,具有非常高的效率。
- 进程隔离性:每个进程都有自己独立的内存空间,一个进程崩溃后,其内存空间会被OS回收,不会影响其他进程的内存空间。
- 进程独立性:每个进程都是独立运行的,其之间不会共享资源,比如说文件、网络连接等。
各个进程之间是共享CPU资源的,在不同的时候,进程之间需要进行切换。让不同的进程可以在CPU上执行,这个一个进程切换到另一个进程运行,称为进程的上下文切换
操作系统需要先帮CPU设置好CPU寄存器和程序计数器
CPU寄存器和程序计数器是CPU在运行任何任务之前,必须依赖的环境。这些环境,就叫做CPU上下文
CPU上下文切换就是将前一个任务的CPU上下文(CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器。最后跳转到程序计数器所指向的新的位置,运行新的任务。
上面提到的任务,主要包含进程、线程和中断。
由此,可以根据任务的不同,将CPU上下文分为:
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
进程的上下文切换,不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源
Linux内核提供了不少进程间通信的方式:
- 管道
- 消息队列
- 共享内存
- 信号
- 信号量
- socket
Linux内核对于进程间通信,最简单的方式是管道
匿名管道:通信的数据是无格式的流并且大小受限,通信方式单向。如果要双向通信,需要创建两个管道。同时,匿名管道式只能用于存在父子关系的进程间通信。
命名管道:其可以在毫无关系的进程间通信,通过一个类型为p的设备文件。同时,通信数据遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的消息链表。同时,消息队列的速度不是及时的,因为每次数据的写入和读取都需要进过用户态和内核态之间的拷贝过程
共享内存可以解决消息队列通信中用户态和内核态之间数据拷贝带来的开销,其直接分配一个共享空间,每个进程都可以访问
共享内存有最快的进程间通信方式之名,但是带来了新的问题,多线程竞争同个共享资源会造成数据的错乱。
通过信号量来保护共享资源,确保共同时刻,只有一个进程能访问共享资源,其就是互斥访问
信号量不仅可以实现访问的互斥性,还可以实现进程间的同步。信号量实际上是计数器,表示的是资源个数。可以通过 P 操作和 V 操作这两个原子操作解决。
注意:信号和信号量不是同一个东西!!!
信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件。
一般来讲,信号事件的来源主要有硬件来源和软件来源,一旦有信号发生,进程有三种方式响应信号:
- 执行默认操作
- 捕捉信号
- 忽略信号
之前上面所说的通信机制,都是工作于同一台主机。如果说要与不同主机之间的进程通信,那么就需要 Socket 通信。
Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信。
可以根据 Socket 类型的不同,分为三种常见的通信方式:
- 基于 TCP 协议的通信方式
- 基于 UDP 协议的通信方式
- 本地进程间通信方式
共享内存的机制:拿出一块虚拟地址空间来,然后映射到相同的物理内存中去
- 互斥锁
- 读写锁
- 条件变量
- 自旋锁
- 信号量
注意此处是针对线程间
- 先来先服务调度算法
- 最短作业优先调度算法
- 高响应比优先调度算法
响应比优先级 =
$\frac{等待时间 + 要求服务时间}{要求服务时间}$
- 时间片轮转调度算法
这是最古老、最公平、最简单并且使用最广的算法 每个进程会被分配一个时间段,称为时间片,允许该进程在该时间段中运行
时间片的长度是关键之处
- 时间片设的果断,会导致过多的进程上下文切换
- 时间片设置的过长,会导致对短作业进程的响应时间边长
-
最高优先级调度算法
-
多级反馈队列调度算法
- 多级:表示有多个队列,每个队列优先级从高到低,同时优先级越高,时间片越短
- 反馈:如果有新的进程加入到优先级高的队列时,立即停止当前正在运行的进程,转而去运行优先级更高的队列
操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存
虚拟内存带来的优势:
- 虚拟内存可以使得进程对运行内存超过物理内存的大小
- 每个进程都有自己的页表,所以每个进程的虚拟内存是相互独立的
- 页表里的页表项除了物理地址外,还有一些标记属性的bit。在内存访问方面,操作系统提供了更好的安全性
Linux系统通过内存分页的方式去管理内存,分页是将整个虚拟和物理内存空间去切成一段段固定内存的大小;这样一个连续而且尺寸固定的内存空间,叫做页(page)
而虚拟地址和物理地址之间,通过页表来进行映射。页表是存储在内存 中的,内存管理单元(MMU)执行将虚拟内存地址转换为物理地址的工作
如果说,进程访问的虚拟地址在页表中查不到的时候,系统会产生一个缺页异常
用户空间内存从低到高分为六段
- 代码段
- 数据段
- BSS段
- 堆段
- 文件映射段
- 栈段
在上面七个内存段中,堆和文件映射段的内存是动态分配的。比如说去使用C标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段去动态分配内存
- 分配方式:堆是动态分配内存,由程序员手动申请和释放内存;而栈是静态分配内存
- 内存管理:堆需要程序员手动管理内存的分配和释放;栈由编译器去自动管理内存,变量的生命周期由其作用域而决定,函数调用时分配内存,函数返回时释放内存
- 大小和速度:堆通常比栈大,内存空间较大,动态分配和释放内存需要时间开销
操作系统中所谓的fork(),指的是让一个正在运行的进程(父进程)创建一个自身的精确副本(子进程)
子进程拥有父进程的精确副本,在现代操作系统中,通过写时复制实现
所谓的写时复制:父进程和子进程最初共享相同的物理内存页面。只有当其中任何一个进程试图写入该页面时,内核才会真正复制一份该页面,确保进程间的内存隔离
fork()调用后,会有一个很特殊的行为:其只调用一次,但是返回两次
- 子进程会接收到 0,因为它无需知道父进程的 PID
- 父进程会接收到新创建的子进程的进程标识符(PID)
- 如果创建失败,那么返回 -1






