LinuxIO总结

CPU访问IO的方式

程序查询

cpu持续访问寄存器是否完成IO输入,直到检测到了完成会取出数据

程序中断(interrupt)

cpu去干别的事,IO接口根据寄存器状态判断是否写入结束。结束后给cpu发送信号让cpu来取数据

中断并不是是指程序中断,只是一个提醒。类似于通知,比如处理器收到了软件或者硬件的提示该做某事了,这就是一个中断。

通常来说外围设备发送的是异步信号。

DMA接口

cpu和DMA(Direct Memory Access)总线直接链接,cpu告诉DMA接口需要把数据存在哪,DMA接口就会直接把数据写入主存(内存)

允许外设可以直接读写主存而不需要cpu干预。硬盘控制器,显卡,网卡,声卡等。

系统调用中的IO

系统调用(syscall)是一系列系统的函数,区别于用户自定义的函数(User)。系统调用通常是运行于内核态(Kernel),更接近系统底层。系统调用可以直接访问硬件设备或者与内核交互,而用户程序是不可以的。用户想访问内核得先调用系统调用。

内存:brk,nmap,free。unmap等函数

文件:open,read.write,close

网络:select,poll,epoll.sendfile

linux中网络的IO模型

以下例子数据流向 网卡缓冲区>内核缓冲区>用户缓冲区

阻塞IO

一个syscall会变成阻塞状态,直到内核完成工作(比如成功把数据发送到用户缓冲区),这时才会解开阻塞。

一个线程只能有一个链接

非阻塞IO

在内核就绪前(比如已经把网卡缓冲区的数据拷贝到了内核区)可以一直发起syscall。当内核状态为就绪时启动阻塞。

一个线程可以多个链接,但是频繁的syscall很吃资源。

初步判断这么设计是可以让多个链接的请求放成一个队列,增加并发能力。

多路复用

在linux中,fd全称“File descriptor”,中文名为“文件描述符”,它是内核为了高效管理这些已经被打开的文件所创建的一种索引;它其实是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。

在windows中句柄和fd是一个意思,用来标记被系统打开的资源,

两者在形式上都是“唯一的整数”,是int类型

select/poll

当发起syscall时阻塞执行。当内核变为就绪态,其他syscall可以直接已经就绪的数据。此时阻塞与否都可以。然后内核继续工作。

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
调用后 select 函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),
函数返回。当 select 函数返回后,可以 通过遍历 fdset,来找到就绪的描述符。

优点:一次可以处理多个请求,返回批量数据。返回数据是用户自己遍历。这两者降低了syscall次数。

缺点:整个数据传递的过程是拷贝整个数组,开销特别大。同时该函数返回值只有个数,具体数据仍需read自己遍历读取。

epoll

//创建epollFd,底层是在内核态分配一段区域,底层数据结构红黑树+双向链表
//size用来告诉内核这个监听的数目一共有多大
int epoll_create(int size)//往红黑树中增加、删除、更新管理的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//用来阻塞等待就绪的fd,类似select调用
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

流程是create先创建epoll fd,在用ctl添加scoket fd 。打开 wait等待数据写入。当有异步IO变化时,wait返回有IO事件的FD。此时只返回有变动的FD。

注意,此次创建的fd是内核和用户共享的内存区域。故只有一份文件且不用拷贝。

优点:fd不用来回拷贝占用磁盘IO.内核也不用持续遍历fd,只需要等待异步IO唤醒。最后返回的是有变动的fd,用户程序不用遍历。

IO模型应用

redise和netty采用了Reactor的模型。使用了epoll方式。

磁盘IO

对于磁 盘的一次读请求,首先经过虚拟 文件系统层(VFS Layer),其次 是具体的文件系统层(例如 Ext2),接下来是Cache层(Page Cache Layer)、通用块层 (Generic Block Layer)、I/O调度 层(I/O Scheduler Layer)、块设 备驱动层(Block Device Driver Layer),最后是物理块设备层 (Block Device Layer)。

磁盘IO-Page Cache

cpu取文件有两种方式,第一种是直接读取。第二种是让主存先加载好数据后cpu在读取。

Cache在主存上,是专门分配的。

两者比较看起来在读取的时候好像直接读取似乎更快,但其实数据还涉及到修改和写入的问题。对内存的直接修改比对硬盘会快数倍。与此同时加了内存这一层的封装可以实现更多功能。

预读和回写

预读是利用局部性原理,把数据从磁盘加载到主存时会把后面几页(默认三页)也加载。

回写是当数据加载到内核的cache时,系统才会把数据copy到应用程序的地址。分离直接读写,大多数操作在内存完成。提高性能。

只有当一些特定条件才会把缓存写入磁盘。

磁盘io的三种方式

BufferIO

访问filesystem>PageCache>BlockIOlayer>Device

Java 中常用的FileInputStream, FileOutputStream(包含FileReader, FileWriter, BufferedInputStream, BufferedOutputStream等)都是Buffer IO 首次读/写涉及 1次 用户态<->内核态的切换, 2次 数据拷

需要注意的是,所有对数据的修改动作都是在用户缓冲区实现的。内核和硬件层只负责数据的传递。

DirectIO

直接访问BlockIOLayer

在随机读的场景下,通过Page Cache可能会好心办坏事,缓存命中概率较低,Direct IO性 能可能会更好。 Java 原生并没有提供Direct IO的方式(不要和堆外内存DirectByteBuffer混淆),但可以通 过 JNA/JNI 调用 native 方法做到。github 地址:https://github.com/smacke/jaydio 每次读/写都涉及 1次 用户态<->内核态的切换, 1次 数据拷

mmp

直接访问pageCache

需要修改的数据(和数据地址)被缓存在内核中。(多个也可)进程可以直接访问。然后修改,之所以说是映射是因为数据在磁盘中不一定是连续的。所以需要地址来拼接起来。

mmap带来的好处: 1、减少系统调用。我们只需要一次 mmap() 系统调用,后续所有的调用像操作内存一样,而不会出现大量的 read/write 系统调用。 2、减少数据拷贝。普通的 read() 调用,数据需要经过两次拷贝;而 mmap 只需要从磁盘拷贝一次就可以了, 并且由于做过内存映射,也不需要再拷贝回用户空间。 使用mmap也要注意: 1、一次 map 的大小限制在 1.5G 左右; 2、会大量占用内存,如果使用不当可能会造成频繁的swap

epoll 就是通过mmap 实现的共享内存

这里所说的映射就是指,文件数据和文件地址的关联由mmap实现。比如获得一个修改文件的需求,他会返回文件在系统中的指针。然后由其他人直接操作文件指针。通过使用指针避免了整个文件的来回拷贝。同时因为文件在磁盘中通常是不连续的。mmap封装更能增强性能。

磁盘IO的实践

kafka

kafka的topic有很多个partition,每个partition对应多个stagement文件。stage文件在磁盘中是顺序读写的。

其中.index和.log文件就是使用mmap方式写入。

比如index索引记录了文件开始的地方,文件长度(偏移量),该记录在log文件的地址。

然后去log中找具体的消息。

1、Kafka 将 Partition 划分为多个 Segment, 每个 Segment 对应一个物理文件,Kafka 对 segment 文件追加写(顺序写) 2、producer 发送消息到 Broker 时,Broker 会使用 write() 系统调用 (对应到 Java NIO 的 FileChannel.write() API),按偏移量写入数据, 此时数据都会先写入page cache。consumer 消费消息时,Broker 使用 sendfile() 系统调 用 (对应 FileChannel.transferTo() API),以零 拷贝 的方式将数据从 page cache 传输到 broker 的 Socket buffer,然后再通过网络传 输。 3、消息从producer到consumer,通过批量 和压缩,减低网络传输次数及数据大小; 4、使用稀疏索引.index访问.log文件,且通 过mmap的方式,将 index 文件映射到内存, 减少系统调用次数,减少数据拷贝;