操作系统笔记(持续更新中)
综述
1.知识体系
操作系统就像一家外包公司:
为了实现系统的运作,其实是由几个子系统支撑完成的:
2. 系统调用
2.1 立项与进程管理
- 父进程进行fork操作,得到子进程,先从父进程拷贝数据结构,再修该
- 子进程调用execve执行另一个程序
-有个系统调用叫 waitpid,父进程能知道子进程是否运行完毕
2.2 会议室与内存管理
- 进程空间内,存放代码的部分叫做代码段(Code Segment)
- 进程内,存放数据的部分叫做数据段(Data Segment
- 局部变量在当前函数有效,离开函数则释放
- 动态分类的,指明才销毁的,称为堆 - 进程不用的部分就不管,进程需要使用内存的时候,会调用内存管理系统,但是也不代表对应到了真正的物理内存,只有要写入且发现没有物理内存,才会触发一个中断,现分配物理内存
- 两个系统调用
- brk:内存数据量小时,和原来的数据连在一起
- mmap: 数据量大时重新划分一个区域
档案库管理与文件管理
最重要的6个文件操作:
- 对已有文件的打开和关闭
- 创建
- 打开文件以后使用lseek跳到文件某个位置
- read和write
每个文件,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), 取下一条指令, 放入指令寄存器中
- 指令包括操作和目标数据
- 数据单元 根据控制单元的指令, 从数据段读数据到数据寄存器中
- 运算单元 开始计算, 结果暂时存放到数据寄存器
- 控制单元 通过指令指针寄存器(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 里面有指针指向这个进程属于哪个调度类。
内存管理
概述
内存管理要做到三件事:
-
- 虚拟内存空间的管理,每个进程看到的地址空间是独立的,互不干扰的
-
- 物理内存的管理,只有内存管理模块能够使用
-
- 内存映射,需要将虚拟内存和物理内存管理起来
虚拟内存的布局
- 我们从最低位开始排起,先是 Text Segment、Data Segment 和 BSS Segment。Text Segment 是存放二进制可执行代码的位置,Data Segment 存放静态常量,BSS Segment 存放未初始化的静态变量。是不是觉得这几个名字很熟悉?没错,咱们前面讲 ELF 格式的时候提到过,在二进制执行文件里面,就有这三个部分。这里就是把二进制执行文件的三个部分加载到内存里面。
- 接下来是堆(Heap)段。堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的。
接下来的区域是 Memory Mapping Segment。这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中。 - 再下面就是栈(Stack)地址段。主线程的函数调用的函数栈就是用这里的。
- 如果普通进程还想进一步访问内核空间,是没办法的,只能眼巴巴地看着。如果需要进行更高权限的工作,就需要调用系统调用,进入内核。
一旦进入了内核,就换了一种视角。刚才是普通进程的视角,觉着整个空间是它独占的,没有其他进程存在。当然另一个进程也这样认为,因为它们互相看不到对方。这也就是说,不同进程的 0 号到 29 号会议室放的东西都不一样。 - 但是到了内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表。虽然内核栈是各用各的,但是如果想知道的话,还是能够知道每个进程的内核栈在哪里的。所以,如果要访问一些公共的数据结构,需要进行锁保护。也就是说,不同的进程进入到内核后,进入的 30 号到 39 号会议室是同一批会议室。