常用系统调用

内存映射机制mmap

mmap:把内核的pagecache和文件的数据地址空间映射起来

读写文件,一般有两种方式。

一种是open一个文件,然后使用read系统调用读取文件的一部分或全部。这个read过程是这样的:内核将文件中的数据从磁盘区域读取到内核页高速缓冲区,再从内核的高速缓冲区读取到用户进程的地址空间。这里就涉及到了数据的两次拷贝:磁盘->内核,内核->用户态。

而且当存在多个进程同时读取同一个文件时,每一个进程中的地址空间都会保存一份副本,这样肯定不是最优方式的,造成了物理内存的浪费。看下图:

共享存储映射mmap

open一个文件,然后调用mmap系统调用,将文件的内容的全部或一部分直接映射到进程的地址空间,映射完成后,进程可以像访问普通内存一样做其他的操作,比如memcpy等等。mmap并不分配物理地址空间,它只是占有进程的虚拟地址空间。这跟第一种方式不一样的,第一种方式需要预先分配好物理内存,内核才能将页高速缓冲中的文件数据拷贝到用户进程指定的内存空间中。
进程A和进程B都将该页映射到自己的地址空间, 当进程A第一次访问该页中的数据时, 它生成一个缺页中断。内核将文件的这一页数据读入到内核高速缓冲区中,并更新进程的页表,使页表指向内核缓冲中的这一页。以后, 当进程B访问同一页面而出现缺页中断时, 该页已经在内存, 内核只需要将进程B的页表登记项指向次页即可. 如下图所示:

1,将文件的内容的全部或一部分直接映射到进程的地址空间,而不是预先分配物理内存。

2,第一次访问该页中的数据时, 它生成一个缺页中断。内核将文件的这一页数据读入到内核高速缓冲区(page cache)中,并更新进程的页表,使页表指向内核缓冲中的这一页。page cache就是内核的缓存区,让这一块内存被用户进程和内核都访问。

3,因为文件和进程地址具有映射关系,所以只要一个内核态的缓存(page cache),没有用户态的缓冲区(user buffer),所以通过mmap读的时候可以直接从内核态的缓存读(page cache),不用从page cache拷贝到user buffer的过程。

 

mmap只能使用在file,data上,不能使用在socket网络传输上。

零拷贝sendfile

1.传统拷贝过程
       首先我们先来了解一下传统的拷贝流程。当消息从发送到写入磁盘,Broker 维护的消息日志本身就是文件目录形式,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从磁盘读取数据到内存,然后把内存中的数据原封不动的通过 socket 的形式发送给消费者。虽然这个操作看起来简单,但是实际上中间经历了很多步骤。如下图所示:

以下两个操作是java语义的读取文件和socket发送数据包

File.read(fileDesc, buf, len);  //对应1,2
Socket.send(socket, buf, len);  //对应3,4

       这个过程涉及到 4 次上下文切换以及 4 次数据的复制,并且有两次复制操作是由 CPU 完成。但是这个过程中,数据完全没有进行变化,仅仅是从磁盘复制到网卡缓冲区。 

       在这种情况下,如果能够减少用户空间与内核空间之间的切换,是不是会比传统拷贝快一点呢?如下图:

       结果显而易见,毕竟少了 1 次传输过程,肯定会比传统的拷贝性能高。这样子首先数据被从磁盘读取到 Read Buffer 中,然后再发送到 Socket Buffer,最后才发送到网卡。虽然减少了用户空间和内核空间之间的数据交换,但依然存在多次数据复制。

       明显性能的开销,都消耗在彼此之间的数据复制过程中,那么进一步减少数据的复制过程,或者干脆没有数据复制这一过程,性能会明显增强。这里就需要介绍到 DMA 技术 了

2.DMA 技术的出现
       DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它的出现就是为了解决批量数据的输入/输出问题。它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

       传统的内存访问,所有的请求都会发送到 CPU ,然后再由 CPU 来完成相关调度工作。如下图所示:

        当 DMA 技术的出现,数据文件在各个层之间的传输,则可以直接绕过CPU,使得外围设备可以通过DMA控制器直接访问内存。与此同时,CPU可以继续执行程序。如下图:

        在现代电脑中,很多硬件都是支持 DMA 技术的,这里面其中就包括我们此处用到的网卡。还有其他硬件也都是支持 DMA 技术的,例如:磁盘、显卡、声卡等其他硬件。

3.零拷贝技术
       有了 DMA 技术的,通过网卡直接去访问系统的内存,就可以实现现绝对的零拷贝了。这样就可以最大程度提高传输性能。通过“零拷贝”技术,我们可以去掉那些没必要的数据复制操作, 同时也会减少上下文切换次数。

       现代的 Unix 操作系统提供 了一个优化的代码路径,用于将数据从页缓存直接传输到 Socket; 在 Linux 中,是通过 sendfile 系统调用来完成的。Java 提供了访问这个系统调用的方法:FileChannel.transferTo API 。使用 sendfile ,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中, 只有最后一步将数据拷贝到网卡缓存中是需要的。

可以将4次数据拷贝减少到3次, 4次上下文切换减少到2次, 2次CPU中断减少到1次。

零拷贝的使用场景一般是:

  • 较大,读写较慢,追求速度
  • 内存不足,不能加载太大数据
  • 带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽不够

mmap 和 sendFile 的区别。

  1. mmap 适合小数据量读写,sendFile 适合大文件传输。
  2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 2 次上下文切换, 3 次数据拷贝。
  3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

epoll多路复用

epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。当然linux中IO多路复用不仅仅是epoll,其他多路复用机制还有select、poll,但是接下来介绍epoll的内核实现。

网上关于epoll接口的介绍非常多,这个不是我关注的重点,但是还是有必要了解。该接口非常简单,一共就三个函数,这里我摘抄了网上关于该接口的介绍:

  1. int epoll_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
    第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
 __uint32_t events;  /* Epoll events */
 epoll_data_t data;  /* User data variable */
};

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size(备注:在4.1.2内核里面,epoll_create的size没有什么用),参数timeout是超时时间(毫秒,0会立即返回,小于0时将是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时

epoll相比select/poll的优势

  • select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
  • select/poll一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。
  • 当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

write命令和fsync命令的区别


            wire命令只能确定数据成功写入io缓冲区。数据是否成功写入aof文件,是由操作系统定期或当io缓冲区填满时,
自动将io缓冲区的数据写入到aof文件。所以在io缓冲区的数据存在断电数据丢失的风险。
    fsync命令具有强制将io缓冲区数据写入到aof文件的机制,可以确定数据是否成功写入aof文件。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值