OS基础知识
目录
一、进程与线程
- ROM在工厂就被编程完毕,然后不能再被修改
- “原子操作(atomic operation)是不需要synchronized”,这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
- 临界区: 对共享内存进行访问的程序片段称作临界区。定义貌似有误?java中的定义:不可同时被多个线程访问的代码段称为临界区。互斥量或互斥锁的作用就相当于一个通行证,这个通行证只可以给一个线程使用,线程得到这个通行证后才可以执行后续代码,否则会阻塞(与两个线程是否使用了需要同步的变量无关),直到另外一个线程把通行证交出来,这里通行证必须是相同名字(即同一个互斥锁,不同的互斥锁之间没有任何关系)。
- 线程与进程的三点区别:线程的划分尺度小于进程,使得多线程程序的并发性高;另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;最后,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些
- 线程的使用,要注意当线程使用时,它是和其他线程以及调用它的线程一起执行的,甚至当调用它的线程结束后,它仍旧执行,直到它完成了任务后才会停止。还有需要注意的是,当线程访问的资源可能随着其他线程的结束而不存在了,以及资源的竞争等问题。
- 信号量(semaphore): 一个信号量的取值可以为0或者正值,有两种操作down和up,对一个信号量执行down操作,则检查其值是否大于0。若该值大于0,则将其值减1并继续,若该值为0,则进程将睡眠,而且此时down操作并未结束。down操作是一个原子操作,up操作对信号量增1,如果有一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中的一个并允许该进程完成它的down操作。 于是,对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量的值仍旧为0,但其上睡眠的进程却少了一个。信号量的值增1和唤醒一个进程同样是密不可分的,不会有某个进程因执行up而阻塞。例子中,假设当mutex = 1 producer()执行down(&mutex)是发现其值为1,则将其减1并继续,如果这时consumer()运行,则在执行down(&mutex)时发现mutex为0,则睡眠。当producer()中执行到up(&mutex)时,mutex仍为0,但consumer被唤醒,并且这是producer被阻塞。例子见来自手机->图片->1
- 互斥量(mutex):互斥量一个线程获得访问资源时,不允许其它线程使用,它锁住的是临界区(对共享内存进行访问的程序片段称作临界区),假如在一个函数的内部定义的开头处声明一个互斥量(锁),则该锁的作用域的范围内的程序片段只能被锁住这段临界区的线程访问,直到该被解锁,无论声明互斥锁,都只是为了锁住临界区,互斥量(锁)之间没有任何联系。
- 条件变量(condition variables):互斥量在允许或阻止对临界区的访问上很有用,条件变量则允许线程由于一些未达到的条件而阻塞。**条件变量主要用于线程间的协作。条件变量经常搭配互斥量一起使用,这种模式让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量,最后另一个线程会向它发信号,使它可以继续执行。例:pthread_cond_wait(&condp,&the_mutex)原子性地调用并解锁它持有的互斥量 , pthread_cond_signal(&condc) 向另一个线程发信号来唤醒它。下面是一个关于条件变量和互斥量的多线程的代码。 responsive.cpp
- 管程(monitor):管程是语言概念,管程有一个很重要的特性,即任一时刻管程中只能有一个活跃的进程,这一特性使得管程能有效地完成互斥。通过临界区互斥的自动化,管程比信号量更容易保证并行编程的正确性。一个利用java的来模拟管程的例子见来自手机->图片->2,这里将对共享内存的访问,转移到一个类our_monitor中,将synchronized加入到了方法中,java保证一旦某个线程执行该方法,就不允许其它线程执行该对象(注意这里是对象,这意味着任意时刻只能执行insert()和remove()中的一个方法,这与管程概念类似,但不等同)中的任何synchronized方法。
- 消息传递(message passing):这种进程间通信的方法使用两条原语send和receive,这里生产者向消费者发送包含实际数据的信息,消费者提取信息后向生产者发送空的信息,当使用信箱时,缓冲机制的作用是很清楚的:目标信箱容纳那些已被发送但尚未被目标进程接受的信息。
- 屏障(brrrier): 除非所有的进程都准备着手下一个阶段,否则任何进程都不能进入下一个阶段。
- 调度(schedule):多个进程或线程同时竞争CPU,决定将CPU分配给哪个进程的程序就称为调度程序,该程序使用的算法就称为调度算法。
- 非抢占式调度: 让一个进程一直运行,直至被阻塞或该进程自动释放CPU。抢占式调度: 挑选一个进程,并且让该进程运行某个固定时段的最大值
- 在不同系统中,调度程序的优化是不同的,主要划分为三种环境:批处理、交互式、实时。交互式最重要的指标是最小响应时间,因为用户不愿等待,但不同的事情用户愿意等待的时间是不同的。批处理中不会有用户不耐烦地在终端旁等待一个短请求的快捷响应,因此非抢占式算法,或长周期的抢占式算法,通常都是可以接受的。
- 死锁:哲学家就餐问题,附件是一个会死锁的程序
deadlock.zip
, - 为了修正这个问题,必须明白如果同时满足以下4种条件,死锁就会发生:
- 相互排斥(互斥条件)。线程使用的资源至少有一个必须是不可共享的。在这种情况下,一根筷子一次就只能被一个哲学家使用。
- 至少有一个进程必须持有某一种资源,并且同时等待获得正在被另外的进程所持有的资源(占有和等待条件,已经得到了某个资源的进程可以再请求新的资源)。也就是说,要发生死锁一个哲学家必须持有一根筷子并且等待另一根筷子。
- 不能以抢占的方式剥夺一个进程的资源(不可抢占条件)。所有进程只能把释放资源作为一个正常事件。我们的哲学家是有礼貌的,他们不会从别的哲学家手中抢夺筷子。
- 出现一个循环等待,一个进程等待另外的进程所持有的资源,而这个被等待的进程又等待另一个进程所持有的资源,以此类推直到某个进程去等待被第1个进程所持有的资源。因此,头尾相接环环相扣,因此大家都被锁住了。在DeadlockingDiningPhilosophers.cpp中,是因为每个哲学家都试图先得到右边的筷子,而后再得到左边的筷子,所以发生了循环等待(环路等待条件)。
- 因为必须所有这些条件都满足才会引发死锁,那么只需阻止其中一个条件发生就可防止产生死锁。在这个程序中,防止死锁最容易的办法是破坏条件四。这个条件发生的原因是由于每个哲学家都试图以特定的顺序拿筷子:先右后左。正因为如此,才可能进入这样的情形:每个人都把持着其右边的筷子,而等待得到其左边的筷子,由此导致循环等待条件产生。然而,如果最后一个哲学家被初始化为先尝试拿左边的筷子,然后再拿右边的筷子,那么该哲学家将永远无法阻止右边紧挨着的哲学家(假设为B)拿到他自己(B)左边的筷子。在这种情形下,就防止了循环等待。这只是问题的一种解决方法,读者也可以通过阻止其他条件发生来解决该问题(更具体的细节请参考论述高级的线程处理的书籍)
LINUX进程调度
- Linux调度的总体思想是:实时进程优先于普通进程,实时进程以进程的紧急程度为优先顺序,并为实时进程赋予固定的优先级;普通进程则以保证所有进程能平均占用处理器时间为原则。所以其具体做法就是:
- 对于实时进程来说,总的思想是为实时进程优先级大于普通进程,以确保实时进程的优先级,并且实时进程总是活动进程。在此基础上,还分为两种做法:一种与时间片无关,另一种与时间片有关,即SCHED_FIFO和SCHED_RR,这两者都是实时调度策略。
- 对于普通进程来说,先根据静态优先级确定时间片,后续会根据睡眠时间等调整优先级,即动态优先级。当优先级高的进程执行完后,会被放入过期进程中,然后等所有活动进程执行完后,才会再次被放入活动进程中,并根据优先级判断是否要执行。其中,有一种特殊情况是,交互式进程,系统会自动根据平均睡眠时间(粗略地讲,是进程在睡眠状态所消耗的平均纳秒数)来判断是否是交互式进程,进程过去的睡眠时间越多,则越有可能属于交互式进程。则系统调度时,会给该进程更多的奖励(bonus),以便该进程有更多的机会能够执行。奖励(bonus)从0到10不等. 一般情况下,交互式进程用完时间片后,仍然是活动进程,特殊情况除外。
- CFS完全公平调度是针对非实时进程的。
实时进程会不会导致其它进程得不到运行机会?
- 通常不会。因为Linux kernel有一个RealTime Throttling机制,就是为了防止CPU消耗型的实时进程霸占所有的CPU资源而造成整个系统失去控制。它的原理很简单,就是保证无论如何普通进程都能得到一定比例(默认5%)的CPU时间。所以,Linux kernel默认情况下保证了普通进程无论如何都可以得到5%的CPU时间,尽管系统可能会慢如蜗牛,但管理员仍然可以利用这5%的时间设法恢复系统,比如停掉失控的实时进程,或者给自己的shell进程赋予更高的实时优先级以便执行管理任务,等等
- 可参考的好博文进程管理
- Linux的进程分普通进程和实时进程,普通进程即非实时进程SCHED_OTHER或SCHED_NORMAL,而实时进程又分SCHED_FIFO与SCHED_RR,实时进程的优先级(0~99)都比普通进程的优先级(100~139)高,且直到死亡之前始终是活动进程,当系统中有实时进程运行时,普通进程几乎是无法分到时间片的(只能分到5%的CPU时间)。
二、锁
锁的类型
- 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
- 互斥锁:共享资源的使用是互斥的,即一个线程获得资源的使用权后就会将该资源加锁,使用完后会将其解锁,如果在使用过程中有其他线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒,如果被阻塞的线程不止一个,那么它们都会被唤醒,但是获得资源使用权的是第一个被唤醒的线程,其它线程又陷入沉睡.
- 递归锁:同一个线程可以多次获得该资源锁,别的线程必须等该线程释放所有次数的锁才可以获得。
- 读写锁:它拥有读状态加锁、写状态加锁、不加锁这三种状态。
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
四种处理死锁的策略
- 忽略该问题。也许如果你忽略它,它也会忽略你。 例:鸵鸟算法,如果一个死锁平均五年发生一次,而每个月系统都因硬件故障等原因而崩溃一次,则可以不理会死锁
- 检测死锁并恢复。让死锁发生,检测它们是否发生,一旦发生死锁,采取行动解决问题
- 对于每种类型一个资源的死锁检测,可通过检测有向图环路的方法
- 对于每种类型多个资源的死锁检测,见书P250
- 死锁恢复,可通过抢占恢复、利用回滚恢复,杀死进程恢复
- 仔细对资源进行分配,动态对避免死锁
- 单个资源的银行家算法
- 多个资源的银行家算法
- 死锁预防,通过破坏引起死锁的四个必要条件之一,动态地避免死锁发生
三、内存管理
- 基址寄存器与界限寄存器: 当一个进程运行时,程序的起始物理地址装载到基址寄存器,程序的长度装载到界限寄存器中,这是给每个进程提供私有地址空间的非常容易的方法(比如,每个进程都可以有28这个地址却不会冲突),因为每个内存地址在送到内存之前,都会自动先加上基址寄存器的地址的值。使用基址寄存器的缺点是每次访问内存都需要进行加法和比较运算。
- 空闲内存管理:
- 使用位图的存储管理, 缺点是在将一个需要k个分配单元的进程调入内存时,存储器必须搜索位图,找出k个连续0的串,查找k个连续0的串很耗时。(PS: 终端服务器mmgr_malloc就是利用这种思想来实现内存分配)
- 使用链表的存储管理: 通过维护一个记录已分配内存段和空闲内存段的链表来实现
- 内存的分配与回收 有三种策略:1. 首次拟合法 2. 最佳拟合法 3.最差拟合法
- 首次拟合(适配)法, 内存分配:从 利用空间表里 从表头顺序查找第一个满足大小的内存,并将其分配给用户,所以其分配算法时间 复杂度为O(n),n为表长 . 内存回收:直接插在表头,时间复杂度 为 O(1)
- 最佳拟合(适配)法, 内存分配:从 表里 寻找 一个大于等于并且最接近指定大小的内存。所以每次查找需要遍历表。为了不用每次都遍历表,将 表 节点 按空间 从小到大排列。分配时间复杂度 为 O(n),n为表长。内存回收:需要按顺序插入指定的位置, 时间复杂度也是 O(n)
- 最差拟合(适配)法, 内存分配:与 表里 寻找 一个大于等于并且是最大的内存。所以也需要遍历表。为了不用每次都遍历表,将表节点从大到小排列。查找时间复杂度为O(1),(最大的总是在第一个)。内存回收:同样 需要按顺序插入指定位置。时间复杂度O(n)
- 三者比较: 从效率上来说,首次拟合法 > 最差拟合法 > 最佳拟合法, 但是不同策略适合不同的场景:最佳拟合法 总是 寻找 最接近的内存,有可能生成 一些 存储量非常小的内存,造成无法分配。但是同时也保留了很大的内存以备以后分配 大内存之需。适合 内存分配大小比较广的情况。最差拟合法:总是寻找 最大的内存,从而使空闲表的大小接近均匀。适合分配内存大小 比较窄的情况。首次拟合法:内存分配是 随机的,因此它介于两者之间。三种策略 都会 造成 一些 内存非常小的空间,从而使这些内存空间无法被分配。为了更有效的利用内存,需要将相邻的空闲的内存 合并成一个更大的内存。
- 碎片: 这里的碎片有两方面:
- 内部碎片 ,: 因为,内存需要对齐或者块是固定的大小,但是我们在请求空间的时候是任意的大小,其结果很可能不是4或8的整数倍,一旦我们请求一个空间,它小与我们的分配块大小,就会发生一部分空间分配给了我们但是我们却没有使用的情况。这部分空间我们就叫它内部碎片。
- 外部碎片 : 空闲的存储器合计起来可以满足我们一个分配请求,但是却没有一个独立的块可以满足我们要求,只能向内核请求额外的存储器时,原来剩下的空间大小我们就称之为外部碎片
- 内存的分配与回收 有三种策略:1. 首次拟合法 2. 最佳拟合法 3.最差拟合法
- 边界标识法:
- 将一个大内存分成块,每一个块包含表头,内存块、表尾组成。一开始是只有一个块(即一整块可用内存和表头、表尾)。其中表头由4个部分组成。 llink : 这个数据结构主体是循环链表,所以这个指针指向前一个节点。 tag : 标示位:1标示为已分配块,0标示未分配块。 size : 标示这个节点的大小(包括头部和尾部)。 rlink : 由于是双向循环链表,这个指针指向后一个节点。表尾由2个部分组成: uplink : 指向本节点的头部。 tag : 同上,标示分配情况。
- 这个算法其他一些要注意的地方:
- 假设每次需要找m 个大小,但是我们每次都分配n 给它,那么久而久之,就会有很多的m-n 个空间散落于链表中,所以我们需要设置一个标准值e, 当m-n <= e 的时候,就将m 的空闲整块分配给它,反之,我们就分配想当需求大小的空间。
- 如果收每一次都从头开始寻找就是首次匹配,由于已经进行多次,必然前边会聚集较多的小块,所以我们应当每次分配一次就将,表头指向它已经分配的后边的一个节点,这样就能基本保证每一次的进行首次匹配的效果了。
- 分配与回收:分配时其实是通过表头和表尾来定位内存是否可用的,也就是说,当前内存块是否被使用不影响内存的分配和回收。分配时就直接将块地址反回,回收时,通过获取之前的块和之后的块(不是通过链表指针而是地址加减1来获得),然后根据前后是否被使用,来决定是否要合并。
- 源码见 http://data.biancheng.net/view/49.html, 这个不支持多线程。
**伙伴算法(现代主流内存管理方法) **
- 我的理解: 其实就是将内存让不同大小分类,每一类对应一个链表,这样小块内存申请释放不容易影响大块内存,大小块之间又可以转换。从而减少外部碎片。之所以能减少碎片的核心就在于合并和分裂。其它内存分配也可以实现合并,但分裂可能没有实现。伙伴将两个结合了。
- 伙伴算法,简而言之,就是将内存分成若干块,然后尽可能以最适合的方式满足程序内存需求的一种内存管理算法,伙伴算法的一大优势是它能够完全避免外部碎片的产生。什么是外部碎片以及内部碎片,前面博文slab分配器后面已有介绍。申请时,伙伴算法会给程序分配一个较大的内存空间,即保证所有大块内存都能得到满足。很明显分配比需求还大的内存空间,会产生内部碎片。所以伙伴算法虽然能够完全避免外部碎片的产生,但这恰恰是以产生内部碎片为代价的。
- Linux 便是采用这著名的伙伴系统算法来解决外部碎片的问题。把所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。对1024 个页框的最大请求对应着 4MB 大小的连续RAM 块。每一块的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16个页框的块,其起始地址是 16 * 2^12 (2^12 = 4096,这是一个常规页的大小)的倍数。下面通过一个简单的例子来说明该算法的工作原理:
- 假设要请求一个256(129~256)个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两等分,一般用作满足需求,另一半则插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果这样的块存在,内核就把1024个页框块的256个页框用作请求,然后剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错误信号。简而言之,就是在分配内存时,首先从空闲的内存中搜索比申请的内存大的最小的内存块。如果这样的内存块存在,则将这块内存标记为“已用”,同时将该内存分配给应用程序。如果这样的内存不存在,则操作系统将寻找更大块的空闲内存,然后将这块内存平分成两部分,一部分返回给程序使用,另一部分作为空闲的内存块等待下一次被分配。
- 看懂这个链接就懂了伙伴算法【Linux 内核】内存管理之伙伴算法
- 分配内存:
- 寻找大小合适的内存块(大于等于所需大小并且最接近2的幂,比如需要27,实际分配32)
- 如果找到了,分配给应用程序。
- 如果没找到,分出合适的内存块。方法为 1.对半分离出高于所需大小的空闲内存块 2. 如果分到最低限度,分配这个大小。3.回溯到步骤1(寻找合适大小的块) 4.重复该步骤直到一个合适的块
- 释放内存:
- 释放该内存块
- 寻找相邻的块,看其是否释放了。
- 如果相邻块也释放了,合并这两个块,重复上述步骤直到遇上未释放的相邻块,或者达到最高上限(即所有内存都释放了)。
- 优点:较好的解决外部碎片问题;当需要分配若干个内存页面时,用于DMA的内存页面必须连续,伙伴算法很好的满足了这个要求; 只要请求的块不超过512个页面(2K),内核就尽量分配连续的页面; 针对大内存分配设计。
- 缺点:1. 合并的要求太过严格,只能是满足伙伴关系的块才能合并,比如第1块和第2块就不能合并; 2. 碎片问题:一个连续的内存中仅仅一个页面被占用,导致整块内存区都不具备合并的条件;3. 浪费问题:伙伴算法只能分配2的幂次方内存区,当需要8K(2页)时,好说,当需要9K时,那就需要分配16K(4页)的内存空间,但是实际只用到9K空间,多余的7K空间就被浪费掉。; 4.算法的效率问题: 伙伴算法涉及了比较多的计算还有链表和位图的操作,开销还是比较大的,如果每次2n大小的伙伴块就会合并到2(n+1)的链表队列中,那么2n大小链表中的块就会因为合并操作而减少,但系统随后立即有可能又有对该大小块的需求,为此必须再从2(n+1)大小的链表中拆分,这样的合并又立即拆分的过程是无效率的。
TLSF(实时系统动态内存算法)
- 它用2层链接记录空闲块,第一层是2的幂次方,如16,32,64,128。如果只用这个,内存浪费会比较严重,如分配33字节需要分配64字节。因此引入第二层链表(伙伴算法只有第一层,引入这一层的目的是可以根据 size 查找尽量最小符合要求的空闲块,切割空闲块,余下的空闲块插入到合适的位置),比如64这级,再分为4个区间,64-80,80-96,96-112,112-128,这样保证效率的同时提高内存利用率(还是分配33,现在只需要分配65-112这一段共48字节,比一层的情况少了16字节)。这个算法的另一个好处是稳定,无论申请多少空间的内存,分配所消耗的时间是差不多的,因此可以预估分配内存需要多少时间,多用于实时要求高的系统。注意,是有保证,快不快是另一回事。实时系统要的是可预期,不是快。
- 链接见http://shengrang.github.io/2018/05/22/tlsf/
虚拟内存
- 解决所有进程所需要RAM数量总和超出内存总量的两种方法:最简单的策略是交换技术,即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘,然后调用另一个进程。另一种策略是虚拟内存
- 虚拟内存的基本思想: 每个程序都有自己的地址空间,这些空间分割成多个页面,每个页面有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序,当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不再物理内存中的地址空间时,由操作系统通过算法将物理内存中很少使用的页框保存到磁盘上,腾出空间来将这部分地址映射到内存空间,并重新执行失败的命令。图片见来自手机->6
- 虚拟地址按照固定大小划分为成为页面的若干单元,在物理内存中对应的单元称为页框, 页面到页框的映射关系保存在页表中,
- 缺页中断:如果一个页面没有被映射到页框,于是CPU陷入到操作系统(需要等操作系统找到一个很少使用的页框,并把它的内容写入磁盘(如果不在的话),随后把没有映射的页面映射过去,并修改映射关系), 这称为缺页中断, 页表不在内存中也会引发缺页中断
- 加速分页过程(任何分页式系统中,都必须考虑两个问题):(1)虚拟地址到物理地址的映射必须非常快,(2)如果虚拟地址空间很大,页表也会很大 。
- 问题1的解决方法是,由于大多数程序总是对少量的页面进行多次的访问,而不是相反。因此,只有很少的页表项会被反复读取,为此,可以设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表,这种设备称为TLB(转换检测缓冲区或快表), 它通常包含在MMU中,MMU是内存管理单元,负责将虚拟地址映射为物理内存地址。
- 针对大内存的页表:
- 多级页表:引入多级页表的原因是避免把全部页表一直保存在内存中,特别是那些不需要的页表就不应该保留。页表数量是更多了,但只有需要的页表才放在内存中,大多数页表其实一般很少访问(除去底层的程序正文段和数据等,以及顶部的堆栈等)。 多级页表可以省内存的原理如下,理解的关键是每一个虚拟地址到物理地址的页表可以不再内存中,但必须有对应位表示其是否在内存中,一级页表需要一个页表框表示,而多级页表,顶级页表可以表示指数级的子页表是否在内存中,而通常有很大一部分内存时用不到的。假设原先100万个页表,需要100W个页表框来表示其是否在内存中(一直放在内存中不可能实现), 而换成多级页表后,那些很少被使用的页表,就只需要初级页表或次级页表表示其是否在内存中即可。比如:原先需要8个字段表示8个连续页表不在内存,现在只需要1个一级页表的字段就可以表示这8个不在内存中。主要两个优点如下:
- 使用多级页表可以使得页表在内存中离散存储
- 减少内存,通过一个顶级页表为真正有用的页表提供索引,这是我所理解的二级页表的本质。多级页表的原理类似
缺点为要多访问一次内存
- 倒排页表: 在这种设计中,在实际内存中每一个页框有一个表项,而不是每一个虚拟页面有一个表项。
- 多级页表:引入多级页表的原因是避免把全部页表一直保存在内存中,特别是那些不需要的页表就不应该保留。页表数量是更多了,但只有需要的页表才放在内存中,大多数页表其实一般很少访问(除去底层的程序正文段和数据等,以及顶部的堆栈等)。 多级页表可以省内存的原理如下,理解的关键是每一个虚拟地址到物理地址的页表可以不再内存中,但必须有对应位表示其是否在内存中,一级页表需要一个页表框表示,而多级页表,顶级页表可以表示指数级的子页表是否在内存中,而通常有很大一部分内存时用不到的。假设原先100万个页表,需要100W个页表框来表示其是否在内存中(一直放在内存中不可能实现), 而换成多级页表后,那些很少被使用的页表,就只需要初级页表或次级页表表示其是否在内存中即可。比如:原先需要8个字段表示8个连续页表不在内存,现在只需要1个一级页表的字段就可以表示这8个不在内存中。主要两个优点如下:
- 页面置换算法: 当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。如何选择相应的页面置换就是页面置换算法。有LRU(最近最少使用)、FIFO(最先进入的页面放在链表尾部,最久以前的页面放在表头,当发生中断时,淘汰表头的页面并把新调入的页面加到表尾)
LINUX内存管理
- Linux内存管理专题** , 强烈推荐这篇博文,写得非常好**
- linux内存分配使用了伙伴算法和slab机制: 一般来说,伙伴算法的改进算法用于操作系统分配和回收内存,而且内存块的单位较大,利于Linux使用的伙伴算法以页为单位.对于小块内存的分配和回收,伙伴算法容易导致内部碎片. 对于小块内存,一般采用slab算法,或者叫做slab机制. 它使用伙伴算法获得内存块,但是之后从其中切出更小的单元并且分别进行管理。slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct ,文件描述符等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab其实就是申请后不返回给伙伴,一直放在这里,特别适合内核中的数据结构,如file_struct ,文件描述符等。通过将大小相同的对象存储在一起,SLAB分配器能够有效地减少内存碎片, 当对象被频繁创建和销毁时,SLAB分配器能够快速地从已存在的SLAB中分配和回收对象,从而提高性能
四、文件系统
- 普通文件一般分为ASCII文件和二进制文件, ASCII文件的最大优势是可以显示和打印,还可以用任何文本编辑器进行编辑;而打印出来的二进制文件是无法理解的,通常二进制文件有一定的内部结构。以UNIX的某个文件为例,这个文件有五个段:文件头、正文、数据、重定位位和符号表。文件头以所谓的魔数开始,表明该文件是一个可执行的文件。魔数后面是文件中各段的长度(如数据长度、正文长度等)
- 文件的实现:
- 连续分配: 把每个文件作为一连串连续数据存储在磁盘上。这样的话,块大小为1KB的磁盘上,50KB的文件要分配50个连续的块。连续分配的优势是实现简单,记录文件用到的磁盘块简化为只需要记住两个数字即可:第一块的磁盘地址和文件的块数,同时读操作性能较好,只需一次寻找(对某一个块)。连续分配的缺点是随时间推移,磁盘会变得零碎
- 链表分配: 每一个块的第一个字作为指向下一块的指针,块的其它部分存放数据。优点是不会因为磁盘碎片而浪费存储空间,缺点是要获得块n,操作系统每一次必须都从头开始,并且要先读前面的n-1块, 太慢了。如果取出每个磁盘块的指针字,把它放在内存的一个表中,就能解决上述链表分配的不足(访问速度加快了),内存中的这样一个表格称为内存分配表(FAT), 这种方法的缺点是需要把整个表放在内存中,需要内存空间,FAT方案对于大磁盘而言不太合适。链表分配方法的缺点还包括如果同一个档案数据写入的 block 分散的太过厉害时,则我们的磁盘读取头将无法在磁盘转一圈就读到所有的数据, 因此磁盘就会多转好几圈才能完整的读取到这个档案的内容,通常所说的碎片整理就是针对这种情况。需要碎片整理的原因就是档案写入的 block 太过于离散了,此时档案读取的效能将会变得很差所致。 这个时候可以透过碎片整理将同一个档案所属的blocks 汇整在一起,这样数据的读取会比较容易啊。i节点的方法一般不需要碎片整理
- i节点: 给每个文件赋予一个称为i节点的数据结构,其中列出文件属性和文件块的磁盘地址。给定i节点,就有可能找到文件的所有块,相比在内存中采用表的方式而言,只有在对应文件打开时,其i节点才在内存中。i节点的一个问题是,如果每个i节点只能存储固定数量的磁盘地址,那么当一个文件所含的磁盘块的数目超出i节点所能容纳的数目怎么办?一种解决方案是最后一个“磁盘地址”不指向数据块,而是指向一个包含磁盘块地址的块的地址。
- 文件系统
-
文件系统存放在磁盘上,多数磁盘划分为一个或多个分区,每个分区中有一个独立的文件系统。磁盘的0号扇区称为主引导记录(MBR),用来引导计算机。在MBR的结尾是分区表,该表给出了每个分区的起始和结束地址。 一个可能的文件系统布局图如下:
-
引导区存放的是程序(该程序负责找到操作系统的镜像,并加载到内存,从而启动操作系统),一般第一个分区才有程序,为了统一起见,每个分区都会有一个引导区,即使其中没有程序。
-
超级数据块占用第1号物理块,是文件系统的控制块,超级块包括:文件系统的大小、空闲块数目、空闲块索引表、空闲i节点数目、空闲i节点索引表、封锁标记等。超级块是系统为文件分配存储空间、回收存储空间的依据
-
- 目录的实现:
- 目录是管理文件系统结构的系统文件, 打开文件时,操作系统利用用户给出的路径名找到相应目录项,目录项中提供了查找文件磁盘块所需要的信息。目录系统的主要功能就是把ASCII文件名映射成文件数据所需的信息。目录中应该有一个目录项列表,每个文件名和对应的文件属性,文件属性可以是文件的i节点。
- windows下将文件属性直接存放在目录项中,对于i节点的系统,如UNIX,则把文件属性放在i节点中而不是目录项中
- 磁盘:
五、LINUX文件系统
- Linux有一个虚拟文件系统VFS,从而能够兼容不同的文件系统
EXT2
- 这些块的不同含义请参考
鸟哥的linux私房菜
第8章 - inode 要记录的数据非帯多,但偏偏又叧有 128bytes 而已, 而 inode 记录一个 block 号码要花掉 4byte ,假设我一个档案有 400MB 且每个block 为 4K 时, 那举至少也要十万笔 block 号码的记录呢!inode 哪有这举多可记录得信息?为此我们得系统徆聪明得将 inode 记录 block 号码得区域定义为 12 个直接,一个间接, 一个双间接与一个三间接记录区。如果看不懂,请查看
鸟哥
第8章
目录
- 当我们在 Linux 下的 ext2 文件系统建立一个目录时, ext2 会分配一个 inode 与至少一块 block 给该
目录。其中,inode 记录该目录的相关权限与属性,并可记录分配到的那块 block 号码; 而 block 则
是记录在这个目录下的文件名与该文件名占用的 inode 号码数据 - 由此可知文件的inode 本身并不记录文件名,文件名的记录是在目录的 block 当中
Linux读取和新增档案的过程
- 如果我想要读取 /etc/passwd 这个档案时,系统是如何读取的呢?
- 假设我们想要新增一个档案,此时文件系统的行为是:
- 先确定用户对于欲新增档案的目录是否具有 w 与 x 的权限,若有的话才能新增;
- 根据 inode bitmap 找到没有使用的 inode 号码,并将新档案的权限/属性写入;
- 根据 block bitmap 找到没有使用中的 block 号码,并将实际的数据写入 block 中,且更新inode 的 block 指向数据;
- 将刚刚写入的inode 与 block 数据同步更新 inode bitmap 与block bitmap,并更新superblock 的内容。
EXT3是日志文件系统
- 一般来说,我们将 inode table 不 data block 称为数据存放区域,至于其他例如 superblock、 block、bitmap 与 inode bitmap 等区段就被称为 metadata(中介资料) ,因为 superblock, inode、bitmap 及 block bitmap 的数据是经常变动的,每次新增、移除、编辑时都可能会影响到这三个部分的数据,因此才被称为中介数据。
- **日志式文件系统 (journal) ** 会多出一块记录区,随时记载文件系统的主要活动,可加块系统复原时间。也就是说:
- 预备:当系统要写入一个档案时,会先在日志记录区块中记录某个档案准备要写入得信息;
- 实际写入:开始写入档案的权限与数据;开始更新 metadata的数据;
- 结束:完成数据与 metadata 得更新后,在日志记录区块当中完成该档案的记录。
- 日志文件系统优点
- 在这样得程序当中,万一数据得记录过程当中収生了问题,那举我们得系统只要去检查日志记录区块,就可以知道那个档案収生了问题,针对该问题来做一致性得检查即可,而不必针对整块filesystem 去检查, 这样就可以达到快速修复 filesystem 的能力了!这就是日志式档案最基础的功能啰~
- 日志可以记录所有文件系统操作,从而可以顺序写出文件系统或元数据(i节点、超级块)等的改动,避免了随机磁盘访问时磁头移动带来的开销
EX4与EX3差别
- 主要差别是以下两点:
- Extents。 Ext3 采用间接块映射,当操作大文件时,效率极其低下。比如一个 100MB 大小的文件,在 Ext3 中要建立 25,600 个数据块(每个数据块大小为 4KB)的映射表。而 Ext4 引入了现代文件系统中流行的 extents 概念,每个 extent 为一组连续的数据块,上述文件则表示为“该文件数据保存在接下来的 25,600 个数据块中”,提高了不少效率。
- 多块分配。 当写入数据到 Ext3 文件系统中时,Ext3 的数据块分配器每次只能分配一个 4KB 的块,写一个 100MB 文件就要调用 25,600 次数据块分配器,而 Ext4 的多块分配器“multiblock allocator”(mballoc) 支持一次调用分配多个数据块。
Linux文件系统的运作
挂载点的意义
- 每个 filesystem 都有独立的 inode / block / superblock 等信息,这个文件系统要能够链接到目录树才
目录树才
能被我们使用。 将文件系统与目录树结合的动作我们称为『挂载』。
六、输入输出
- 内存映射I/O:每个控制寄存器被分配唯一的一个内存地址,并且不会有内存被分配这一地址
- DMA: 通常CPU要比DMA控制器快得多,做同样的工作可以更快(当限制因素不是I/O设备的速度时)
- I/O控制方式:
- **程序控制I/O: **让cpu来做所有的工作,以打印一连串字符为例,程序打印一个字符,然后一直检查打印机是否打印完毕,收到完毕的消息后再发送打印的字符给打印机。这样做的缺点是忙等待会浪费大量的时间。
- 中断驱动I/O: 当打印机打印完毕并且准备好接收下一个字符时,它将产生一个中断
- 使用DMA的I/O: 让DMA来给打印机提供字符,而不必打扰CPU
魔数:很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)。根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。此外在一些程序代码中,程序员常常将在代码中出现但没有解释的数字常量或字符串称为魔数 (magic number)或魔字符串.