一、硬件结构
[!指令周期(Instrution Cycle)] CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令); CPU 对指令进行解码,这个部分称为 Decode(指令译码); CPU 执行指令,这个部分称为 Execution(执行指令); CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写);
算术操作、逻辑操作,还是进行数据传输、条件分支操作,都是由算术逻辑单元操作的
无条件地址跳转,则是直接在控制器里面完成的,不需要用到运算器。
时钟频率是 1 G
时钟周期时间就是 1/2.4G
。
CPU
时钟周期数
每条指令的平均时钟周期数 CPI,表示一条指令需要多少个时钟周期数
流水线
所以只有运算大数字的时候,64 位 CPU
的优势才能体现出来,否则和 32 位 CPU
的计算性能相差不大
可以寻址更大的内存空间
64 位和 32 位软件,实际上代表指令是 64 位还是 32 位的:
如果 64 位指令在 32 位机器上执行,就比较困难了,因为 32 位的寄存器存不下 64 位的指令
硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽。
优先级
在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:
优先级在0~99
范围内的就算实时任务;
对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片
用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;
完全公平调度(Completely Fair Scheduling)
在 CFS
算法调度的时候,会优先选择 vruntime
少的任务
多任务的数量基本都是远超 CPU 核心数量,因此这时候就需要排队。
其中 csfrq
是用红黑树来描述的,按 vruntime
大小来排序的,最左侧的叶子节点,就是下次会被调度的任务。
**实时任务总是会比普通任务优先被执行 **
nice 的值能设置的范围是 -20 ~ 19
, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。
nice 调整的是普通任务的优先级,所以不管怎么缩小 nice 值,任务永远都是普通任务,
中断
中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。
中断是一种异步的事件处理机制,可以提高系统的并发处理能力。
中断请求的响应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度地影响。
而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。
上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。下半部用来延迟处理上半部未完成的工作,一般以内核线程的方式运行。
网卡收到网络包后,会通过硬件中断通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来响应该事件,这个事件的处理也是会分成上半部和下半部。
接着,内核会触发一个软中断,把一些处理比较耗时且复杂的事情,交给软中断处理程序去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。
上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行; 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;
软中断不只是包括硬件设备中断处理程序的下半部,一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁(内核里常用的一种锁)等。
但是系统的中断次数的变化速率才是我们要关注的,
watch -d cat /proc/softirqs
命令查看每个软中断类型的中断次数的变化速率。
如果发现 NET_RX
网络接收中断次数的变化速率过快,接下里就可以使用sar -n DEV
查看网卡的网络包接收速率情况,然后分析是哪个网卡有大量的网络包进来。
Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs
来观察软中断的累计中断次数情况,如果要实时查看中断次数的变化率,可以使用watch -d cat /proc/softirqs
命令。
用 ps
命令来查看内核线程,一般名字在中括号里面到,都认为是内核线程。
如果在top
命令发现,CPU 在软中断上的使用率比较高,而且 CPU 使用率最高的进程也是软中断ksoftirqd
的时候,这种一般可以认为系统的开销被软中断占据了。
可以用sar
命令查看是哪个网卡的有大量的网络包接收,再用tcpdump
抓网络包,做进一步分析该网络包的源头是不是非法地址
话不多说,我们就以 8.625 转二进制作为例子,直接上图:
[插图]
进制转换
最后把「整数部分 + 小数部分」结合在一起后,其结果就是 1000.101。
但是,并不是所有小数都可以用二进制表示,前面提到的 0.625 小数是一个特例,刚好通过乘 2 取整法的方式完整的转换成二进制。
把这种整数部分没有前导 0 的数字称为规格化
基数为 2
保证小数点左侧只有 1 位,而且必须为 1
000101 称为尾数,即小数点后面的数字
3 称为指数,指定了小数点在数据中的位置;
由于同时都带有一个固定隐含位
把小数点,移动到第一个有效数字后面
右移 3 位就代表 +3
float
中的「指数位」就跟这里移动的位数有关系,把移动的位数再加上「偏移量」,float
的话偏移量是 127
,相加后就是指数位的值了
加上偏移量
把指数转换成无符号整数
既然这一位永远都是 1,那就可以不用存起来了。
可以节约 1 位的空间,尾数就能多存一位小数,相应的精度就更高了一点
[插图]
统一和正数的加减法操作一样
十进制整数转二进制使用的是「除 2 取余法」,十进制小数使用的是「乘 2 取整法」。
二、操作系统结构
让内核作为应用连接硬件设备的桥梁
内核空间,这个内存空间只有内核程序可以访问; 用户空间,这个内存空间专门给应用程序使用; 用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。
因此,当程序使用用户空间时,我们常说该程序在用户态执行,而当程序使内核空间时,程序则在内核态执行。
当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作
SMP
的意思是对称多处理,代表着每个 CPU
的地位是相等的,对资源的使用权限也是相同的,多个 CPU
共享同一个内存,每个 CPU
都可以访问完整的内存和硬件资源。
ELF
的意思是可执行文件链接格式,它是 Linux
操作系统中可执行文件的存储格式
编写的代码「编译器」编译成汇编代码 →「汇编器」变成目标代码(目标文件) →「链接器」把多个目标文件以及调用的各种函数库链接起来 → 形成一个可执行文件(ELF 文件)
意味着 Linux 的内核是一个完整的可执行程序,且拥有最高的权限。
宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。
Window 的内核设计是混合型内核
三、内存管理
单片机的 CPU 是直接操作内存的物理地址
Linux中每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
内存管理单元(MMU
)
内存分段和内存分页
分段
段选择和段内偏移量
[插图]
段表里面保存的是这个段的基地址、段的界限和特权等级等
段基地址加上段内偏移量得到物理内存地址
不足之处
内存碎片
内存交换的效率低
外部内存碎片,也就是产生了多个不连续的小物理内存,导致新的程序无法被装载; 内部内存碎片,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使用,这也会导致内存的浪费;
解决外部内存碎片的问题就是内存交换。
这个内存交换空间,在 Linux 系统里,也就是我们常看到的 Swap
空间,这块空间是从硬盘划分
出来的,用于内存与硬盘的空间交换。
如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿
分页
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小
在 Linux
下,每一页的大小为 4KB
采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。
也就是暂时写在硬盘上,称为换出(Swap Out
)。一旦需要的时候,再加载进来,称为换入(Swap In
)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去
把虚拟内存地址,切分成页号和偏移量;根据页号,从页表里面,查询对应的物理页号;直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
多级页表
如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表
页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项
在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer)
,通常称为页表缓存、转址旁路缓存、快表等。
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
TLB 的命中率
段页式内存管理
[!段页式地址变换中要得到物理地址须经过三次内存访问] 第一次访问段表,得到页表起始地址 第二次访问页表,得到物理页号 第三次将物理页号与页内位移组合,得到物理地址
页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上一层地址映射。
程序所使用的地址,通常是没被段式内存管理映射的地址,称为逻辑地址; 通过段式内存管理映射的地址,称为线性地址,也叫虚拟地址; 逻辑地址是「段式内存管理」转换前的地址,线性地址则是「页式内存管理」转换前的地址。
Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制
Linux 系统中的每个段都是从 0 地址开始的整个 4GB
虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间的内存;
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
多进程环境
为每个进程独立分配一套虚拟地址空间
于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)。
根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度
四、进程与线程
公平
(时间片)
存储在硬盘的静态文件
编译后就会生成二进制可执行文件
运行
装载到内存中
CPU 会执行程序中的每一条指令
「进程」
当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。
多个程序、交替执行
在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。
[插图]
进程的状态变迁:
NULL → 创建状态:一个新进程被创建时的第一个状态;
创建状态 → 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
就绪态 → 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
运行状态 → 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
运行状态 → 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
运行状态 → 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
阻塞状态 → 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现; 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
进程控制块(process control block
,PCB
)
PCB
是进程存在的唯一标识
进程描述信息:
进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息:进程当前状态,如 new、ready、running、waiting 或 blocked 等;
进程优先级:进程抢占 CPU 时的优先级;
资源分配清单:有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
CPU 相关信息:CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。可见,PCB 包含信息还是比较多的。
链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
将所有处于就绪状态的进程链在一起,称为就绪队列;
就绪队列
操作系统允许一个进程创建另一个进程
子进程继承父进程所拥有的资源
当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。
Linux 操作系统对于终止有子进程的父进程,会把子进程交给1 号进程接管。
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。
操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器
CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
CPU
先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来, 然后加载新任务的上下文到这些寄存器和程序计数器, 最后再跳转到程序计数器所指的新位置,运行新任务。
CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;进程在系统资源不足(比如内存不足) 时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
发生硬件中断时,CPU
上的进程会被中断挂起,转而执行内核中的中断服务程序;
实体之间共享相同的地址空间;
线程( Thread
),线程之间可以并发运行且共享相同的地址空间。
线程是进程当中的一条执行流程。
[!当进程中的一个线程崩溃] 原因: 一个进程中可以同时存在多个线程; 各个线程之间可以并发执行; 各个线程之间可以共享地址空间和文件等资源; 结果: 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃。
进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
线程能减少并发执行的时间和空间开销;
同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
线程是调度的基本单位,而进程则是资源拥有的基本单位。
用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理; 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程; 轻量级进程(LightWeight Process):在内核中来支持用户线程;
用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
内核线程是由操作系统管理的,线程对应的 TCB
自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。
多个排队(就绪)队列,每个队列都有不同的优先级,各个队列优先级从高到低,同时每个队列执行时间片的长度也不同,优先级越高的时间片越短。
总结
匿名管道是只能用于存在父子关系的进程间通信
消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,
带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
信号量不仅可以实现访问的互斥性,还可以实现进程间的同步
P 操作和 V 操作。
信号是进程间通信机制中唯一的异步通信机制
硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)
有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
线程之间是可以共享进程的资源,比如代码段、堆空间、数据段、打开的文件等资源,但每个线程都有自己独立的栈空间
多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
信号量比锁的功能更强一些,它还可以方便地实现进程/线程同步。
使用加锁操作和解锁操作可以解决并发线程/进程的互斥问题。
现代 CPU 体系结构提供的特殊原子操作指令 —— 测试和置位(Test-and-Set)指令。
原子操作就是要么全部执行,要么都不执行,不能出现执行到一半的中间状态
无等待锁
没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把 CPU 让给其他线程执行。
信号量表示资源的数量
对应的变量是一个整型(sem)变量。另外,还有两个原子操作的系统调用函数来控制信号量
生产者在生成数据后,放在一个缓冲区中;消费者从缓冲区取出数据处理;任何时刻,只能有一个生产者或消费者可以访问缓冲区;
任何时刻只能有一个线程操作缓冲区,说明操作缓冲区**是临界代码,需要互斥
互斥信号量 mutex
:用于互斥访问缓冲区,初始化值为 1;
资源信号量 fullBuffers
:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空);
资源信号量 emptyBuffers
:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n (缓冲区大小);
五、调度算法
磁盘调度算法
六、 文件系统
基本概念
索引节点
索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。
目录项
目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。
目录项是内核一个数据结构,缓存在内存。
目录项这个数据结构不只是表示目录,也是可以表示文件的。
磁盘
磁盘读写的最小单位是扇区,扇区的大小只有512B
大小
磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。
超级块
超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。 当文件系统挂载时进入内存
索引节点区
索引节点区,用来存储索引节点; 当文件被访问时进入内存
数据块
数据块区,用来存储文件或目录数据
虚拟文件系统(Virtual File System,VFS)
操作系统希望对用户提供一个统一接口
用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS
)。
内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间
,我们经常用到的 /proc
和 /sys
文件系统都属于这一类
读写这类文件,实际上是读写内核中相关的数据。
磁盘空间管理
磁盘的空闲空间也是要引入管理
空闲表法
空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个方式是连续分配的。
空闲链表法
只要在主存中保存一个指针,令它指向第一个空闲块。其特点是简单,但不能随机访问,工作效率低,因为每当在链上增加或移动空闲块时需要做很多 I/O 操作,同时数据块的指针消耗了一定的存储空间。
位图法
位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。
当值为 0
时,表示对应的盘块空闲
在 Linux
文件系统就采用了位图的方式来管理空闲空间
不仅用于数据空闲块的管理,还用于
inode
空闲块的管理,因为inode
也是存储在磁盘的,自然也要有对其管理。
在 Linux 文件系统,把这个结构称为一个块组,那么有 N
多的块组,就能够表示 N
大的文件。
最前面的第一个块是引导块,在系统启动时用于启用引导,接着后面就是一个一个连续的块组了
如超级块和块组描述符表,这两个都是全局信息,而且非常的重要
通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能。
普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
最简单的保存格式就是列表,就是一项一项地将目录下的文件信息(如文件名、文件 inode、文件类型等)列在表里。
保存目录的格式改成哈希表,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。
硬链接与软链接
有时候我们希望给某个文件取个别名,那么在 Linux
中可以通过硬链接(Hard Link
) 和软链接(Symbolic Link
) 的方式来实现
硬链接
硬链接是多个目录项中的索引节点指向一个文件
硬链接不可用于跨文件系统。
由于多个目录项都是指向一个 inode
,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接
软链接相当于重新创建一个文件,这个文件有独立的 inode
,但是这个文件的内容是另外一个文件的路径
软链接是可以跨文件系统的
I/O
缓冲与非缓冲 I/O 直接与非直接 I/O 阻塞与非阻塞 I/O VS 同步与异步 I/O
缓冲 I/O 和非缓冲 I/O
根据是否利用标准库缓冲,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O
减少系统调用的次数,毕竟系统调用是有 CPU 上下文切换的开销的。
I/O 与非直接 I/O
以下几种场景会触发内核缓存的数据写入磁盘:
- 在调用
write
的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上; - 用户主动调用
sync
,内核缓存会刷到磁盘上; - 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
- 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上;
阻塞与非阻塞 I/O
阻塞 I/O
,当用户程序执行 read
,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read
才会返回,阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程
同步与异步 I/O
这里最后一次 read
调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
I/O
多路复用技术就出来了,如 select、poll
,它是通过 I/O
事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
(数据从内核态拷贝到用户态的过程),也是一个同步的过程,需要等待:
实际上,无论是阻塞 I/O
、非阻塞 I/O
,还是基于非阻塞 I/O
的多路复用都是同步调用。因为它们在 read
调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read
调用就会在这个同步过程中等待比较长的时间。
真正的异步 I/O
是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
当我们发起 aio_read
之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。
[插图]
阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,而非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在「过程 2」,所以这三个都可以认为是同步 I/O。
七、设备管理
设备控制器
屏蔽设备之间的差异,每个设备都有一个叫设备控制器(DeviceControl)
的组件,比如硬盘有硬盘控制器、显示器有视频控制器等。
控制器是有三类寄存器,它们分别是状态寄存器(StatusRegister)
、 命令寄存器(Command Register)
以及数据寄存器(Data Register)
输入输出设备可分为两大类 :块设备(Block Device)
和字符设备(Character Device)
。
每个块有自己的地址,硬盘、USB
是常见的块设备。
字符设备是不可寻址的,也没有任何寻道操作,鼠标是常见的字符设备。
内存映射 I/O
,将所有控制寄存器映射到内存空间中,这样就可以像读写内存一样读写数据缓冲区。
中断
中断有两种,一种软中断,例如代码调用 INT
指令触发,一种是硬件中断,就是硬件通过中断控制器触发的。
DMA(Direct Memory Access
了设备驱动程序。
通用块层
通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,它主要有两个功能:
通用层还会给文件系统和应用程序发来的 I/O
请求排队,接着会对队列重新排序、请求合并等方式,也就是 I/O 调度,主要目的是为了提高磁盘读写的效率。
5 种 I/O 调度算法
~~没有调度算法先入先出调度算法完全公平调度算法优先级调度最终期限调度算法第一种,~~没有调度算法,是的,你没听错,它不对文件系统和应用程序的 I/O 做任何处理,这种算法常用在虚拟机 I/O 中,此时磁盘 I/O 调度算法交由物理机系统负责。
可以把 Linux
存储系统的 I/O
由上到下可以分为三个层次,分别是文件系统层、通用块层、设备层。
[插图]
设备在Linux
下,也只是一个特殊的文件
需要 ioctl
接口,它表示输入输出控制接口,是用于配置和修改特定设备属性的通用接口。
提高文件访问的效率,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,目的是为了减少对块设备的直接调用。
提高块设备的访问效率, 会使用缓冲区,来缓存块设备的数据。
显示出结果后,恢复被中断进程的上下文。
八、网络系统
OSI七层
应用层,负责给应用程序提供统一的接口;
表示层,负责把数据转换成兼容另一个系统能识别的格式;
会话层,负责建立、管理和终止表示层实体之间的通信会话;
传输层,负责端到端的数据传输;
网络层,负责数据的路由、转发、分片;
数据链路层,负责数据的封帧和差错检测,以及 MAC
寻址;
物理层,负责在物理网络中传输数据帧;
TCP/IP
TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层
应用层,负责向用户提供一组应用程序,比如 HTTP
、DNS
、FTP
等;
传输层,负责端到端的通信,比如 TCP
、UDP
等;
网络层,负责网络包的封装、分片、路由、转发,比如 IP
、ICMP
等;
网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、MAC
寻址、差错检测,以及通过网卡传输网络帧等;
七层和四层负载均衡,是用 OSI
网络模型来描述的,七层对应的是应用层,四层对应的是传输层。
应用程序需要通过系统调用,来跟 Socket
层进行数据交互;Socket
层的下面就是传输层、网络层和网络接口层;
NAPI
机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后poll
的方法来轮询数据。
中断处理函数处理完需要暂时屏蔽中断,然后唤醒软中断来轮询处理数据,直到没有新数据时才恢复中断,这样一次中断处理多个网络包,
软中断是怎么处理网络包的呢?它会从 Ring Buffer
中拷贝数据到内核 struct sk_buff
缓冲区中,从而可以作为一个网络包交给网络协议栈进行逐层处理,根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识,找出对应的 Socket
,并把数据拷贝到 Socket
的接收缓冲区。
[插图]
应用程序会调用 Socket
发送数据包的接口,由于这个是系统调用,所以会从用户态陷入到内核态中的 Socket
层,Socket
层会将应用层数据拷贝到 Socket
发送缓冲区中。
Linux
网络协议栈就是按照了该模型来实现的。
磁盘优化
磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10
倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。
[插图]
[插图]
DMA
收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU
,CPU
可以执行其他任务;
要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。
mmap()
mmap()
系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
应用进程调用了 mmap()
后,DMA
会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;应用进程再调用write()
,操作系统直接将内核缓冲区的数据拷贝到 socket
缓冲区中,这一切都发生在内核态,由 CPU
来搬运数据;最后,把内核的 socket
缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA
搬运的。
使用
mmap()
来代替read()
, 可以减少一次数据拷贝的过程。
磁盘高速缓存(PageCache
)
文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache
)。
PageCache
使用了「预读功能」缓存最近被访问的数据;
但是,在传输大文件(GB
级别的文件)的时候,PageCache
会不起作用,那就白白浪费 DMA
多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache
的零拷贝也会损失性能
大文件传输
针对大文件的传输,不应该使用 PageCache
,也就是说不应该使用零拷贝技术
当调用 read
方法读取文件时,进程实际上会阻塞在 read
方法调用,因为要等待磁盘数据的返回
在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术。
传输大文件的时候,使用「异步 I/O + 直接 I/O」;传输小文件的时候,则使用「零拷贝技术」
I/O 多路复用:select/poll/epoll
要想客户端和服务器能在网络中通信,那必须得使用 Socket
编程
Socket是进程间通信里比较特别的方式,特别之处在于它是可以跨主机间通信。
服务器的程序要先跑起来,然后等待客户端的连接和数据
编程过程是怎样的。服务端首先调用 socket()
函数,创建网络协议为 IPv4
,以及传输协议为 TCP
的 Socket
,
接着调用 bind()
函数,给这个 Socket
绑定一个IP 地址和端口
[!绑定端口的目的] 当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们。
[!绑定 IP 地址的目的] 一台机器是可以有多个网卡的,每个网卡都有对应的 IP 地址,当绑定一个网卡时,内核在收到该网卡上的包,才会发给我们
- TCP 半连接队列,没有完成三次握手的连接,此时服务端处于
syn_rcvd
的状态; - TCP 全连接队列,完成了三次握手的连接,此时服务端处于
established
状态; - 监听
Socket
- 已连接
Socket
每个文件都有一个 inode
,Socket
文件的 inode
指向了内核中的 Socket
结构
在这个结构体里有两个队列,分别是发送队列和接收队列,这个两个队列里面保存的是一个个 struct sk_buff
,用链表的组织形式串起来。
sk_buff
可以表示各个层的数据包,在应用层数据包叫 data
,在 TCP
层我们称为 segment
,在 IP
层我们叫 packet
,在数据链路层称为 frame
。
多次拷贝,这将大大降低
CPU
效率。
并发 1 万请求,也就是经典的 C10K 问题 ,C 是 Client 单词首字母缩写,C10K 就是单机同时处理 1 万个请求的问题。
「子进程」退出时,实际上内核里还会保留该进程的一些信息,也是会占用内存的,如果不做好“回收”工作,就会变成僵尸进程
子进程退出后回收资源,分别是调用 wait()
和 waitpid()
函数。
轻量级的模型来应对多用户的请求 —— 多线程模型。
那么,我们可以使用线程池的方式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建若干个线程,这样当由新连接建立时,将这个已连接的 Socket
放入到一个队列里,然后线程池里的线程负责从队列中取出已连接 Socket
进程处理。
这个队列是全局的,每个线程都会操作,为了避免多线程竞争,线程在操作这个队列前要加锁。
新到来一个 TCP 连接,就需要分配一个进程或者线程,那么如果要达到 C10K,意味着要一台机器维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统就算死扛也是扛不住的。
只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。
select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合
集合拷贝到内核里
2 次「遍历」文件描述符集合
2 次「拷贝」文件描述符集合
都是使用「线性结构」存储进程关注的 Socket
集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket
,时间复杂度为 O(n)
,而且也需要在用户态与内核态之间拷贝文件描述符集合
epoll
在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket
通过 epoll_ctl()
函数加入内核中的红黑树里
O(logn)
第二点, epoll
使用事件
内核里维护了一个链表来记录就绪事件
epoll
支持两种事件触发模式,分别是边缘触发(edge-triggered,ET和水平触发(level-triggered,LT)。
服务器端只会从 epoll_wait
中苏醒一次
- 水平触发模式时,当被监控的
Socket
上有可读事件发生时,服务器端不断地从epoll_wait
中苏醒,直到内核缓冲区数据被read
函数读完才结束
边缘触发模式
我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里
所以,边缘触发模式一般和非阻塞 I/O
搭配使用,程序会一直执行 I/O
操作
多路复用 API
返回的事件并不一定可读写的,如果使用阻塞 I/O
, 那么在调用 read/write
时则会发生程序阻塞
阻塞 I/O
模型,基本上只能一对一通信
socket
默认情况是阻塞 I/O
),不过这种阻塞方式并不影响其他线程。
I/O
多路复用技术会用一个系统调用函数来监听我们所有关心的连接,也就说可以在一个监控线程里面监控很多的连接。
我们熟悉的 select/poll/epoll
就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。
「对事件反应」,也就是来了一个事件,Reactor
就有相对应的反应/响应。
即 I/单 Reactor 单进程 / 线程;单 Reactor 多线程 / 进程;多 Reactor 多进程 / 线程
一般来说,C 语言实现的是「单 Reactor 单进程」的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程。
单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能
另外,「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
阻塞 I/O
阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程。
这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。
Proactor 是异步网络模式, 感知的是已完成的读写事件。
Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
基于「事件分发」的网络编程模式
区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。
九、Linux 命令
应用程序要发送数据包时,通常是通过 socket 接口,于是就会发生系统调用,把应用层的数据拷贝到内核里的 socket 层,接着由网络协议栈从上到下逐层处理后,最后才会送到网卡发送出去。
通常是以 4 个指标来衡量网络的性能,分别是带宽、延时、吞吐率、PPS(Packet Per Second
带宽,表示链路的最大传输速率,单位是 b/s (比特 / 秒),带宽越大,其传输能力就越强。延时,表示请求数据包发送后,收到对端响应,所需要的时间延迟。不同的场景有着不同的含义,比如可以表示建立 TCP 连接所需的时间延迟,或一个数据包往返所需的时间延迟。
吞吐率,表示单位时间内成功传输的数据量,单位是 b/s(比特 / 秒)或者 B/s(字节 / 秒),吞吐受带宽限制,带宽越大,吞吐率的上限才可能越高。
PPS,全称是 Packet Per Second(包 / 秒),表示以网络包为单位的传输速率,一般用来评估系统对于网络的转发能力。
网络的可用性,表示网络能否正常通信;并发连接数,表示 TCP 连接数量;丢包率,表示所丢失数据包数量占所发送数据组的比率;重传率,表示重传网络包的比例;
IP 地址、子网掩码、MAC 地址、网关地址、MTU 大小、网口的状态以及网路包收发的统计信息
第一,
第二,MTU 大小。默认值是 1500 字节,其作用主要是限制网络包的大小,如果 IP 层有一个数据报要传,而且数据帧的长度比链路层的 MTU 还大,那么 IP 层就需要进行分片,即把数据报分成干片,这样每一片就都小于 MTU。
第三,网口的 IP 地址、子网掩码、MAC 地址、网关地址。这些信息必须要配置正确,网络功能才能正常工作
第四,网路包收发的统计信息。通常有网络收发的字节数、包数、错误数以及丢包情况的信息,如果 TX(发送) 和 RX(接收) 部分中 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,则说明网络发送或者接收出问题
使用 netstat 或者 ss,这两个命令查看 socket、网络协议栈、网口以及路由表的信息。
使用性能更好的 ss 命令
Recv-Q 表示 socket 缓冲区中还没有被应用程序读取的字节数;Send-Q 表示 socket 缓冲区中还没有被远端主机确认的字节数;而当 socket 状态处于 Listen 时:Recv-Q 表示全连接队列的长度;Send-Q 表示全连接队列的最大长度;
sar -n DEV,显示网口的统计数据;sar -n EDEV,显示关于网络错误的统计数据;sar -n TCP,显示 TCP 的统计数据
务器的防火墙是会禁用 ICMP 协议的。
分析用户行为
页面访问次数(PV)最多,访问人数(UV)最多,以及哪天访问量最多,哪个请求访问最多等等
对于大文件,我们应该养成好习惯,用 less 命令去读文件里的内容,因为 less 并不会加载整个文件,而是按需加载,先是输出一小页的内容,当你要往下看的时候,才会继续加载。
使用 uniq -c 命令前,先要进行 sort 排序,因为 uniq 去重的原理是比较相邻的行,然后除去第二行和该行的后续副本,因此在使用 uniq 命令之前,请使用 sort 命令使所有重复行相邻。
十、学习心得
另外,当 TCP 全连接队列溢出后,由于 tcp_abort_on_overflow 内核参数默认为 0,所以服务端会丢掉客户端发过来的 ack,如果你把该参数设置为 1,那现象
将变成,服务端会给客户端发送 RST 报文,废弃掉连接。
Linux 提供的 TCP 内核的参数的作用
操作系统比较重要的四大模块,分别是内存管理、进程管理、文件系统管理、输入输出设备管理
向下屏蔽差异,向上提供统一
进程与线程最大的区别在于上下文切换过程中,线程不用切换虚拟内存,因为同一个进程内的线程都是共享虚拟内存空间的,线程就单这一点不用切换,就相比进程上下文切换的性能开销减少了很多。