浅入分析Linux

Linux

操作系统必须完成的两个主要目的

  • 与硬件部分交互, 为包含在硬件平台上的所有底层可编程部件提供服务
  • 为运行在计算机系统上的应用程序(即所谓的用户空间)提供执行环境

一些操作系统运行所有的用户程序都直接与硬件部分进行交互, 比如典型的MS-DOS。而类Unix操作系统把与计算机物理组织相关的所有底层细节都对用户运行的程序隐藏起来。当程序想要使用硬件资源的时候, 必须向操作系统发出一个请求, 内核对这个请求进行评估, 如果允许使用这个资源, 那么内核代表应用程序与这个硬件进行交互。

为了实现这个机制, 现代的操作系统依靠特殊的硬件特性来禁止用户程序直接与底层的硬件部分进行交互, 或者禁止直接访问任意的物理地址, 为此, 硬件为CPU引入了至少了两种执行模式: 用户态和内核态。

进程

  • 进程是"程序执行的一个实例, 或者是一个程序运行的上下文"
  • 一个进程在自己的地址空间中执行指令, 地址空间是允许进程引用的内存地址集合, 在相同的地址空间可以执行读个指令序列
进程与程序的微妙关系
  • 几个进程可以并发执行一个程序
  • 一个进程可以按照顺序执行多个程序

抢占式与非抢占式

  • 非抢占式: 只有进程资源放弃CPU时, 调度程序才会被调用去执行下一个进程
  • 抢占式: 进程对CPU等资源的控制是处于争抢状态的, 当一个进程占用了过多的CPU时间时, 调度程序就会被执行, 将CPU资源让给下一个优先级高的进程
  • 在类Unix操作系统中, 每一个进程都以为他是系统中唯一的进程, 可以独占操作系统所提供的服务。只要进程发出系统调用, 硬件就会把特权模式有用户态转换为内核态, 然后进程以非常有限的目的开始一个内核过程的执行。一旦这个请求完全得到满足, 内核过程将迫使硬件返回到用户态, 然后进程从系统调用的下一条指令继续执行

单内核与微内核

  • 微内核操作系统只需要内核有一个很小的函数集, 运行在内核之上的几个系统进程实现从前操作系统级实现的功能, 如内存分配程序, 设备驱动程序, 系统调用处理程序等等
  • 单内核则与之相反, 为了达到微内核的优点, Linux内核提供了模块, 模块是一个.o文件, 其代码可以在运行时链接到内核或者从内核中解除链接。这些.o文件通常由一组函数组成, 用于实现文件系统, 驱动程序或者其他内核上层功能

suid,sgid, sticky对可执行文件的影响

  • suid: 一个通过属主运行的进程, 其执行时的属主为二进制程序的原始属主
  • sgid: 一个通过属组运行的进程, 其执行时的属组为二进制程序的原始属组
  • sticky: 当程序执行完毕之后, 依然将其保存在内存中, ==但是这个方式已经过时了==

文件操作的系统调用

  • 当用户访问一个文件时, 实际上是访问存储在硬件块设备上的一些数据, 从某种程度上来讲, 文件系统就是对存储在硬件块设备上的数据的用户级视图, 因为与硬件交互是不被允许的, 所有文件系统必须运行在内核态

Unix内核概述

  • 内核本身不是进程, 而进程的管理者
  • 应用程序如何激活内核? ① 系统调用 ② 中断程序

    内核管理进程

  • 每一个进程都有一个进程描述符(存储在进程描述符队列中, FIFO(先进先出, 这样可以保证CPU在执行完了其他进程之后, 可以恢复执行之前暂停的进程)), 内核通过该进程描述符来管理进程, 其记录了有关进程当前状态的信息
  • 当内核暂停一个进程的执行时, 就会将几个相关处理寄存器的内容保存到进程描述符中, 当内核恢复执行一个进程时, 就将进程描述符中合适的字段装载到CPU寄存器中m 程序计数器中所存的值指向下一条要执行的指令, 所以进程才可以从它停止的地方恢复执行, 这就是恢复现场

一个进程对硬件块设备进行写操作时

  • 与硬件进行交互, 硬件将用户态转为内核态, 此时该进程还是占用着CPU时间的, 当要将数据写入到硬盘中时, CPU通过中断通知磁盘处理器, 由它完成写操作, 这是CPU空闲下来, 继续执行下一个进程, 当磁盘处理器完成的写操作后, 会通过中断通知内核继续执行上一个进程, 通过恢复现场(从进程描述符队列中取出队首的元素, 即为要恢复进程的进程描述符, 将该进程描述符中相应的字段存放到相应的寄存器中)实现。

多用户内核的进程同步

  • 如果内核控制路径对某一个内核数据结构进行操作时被挂起, 那个其他的内核控制路径就不应该在该数据结构进行操作, 防止类似"并发抢彩票"现象
  • 类Unix内核通过信号量这个对象实现同步, 类似于进程锁, 当一个进程访问一个数据结构时, 会获得锁, 这是如果该进程被挂起, 另一个进程也需要访问该数据结构, 因为每一个获得锁, 这是该进程就会被阻塞并放置到一个等待链表中。
  • 对于多处理器的Unix内核可以采用自旋锁的方式, 当一个进程访问一个数据结构时被挂起, 会获得锁, 这时另一个进程也需要访问该数据结构, 则该进程会执行一个紧凑的循环指令, 知道获得锁。注意: 自旋锁的实现要在多处理器上, 我们看到, 如果单处理器的话, 一个进程被挂起, 另一个进程在自旋, CPU执行执行其中一个进程, 如果执行自旋的进程, 那个挂起的进程就不会被执行, 锁就不会被释放, 这样久而久之操作系统就会被挂起, 因此自旋锁的实现是在多处理器的基础上的

进程中通信

  • 信号量, 该信号量与进程同步中的信号量相似, 这是这个是运行在用户态的
  • 共享内存
  • 套接字

进程组

  • 每一个进程都在一个进程组中, 进程组的GID就是leader进程的PID
  • 新创建的子进程插入到父进程的进程组中, 对于子进程, 父进程就是一个leader进程

虚拟内存

  • RAM(随机访问存储器)的主要作用的是存储内核代码, 内核中静态的数据结构。而剩余的部分有虚拟内存管理

内存管理

内存地址

  • 逻辑地址: 每一个逻辑地址都有一个段选择符和偏移量组成
  • 线性地址: 在逻辑地址的基础上形成, 在Linux中逻辑地址就是线性地址
  • 物理地址: 实际存在在内存条上的

  • 段选择符存储6个段寄存器上
  • 组成: 索引号, 请求者级别(0|3)
  • 段描述符描述了段的特征, 存储在GDT或者LDT中, 而GDT的内存地址值和大小存放在gdtr寄存器中, 当前正在被使用的LDT的存放在ldtr寄存器
  • 逻辑地址与物理地址指向的转换就是通过段选择符与段描述符完成的, 当需要访问一个线性地址上的数据时, 通过段选择符中的索引号index, 计算出其对应的段描述符在GDT或者LDT中的位置, 而GDT和LDT的位置通过gdtr或者ldtr寄存器获取, 接着通过段描述符中相应字段的信息, 进行计算, 算出逻辑地址所对应的线性地址的位置
  • 线性地址被分成了固定长度为单位的组, 称之为"页", 页内部连续的空间对应于物理内存中的连续空间, 线性地址与物理地址执行的转换基于页表(在系统初始化时启动分页机制时生成)

CPU实模式下是没有分页机制的

因为Linux的内存管理过于复杂, 所以现在暂时不深入分析

Linux中的进程管理

  • Linux中的进程都是轻量级的进程, 每一进程都是在一个进程组中的, 进程组的GPID就是这个进程的Leader的PID, 除了Leader进程, 组中的其他进程应该是该Leader进程的子进程
  • 内核对进程的管理是通过进程描述符管理的, 进程描述符就是一个task_struct结构体, 里面的字段包含了优先级, pid, ppid, gpid, thread_info(存储进程的基本信息, 目的是防止单个的task_struct过大)等
  • 进程的状态:

可执行状态: 进程在CPU中执行, 或者可以被CPU执行

可中断状态: 进程被挂起, CPU在执行其他进程的时, 那个进程发出一个中断信号, 要执行刚才那个挂起的进程, 刚刚进程被唤醒, 内核从指定优先级的队列中取出进程的进程描述符, 将里面的信息放置到对应的寄存器中, 进行现场的恢复, 接着执行该进程

不可中断状态: 当进程(A)需要打开一个文件的时候, 内核通过中断调用磁盘处理器读取数据, 此时进程被挂起, CPU执行下一个优先级高的进程, 如果该进程发出一个硬件中断请求唤醒处于不可中断的进程, 该中断信号是不会相应的, 如果相应了, A进程就是在没有需要的数据的情况下执行, 会产生意想不到的后果

还有其他的状态, 这里就先按下不表

  • 在Linux中, 通过140个队列(该队列是有双向链表构建的)来管理进程, 140个队列分别表示0-139之间的140个优先级, 一个进程被挂起, 内核根据该进程的优先级append到对一个优先级的队列中, 内核接下来要执行下一个进程的指令, 只需要从高到低遍历这些队列即可, 不需要遍历全部的队列, 只要在一个队列中找到一个task_struct即可, 这个进程一定是当前所有优先级最高的, 并且与他同优先级的进程中等待的时间最长的
  • 普通进程既可以在用户态运行, 又可以在内核态运行, 但是有一些特殊的进程只能在内核态运行, 专门为内核服务的
  • 进程的切换是在内核态进行的
  • 一个进程死亡时必须通知其父进程, 有父进程通知内核, 内核才会去回收该进程的资源, 如果父进程在子进程之前死亡, 那么子进程的task_struct资源就永远不会被释放, 成为了僵死进程
用户空间的应用程序,通过系统调用,进入内核空间。由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和
内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程
的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。

中断和异常

  • 中断事件改编处理器执行指令的顺序, 这样的事件与CPU芯片内外部硬件电路产生的电信号相对应
  • 同步中断与异步中断:

同步中断: 之后CPU执行完一个指令之后, 才会发出中断

异步中断: 随机中断

  • 中断: 中断是由间隔定时器和I/O设备产生的, 例如用户的一个按键就会发生一个中断

中断描述符

  • 每一个中断在表中都有对应的中断或者异常处理程序的入口地址, 这样当中断来时, 就可以知道应该在哪里有处理这个中断的执行
  • 内核在允许中断发生前, 必须初始化中断描述符(操作系统没有加载时就已将完成了初始化, 并有BIOS使用)
  • 中断发生在内核态, 中断可以嵌套, 改变内核的控制路径
  • 中断处理程序在运行时不能发生进程切换
  • 中断描述符的类型

任务门

中断门

陷阱门

在Linux中处理中断使用中断门, 处理异常使用陷阱门

  • 内核态的唯一一个能够引发异常操作就是缺页, 通过陷阱门来出来该异常, 而用户态则可能有很多的异常, 但是不会有内核态下的缺页异常
上一篇:MakeFile基本使用


下一篇:机器学习中的常用操作