前言
本文主要是《Linux内核设计与实现》这本书的读书笔记,这本书我读了不下十遍,但依然感觉囫囵吞枣。我结合自己的理解,从这本书中整理出了一些运维应该了解的内核知识,希望对大家能够有所帮助。另外,推荐大家读下这边书,这本书主要讲内核设计、实现原理和方法,有利于理解内核的一些机理。
目录
- 运维为什么要了解内核
- 进程
- 系统调用
- 中断
- 内核同步
- 定时器和时间管理
- 内存分配
- 虚拟文件系统
- 块I/O层
- I/O算法
- 页高速缓存和页回写
- 关于内核的几个概念
一、运维为什么要了解内核
运维为什么要了解内核
大神Linus说了解内核的方法就是阅读源码(*Read The Fucking Source Code*),但是Linux内核学习曲线公认的陡峭,对于运维来说难度非常大,而且现代Linux已经非常庞大,别说运维了,就是专门从事Linux内核开发的人,也不可能了解到内核的全部代码。
但是运维应该了解内核的工作原理,设计哲学,了解CPU、网络的调度方法,了解内存、文件系统的结构。
了解了Linux系统如何工作,我们才能更好的使用它,让它为我们服务。
Linux的由来
内核为什么吸引人,很重要的一个原因是自由精神,可以随手拿到源码,只有愿意,可以了解到每个功能非常细微的地方。
Linux内核是如何来的,1991年,芬兰的大学生Linus热衷于使用Minix,一种教学用的Unix系统,但是他不能随意修改和发布该系统的源代码,这令他对这个系统的设计理念感到失望,于是就自己在386上设计了一款系统,并发布到了互联网上,很快就流行了起来。
顺便说下,Linux的吉祥物为什么是企鹅,那是因为Linus小的时候,被一只企鹅咬过,令他印象深刻。关于Linus还有一本书,叫做《只是为了好玩--linux之父林纳斯自传》,大家有兴趣可以阅读下。
我这里有一些数据,来自2017年度Linux内核开发者报告,通过这些数字,大家对目前的内核生态会有简单的了解。
目前,已有超过1400家公司的15600名开发人员参与了Linux内核的开发。仅就2016年到2017年,超过500家公司的4300多名开发人员对内核做出了贡献;其中有1670个开发者是第一次贡献,约占贡献者的三分之一。
2017年度,赞助Linux内核开发的十大组织包括英特尔、Red Hat、Linaro、IBM、三星、SUSE、谷歌、AMD、Renesas和Mellanox。
Linux开发的速度继续增加,参与开发的人员和公司的数量也在不断增加。内核每小时的平均变化量为8.5,比2016年报告中的7.8个变化显著增加,这意味着每天有204个变化,每周超过1400个变化。
从2016年的66天开始,平均每个版本的开发天数从去年的66天增加到67.66天,每一个版本的间隔时间分别为63或70天,提供了显著的可预测性。4.9和4.12开发周期的特点是,在内核项目历史上看到的最高补丁率。
未领取薪酬的开发者可能正在趋于稳定,这些开发者贡献了8.2%的贡献,比去年的7.7%有所增加。这一数字仍远低于2014年的11.8%。这可能是由于内核开发人员短缺,导致那些有能力提交一定质量补丁的人,在找到工作时没有困难。
新加入内核开发的前三名是英特尔、谷歌、华为,其中华为投入33名工程师。
Linux内核的设计哲学
Linux内核设计参考了Unix,并且兼容Unix API,但是Linux内核吸收了Unix系统的优点,摒弃了一些缺点。
先来了解一个概念,单内核和微内核。
- 单内核是整体单独的一个过程,存储方式往往也是一个大的二进制文件,使用的也是连续的一整块内存。所有服务都运行在内核态,内核之间的通信就很容易,内核可以直接调用函数。
- 微内核是按照功能划分为多个独立过程,这个过程叫做服务器,只有少数特权服务的服务器才运行在特权模式下,大部分服务运行在用户空间。大部分服务都使用自己的内存地址,不可能像单内核那样直接调用函数,而是要通过消息传递,系统采用进程间通讯的机制,专业术语叫IPC机制。这样的好处是一项服务失效,并不会影响到其他服务,因为彼此隔离。
因为IPC机制的开销多用于函数调用,有大量的内核空间和用户空间的上下文切换,因此,消息传递需要一定的周期,而单内核就没有这个问题。
这样还造成一个结果,就是实际上,微内核为了提高效率,会让大部分服务位于内核态。
Windows NT内核系统,包括Windows7 Windows10 Windows Server系列,MacOS都是典型的微内核系统。
前段时间,华为推出的鸿蒙系统,也宣称是微内核系统。
Linux系统是单内核系统,也就是说Linux系统运行在单独的内核地址空间上,不过Linux吸取了微内核的精华,引入了模块化设计,抢占式内核,支持内核线程,及动态装载内核的能力。同时还避免了微内核设计上的性能损失。
可见Linux的设计哲学是实用主义优先。
再解释下什么是内核抢占,抢占指的是内核具有允许在内核运行的任务优先执行的能力,大部分Unix系统是不支持这个能力的。
再来介绍下内核的版本,内核有两种版本,稳定版和开发版,稳定版有工业级的强度,可以广泛部署,开发版主要用于实现新的功能。
Linux内核通过简单的命名机制区分稳定版和开发版,使用3个或者4个点分隔数字,代表不同的版本,第一个数字是主版本号,第二个数字是从版本号,第三个数字是修改版本号,第四个数字是可选,是稳定版本号。从第二个数字可以看出是稳定版还是开发版,如果是偶数就是稳定版,如果是奇数就是开发版。
比如内核版本2.6.26.1就是稳定版,因为它的第二个数字是6,是偶数。内核版本4.9就是开发版,因为9是奇数。
二、进程
先来聊聊Linux内核开发,内核开发和普通应用开发有两个地方不一样:
- 自己要管理内存,普通应用跑在内核之上,内核可以帮你管理内存,但是你自己就是内核,你必须自己做好内核管理,要不很容易就内核溢出了。
- 没有库文件,普通应用程序有很多库文件可以调用,内核开发则没有,内核开发就是标准的C。
由此看见,做内核开发还是要对内核有深刻的理解才可以,请注意,这里的内核开发指的是内核核心功能的开发。
我们再来看看进程,进程简单的讲,就是运行中的程序,我个人理解,进程是一种生命形式,就像一个人的生命,从呱呱坠地开始一直到生命的终结,中间需要不停的从周围的环境吸收资源,并且对环境也施加影响。
进程需要的资源就是CPU、内存、文件、网络等资源,进程虽然是从程序文件开始,但是不等于程序文件,一个程序文件可以启动多个进程,一个进程也可能是由多个程序文件产生的,所以进程是一种运行中的状态。
内核用一个双向循环链表来描述进程的状态,这一链表在32位的机器上是1.7KB大小,链表中的每一项都是类型为task_struct,称为进程描述符的结构。进程描述符就不详细介绍了。
下面我们来看看进程的状态标志,进程有5种状态标志:
- 第一 task_running 运行,进程正在运行,或者正在队列中等待运行,运行的进程可以在用户空间,也可以在内核中。
- 第二 task_interruptible 可中断,或者被阻塞,等待某些条件,一旦达到条件就被唤醒,然后进入运行状态。
- 第三 task_uninterruptible 不可中断,这种状态,即使收到外部的信号,也不会被唤醒,这种状态一般用的比较少。
- 第四 task_traced 被其他进程跟踪,例如通过ptrace对进程进行跟踪调试。
- 第五 task_stoped 停止,进程没有运行也不能运行的状态。
下面在介绍几个概念
第一个概念,进程上下文
进程从可执行文件载入进程的内存地址空间运行,一般是在用户空间,当进程调用了系统接口,或者触发了某种异常,它就进入了内核空间,此时,我们称内核代表进程执行,并处于进程上下文中,总结下,就是内核和进程交互的时候,就是上下文状态,请注意,后面我们还会介绍中断的上下文,和进程的上下文是有区别的,中断上下文中,系统不代表进程执行,而是执行一个中断程序。
第二个概念,进程家族树
Linux系统中,所有的进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本,并执行其他的相关程序,最终完成系统的启动。
系统中的每个进程必有一个父进程,每个进程也会拥有灵感或者多个子进程,拥有同一父进程的所有进程被称为兄弟进程,进程间的关系也保存在前面提到的进程描述符中。
第三个概念,写时拷贝
Linux系统创建新的进程的时候,使用的是写时拷贝的技术,这样的好处是可以推迟甚至免除数据拷贝,子进程共享父进程的资源,只有当需要写入的时候
我们通过几个概念的解释,来清晰化下内核对进程的调度
第一 什么是进程调度
进程调度就是决定进程什么时候运行,可以运行多长时间,进程调度程序的使命就是尽可能的让进程多运行,提高效率。
第二 什么是多任务
多任务就是能够并发的执行多个进程,在单处理器上,这是一个假象,其实就是多个进程快速的在处理器上快速切进切出。
第三 什么是抢占式内核
多任务系统可以划分两类,非抢占式多任务和抢占式多任务,抢占式多任务就是由内核决定什么时候停止进程的运行,这个强制的动作就叫抢占。相反,除非进程主动停止,否则就一直运行,就是非抢占式多任务,显然,非抢占式多任务要依靠进程的自觉和良好设计,很古老的Windows3.1就是这样的系统,我大概是20年前接触到的,1996年的时候,这样的系统一个特点就是容易死机,但是当时看惯了黑黑屏幕的dos,看到窗口式的Windows,给人还是非常震撼的感觉。
第四 时间片
进程被抢占之前的时间是预先设置好的,有一个专门的名字,就是进程的时间片,调度策略必须规定一个默认的时间片,这里需要平衡,时间片太长影响系统的交互体验,时间片太短,会增加进程切换的频率,引起过多处理器消耗。
许多操作系统有默认的时间片长度,比如10ms,但是linux没有默认的时间片长度,Linux按照比例来划分,这样负载大的进程获得的处理器使用时间就更长。
第五 Linux的调度算法
在2.4内核以前,Linux内核调度很简陋,2.5内核中引入了Q(1)的调度程序,可以完美支持几十个处理器的进程调度,但是Q(1)算法对对时间敏感的程序有一些先天不足,因此Q(1)适合服务器,但是不适合桌面系统。
2.6内核中,引入了完全公平算法,简称是CFS,目前Linux系统默认使用的都是CFS算法。
第六 IO消耗型和处理器消耗型的进程
IO消耗型的进程总在等待IO请求,占有处理器时间比较少,大部分用户图形界面程序都是IO消耗型。相反,如果处理器消耗型进程,就是把时间大多用于代码执行上,IO请求比较少。
当然也有即是IO消耗型也是处理器消耗型的进程,比如字处理程序,大部分时间是IO消耗型,但是当执行拼写检查的时候,就是处理器消耗型。
进程调度策略经常要在进程响应速度和最大系统利用率之间找平衡,这个背后是复杂的算法,不同操作系统的倾向性也不一样,Linux系统倾向io消耗型,这样响应速度快,用户体验好。
第七 进程优先级
Linux采用两种不同的优先级范围:
- 第一种是nice值,范围是-20到 19,默认是0,越低的nice值,可以获得更多的处理器时间。
- 第二种是实时优先级,变化范围是0到99,和nice值相反,越高的值,优先级越高。
两种优先级划分有什么区别,任何实时进程优先级高于普通进程,就是说两种优先级处于互不交互的两个范畴。
进一步说明下, 进程分为普通进程和实时进程,普通进程使用CFS算法调度,优先级按照nice值区分。
实时进程有两种调度算法,FIFO,即先进先出,这种进程一直占用处理器,直到自己受阻塞或者释放处理器,如果有多个FIFO优先级进程,则会轮流执行。
另外一种实时进程调度算法是RR,RR进程是按照时间片分配的,优先级范围就是0到99 。
注意,再强调下,实时进程总会抢占普通进程。
三、系统调用
用户空间进程不是和硬件设备直接通讯的,而是有一个中间层,这样做的好处有三个:
- 第一, 为用户空间提供了一种硬件的抽象接口,这样用户空间进程就不用关心具体的硬件信息。
- 第二,限制了用户空间进程的行为,防止对其他进程造成影响,保证了系统的稳定和安全。
- 第三,隔离进程使用的资源, 方便内核调度。
一般的进程调用是通过API实现的,不是直接调用内核,API有一套标准,叫POSIX,Unix,Linux,甚至Windows都支持POSIX,只是大家支持的程度不一样。
具体内核的API如何实现,这个要依靠Linux内核程序员,关于系统调用,运维了解到这些知识就可以了。
四、中断
还是通过几个概念来了解中断。
第一 什么是中断
中断就是键盘、鼠标、硬盘、显卡、网卡等硬件和处理器的通讯。
大部分硬件的运行速度和处理器比起来低很多,硬件要和处理器通讯,有两种方式,一种方式是处理器轮询各个硬件,一种方式是硬件主动来找处理器,实际上是硬件给处理器主动上报,因为这种方式效率更高,硬件在需要的时候给处理器发出信息,处理器来响应,这个就是中断处理。
中断信息实际就是电信号,硬件,比如键盘控制器,在你敲击键盘的时候会发出中断,信号进入中断控制器,然后进入处理器,处理器再通知操作系统。
第二 IRQ
不同的设备对应的中断不同,每个中断都有唯一的数字标志,这样系统就能区分具体的设备,这些中断值被称作IRQ,中文的意思就是中断请求线。
比如,在经典的PC机上,IRQ0是时钟中端,IRQ1是键盘中断,但是这样也有问题,设备越来越多,原来的设计,中断号有限,经常会引起冲突,我记00年初,刚有声卡的时候,经常声卡因为中断冲突而不能使用,解决方法就是更换一个PCI插槽。
所以后来就有了动态分配中断值的方法,PCI设备都是动态分配中断号的,最终的目标关键是硬件能和处理器通讯,能够引起处理器注意。
第三 异常
异常简单的说,就是程序出错,需要内核来处理的时候,通常由于编程失误而导致的错误指令,比如被0除,或者是在执行期间出现特殊情况,比如缺页。这时候就需要内核来处理,因为处理器体系结构处理异常与处理中断方式类似,因此,内核对他们的处理也很类似,实际上,异常也常常被称为同步中断。
第四 中断处理程序
在响应一个特定的中断的时候,内核会执行一个函数,这个函数就是中断处理程序interrput handler ,或者中断服务例程interrupt service routine,简称ISR。产生中断的每个设备都一个相应的中断处理程序。
第五 中断的上半部和下半部
又想中断处理程序运行的快,又想中断处理程序完成的工作量多,这是矛盾的,为了解决这个矛盾,我们把中断处理切为两个部分,中断处理程序是上半部top half,接收到中断,立即开始执行,但只做严格时限的工作,例如对接收的中断进行应答和复位硬件,这些工作都是在所有中断被禁止的情况下完成的,所以必须尽可能快的完成。能够被允许稍后完成的工作推迟到下半部,bottom half.
用网卡做一个例子解释下,当网卡接收到网络的数据包的时候,需要通知内核数据包到了,网卡需要立即完成这件事,从而优化网络的吞吐量和传输周期,以避免超时。这时候中断开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包,这些都是重要、紧张而又与硬件相关的工作。
内核需要快速拷贝网络数据包到内存,因为网卡的缓存的大小是固定的,如果速度不够快,就会造成溢出,网卡就会丢弃数据包。
当数据拷贝到内存,中断的任务就完成了,它将控制权交还给系统系统中断前运行的程序,数据处理在随后的下半部进行。
第六 中断上下文
当执行一个中断处理程序的时候,内核处于中断上下问interrput context,我们回忆下前面提到的进程上下文,进程上下文是内核所处的操作模式,此时内核代表进程执行。
与进程上下文相反,中断上下文和进程没有关系,因为没有后备进程,所以中断上下文不可以睡眠,中断上下文有严格的时间限制,因为它打短了其他代码,
在Linux系统中,查看中断的情况,可以使用命令,可以看出详细的中断情况:
cat /proc/interrputs
中断上半部处理需要紧急处理的任务,包括对时间敏感,和硬件息息相关,不希望被其他中断打断的任务,其他不紧急的任务,都交给下半部处理。
通常我们希望尽可能的将任务交给中断下半部处理,因为上半部处理的时候,会造成其他中断被屏蔽,那么下半部是如何处理的呢,有三种方法。
- 第一种方法, BH,即bottom half,这是最早的中断处理机制,也是早期的唯一方法,同时只能有一个BH处理,即使有多个处理器。从内核2.5 版本开始,BH方法已经被放弃。
- 第二种方法,任务队列,为了充分使用多处理性能,内核开发者引入了任务队列的机制,task queue,内核定义了一组队列,驱动程序来和队列匹配,任务队列的方案在处理性能要求比较高的子系统,比如网络部分,也不能胜任。
- 第三种方法,软中断和tasklet,这种方法是在内核2.3版本中引入的,软中断可以在所有处理器上同时执行,tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制,两个不同类型的tasklet可以同时在不同的处理器上执行,但是类型相同的tasklet不能同时执行,tasklet是性能和易用性之间平衡的产物,可以处理大部分下半部中断处理。像网络这样对性能要求比较高的情况,才需要使用软中断。
五、内核同步
我们还是通过几个概念来了解下什么是内核同步。
第一个概念 为什么会有内核同步问题
在使用共享内存的应用程序中,程序员必须特别留意保护共享资源,防止共享资源并发访问,防止多个线程同时访问和操作数据,造成数据互相覆盖,和数据不一致。
在单一处理器的时候,这个还好办,只有在中断发生,或者重新调度另一个任务的时候,数据才可能被并发访问。
到了多处理的时代,问题变的复杂,多处理器意味者着内核代码可以同时在两个或者两个以上的处理器上运行,为了防止同时改写内存数据的情况发生,就必须引入内核同步机制。
第二个概念 临界区和竞争条件
临界区是指访问和操作共享数据的代码段,多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码是原子的执行,也就是说,操作在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。如果两个执行线程有可能处于同一个临界区中同时执行,那么就是程序包含的bug。如果这种情况确实发生了,我们就称它为竞争条件,这种情况出现的机会非常小,就是因为竞争引起的错误非常不容易重现,所以调试这种错误才会非常困难,避免并发和防止竞争条件称为同步。
第三个概念,加锁
为了防止一个处理器的进程在处理数据,而另外一个处理器上的进程也同时修改这些数据,就需要给这块数据加锁,确保同时只能有一个进程访问数据。
加锁也是技术活,锁有多种多样的形式,加锁的粒度和范围也各不相同。
第四个概念 伪并发和真并发
在单处理器上,用户进程可能在任何时刻被抢占,也可能造成共享内存被修改,两个进程是交叉进行的,所以被称为伪并发。
在多处理器上,有可能真的两个进程在同时访问共享内存,因此被称为真并发。
内核中有以下类似的可能,造成并发执行,他们是:
- 中断,中断可能随时打断正在执行的代码。
- 软中断和tasklet,内核能在任何时刻唤醒或者调度软中断和tasklet,打断当前正在执行的代码。
- 内核抢占,因为内核具有抢占性,内核中的任务可能会被另一任务抢占。
- 睡眠及用户空间的同步,在内核执行的进程可能睡眠,这就会唤醒调度程序执行另外一个进程。
- 对称多处理,两个或者多个处理器同时执行代码。
第五个概念,死锁
死锁的产生需要一定条件,要有一个或多个执行线程和一个或者多个资源,每个线程都在等待其中的一个资源,但所有的资源都被占用了。所有线程都在等待,但他们永远不会释放已经占有的资源,于是所有线程都无法继续,这便意味着死锁的发生。如何防止死锁的发生,也是程序设计的时候要考虑的问题。
第六个概念 争用和扩展性
一个资源被锁定,多个进程都在竞争这个资源,被称为锁的争用,锁的争用会造成系统瓶颈,严重降低系统性能。
解决办法就是扩展性,将锁的范围尽量精细,这样就可以减少锁的争用,但是过于精细,也会额外消耗系统资源,所以掌握好平衡就需要技巧。
六、定时器和时间管理
时间管理在内核中占有非常重要的位置,内核中的函数驱动方式,可以分为事件驱动和时间驱动,其实时间驱动也可以认为是特殊的事件驱动,但是内核中,时间驱动的频率特别高。
时间驱动也可以分为周期驱动,比如每秒100次,或者推后执行,比如500ms以后执行某个任务。
另外,内核还必须管理系统的运行时间以及当前日期和时间。
这里还有一个概念,相对时间和绝对时间,如果某个事件在5s之后被执行,那么系统需要的是相对时间,相反,如果要求管理当前日期和当前时间,则内核不但要计算流逝的时间而且还要计算绝对时间。
周期性产生的事件,比如每10ms一次,都是由系统定时器产生的,系统定时器是一种硬件可编程芯片,可以固定频率产生中断,这个中断就是定时器中断.
在x86体系中,系统定时器默认频率是100Hz,也就是说i386处理器上每秒中断100次,即10ms一次,注意,每种体系的频率可能不一样,有的是250,有的是1000。频率可以在编译内核时指定。
从2.5内核版本开始,中断频率被设定为1000Hz,使用高频率的好处是准确度,精确性更高,但是同时系统负担更重,也更耗电,但是处理器性能越来越高,这点消耗不会对系统造成过大的影响。
七、内存分配
内核把物理页作为内存管理的基本单元,尽管处理器的最小可寻址单元通常为字(甚至字节),但是,内存管理单元MMU通常以页为单位进行处理。
体系不同,页的大小也不一样,大部分32位体系结构支持4KB的页,64位体系结构一般支持8KB的页,这意味着,在1GB物理内存的机器上,4KB页大小,物理内存会被划分为262144个页。
由于硬件的限制,内核并不能对所有的页一视同仁,有些页位于特定的物理地址上,所以不能将其用于特定的任务,由于存在这种限制,所以内核把页划分为不同的区zone。
Linux必须处理如下两种由于硬件存在缺陷引起的寻址问题:
- 一些硬件只能用某些特定的内存地址来执行DMA,即直接内存访问。
- 一些体系结构的内存物理寻址范围比虚拟寻址范围大得多,这样,就有一些内存不能永久地映射到内核空间上。
因为存在这些限制条件,Linux主要使用了四种区:
- ZONE_DMA 这个区包含的页用来执行DMA操作
- ZONE_DMA32 和ZONE_DMA相似,但是这个区只能被32位设备访问
- ZONE_NORMAL 这个区包含的都是能正常映射的页
- ZONE_HIGHEM 这个区包含高端内存,其中的页不能永久映射到内核地址空间。
一般DMA区使用0-16MB的内存,NORMAL区使用16-896MB的内存,HIGHEM区使用896MB以上的内存。
八、虚拟文件系统
虚拟文件系统作为内核的子系统,简称VFS,为用户空间程序提供了文件和文件系统相关的接口,系统中的所有文件系统不但依赖VFS共存,并且依靠VFS协同工作。
VFS提供了通用的接口和方法,比如open(),read(),write(),系统调用的无需考虑具体文件系统和实际物理介质。
之所以可以这样,是因为内核在底层文件系统接口上建立一个抽象层,抽象层使Linux能够支持各种文件系统。VFS抽象层定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。任何新的文件系统和新介质只要符合VFS规范,都可以直接使用。
unix系统使用四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和挂载点。从本质上讲文件系统是特殊的数据分层存储结构,包含文件、目录和相关的控制信息。
VFS采用面向对象的设计思路,使用一组数据结构来代表通用文件对象。
VFS有四个主要的对象类型:
- 超级块对象,代表一个具体的已安装文件系统;
- 索引节点对象,代表一个具体文件;
- 目录项对象,代表一个目录项,是路径的一个组成部分;
- 文件对象,代表由进程打开的文件。
另外,说明下,因为VFS将目录作为文件来处理,所以不存在目录对象。
我们总结下,Linux支持了多种类型的文件系统,从本地文件系统,例如ext3,ext4,到网络文件系统比如NFS。LInux在标准内核中已支持的文件系统超过60种。VFS层提供给这些不同文件系统一个统一的实现框架,而且也提供了能和标准系统调用交互工作的统一接口。由于VFS层的存在,使得Linux上实现新文件系统的工作变得简单起来,它可以轻松地使这些文件系统通过标准Unix系统调用而协同工作。
九、块I/O层
我们还是通过五个概念了解块IO层
第一个概念 块设备和字符设备。
系统能够随机访问固定大小数据片的硬件设备称为块设备,数据片的英文术语是chunk,硬盘、软盘、光盘、SSD、U盘都属于块设备,因为系统随时可以访问这些介质上的任意位置数据,另外,说明下,对这些介质的访问,是通过访问文件系统实现的。
字符设备是按照字符流的方式被有序访问的设备,像串口和键盘就属于字符设备。
块设备和字符设备主要的区别就是随机访问方式还是顺序访问方式。
第二个概念 扇区和块。
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,硬盘最常见的扇区大小是512字节,CD-ROM的扇区一般是2KB。
每种文件系统都有自己最小的逻辑可寻址单元,块。块是文件系统的抽象,只能基于块来访问文件系统。
扇区和块的区别是,物理磁盘寻址是按照扇区级进行的,文件系统是按照块来进行的。块大小必须是扇区的倍数,一般是2的整数倍,并且不能超过一个内存页大小,因为文件块需要被缓存到内存中。所以一般文件块的大小是512字节,1KB,4KB。
另外,磁盘还有一些术语,比如簇,柱面,磁头,请大家自己找资料看下。
第三个概念 缓冲区。
当一个块被调入内存时,就存储在一个缓冲区中,每个缓冲区与一个块对应,相当于磁盘块在内存中的表示。像前面介绍的,块包含一个或多个扇区,但是大小不能超过一个页面,所以一个内存页可以容纳一个或者多个内存中的块。
第四个概念 请求队列。
块设备将它们挂起的块IO请求保存在请求队列中,请求队列只要不为空,队列对应的块设备驱动程序就会从队列头部获取请求,然后将其送入对应的块设备上去。
第五个概念 IO调度程序。
如果简单的以内核产生请求的次序直接将请求发向块设备的话,性能肯定让人难以忍受,磁盘寻址是整个计算机中最慢的操作之一,每次寻址,定位磁头到特定的块上的某个位置,需要花费不少时间,所以尽量缩短寻址时间无疑是提高系统性能的关键。
为了优化寻址操作,内核既不会简单的按请求接收文件,也不会立刻将其提交给磁盘。相反,内核会在提交前,先执行合并与排序的操作,这种操作可以极大的提高系统性能,在内核中负责提交IO请求的子系统,称为IO调度程序。
IO调度程序将磁盘IO资源分配给系统中所有挂起的块IO请求,这种资源分配是通过请求队列中挂起的请求合并和排序来完成的。
IO调度器的工作是管理块设备的请求队列,它决定队列中的请求排列顺序以及在什么时刻发送请求到块设备,这样做有利于减少磁盘寻址时间,从而提高全局吞吐量。注意,全局这个定语很重要,因为IO调度器可能为了提高系统整体性能,会对某些请求不公。
IO调度器通过两种方法减少磁盘寻址时间,合并与排序。举个例子,文件系统接到多个请求队列,IO调度器可以按照磁盘扇区顺序进行排序,那么相邻扇区的访问就可以合并为一次,这样就大大减少了磁盘寻址消耗。即使没有相邻扇区的访问,通过IO调度器,按照磁盘旋转方向访问,也缩短了所有请求的磁盘寻址时间。
十、I/O算法
第一种算法 linus电梯
在2.4版本内核中,linus是默认的IO调度程序,linus算法能够执行合并与排序预处理,当有新的请求加入队列时,它首先检查其他每一个挂起的请求是否可以和新请求合并。linus电梯算法可以执行向前和向后合并,如果新的请求没有合适的插入点,则会被放入队列尾部。
另外,系统中如果有驻留时间过长的请求,新的请求也会被放到队列尾部,这样做的目的是防止对一个磁盘位置访问的过多,造成对其他磁盘位置的请求被饿死。但是这样的做法,因为仅仅是改变队列排序,没有队列的时间检测,不能完全避免有队列被饿死的情况。
第二种算法 最终期限
最终期限deadline IO调度算法是为了解决linus电梯算法所带来的饥饿问题而提出的。出于减少磁盘寻址时间的考虑,对某个磁盘区域的频繁操作,会使对磁盘其他位置的操作请求饿死。
更糟糕的是,普通的请求还会造成写-饥饿-读这种问题。
写请求通常可以缓存,但是读请求的时候,程序会被阻塞,直到拿到请求的读数据,也就是写请求是异步的,读请求是同步的,如果有大量的读请求的时候,写请求就会被饿死。
问题可能还会更严重,如果读请求和写请求是相互依靠的,写请求没有操作,读操作又去请求数据,就会造成应用更长时间的等待。
最终期限算法中,每个请求都有一个超时时间,默认读请求的超时时间是500ms,写请求的超时时间是5s。
最终期限算法有三个队列,在超时时间内,调度类似于linus电梯,有一个排序队列,另外维护两个按照时间顺序的读fifo队列,和写fifo队列。
在超时时间内,按照排序队列派发操作,如果读写队列的列头请求超时,那么IO调度程序便从队列中提取请求进行服务,这样就能保证不发生磁盘操作请求超时的情况。
通过最终期限算法,可以避免写操作饿死,同时因为读操作超时时间短,这种算法也优化了大量读操作的响应。
第三种 算法 预测IO调度
预测IO调度和最终期限一样,也是维护三个一样的队列,不同的是,在提交请求的之前,会有意等待一段时间,默认是6ms,如果有新的请求来,在将相邻扇区的请求合并,这样可以优化磁盘操作。当然,如果没有操作请求,会浪费几毫秒的时间。
第四种算法 完全公平的排队IO调度CFQ
CFQ调度程序把进入IO的请求放去特定的队列中,这种队列请求是根据引起IO请求的进程组织的,在每个队列中,刚进入的请求和相邻请求进行合并。
CFQ调度程序以时间片轮转调度队列,从每个队列中选取请求固定数字的操作,默认为4,然后进入下一轮调度。这样在进程级实现了公平。
目前内核默认的调度算法是CFQ。
第五种算法 空操作
之所以这样命名,是因为这种算法基本不作什么事情,基本就是先进先出,当然,如果相邻的操作能够合并,还是会合并,空操作懒惰是有道理的,因为这种算法是用在闪存设备上,如果设备没有寻址负担,那么也没有必要对其排序。
十一、页高速缓存和页回写
我们还是通过解答几个问题,来了解页高速缓存和页回写。
第一个问题,为什么会有页高速缓存
这个主要原因是因为内存和磁盘的速度差距非常大,磁盘的读写速度是毫秒级别的,内存的读写速度是纳秒级别的,如果能够通过内存缓存磁盘数据,就可以大大提高系统速度。另外,被访问的数据,很有可能再次被访问,如果能够把数据缓存到内存中,那么数据如果再次被频繁访问,就可以提高系统性能。
第二个问题,写磁盘如何缓存
写缓存有三种方式,第一种是不缓存,就是当写数据时,直接写到磁盘,这种方式数据最安全,但是性能最低。第二种方式是透写,数据先写到缓存,然后立刻写磁盘,这样数据也是很安全,但是性能也比较低。第三种方式是回写,writeback,数据写到缓存,就认为成功,到一定时间,或者数据比较多的时候,再写盘,这种方式性能很好,但是如果数据在缓存中的时候,机器突然断电,有可能数据丢失。
第三个问题,读缓存的回收策略是什么
因为内存有限,不可能把整个磁盘的数据缓存到内存中,只能保证把比较热的数据缓存起来,那么如何确认数据比较热呢,有两种算法,一种是根据时间,系统扫描页面,没有被访问的,时间比较久的页面,就会被释放掉,还有一种算法,是双链表,或者多链表,增加了一些统计的概念,更精确一些。
第四个问题,笔记本电脑模式
在笔记本电脑上,因为有电池,同时为了提升性能,一般启用的都是回写模式,并且刷新磁盘的时间间隔更长,这样还可以省电。目前的大部分系统也可以在笔记本电脑启用电池时,自动修改回写策略。
另外也可以执行命令sync,强制系统刷盘。一般在个人版的系统上,默认都是开启回写,这样性能会好很多,但是在服务器系统上,一般默认都是透写模式,因为在服务器上,数据更重要。这也是为什么有时候你会发现,在个人PC上,磁盘写性能居然要好于服务器的原因。
十二、关于内核的几个概念
第一个概念,Linux的设备类型
在Linux及Unix中,设备被分为三种类型:
- 块设备
- 字符设备
- 网络设备
块设备缩写为blkdev,块设备以块为单位,并且是可以寻址的,即可以随机访问任何位置的数据。块设备通常被挂载为文件系统来使用。
字符设备缩写为cdev,字符设备不可寻址,只能流式访问,与块设备不同,应用程序通常直接和块设备交互。
网络设备通常是通过物理设备和IP协议提供的,网络设备打破了unix一切皆文件的设计原则,对网络设备的访问是通过套接字API实现的。
Linux还提供了其他设备类型,但都是针对单个任务,而非通用的。
另外,并不是所有设备驱动都表示物理设备,有些设备驱动是虚拟的,称之为“伪设备”,最常见的是内核随机数发生器/dev/urandom,空设备/dev/null,零设备/dev/zero等。
尽快Linux内核是单块内核的操作系统,但是整个内核是模块化的,允许在运行时动态的插入或者删除代码,即所谓的可装载内核模块。
第二个概念,内核的可移植性
Linux是可移植性非常好的操作系统,支持许多不同体系的计算机。可移植性是指操作系统代码从一套体系迁移到另外一套体系的方便程度。
在操作系统可移植性方面,设计有两种思路。
- 一种思路是尽量追求通用性,尽量少的使用汇编语言,这样设计出来的操作系统可移植性非常高,但是缺点是不能针对某种体系深入优化。
- 还有一种思路就是基本不考虑可移植性,只对一种体系深度优化,Windows系统就是这样的系统,主要就是针对x86系统优化,但是可移植性极差。
Linux系统走了一条中间道路,差不多所有的接口和核心代码都是独立于硬件的,但是,对于性能要求很严格的部分,内核会针对不同体系调整,这使得linux在可移植性和性能之间取得比较好的平衡。
第三个概念 社区
大家都知道,linux是开源的,社区和代码随时可以访问,只要有兴趣,也可以随时参与社区活动。但是linux入门门槛比较高。需要一个比较长的过程,只要坚持,最终会跨过这个门槛。
后记
本文通过十二部分蜓蜓点水式的介绍,希望能够帮助大家能记住并理解几个概念。如果有兴趣更深入的了解,推荐阅读下《Linux内核设计与实现》这本书。