操作系统笔记(持续更新中)

综述

1.知识体系

操作系统就像一家外包公司:

为了实现系统的运作,其实是由几个子系统支撑完成的:

2. 系统调用

2.1 立项与进程管理

  • 父进程进行fork操作,得到子进程,先从父进程拷贝数据结构,再修该
  • 子进程调用execve执行另一个程序
    -有个系统调用叫 waitpid,父进程能知道子进程是否运行完毕

2.2 会议室与内存管理

  • 进程空间内,存放代码的部分叫做代码段(Code Segment)
  • 进程内,存放数据的部分叫做数据段(Data Segment
    - 局部变量在当前函数有效,离开函数则释放
    - 动态分类的,指明才销毁的,称为堆
  • 进程不用的部分就不管,进程需要使用内存的时候,会调用内存管理系统,但是也不代表对应到了真正的物理内存,只有要写入且发现没有物理内存,才会触发一个中断,现分配物理内存
  • 两个系统调用
    - brk:内存数据量小时,和原来的数据连在一起
    - mmap: 数据量大时重新划分一个区域

档案库管理与文件管理

最重要的6个文件操作:

  • 对已有文件的打开关闭
  • 创建
  • 打开文件以后使用lseek跳到文件某个位置
  • readwrite

每个文件,Linux都会分配一个文件描述符(File Descriptor),这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或者干预进程运行的方方面面。

2.3 项目异常与信号处理

每种信号都定义了默认动作,也可提供处理函数,可以通过sigaction系统调用,注册一个信号处理函数。提供了信号处理服务,进程执行中一旦有变动,就可以及时处理了。

项目组间沟通与进程间通信

  • 消息队列
    - msgsnd发送消息到消息队列
    - msgget创建一个消息队列
    - msgrcv从队列获取消息
  • 共享内存
    - shmget创建共享内存块
    - shmat将共享内存映射到自己的内存空间
  • 如何解决同时访问数据的问——举例:Semaphore信号量

2.4公司间沟通与网络通信

网络服务通过套接字Socket完成

2.5 中介与Glibc

系统调用不是直接使用的,而是使用Glibc。它是Linux下使用的标准C库,为程序员提供丰富的API,封装了操作系统的系统服务。

2.6 小结

3.x86架构

  • CPU 包括: 运算单元, 数据单元, 控制单元
    • 运算单元 不知道算哪些数据, 结果放哪
    • 数据单元 包括 CPU 内部缓存和寄存器, 暂时存放数据和结果
    • 控制单元 获取下一条指令, 指导运算单元取数据, 计算, 存放结果
  • 进程包含代码段, 数据段等, 以下为 CPU 执行过程:
    • 控制单元 通过指令指针寄存器(IP), 取下一条指令, 放入指令寄存器中
      • 指令包括操作和目标数据
    • 数据单元 根据控制单元的指令, 从数据段读数据到数据寄存器中
    • 运算单元 开始计算, 结果暂时存放到数据寄存器
  • 两个寄存器, 存当前进程代码段和数据段起始地址, 在进程间切换
  • 总线包含两类数据: 地址总线和数据总线

  • x86 开放, 统一, 兼容
  • 数据单元 包含 8个 16位通用寄存器, 可分为 2个 8位使用
  • 控制单元 包含 IP(指令指针寄存器) 以及 4个段寄存器 CS DS SS ES
    • IP 存放指令偏移量
    • 数据偏移量存放在通用寄存器中
    • 段地址<<4 + 偏移量 得到地址

  • 32 位处理器
  • 通用寄存器 从 8个 16位拓展为 8个 32位, 保留 16位和 8位使用方式
  • IP 从 16位扩展为 32位, 保持兼容
  • 段寄存器仍为 16位, 由段描述符(表格, 缓存到 CPU 中)存储段的起始地址, 由段寄存器选择其中一项
    • 保证段地址灵活性与兼容性

  • 16位为实模式, 32位为保护模式
  • 刚开机为实模式, 需要更多内存切换到保护模式

4. 从BIOS到BootLoader

5. 内核初始化

5.1 初始化步骤

  • 进程初始化:系统启动首先启动0号进程,
  • 初始化中断门
  • 初始化内存管理
  • 创建1号进程

5.2 从用户态到内核态

用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态

6 系统调用

  • gclib对系统调用的封装

进程管理

7. 进程

7.1 进程如何从代码到运行

  • 文件编译生成so文件和可执行文件
  • 用户态的进程A执行fork,创建进程B
  • B会执行exec系列系统调用
  • 系统调用通过load_elf_binary方法,将可执行文件加载到B的内存中

8.线程

9.进程的数据结构

进程用task_struct表示
包含内容

  • ID
  • 信号处理
    pid 是 process id,tgid 是 thread group ID。
    任何一个进程,如果只有主线程,那 pid 是自己,tgid 是自己,group_leader 指向的还是自己。
    但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。
  • 任务状态
  • 进程调度
  • 运行统计信息
  • 进程亲缘关系
    是一个树形结构,保存了parent,children,sibling的指针
  • 进程权限
    - uid和gid时进程的真实id,意思是谁启动了进程
    - euid和egid,“起作用”的。当进程要操作消息队列,共享内存,信号量等对象时,比较这个id
    - fsuid和fsgid, filesystem,对文件操作会审核权限
    - 使用chmod u+x可以改变文件的set-useri-id标识位,这样文件的euid和fsuid都改成了当前用户
    • 新加入的capabilities机制用位图表示权限
  • 内存管理
  • 文件与文件系统
  • 用户态函数栈
    - 高地址到低地址,往下增长,入栈出栈都从下面的栈顶开始
  • 内核态函数栈
    - 通过一个pt_regs的struct保存寄存器状态
    - 可以通过task_struct找到内核栈和内核寄存器

10. 进程的调度

  • task_struct解决了能”看到“哪些问题,还需要解决如何”做到

10.1 调度策略与调度类

  • 实时进程

  • 普通进程

  • 调度策略
    #define SCHED_NORMAL 0
    #define SCHED_FIFO 1
    #define SCHED_RR 2
    #define SCHED_BATCH 3
    #define SCHED_IDLE 5
    #define SCHED_DEADLINE 6

  • 调度优先级
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;

  • 实时调度策略
    - FIFO
    先来先服务
    - RR
    时间片轮流服务
    - DEADLINE
    选择离当前deadline距离最近的任务

  • 普通调度策略
    - NORMAL
    和名字一样普通
    - BATCH
    后台进程
    - IDLE
    空闲时进行的进程

  • 调度策略的封装

  • 上述变量只是定义了名字,实际上的实现则是由task_struct把调度策略封装在了sched_class中

  • 完全公平调度算法
    CFS:completely fair scheduling

    • 记录下进程运行时间,称为一个tick
    • cfs会为进程安排一个虚拟运行时间vruntime
    • 随着进程运行,tick增加,vruntime也增加
    • vruntime大则运行时间多,小则少,需要给小的进程分配更多的运行时间
    • 不同进程有权重,权重高的vruntime高

10.2 调度队列与调度实体

  • cfs需要一个数据结构对vruntime进行排序——红黑树
    - 红黑树:一种自平衡查找树
    Linux的的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间
    题外话:红黑树的应用
  • cpu运行时的数据结构

    可以看到,一个 CPU 上有一个队列,CFS 的队列是一棵红黑树,树的每一个节点都是一个 sched_entity,每个 sched_entity 都属于一个 task_struct,task_struct 里面有指针指向这个进程属于哪个调度类。

内存管理

概述

内存管理要做到三件事:

    1. 虚拟内存空间的管理,每个进程看到的地址空间是独立的,互不干扰的
    1. 物理内存的管理,只有内存管理模块能够使用
    1. 内存映射,需要将虚拟内存和物理内存管理起来

虚拟内存的布局

  • 我们从最低位开始排起,先是 Text Segment、Data Segment 和 BSS Segment。Text Segment 是存放二进制可执行代码的位置,Data Segment 存放静态常量,BSS Segment 存放未初始化的静态变量。是不是觉得这几个名字很熟悉?没错,咱们前面讲 ELF 格式的时候提到过,在二进制执行文件里面,就有这三个部分。这里就是把二进制执行文件的三个部分加载到内存里面。
  • 接下来是堆(Heap)段。堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的。
    接下来的区域是 Memory Mapping Segment。这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中。
  • 再下面就是栈(Stack)地址段。主线程的函数调用的函数栈就是用这里的。
  • 如果普通进程还想进一步访问内核空间,是没办法的,只能眼巴巴地看着。如果需要进行更高权限的工作,就需要调用系统调用,进入内核。
    一旦进入了内核,就换了一种视角。刚才是普通进程的视角,觉着整个空间是它独占的,没有其他进程存在。当然另一个进程也这样认为,因为它们互相看不到对方。这也就是说,不同进程的 0 号到 29 号会议室放的东西都不一样。
  • 但是到了内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表。虽然内核栈是各用各的,但是如果想知道的话,还是能够知道每个进程的内核栈在哪里的。所以,如果要访问一些公共的数据结构,需要进行锁保护。也就是说,不同的进程进入到内核后,进入的 30 号到 39 号会议室是同一批会议室。
posted @ 2020-08-17 07:58  風酱  阅读(241)  评论(0编辑  收藏  举报