一、Linux简介
1. Linux分类
(1)内核版本:Linux不是一个操作系统,严格来讲,Linux只是一个操作系统中的内核。
(2)发行版本: 一些组织或公司在内核版基础上进行二次开发而重新发行的版本。Ubuntu和CentOS
2. 用户态和内核态
(1)用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取;
(2)内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序
(3)用户态访问内核态的方式
a. 系统调用:read(),write()等;
b. 库函数
c. Shell脚本
(4)用户态和内核态的切换
a. 系统调用
b. 异常
c. 外围设备中断
3. 虚拟地址和物理地址
(1)虚拟地址:虚拟地址是Linux内核虚拟出来的地址,经由内存管理单元(MMU)映射到实际的物理地址
(2)物理地址:寄存器引脚地址。当CPU没有内存管理单元或者不使用内存管理单元时,就是使用物理地址
(3)为什么要使用虚拟地址到物理地址的映射
a. 提高系统安全性:避免程序随意修改内存空间;
b. 为了确定在程序运行时,内存的实际使用期间使用到哪了;
c. 提高程序运行是的效率。不同的用户程序可以运行在同样的虚拟地址上;
(4)虚拟地址的映射方式
a. 分段映射:解决安全隐患,地址不确定问题;
b. 分页映射:将内存以4KB为单位分页,拷贝将运行的页拷贝到内存中而不用拷贝整个进程,解决效率问题。
4. Linux4GB虚拟地址空间
二、文件系统
1. 定义
操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。
2. 组成部分
文件系统由三部分组成:文件系统的接口,对对象操纵和管理的软件集合,对象及属性。从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,撤销文件等。文件系统是软件系统的一部分,它的存在使得应用可以方便的使用抽象命名的数据对象和大小可变的空间。
3. Linux文件组织组织方式:FHS(Filesystem Hierarchy Standard文件系统分层标准)
4. 文件读写流程
(1)读
a. 进程调用库函数向内核发起读文件请求;
b. 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表项;
c. 调用read();
d. 找到对应的地址空间(通过文件内容偏移量计算),访问页缓存树,如果缓存树中存在目标文件那么直接返回文件内容;如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页,并重新查找页缓存。
(2)写
a. 进程调用库函数向内核发起写文件请求;
b.内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表项;
c. 调用write();
d. 找到对应的地址空间(通过文件内容偏移量计算),访问页缓存树,如果缓存树中存在目标文件那么直接更新文件内容;如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,执行更新操作;
e. 页缓存被修改后,会被标记为脏页,脏页需要被写进磁盘;
三、I/O
1. Block-IO:传统的基于字节流,比如InputStream和OutputStream;字符流,比如Reader和Writer;
BIO是基于流模型实现的,其交互方式是同步阻塞的方式
2. NonBlock-IO:构建多路复用、同步非阻塞的IO操作
3. NIO的核心
(1)Channel:Channel可以从Buffers中读取信息,Buffers也可以想Channel中写入信息;
a. FileChannel:文件IO。transferTo()把FileChannel中的数据拷贝到另外一个Channel中,transferFrom()把另外一个Channel中的数据拷贝到FileChannel中;该接口常被用于高效的网络文件的数据传输和大文件拷贝,在操作系统允许的情况下,避免了两次用户态和内核态间的上下文切换,即“零拷贝”
b. DataChannels:UDP-IO
c. SocketChannels:TCP-IO
d. ServerSocketChannels:
(2)Buffers:
a. ByteBuffer
b. CharBuffers
c. DoubleBuffers
d. FloatBuffers
e. IntBuffers
f. LongBuffers
g. ShortBuffers
h. MappedByteBuffers
(3)IO多路复用机制:程序注册一组socket文件描述符给操作系统,表示“我要监视这些文件描述符是否有IO事件发生,有了就告诉程序处理”。Selector线程:允许单线程处理多个Channel,一个selector线程可以管理多个socket(内核态与用户态通信方式netlink)。不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。IO多路复用模型使用了Reactor设计模式实现了这一机制。Reactor模式有三种实现方式:reactor单线程,reactor多线程,reactor主从。
(4)NIO的底层实现了操作系统的多路复用
4. IO多路复用:调用系统级别的select\poll\epoll(多路复用的优势不是单个连接处理更快,而是可以处理多个连接)
5. select、poll缺点和epoll的优点
(1)select:select是无状态的,即每次调用select,内核都要重新检查所有被注册的fd的状态。select返回后,这些状态就被返回了,内核不会记住它们;到了下一次调用,内核依然要重新检查一遍。于是查询的效率很低。
(2)poll:依然需要遍历所有的文件描述符。
(3)epoll:epoll在内核的数据被建立好了之后,每次某个被监听的文件描述符一旦有事件发生,内核就直接标记之,它是一个事件驱动模型。epoll_wait()调用时,会尝试直接读取到当时已经标记好的文件描述符列表,如果没有就会进入等待状态。epoll不会传递所有的文件描述符号。
6. epoll的底层原理
(1)需要调用epoll_create来创建一个epoll的文件描述符,内核会同时创建一个eventpoll的数据结构。这个数据结构里面会包含两个东西,一个是红黑树,专门用于存储epoll_ctl注册进来的fd文件描述符;另外一个是就绪链表,用来存储epoll_wait调用相关的,已经就绪的那些fd文件描述符。
(2)因为epoll中的所有事件,都与网卡驱动程序建立回调关系,当相应的事件发生的时候,会通过这个callback函数,将发生的事件添加到就绪链表当中;
(3)当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有需要处理的事件。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
(4)水平触发和边沿触发
a. 水平触发(epoll默认):若缓冲区中有10k数据,我们可以多次进行读取;比如先读取2k数据,再次调用epoll_wait(),并且会立刻通知socket读事件就绪,可以再次读取剩余的数据;
b. 边沿触发:若缓冲区中有10k数据,第一次只读取了1k,再次调用epoll_wait,已经不是就绪状态了;ET模式下,只有当缓冲区中数据由无到有,由少变多时才会进行读取数据;epoll在ET模式下必须以非阻塞轮询的方式进行读取数据
(5)适合用epoll的应用场景:对于连接特别多,活跃的连接特别少。但是若某一时刻大量FD就绪,就会出现epoll的惊群问题。通过添加EPOLLEXCLUSIVE标志标识在唤醒时,只唤醒一个等待进程,以避免惊群出现。
7. select、poll、epoll的区别(io多路复用模型)
(1)从支持一个进程所能打开的最大连接数
a. select:单个进程所能大可的最大连接数由FD_SETSIZE宏定义,其大小是32个整数的大小(在32位机器上大小是32*32,64位机器上FD_SETSIZE为32*64),我们可以对其进行修改,然后重新编译内核,但是性能无法保证;
b. poll:没有最大连接数限制,它是基于链表来存储的;
c. epoll:有连接数限制,但是上限很大,1G内存的机器上可以打开10万左右的连接;
(2)文件句柄剧增后带来的IO效率问题(FD:文件描述符)
a. select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度的线性下降的性能问题;
b. poll:同上
c. epoll:由于epoll是根据每个FD上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll不会有“线性下降”的问题,但是所有socket都活跃的情况下,可能会有性能问题;
(3)消息传递方式
a. select:内核态到用户态需要经过内核拷贝;
b. poll:同上
c. epoll:通过内核和用户空间共享一块内存来实现,性能较高
8. AsynchronousIO:异步非阻塞IO,基于事件和回调机制。即应用操作直接返回,而不会阻塞。
9. AIO如何进一步加工处理结果
(1)基于回调:实现CompletionHandler接口,调用时触发回调函数
(2)返回Future:通过isDone()查看是否准备好,通过get()等待返回数据
10. BIO、NIO、AIO对比
属性\模型 |
阻塞BIO |
非阻塞NIO |
异步AIO |
Blocking |
阻塞并同步 |
非阻塞但同步 |
非阻塞并异步 |
线程数(s:c) |
1:1 |
1:N |
0:N |
复杂度 |
简单 |
较复杂 |
复杂 |
吞吐量 |
低 |
高 |
高 |
(1)同步阻塞I/O:用户进程发起一个I/O操作后,必须等待I/O操作的完成;
(2)同步非阻塞I/O:用户进程发起一个I/O操作后便可以返回,但用户进程需要时不时询问I/O是否完成;
(3)异步阻塞I/O:用户进程发起一个I/O操作后,不再等待I/O操作完成,等到内核完成I/O操作以后会通知应用程序。
四、fork()和写时复制
1. Unix系统的复制策略
当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。但是这种复制策略非常耗时,因为需要:
(1)为子进程的页表分配页面;
(2)为子进程的页分配页面;
(3)初始化子进程的页表;
(4)把父进程的页复制到子进程相应的页中;
2. Linux系统的复制策略
Linux的fork()使用写时拷贝(copy- on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制地址空间。