目录
一、LiteOS内核简介
华为LiteOS基础内核包括不可裁剪的极小内核和可裁剪的其他模块。
- 极小内核包括任务管理、内存管理、异常管理、中断管理和系统时钟;
- 可裁剪其他模块:互斥锁、信号量、队列管理、事件管理、软件定时器等

二、任务管理
实时操作系统RTOS,他会按照排序运行、管理系统资源。如果有一个任务需要执行,实时操作系统会在较短时间内执行该任务。
提供提供任务的创建、删除、延迟、挂起、恢复等功能,以及锁定和解锁任务调度。支持任务按优先级高低的 抢占调度 以及同优先级 时间片轮转调度。
2.1 任务简介
LiteOS的任务可认为是一系列独立任务(每个任务独立环境运行)的集合。任何时刻有且只有一个任务处于运行态,由LiteOS调度器决定哪个任务运行(处于就绪态的最高优先级任务)。任务默认有32个优先级(0-31),最高优先级为0,最低优先级为31.
LiteOS支持抢占式任务调度机制:高优先级任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度,同时LiteOS也支持时间片轮转调度机制。
2.2 进程、线程基础知识
2.2.1 进程
进程 是一个动态过程,它是程序的一次运行过程,当应用程序被加载到内存中运行之后就被称为一个进程。
1. 进程的状态
在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出(Swap Out)到硬盘,等需要再次运行的时候,再从硬盘换入(Swap In)到物理内存。而描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。
挂起状态分为两种:
- 阻塞挂起状态:进程在 硬盘 并等待某个事件的出现;
- 就绪挂起状态:进程在 硬盘,但只要进入内存,即刻立即运行。

- 就绪态:任务处于就绪列表中,等待调度器进行调度;(新创建的任务会被初始化为该状态)
- 运行态:任务占用处理器执行;(LiteOS调度器选择优先级最高的就绪态任务)
- 阻塞态:任务正在等待某个时序或外部中断,不处于就绪列表中,无法得到调度器的调度;(阻塞态包含任务挂起、任务延时、任务等待信号量、读写队列或等待读写事件等)
- 退出态:任务运行结束,等待系统回收资源;
- 创建态:任务创建,操作系统为该任务分配系统资源、初始化PCB。

2. 进程的控制结构
在操作系统中,是用 进程控制块(process control block, PCB) 数据结构来描述进程的。
PCB中包含的信息:
- 进程描述信息
- 进程描述符:标识各个进程,每个进程都有一个并且唯一的标识符;
- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。
- 进程状态和管理信息
- 进程当前状态,如new、ready、running、waiting或blocked等;
- 进程优先级:进程抢占CPU时的优先级。
- 资源分配清单
- 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的I/O设备信息。
- CPU相关信息
- CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的PCB中,以便进程重新执行时,能从断点处继续执行。
3. 进程的上下文切换
一个进程切换到另一个进程运行,称为 进程的上下文切换。进程是由内核管理和调度的,所以进程的切换只能发生在内核态。所以,进程的上下文切换不仅包含虚拟内存、栈、全局变量等用户空间的资源,还包括内核堆栈、寄存器等内核空间的资源。
大多数操作系统都是多任务,通常支持大于CPU数量的任务同时运行。这些任务的同时运行,只是因为系统在很短的时间内,让各个任务分别在CPU运行,那么在每个任务运行前,CPU需要知道任务从哪里加载,又从哪里开始运行,所以操作系统需要事先帮CPU设置好 CPU寄存器和程序计数器。CPU寄存器和程序计数器是CPU在运行任何人五千,所必须依赖的环境,这些环境叫做 CPU上下文。
CPU寄存器是CPU内部一个容量小,但是速度极快的内存(缓存);程序计数器用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。
4 进程间的通信方式
每个进程的用户地址空间是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
进程间通信方式 | |||||
管道 | 信号 | 信号量 | 消息队列 | 共享内存 | 网络通信 |
普通管道(单工) 流管道(半双工) 只能父子进程之间通信 有名管道(全双工) 没有上面的限制 | 用于通知接收信号的进程事件的发生,也可以发送信号给进程本身 | 信号量是一个计数器,主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,还可以作为锁机制用于进程/线程同步。 | 消息队列是消息的链表,存放在内核中并由消息队列标识符标识。 消息队列是 UNIX 下不同进程之间实现共享资源的一种机制,允许不同进程将格式化的数据流以消息队列形式发送给任意进程,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。 | 共享内存是映射一段能够被其他进程所访问的内存,这段共享内存由一个进程创建,但其他的多个进程都可以访问,使得多个进程可以访问同一块内存。 | Socket是一种基于网络的IPC方法,允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据(网络通信)。 |
只能承载无格式字节流、缓存区大小受限 | 传递信息少 | 消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷 | 共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计,它往往与其他通信机制,如结合信号量来使用,以实现进程间的同步和通信。 |
4.1. 管道
管道就是内核里面的一串缓存,也是以文件的方式存在。
$ ps auxf | grep mysql
命令行里的 | 竖线就是一个匿名管道,功能是将前一个命令的输出作为后一个命令的输入。
$ mkfifo myPipe
使用命名管道前,需要先通过mkfifo命令来创建,并且指定管道名称。
- 普通管道pipe:1)单工,数据只能单向传输;2)只能在有血缘关系的进程间使用。
- 流管道s_pipe:1)半双工,可以双向传输;2)只能在有血缘关系的进程间使用。
- 有名管道name_pipe(FIFO):1)全双工;2)允许在不相关进程间通信。
4.2. 信号
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
在很多应用程序中,都会存在处理异步事件的需求,而信号提供了一种处理异步时间的方法。用于通知接收信号的进程有某种事件发生,所以可用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。
4.3. 信号量
信号量是一个整型的计数器,主要用于实现进程间的互斥和同步,而不是用于缓存进程间通信的数据。
主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
- 1. P操作,信号量减1。相减后如果信号量<0,则表明资源已被占用,进程需要阻塞等待;如果信号量>=0,则表ing还有资源可使用,京城可正常继续执行;
- 2. V操作,信号量加1。相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程
- P操作是用在进入共享资源之前,V操作是用在离开共享资源之后,这两个操作必须成对出现。
-
信号量用于进程互斥
它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。 Linux 提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件 sys/sem.h 中。

4.4. 共享内存
共享内存就是映射一段能够被其他进程所访问的内存,这段共享内存由一个进程创建,但其他的多个进程都可以访问,使得多个进程可以访问同一块内存。从而不需要像消息队列那样内核态和用户态之间的消息拷贝过程。
共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计,它往往与其他通信机制,如结合信号量来使用,以实现进程间的同步和通信。
4.5. 消息队列
消息队列是保存在内核中的消息链表。在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
- 消息队列不适合比较大数据的传输;
- 消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。
消息队列是一种异步的通信方式,是一种常用于系统中通信的数据结构,实现接受来自任务或中断的不固定长度的消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。
消息队列包括 POSIX 消息队列和 System V 消息队列。 消息队列是 UNIX 下不同进程之间实现共享资源的一种机制,UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。
4.6. Socket通信
Socket是一种基于网络的IPC方法,允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据(网络通信)。
在一个典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:
- 各个应用程序创建一个socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它;
- 服务器将自己的socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。
2.2.2 线程
线程 是能够独立运行的基本单位,是进程当中的一条执行流程。同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,从而确保线程的控制流是相对独立的。
1. 线程与进程的比较
线程的优点:
- 一个进程中可以同时存在多个线程;
- 各个线程之间可以并发执行;
- 各个线程之间可以共享地址空间和文件等资源。
线程的缺点:
- 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(C/C++)
- 进程时资源分配的单位,线程是CPU调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销。
2. 线程的上下文切换
- 当两个线程不属于同一进程,则切换的过程就跟进程上下文切换一样;
- 当两个线程是属于同一进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换进程的私有数据、寄存器等不共享的数据。
3. 线程的实现
主要有三种线程的实现方式:
- 用户线程:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
- 内核线程:在内核中实现的线程,是由内核管理的线程;
- 轻量级进程:在内核中来支持用户线程。
用户线程和内核线程之间的对应关系有多对一、一对一和多对多。
3.1 用户线程(多对一):
用户线程是基于用户库的线程管理库实现的,那么线程控制块也是在库里面实现的,对于操作系统而言是看不到这个TCB的,他只能看到整个进程的PCB。所以用户线程的整个线程的管理和调度,操作系统是不直接参与的,而是由用户级线程库来实现线程的管理、包括线程的创建、终止、同步和调度等。
用户线程的优点:
- 每个进程都需要有私有的线程控制块TCB列表,用来跟踪记录各个线程状态信息(PC、栈指针、寄存器),TCB由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
- 用户线程的切换也是有线程库函数来完成,无需用户态和内核态的切换,所以速度特别快。
用户线程的缺点:
- 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了;
- 当一个线程开始运行后,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
3.2 内核线程(一对一):
内核线程是由操作系统管理的,线程对应的TCB自然是放在操作系统里的,这样线程的创建、终止和管理都由操作系统负责。
内核线程的优点:
- 在一个进程中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 分配给线程,多线程的进程获得更多的CPU运行时间。
内核线程的缺点:
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如PCB和TCB;
- 线程的创建、终止和切换都通过系统调用的方式来进行,系统开销比较大。
3.3 轻量级进程
轻量级进程(LWP)是内核支持的用户线程,一个进程可有一个或多个LWP,每个LWP是跟内核线程一对一映射的,也就是LWP都是由一个内核线程支持,而且LWP是由内核管理并像普通进程一样被调度。在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。
4. 线程间同步机制
线程是参与系统调度的最小单位,是进程中的实际运行单位(进程仅仅是一个容器,包含了线程运行所需的数据结构、环境变量等信息)。一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
线程同步的作用:
对共享资源的访问进行保护,目的是解决数据一致性问题;出现数据一致性问题的本质在于进程中的多个线程对共享资源的并发(同时)访问。
4.1. 互斥锁-mutex
- 互斥锁又叫互斥量,本质上是一把锁。在访问共享资源之前对mutex进行上锁,访问完成之后释放互斥锁;
- 任何试图对上锁的互斥锁进行加锁的线程都会被阻塞,待当前线程释放互斥锁;
- 使用互斥锁之前,必须对它进行初始化操作;
- 互斥锁加锁pthread_mutex_lock(),互斥锁解锁pthread_mutex_unlock();
- 调用pthread_mutex_trylock()试图对上锁的互斥锁上锁,加锁失败不会阻塞;
- 调用pthread_mutex_destory()销毁互斥锁;
- 一个线程试图对同一个互斥锁加锁两次会陷入死锁状态
4.2. 条件变量
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或条件满足为止。通常情况下,条件变量和互斥锁搭配使用。
使用条件变量主要包括两个动作:
1. 一个线程等待某个条件满足而被阻塞;
2. 另一个线程中,条件满足时发出“信号”。
4.3. 自旋锁
自旋锁与互斥锁的关系:
- 互斥锁基于自旋锁实现,故自旋锁相较于互斥锁更加底层;
- 线程获取不到互斥锁会陷入阻塞(休眠),直到获得锁时被唤醒;
- 线程获取不到自旋锁会原地“自旋”,直到获得锁;
- 自旋锁效率远高于互斥锁:休眠和唤醒开销很大,所以互斥锁开销远高于自旋;
- 自旋锁不适用于等待时间比较长的情况;
- 自旋锁可以在中断服务函数中使用,而互斥锁不行。
如果自旋锁处于未锁定状态,那么立即获得锁;如果自旋锁已处于锁定状态,那么调用者将一直循环查看该自旋锁的持有者是否释放。
自旋锁的不足之处:
- 一直占用CPU,在未获得锁的情况下,一直处于运行状态(自旋);
- 试图对同一自旋锁加锁两次必然会导致死锁。
4.4. 读写锁
读写锁也叫做共享互斥锁,当读写锁处于读模式锁住时,可以说是共享模式锁住;当写模式锁住时,可以说成是互斥模式锁住。

一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读 写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁的两个规则:
- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞;
- 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
读写锁非常适合于对共享数据读的次数远大于写的次数的情况
2.3 调度
2.3.1 调度时机
- 非抢占式调度算法挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。
- 抢占式调度算法挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制。
2.3.2 调度算法
1. 先来先服务调度算法(First Come First Serve, FCFS);
2. 最短作业优先调度算法(Shortest Job First, SJF);
3. 高响应比优先调度算法(Highest Response Ratio Next, HRRN);
4. 时间片轮转调度算法(Round Robin, RR)
2.4 如何避免死锁?
死锁只有同时满足以下四个条件才会发生:
- 互斥条件,多个线程不能同时使用同一个资源;
- 持有并等待条件,线程持有某资源同时等待其他线程持有的资源;
- 不可剥夺条件,在持有资源的线程程使用完之前不能被其他线程释放;
- 环路等待条件,两个线程获取资源的顺序构成了环形链;
2.4.1 利用工具排查死锁问题
pstack + gdb 定位死锁问题
pstack <pid> : pstack命令可以显示每个线程的栈跟踪信息(函数调用过程);
多次执行pstack命令查看线程的函数调用过程,多次对比结果,确认那几个线程一直没有变化,且是因为在等待锁。
三、内存管理(LiteOS+Linux)
- 提供静态 和 动态 内存两种算法,支持内存申请、释放;
(目前支持内存管理算法有固定大小的BOX算法、动态申请的bestfit和bestfit_little算法)
- 提供内存统计、内存越界检测功能。
内存管理模块管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。在操作系统运行过程中,内存管理模块通过堆内存的申请/释放来管理用户和OS对内存的使用,是内存的利用率和使用率达到最优,同时最大限度地解决系统的 内存碎片问题。
3.1 内存管理方式
操作系统引入 虚拟内存,将不同进程的虚拟地址和不同内存的物理地址通过CPU芯片中的内存管理单元(MMU)映射起来。
虚拟内存的作用:
1. 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,可以把它换出到物理内存之外,比如硬盘上的swap区域;
2. 由于每个进程都有自己的页表,所以每个进程的虚拟地址是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,解决了多进程之间地址冲突的问题;
3. 页表里的页表项中处于物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
3.1.1 内存分段
程序是由若干逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段有不同的属性,所以可以使用分段(Segmentation)的形式把这些段分离出来。
物理地址 = 段选择因子(段号确定段基地址) + 段内偏移量
3.1.2 内存分段的不足
- 外部内存碎片:多个分段未必能恰好使用所有的内存空间;
- 内存交换效率低:通过将程序读入硬盘再写入内存,从而获取连续的内存空间。
3.1.3 内存分页
分页是将整个虚拟和物理内存空间切成一段段固定尺寸的大小。Linux下,每页4KB。
通过内存分页的方式能够解决内存分段的问题,因为页与页之间是紧密排列的,而且需要进行内存交换的也只有少数的一个页或几个页。
物理地址 = 页号(作为页表的索引,读取物理内存基地址) + 页内偏移
3.1.4 内存分页的不足
- 内部内存碎片:因为内存分页的最小分配单位是一页,所以程序不足一页,最小也只能分配一页,导致内部内存碎片的产生。
- 简单分页存在空间上的缺陷:操作系统可以同时运行非常多的进程,页表的数量会非常庞大。
3.1.5 多级页表
为了解决简单分页所在来的页表占用内存空间庞大的问题,需要引入多级页表的解决方案。
例如:我们将100多万格页表项的单级页表再分页,将一级页表分为1024个二级页表,每个二级页表包含1024个页表项,形成二级分页。
3.1.6 TLB
多级分页虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了转换工序,带来了时间上的开销。程序是有局限性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。
计算机科学家们在CPU芯片中,加入了一个专门存放程序最常访问的页表项的cache,这个cache就是 TLB(Translation Lookaside Buffer),通常称为页表缓存、转址旁路缓存、块表等。CPU在寻址时,会先查TLB,如果没找到,才会继续查常规的页表。
3.1.7 段页式内存管理
将内存分段和内存分页组合起来在同一个系统中使用,通常称为 段页式内存管理
物理地址 = 段号 + 段内页号 + 页内位移
3.2 Linux内存布局
Linux系统主要采用了分页管理,但是由于intel处理器的发展史,Linux系统无法避免分段管理。于是Linux把所有段的基地址设为0,意味着所有程序的地址空间都是线性地址空间(虚拟地址)。
Linux系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。
3.3 内存分配
3.3.1 malloc申请内存
malloc申请的是虚拟内存,内存分配方式分为 系统调用brk()从堆内存分配内存(用户分配内存小于128KB) 和 mmap()系统调用在文件映射区分配内存(用户分配内存大于128KB);
3.3.2 free 内存释放
通过free()函数释放内存后,堆内存还是存在的,并没有归还给操作系统而是缓存着放进malloc的内存池里,当进程再次申请直接复用。但是malloc()通过mmap方式申请的内存,free释放内存后就会归还给操作系统。
3.3.3 内存满了,会发生什么?
1. 应用程序通过malloc申请内存时,实际上申请的是虚拟内存,此时并不会分配物理地址。
2. 当应用程序读写了这块虚拟地址,CPU就会去访问这个虚拟地址,会发现这个虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler(缺页中断函数)处理。
3. 缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
四、中断管理(LiteOS)
中断 是指出现需要时,CPU暂停执行当前程序,转而执行新的程序的过程。
外设可以在没有CPU介入的情况下完成一定的工作,但某些情况下也需要CPU为其执行一定的工作。通过中断机制,在外设不需要CPU介入时,CPU可以执行其他任务,而当外设需要CPU时,将通过产生中断信号使CPU立即中断当前任务来响应中断请求。从而大大提高系统实时性以及执行效率。
中断服务流程:
- 保护现场:在中断服务程序的起始部分安排若干条入栈指令,将寄存器的内容压入堆栈;
- 执行中断子函数;
- 恢复现场:中断服务程序结束前,弹出堆栈中的现场信息;
- 中断返回:返回到原程序的断点处,继续执行原程序。
五、文件系统(Linux)
一切皆文件,不仅普通的文件和目录,块设备、管道、socket等,也都是统一交给文件管理系统管理。
5.1 文件系统的基本组成
Linux文件系统会为每个文件分配两个数据结构:索引节点 (index node) 和目录项 (directory entry),他们主要用来记录文件的元信息和目录层次结构。
- 索引节点(占用磁盘空间),用来记录文件的元信息,比如inode编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等。索引节点是文件的唯一标识,它们之间一一对应。
- 目录项(缓存在内存),用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构。目录项是由内核维护的一个数据结构。
5.1.1 文件数据如何存储在磁盘中呢?
磁盘读写的最小单位是扇区,扇区的大小只有512B大小。如果每次读写都以这么小的单位,那读写效率会非常低,所以文件系统将多个扇区组成一个逻辑块,每次读写的最小单位就是逻辑块,Linux中的逻辑块大小为4KB,即读写8个扇区,大大提高了磁盘的读写效率。
5.2 虚拟文件系统
文件系统的种类众多,而操作系统希望 对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层称为虚拟文件系统(Virtual File System,VFS)。
Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:
- 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
- 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的
/proc
和/sys
文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。 - 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
5.3 文件的使用
5.4 文件的存储
文件中的数据是存储在硬盘上面的,数据在磁盘上的存放方式有两种:
- 连续空间存放方式;
- 非连续空间存放方式,链表、索引。
5.4.1连续空间存放方式
文件存放在磁盘连续的物理空间中,文件头内需要指定 起始块的位置 和 长度。
优缺点:1、读写效率高;2、磁盘空间碎片和文件长度不易扩展。
5.4.2 非连续空间存放方式
链表方式存在是离散的,不用连续的,可以消除磁盘碎片,可大大提高磁盘空间的利用率,同时文件的长度可以动态扩展。根据实现的方式的不同,链表可分为「隐式链表」和「显式链接」两种形式。
- 文件要以「隐式链表」的方式存放的话,实现的方式是文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置,这样一个数据块连着一个数据块,从链头开始就可以顺着指针找到所有的数据块,所以存放的方式可以是不连续的。
- 隐式链表的存放方式的缺点在于无法直接访问数据块,只能通过指针顺序访问文件,以及数据块指针消耗了一定的存储空间。隐式链接分配的稳定性较差,系统在运行过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致文件数据的丢失。
- 如果取出每个磁盘块的指针,把它放在内存的一个表中,就可以解决上述隐式链表的两个不足。那么,这种实现方式是「显式链接」,它指把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中,该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号。
索引的实现是为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。另外,文件头需要包含指向「索引数据块」的指针,这样就可以通过文件头知道索引数据块的位置,再通过索引数据块里的索引信息找到对应的数据块。
索引的方式优点在于:
- 文件的创建、增大、缩小很方便;
- 不会有碎片的问题;
- 支持顺序读写和随机读写;
由于索引数据也是存放在磁盘块的,如果文件很小,明明只需一块就可以存放的下,但还是需要额外分配一块来存放索引数据,所以缺陷之一就是存储索引带来的开销。
六、设备管理(Linux)
6.1 设备控制器
为了屏蔽设备之间的差异,每个设备都有一个叫设备控制器(Device Control) 的组件,控制器是有三类寄存器,它们分别是状态寄存器(Status Register)、 命令寄存器(Command Register)以及数据寄存器(Data Register)
CPU 通过读写设备控制器中的寄存器控制设备,这可比 CPU 直接控制输入输出设备,要方便和标准很多。另外, 输入输出设备可分为两大类 :块设备(Block Device)和字符设备(Character Device)。
- 块设备,把数据存储在固定大小的块中,每个块有自己的地址,硬盘、USB 是常见块设备。
- 字符设备,以字符为单位发送或接收一个字符流,字符设备是不可寻址的,也没有任何寻址操作,鼠标是常见的字符设备。
6.2 I/O控制方式
每种设备都有一个设备控制器,控制器相当于一个小 CPU,它可以自己处理一些事情,但有个问题是,当 CPU 给设备发送了一个指令,让设备控制器去读设备的数据,它读完的时候,要怎么通知 CPU 呢?
控制器的寄存器一般会有状态标记位,用来表示输入或输出操作是否完成。
方式一:轮循等待,让CPU一直查寄存器状态,直到状态标记为完成,但会占用CPU全部时间。
方式二:中断,通知操作系统数据已经准备好了。中断有两种,一种软中断,例如代码调用INT指令触发,一种是硬件中断,就是硬件通过中断控制器触发。但是中断的方式对于频繁读写数据的磁盘,并不友好,CPU容易被经常打断。
对于这一类设备的问题的解决方法就是使用DMA(Direct Memory Access)功能,它可以使得设备在CPU不参与的情况下,能够自行完成I/O数据放入到内存。实现DMA功能要DMA控制器硬件的支持。
DMA 的工作方式如下:
- CPU 需对 DMA 控制器下发指令,告诉它想读取多少数据,读完的数据放在内存的某个地方就可以了;
- 接下来,DMA 控制器会向磁盘控制器发出指令,通知它从磁盘读数据到其内部的缓冲区中,接着磁盘控制器将缓冲区的数据传输到内存;
- 当磁盘控制器把数据传输到内存的操作完成后,磁盘控制器在总线上发出一个确认成功的信号到 DMA 控制器;
- DMA 控制器收到信号后,DMA 控制器发中断通知 CPU 指令完成,CPU 就可以直接用内存里面现成的数据了;
6.3 设备驱动程序
虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的,所以为了屏蔽「设备控制器」的差异,引入了设备驱动程序。
前面提到了不少关于中断的事情,设备完成了事情,则会发送中断来通知操作系统。那操作系统就需要有一个地方来处理这个中断,这个地方也就是在设备驱动程序里,它会及时响应控制器发来的中断请求,并根据这个中断的类型调用响应的中断处理程序进行处理。
中断处理程序的处理流程:
- 在 I/O 时,设备控制器如果已经准备好数据,则会通过中断控制器向 CPU 发送中断请求;
- 保护被中断进程的 CPU 上下文;
- 转入相应的设备中断处理函数;
- 进行中断处理;
- 恢复被中断进程的上下文;
6.4 通用块层
对于块设备,为了减少不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理不同的块设备。
通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,它主要有两个功能:
- 第一个功能,向上为文件系统和应用程序,提供访问块设备的标准接口,向下把各种不同的磁盘设备抽象为统一的块设备,并在内核层面,提供一个框架来管理这些设备的驱动程序;
- 第二功能,通用层还会给文件系统和应用程序发来的 I/O 请求排队,接着会对队列重新排序、请求合并等方式,也就是 I/O 调度,主要目的是为了提高磁盘读写的效率。
6.5 存储系统I/O软件分层
可以把 Linux 存储系统的 I/O 由上到下可以分为三个层次,分别是文件系统层、通用块层、设备层。他们整个的层次关系如下图:
七、网络系统
主要是为了优化磁盘,提高系统的吞吐量。
参考文献:
- 小林coding——图解操作系统;
- 华为LiteOS官方文档;