1.fsync
应用程序通过write系统调用要向某个文件写入数据的时候,内核通常是把数据写入到内核缓冲区中,而不是直接写到磁盘(显式指定同步方式除外),通过这种机制,write()就可以频繁调用并立即返回。 此时,数据未必已经真正写入到磁盘中,一般内核通过定时刷新的方式,把缓冲区中的数据批量写入到文件中。 但应用程序也可以通过fsync系统调用来进行显式的控制,要求某个文件对应缓冲区中的数据进行刷盘。
int fsync(int fd); //刷新某个文件描述符对应的缓冲区数据
void sync(); //不指定具体的文件描述符,刷新所有的缓冲区
2.mmap
除了标准文件操作,如read,write等,内核还提供了mmap内存映射,支持应用程序将某个文件映射到内存中(虚拟内存),既内存地址和文件数据一一对应。这样,应用程序就可以直接通过内存来访问文件,就像操作内存中的数据块一样,比如可以通过写入内存数据区,然后通过透明的映射机制就可以实现将文件写入磁盘。
比标准文件读写效率高的原因:
标准文件操作(调用read/write等系统函数)为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接访问,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
而使用mmap操作文件,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。读写数据的时候,只需要从磁盘到用户控件一次数据拷贝就可以完成,效率上更高.
使用场景:一般用于对文件读写性能要求较高的场景下,如rocketmq中针对commitlog文件的处理
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
参数说明:
addr 告诉内核映射文件的内存最佳地址,大部分情况下该参数传0
prot 描述对内存区域的所请求的访问权限,如PROT_READ表示可读, PROT_WRITE表示可写, PROT_EXEC表示页可执行,该参数不能和文件本身的访问模式冲突,如文件本身是只读的,就不能设置为可写
flags 描述映射的类型和一些行为
fd 要映射的文件描述符,该参数为-1的时候,代码的是申请一块匿名内存映射,一般用于申请一块大内存 |
offset 可以选择只映射文件中的某一部分内容,offset表示开始的位移
3.select
Linux系统提供select函数来实现多路复用输入/输出模型,select系统调用是用来让我们的应用程序监视多个文件句柄(包括socket文件句柄)的状态变化,程序会在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变(如可读/可写)。
尤其是针对网络IO,因为如果采用单个连接提供一个线程的话,当线程数量较大时,服务端系统资源消耗过大,并且多个线程上下文切换导致性能降低,IO多路复用方案(如select,epoll,kqueue等)作为效率较高的替代方案。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
n 一般为最大文件描述符 + 1, 既STDIN_FILENO + 1 ,该值会有limit限制,一般为1024,所以生产环境基本用epoll,kqueue等代替
readfds 监视是否有可读的文件描述符集合
writefds 监视是否有可写的文件描述符集合 | |
exceptfds 监视是否有异常情况发生的文件描述符集合 | |
timeout 超时时间,如果在某一段时间内依然没有相应事件触发,则会阻塞直到timeout时间过期, timeout.tv_sec单位为秒, timeout.tv_usec单位为微秒
4.epoll
由于select和poll系统调用存在以下几个问题,Linux内核2.6环境新增Event Poll的方式。
select/poll每次检查的时候是通过遍历所有的文件描述符(fd), 尤其是对于网络scoket而言,大部分存在这么一个特性,既某一个时间点里,只有很少一部分的socket是“活跃”状态,如果每次都是遍历所有的网络文件描述符的话,当文件描述符变大之后,性能就会随着连接数变大之后线型下降
select存在最大文件描述符的限制,具体取决于常量FD_SETSIZE,不能满足大批量的客户端连接.(如上万个连接的情况)
反观epoll,则改进了以上不足的地方
1.在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会
2.epoll没有对fd描述符有限制,理论上取决于系统内存大小, 可以通过命令 cat /proc/sys/fs/file-max查看,大概1G内存可以创建10w个连接
3.epoll的具体实现使用mmap加速内核与用户空间的消息传递,进一步提高性能
epoll实际包含3个系统调用组成
int epoll_create(int size);
创建epoll的实例,并返回epoll本身的描述符, 用于epoll_ctl和epoll_wait作为参数传入. 参数size只要大于0即可,内核会动态获取大小
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
添加(op=EPOLL_CTL_ADD)、修改(op=EPOLL_CTL_MOD)、删除(op=EPOLL_CTL_DEL)要监听的event事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
监视等待有IO事件发生,直到timeout过期, 类似select()调用
5.malloc
包含标准库的方法
//在堆上申请size大小的内存
void *malloc(size_t size);
//数组形式的内存申请,且用0初始化了内存
void *calloc(size_t n, size_t size);
//重新调整已分配的内存,如果size=0则等效于free()调用
void *realloc(void *ptr, size_t size);
//释放内存
void free(void *ptr);
//功能跟malloc一致,但返回的地址是页对齐的
void *valloc(size_t size);
//当fd=-1的时候,表示申请匿名内存映射,一般用于申请较大内存的场景
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
//在栈中申请内存,方法执行完之后会自动释放,无需显式调用free()方法
void *alloca(size_t size);
6.mlock
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间,而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
基于某些原因如确定性和安全性,某些应用中的部分内存不允许交换到外部磁盘中,可以使用mlock(内存锁定)来实现。
//锁定内存地址不允许交换
int mlock(const void *addr, size_t len);
//某个进程想锁定它全部的地址空间
int mlockall(int flags);
//内存解锁,允许再次被swap到外部磁盘中
int munlock(const void *addr, size_t len);
//解锁全部的地址空间
int munlockall();
7.sendfile
zero-copy原理,比如有如下场景,服务端响应客户端的请求,从文件系统中读取某一个文件的内容,然后通过socket方式把该内容发送给客户端。
则具体过程可以分为以下2个步骤 (2个文件拷贝的场景同理)
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
系统底层执行上面2行代码的过程如下
1.系统调用 read() 产生一个上下文切换:从 user mode 切换到 kernel mode,然后 DMA 执行拷贝,把文件数据从硬盘读到一个 kernel buffer 里
-
数据从 kernel buffer 拷贝到 user buffer,然后系统调用 read() 返回,这时又产生一个上下文切换:从kernel mode 切换到 user mode
-
系统调用 write() 产生一个上下文切换:从 user mode 切换到 kernel mode,然后把步骤2读到 user buffer 的数据拷贝到 kernel buffer(数据第2次拷贝到 kernel buffer, 不过这次是不同的 kernel buffer,这个 buffer 和 socket 相关联
-
系统调用 write() 返回,产生一个上下文切换:从 kernel mode 切换到 user mode(第4次切换),然后 DMA 从 kernel buffer 拷贝数据到协议栈(第4次拷贝)
以上细节是传统read/write方式进行网络文件传输的方式,我们可以看到,在这个过程当中,文件数据实际上是经过了四次copy操作:
硬盘==>内核buf==>用户buf==>socket相关缓冲区==>协议引擎
如果能减少切换次数和拷贝次数将会有效提升性能。linux通过系统调用 sendfile简化上面步骤提升性能的,sendfile不但能减少切换次数而且还能减少拷贝次数。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
运行流程如下:
1.sendfile系统调用,文件数据被copy至内核缓冲区
2.再从内核缓冲区copy至内核中socket相关的缓冲区
3.最后再socket相关的缓冲区copy到协议引擎
相较传统read/write方式,2.1版本内核引进的sendfile已经减少了内核缓冲区到user缓冲区,再由user缓冲区到socket相关 缓冲区的文件copy,而在内核版本2.4之后,文件描述符结果被改变,sendfile实现了更简单的方式,系统调用方式仍然一样,细节与2.1版本的 不同之处在于,当文件数据被复制到内核缓冲区时,不再将所有数据copy到socket相关的缓冲区,而是仅仅将记录数据位置和长度相关的数据保存到 socket相关的缓存,而实际数据将由DMA模块直接发送到协议引擎,再次减少了一次copy操作。
网友评论