https://byvoid.com/zhs/blog/fast-readfile/
https://www.cnblogs.com/sumuyi/p/12813787.html
https://blog.csdn.net/weixin_37782390/article/details/103833306【Java零拷貝】
緩沖I/O和直接I/O
- 應用程序內(nèi)存(用戶空間):你malloc荣堰、new的內(nèi)存
- 用戶緩沖區(qū)(用戶空間):C語言里面的FILE*里面的buffer
typedef struct
{
short bsize;
....
unsigned char* buffer;
}
- 內(nèi)核緩沖區(qū)(內(nèi)核空間床未,內(nèi)存):Linux的Page Cache,為了加快磁盤IO振坚,將磁盤上的page(一個page一般4K)加載到內(nèi)存中的內(nèi)核緩沖區(qū)
緩沖I/O:讀寫都是三次數(shù)據(jù)拷貝薇搁,方向相反
磁盤<->內(nèi)核緩沖區(qū)<->用戶緩沖區(qū)<->應用程序內(nèi)存
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
int main(){
char *charFilePath="1.txt";
FILE *pfile=fopen(charFilePath,"rb");//打開文件,返回文件操作符
char *pread;
size_t result;
if(pfile)//打開文件一定要判斷是否成功
{
fseek(pfile,0,SEEK_END);//將文件內(nèi)部的指針指向文件末尾
long lsize=ftell(pfile);//獲取文件長度渡八,(得到文件位置指針當前位置相對于文件首的偏移字節(jié)數(shù))
rewind(pfile);//將文件內(nèi)部的指針重新指向一個流的開頭
pread=(char *) malloc(lsize*sizeof(char)+1);//申請內(nèi)存空間啃洋,lsize*sizeof(char)是為了更嚴謹,16位上char占一個字符屎鳍,其他機器上可能變化
//用malloc申請的內(nèi)存是沒有初始值的宏娄,如果不賦值會導致寫入的時候找不到結(jié)束標志符而出現(xiàn)內(nèi)存比實際申請值大,寫入數(shù)據(jù)后面跟隨亂碼的情況
memset(pread,0,lsize*sizeof(char)+1);//將內(nèi)存空間都賦值為‘\0’
result=fread(pread,1,lsize,pfile);//將pfile中內(nèi)容讀入pread指向內(nèi)存中
}
std::cout<<pread<<std::endl;
fclose(pfile);//關掉文件操作符逮壁,和句柄一樣孵坚,有open就一定有close
free(pread);//釋放內(nèi)存
pread=NULL;//指針不再使用,一定要“刪除”窥淆,防止產(chǎn)生野指針
}
直接I/O:讀寫都是兩次數(shù)據(jù)拷貝卖宠,方向各自相反。
磁盤<->內(nèi)核緩沖區(qū)<->應用程序內(nèi)存
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int flag;
int in=open("1.txt",O_RDONLY,S_IRUSR);
char buffer[1024];
if(in==-1){
return -1;
}
int out=open("2.txt",O_WRONLY|O_CREAT);
if(out==-1){
return -1;
}
while ((flag = read(in, buffer, 1024)) > 0)
{
write(out, buffer, flag);
}
close(in);
close(out);
return 0;
}
- read和fread有什么區(qū)別忧饭?
fread會自動分配緩存FILE buffer扛伍,fread返回的是一個FILE結(jié)構(gòu)指針。
而read返回的是一個int的文件號词裤。從測試結(jié)果看兩者速度差別不是很大刺洒,但還是推薦用read汁咏。
- fflush和fsync的區(qū)別:
fflush用于把C標準緩沖的數(shù)據(jù)寫到內(nèi)核緩沖,而fsync及其其他類似的函數(shù)用于將數(shù)據(jù)從內(nèi)核緩沖寫進磁盤作媚。如果在寫數(shù)據(jù)后不調(diào)用fsync攘滩,斷電的時候最新的部分數(shù)據(jù)會丟失在內(nèi)存中。
內(nèi)存映射文件與零拷貝
內(nèi)存映射
磁盤<->應用內(nèi)存
(跳過內(nèi)核緩沖區(qū))
對變長文件不適合
mmap是一種內(nèi)存映射文件的方法纸泡,即將一個文件或者其它對象映射到進程的地址空間
void *mmap(void *start, size_t length, int prot, int flags, int fd,
off_t offset);
mmap是一種內(nèi)存映射文件的方法漂问,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系女揭。(初始化的時候無需拷貝文件到用戶空間)實現(xiàn)這樣的映射關系后蚤假,進程就可以采用指針的方式讀寫操作這一段內(nèi)存(發(fā)生缺頁中斷然后調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中)吧兔,而系統(tǒng)會自動回寫臟頁面到對應的文件磁盤上磷仰,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反境蔼,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間灶平,從而可以實現(xiàn)不同進程間的文件共享。
內(nèi)存中的內(nèi)容并不會立即更新到文件中箍土,而是有一段時間的延遲逢享,你可以調(diào)用msync()來顯式同步一下。
總結(jié):常規(guī)文件操作為了提高讀寫效率和保護磁盤吴藻,使用了頁緩存機制瞒爬。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間沟堡,不能被用戶進程直接尋址侧但,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對應的用戶空間中。這樣航罗,通過了兩次數(shù)據(jù)拷貝過程禀横,才能完成進程對文件內(nèi)容的獲取任務。寫操作也是一樣伤哺,待寫入的buffer在內(nèi)核空間不能直接訪問燕侠,必須要先拷貝至內(nèi)核空間對應的主存,再寫回磁盤中(延遲寫回)立莉,也是需要兩次數(shù)據(jù)拷貝绢彤。
而使用mmap操作文件中,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步蜓耻,沒有任何文件拷貝操作茫舶。而之后訪問數(shù)據(jù)時發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關系刹淌,只使用一次數(shù)據(jù)拷貝饶氏,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中讥耗,供進程使用。
總而言之疹启,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝古程。而mmap操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程喊崖。說白了挣磨,mmap的關鍵點是實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程。因此mmap效率更高荤懂。
直接對該段內(nèi)存寫時不會寫入超過當前文件大小的內(nèi)容
mmap的缺點: 文件小于4k的頁的大小會造成空間浪費茁裙。且無法擴展空間。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <iostream>
using namespace std;
int main(){
int fd;
void *start;
struct stat sb;
fd = open("/etc/passwd", O_RDONLY);
/*打開/etc/passwd */
cout<<fd<<endl;
fstat(fd, &sb); /* 取得文件大小 */
start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(start == MAP_FAILED) /* 判斷是否映射成功 */
return;
cout<<start<<endl;
munma(start, sb.st_size); /* 解除映射 */
closed(fd);
}
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <memory.h>
#include <iostream>
using namespace std;
int main(){
// mmap讀文件
// 打開文件
int fd = open("1.txt", O_RDONLY);
// 讀取文件長度
int len = lseek(fd,0,SEEK_END);
// 建立內(nèi)存映射
char *addr = (char *) mmap(NULL, len, PROT_READ, MAP_PRIVATE,fd, 0);
close(fd);
// data用于保存讀取的數(shù)據(jù)
char* data=new char(len+1);
// 復制過來
memcpy(data, addr, len);
// 解除映射
// cout<<data;
munmap(addr, len);
// mmap寫文件
len=strlen(data);
// 打開文件
fd=open("output.txt", O_RDWR|O_CREAT, 00777);
// lseek將文件指針往后移動file_size-1位
lseek(fd,len-1,SEEK_END);
// 從指針處寫入一個空字符节仿;mmap不能擴展文件長度晤锥,這里相當于預先給文件長度,準備一個空架子
write(fd, "", 1);
// 使用mmap函數(shù)建立內(nèi)存映射
addr = (char*)mmap(NULL, len, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
// 內(nèi)存映射建立好了廊宪,此時可以關閉文件了
close(fd);
// 把data復制到addr里
memcpy(addr, data, len);
// 解除映射
munmap(addr, len);
}
零拷貝(一般用于磁盤->socket)
跳過Socket緩沖區(qū)矾瘾,把內(nèi)核緩沖區(qū)當socket緩沖區(qū)用。
mmap 適合小數(shù)據(jù)量讀寫挤忙,sendFile 適合大文件傳輸霜威。
新版本sendfile沒有CPU拷貝谈喳,但是會傳輸文件描述符和數(shù)據(jù)長度給socket緩沖區(qū)册烈,一般面試主要還是問新版本。
1. 用戶進程通過sendfile()方法向操作系統(tǒng)發(fā)起調(diào)用婿禽,上下文從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài)
2. DMA控制器利用scatter把數(shù)據(jù)從硬盤中拷貝到讀緩沖區(qū)離散存儲
3. CPU把讀緩沖區(qū)中的文件描述符和數(shù)據(jù)長度發(fā)送到socket緩沖區(qū)
4. DMA控制器根據(jù)文件描述符和數(shù)據(jù)長度赏僧,使用scatter/gather把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到網(wǎng)卡
5. sendfile()調(diào)用返回,上下文從內(nèi)核態(tài)切換回用戶態(tài)
#include <sys/sendfile.h>
size_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- sendfile() 局限于基于文件服務的網(wǎng)絡應用程序扭倾,比如 web 服務器淀零。據(jù)說,在 Linux 內(nèi)核中實現(xiàn) sendfile() 只是為了在其他平臺上使用 sendfile() 的 Apache 程序
- 由于網(wǎng)絡傳輸具有異步性膛壹,很難在 sendfile () 系統(tǒng)調(diào)用的接收端進行配對的實現(xiàn)方式驾中,所以數(shù)據(jù)傳輸?shù)慕邮斩艘话銢]有用到這種技術。
- sendfile方法IO數(shù)據(jù)對用戶空間完全不可見模聋,所以只能適用于完全不需要用戶空間處理的情況肩民,比如靜態(tài)文件服務器。
- 由于CPU和IO速度的差異問題链方,產(chǎn)生了DMA技術持痰,通過DMA搬運來減少CPU的等待時間。DMA(Direct Memory Access)直接內(nèi)存訪問技術本質(zhì)上來說就是一塊主板上獨立的芯片祟蚀,通過它來進行內(nèi)存和IO設備的數(shù)據(jù)傳輸工窍,從而減少CPU的等待時間割卖。
DMA Scatter/Gather 分散/收集功能:
cpu將讀緩沖區(qū)中的數(shù)據(jù)描述信息--內(nèi)存地址和偏移量記錄到socket緩沖區(qū),由 DMA 根據(jù)這些將數(shù)據(jù)從讀緩沖區(qū)拷貝到網(wǎng)卡患雏,相比之前版本減少了一次CPU拷貝的過程鹏溯。但是還是需要占用數(shù)據(jù)總線。
應用場景:
對于文章開頭說的兩個場景:RocketMQ和Kafka都使用到了零拷貝的技術淹仑。
對于MQ而言剿涮,無非就是生產(chǎn)者發(fā)送數(shù)據(jù)到MQ然后持久化到磁盤,之后消費者從MQ讀取數(shù)據(jù)攻人。
對于RocketMQ來說這兩個步驟使用的是mmap+write取试,而Kafka則是使用mmap+write持久化數(shù)據(jù),發(fā)送數(shù)據(jù)使用sendfile怀吻。
總結(jié):
以網(wǎng)卡到磁盤為例:
開銷類型 | 直接I/O | 內(nèi)存映射 | 零拷貝 |
---|---|---|---|
數(shù)據(jù)拷貝次數(shù) | 4 | 3 | 2 |
內(nèi)存拷貝次數(shù) | 2 (內(nèi)核往返用戶) | 1 (內(nèi)核到socket) | 0 (內(nèi)存與IO設備間不算內(nèi)存拷貝) |
上下文切換次數(shù) | 4 | 4 | 2 |