揭秘操作系统中的系统调用核心机制

发表时间: 2024-07-19 16:34

操作系统定义

操作系统(Operating System,简称OS)是计算机系统中最关键的系统软件,它管理和控制计算机硬件和软件资源,为用户和其他软件提供服务。以下是操作系统的一些关键特性和功能:

  • 资源管理:操作系统负责管理计算机的硬件资源,如CPU、内存、存储设备和输入/输出设备。
  • 进程管理:它允许多个进程同时运行,通过进程调度和同步机制来协调这些进程的活动。
  • 内存管理:操作系统负责分配和管理内存资源,包括虚拟内存和物理内存,确保应用程序有足够的内存空间运行。
  • 文件系统管理:操作系统提供了文件和目录的管理机制,允许用户存储、检索和组织数据。
  • 用户界面:操作系统提供了用户与计算机交互的界面,包括图形用户界面(GUI)和命令行界面(CLI)。
  • 设备驱动程序管理:操作系统加载和管理设备驱动程序,这些程序允许计算机与各种硬件设备通信。
  • 网络通信:操作系统支持网络协议,允许计算机与其他设备进行通信和数据交换。
  • 安全性:操作系统提供了用户认证、权限控制和数据保护等安全特性,以保护系统和用户数据的安全。

操作系统本质上是一个运行在计算机上的软件程序 ,主要用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。

应用程序、内核、CPU这三者的关系(操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件; CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作)

应用程序、内核、CPU 三者的关系

系统调用

系统调用是一种特殊的函数调用,用于让用户空间的应用程序请求内核执行某些特权操作,系统调用允许用户程序访问底层硬件资源,如文件系统、网络设备、磁盘等。通过系统调用,用户程序可以执行读写文件、创建进程、打开网络连接等操作。系统调用是用户程序与操作系统内核之间的桥梁,它们允许用户程序访问底层硬件和操作系统提供的服务。

操作系统区分用户态(User Mode)和内核态(Kernel Mode)的主要原因是为了保护系统的稳定性、安全性和效率。操作系统中的用户态(User Mode)和内核态(Kernel Mode)是两种不同的执行级别或模式,它们定义了程序对系统资源的访问权限和能力。以下是用户态和内核态的基本概念。

用户态(User Mode)

  • 用户态是操作系统为运行普通应用程序提供的执行环境。
  • 在用户态下运行的程序只能访问有限的系统资源和执行受限的操作。
  • 用户态程序不能直接访问硬件设备,也不能执行特定的系统级操作,如内存管理、进程调度等。
  • 大多数应用程序在用户态下运行,以确保系统的稳定性和安全性

内核态(Kernel Mode)

  • 内核态是操作系统的核心部分,具有对所有硬件和软件资源的完全访问权限。
  • 在内核态下运行的代码可以直接与硬件交互,执行系统级的操作,如分配内存、管理进程、处理中断等。
  • 操作系统的内核和设备驱动程序通常在内核态下运行。

系统调用的原理包括以下关键方面

1. 用户模式与内核模式

操作系统内核运行在特权模式下,而用户程序通常运行在非特权模式下。为了执行特权操作,用户程序必须通过系统调用进入内核模式。这是通过软中断(软件中断)或硬件中断来实现的。

2. 中断和上下文切换

当用户程序需要执行系统调用时,它会触发一个中断将控制权从用户模式切换到内核模式。这个过程涉及到上下文切换,内核会保存用户程序的状态,并加载内核的状态。一旦系统调用完成,内核将控制权返回给用户程序,再次进行上下文切换。

3. 系统调用表

内核维护了一个系统调用表,其中包含了所有可用的系统调用及其函数指针。当用户程序请求执行特定的系统调用时,内核会查找相应的函数指针并执行对应的内核函数。

4. 参数传递

用户程序通常需要向内核传递参数,以便内核知道用户程序需要执行的具体操作。这些参数通常通过寄存器或栈来传递,具体取决于体系结构和操作系统的设计。系统调用中的参数传递是非常关键的,因为它决定了用户程序与内核之间的通信方式。不同的系统调用可能采用不同的参数传递方式,但一般情况下,参数可以通过寄存器、栈或特定的数据结构进行传递。

5. 中断,异常和系统调用不同点

  • 相同:都是用IDT表(中断向量表)描述的。
  • 不同:源头不同。产生中断或者异常或者系统调用的来源不同。服务响应方式不同。产生后如何响应中断或者异常或者系统调用的方式不同。处理机制不同。响应后如何处理中断或者异常或者系统调用的方式不同


系统调用的详细过程

用户运行库函数(系统调用的封装),函数里面其实是执行的int 0x80指令。系统调用先把系统调用号保存在eax寄存器中,然后执行int0x80指令。

int 0x80指令先进行切换堆栈(找到进程的堆栈,将寄存器值压入到内核栈中,将esp,ss设置成对应内核栈的值),查找相应中断向量的中断处理程序(system_call)并调用,随后system_call 从系统调用表中找到相应的系统调用进行调用,调用结束后从system_call中返回。

系统调用过程

1、触发中断

用户程序在代码中调用系统调用,执行int指令前将系统调用号放入eax寄存器中,执行int 0x80指令(int 指令最后执行的函数是system_call,该函数验证系统调用号的有效性,查找系统调用函数并执行,最后通过itret从中断处理程序返回)

2、切换堆栈(此步在int指令中完成)

在实际执行0x80号中断向量所对应的中断处理程序(system_call)之前,CPU首先要进行堆栈切换,即从用户态切换到内核态。从中断处理函数中返回时,程序当前栈还要从内核栈切换回用户栈。

所谓的当前栈,值得是esp(栈指针)的值所在的栈空间。如果esp的值位于用户栈的范围内,那么程序的当前栈就是用户栈,反之亦然。此外,寄存器ss的值还应该指向当前栈所在的页。

所以,将当前栈由用户栈切换为内核栈的实际行为就是:

(1) 保存当前的esp,ss的值(保证存在内核栈上,有int指令自动地由硬件完成)

(2) 将esp,ss的值设置为内核栈的相应值

当0x80号中断发生的时候,cpu除了切入内核态之外,还会自动完成下列几件事:

(1)找到当前进程的内核栈(每一个进程都有自己的内核栈)

(2)在内核栈中一次压入用户态的寄存器ss、esp、eflags、cs、eip

而当内核从系统调用返回的时候,须要调用iret指令来回到用户态,iret指令则会从内核栈里弹出寄存器ss、esp、eflags、cs、eip的值,使得栈恢复到用户态的状态。

3、中断处理程序

在int指令切换内核栈之后,程序就切换到了中断向量表中的0x80号中断处理程序。Linux中0x80向量对应的中断处理程序是system_call。

system_call中断服务程序首先检查系统调用号的有效性,再根据eax寄存器存储的系统调用号从系统调用表上找到相应的系统调用并调用。

进程与线程

什么是进程和线程?

  • 进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
  • 线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。

进程和线程的区别是什么?

下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧!

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈本地方法栈

总结:

  • 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
  • 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
  • 线程执行开销小,但不利于资源的管理和保护;而进程正相反。

有了进程为什么还需要线程?

  • 进程切换是一个开销很大的操作,线程切换的成本较低。
  • 线程更轻量,一个进程可以创建多个线程。
  • 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
  • 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。

为什么要使用多线程?

先从总体上来说:

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

再深入到计算机底层来探讨:

  • 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
  • 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

线程间的同步的方式有哪些?

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。

下面是几种常见的线程同步的方式:

  1. 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
  3. 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  4. 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。
  5. 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

PCB 是什么?包含哪些信息?

PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。

当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。

PCB 主要包含下面几部分的内容:

  • 进程的描述信息,包括进程的名称、标识符等等;
  • 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
  • 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
  • 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
  • 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
  • ……

进程有哪几种状态?

我们一般把进程大致分为 5 种状态,这一点和线程很像!

  • 创建状态(new):进程正在被创建,尚未到就绪状态。
  • 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
  • 运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
  • 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
  • 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行


资料参考

  • 计算机操作系统—汤小丹》第四版
  • 《深入理解计算机系统》
  • 《重学操作系统》
  • 操作系统为什么要分用户态和内核态:https://blog.csdn.net/chen134225/article/details/81783980
  • 从根上理解用户态与内核态:https://juejin.cn/post/6923863670132850701
  • 什么是僵尸进程与孤儿进程:https://blog.csdn.net/a745233700/article/details/120715371