一.引入
我們發(fā)現(xiàn) printf 和 fwrite (庫函數(shù))都輸出了2次,而 write 只輸出了一次(系統(tǒng)調(diào)用)。為什么呢?肯定和fork有關(guān)!
C接口的函數(shù)被打印了兩次系統(tǒng)接口前后只是打印了一次:和fork函數(shù)有關(guān),fork會(huì)創(chuàng)建子進(jìn)程。在創(chuàng)建子進(jìn)程的時(shí)候,數(shù)據(jù)會(huì)被處理成兩份,父子進(jìn)程發(fā)生寫時(shí)拷貝,我們進(jìn)行printf調(diào)用數(shù)據(jù)的時(shí)候,數(shù)據(jù)寫到顯示器外設(shè)上,就不屬于父進(jìn)程了,數(shù)據(jù)沒被寫到顯示器上,依舊屬于父進(jìn)程,而調(diào)用printf并不一定把數(shù)據(jù)刷到顯示器上,沒有被顯示本質(zhì)就是數(shù)據(jù)沒有從內(nèi)存到外設(shè),所以這份沒有被顯示的數(shù)據(jù)依舊屬于這進(jìn)程,當(dāng)我們?nèi)ork的時(shí)候,進(jìn)程退出要刷新緩沖區(qū),此時(shí)刷新的過程就是把數(shù)據(jù)從內(nèi)存刷新到外設(shè),刷新到外設(shè)的同時(shí),也會(huì)把程序內(nèi)部的緩沖區(qū)的數(shù)據(jù)直接清走,這就是寫入,跟寫時(shí)拷貝有關(guān)系
對(duì)于這個(gè)現(xiàn)象的問題我們可以直接往下看??
二.認(rèn)識(shí)緩沖區(qū)
1.為什么
緩沖區(qū)的本質(zhì)就是一段內(nèi)存。在內(nèi)存空間中預(yù)留了一定的存儲(chǔ)空間,這些存儲(chǔ)空間用來緩沖輸入或輸出的數(shù)據(jù),這部分預(yù)留的空間就叫做緩沖區(qū)。
數(shù)據(jù)如果直接從內(nèi)存到磁盤,在內(nèi)存中速度快,但是訪問外設(shè)效率比較低,那太消耗時(shí)間了,屬于外設(shè)IO,所以緩沖區(qū)的意義就是節(jié)省進(jìn)程進(jìn)行數(shù)據(jù)IO的時(shí)間!進(jìn)程需要把數(shù)據(jù)拷貝到緩沖區(qū)里:我們并不需要拷貝,而是調(diào)用fwrite,與其理解fwrite是寫入到文件的函數(shù),倒不如理解fwrite是拷貝函數(shù),將數(shù)據(jù)從進(jìn)程拷貝到緩沖區(qū)或者外設(shè)當(dāng)中。
數(shù)據(jù)可以直接拷貝到緩沖區(qū),高速設(shè)備不用在等待低速設(shè)備,提高計(jì)算機(jī)的效率。
2.刷新策略
緩沖區(qū)的刷新策略:如果有一塊數(shù)據(jù),一次寫入到外設(shè)(效率最高)vs如果有一塊數(shù)據(jù),多次少量寫入到外設(shè),需要多次IO
緩沖區(qū)一定結(jié)合具體的設(shè)備定制自己的刷新策略:
1.立即刷新——無緩沖 ,場(chǎng)景較少,比如調(diào)用printf直接fflush
2.行刷新——行緩沖——顯示器 ,數(shù)據(jù)的printf帶上\n就會(huì)立馬顯示到顯示器上。顯示器為什么是行緩沖:顯示器是外設(shè),進(jìn)程運(yùn)行時(shí)在內(nèi)存里的,把數(shù)據(jù)定期要刷新到外設(shè),顯示器設(shè)備比較特殊,是給用戶來看的,從左到右,所以顯示器為了保證刷新效率,并且用戶體驗(yàn)良好,所以顯示器采用行緩沖,滿足用戶的閱讀體驗(yàn)并且在一定程度上效率不至于太低
3.緩沖區(qū)滿——全緩沖——磁盤文件,效率最高,只需要一次IO,比如文件讀寫的時(shí)候,直接寫到磁盤文件
但是存在特殊情況:a.用戶強(qiáng)制刷新 b,進(jìn)程退出——一般到要進(jìn)行緩沖區(qū)刷新
所以對(duì)于全緩沖,緩沖區(qū)滿了采取刷新,減少IO次數(shù),提高效率。
3.在哪里
緩沖區(qū)的位置究竟在哪里:從上面的例子我們直接往顯示器上打印結(jié)果為4條,往文件打印為7條,這跟緩沖區(qū)有關(guān),同時(shí)這也說明了緩沖區(qū)一定不在內(nèi)核中,為什么?如果在內(nèi)核中write也應(yīng)該打印兩次,write是系統(tǒng)接口。我們之前談?wù)摰乃芯彌_區(qū)都指的是用戶級(jí)語言層面提供的緩沖區(qū)。這個(gè)緩沖區(qū),在stdout,stdin,stderr對(duì)應(yīng)的類型---->FILE*,FILE是一個(gè)結(jié)構(gòu)體,里面封裝了fd,同時(shí)還包括了一個(gè)緩沖區(qū)!
FILE結(jié)構(gòu)體緩沖區(qū),所以我們直接要強(qiáng)制刷新的時(shí)候fflush(文件指針),關(guān)閉文件fclose(文件指針),這是因?yàn)閭鬟M(jìn)去的文件指針對(duì)應(yīng)的緩沖區(qū)
從源碼出發(fā),我們可以來看一看FILE結(jié)構(gòu)體:
所以我們一般所說的緩沖區(qū)是語言級(jí)別的緩沖區(qū),C語言提供的在FILE結(jié)構(gòu)體里對(duì)應(yīng)的緩沖區(qū)。
現(xiàn)在,我們現(xiàn)在重新來看一看剛開始的現(xiàn)象:
1.如果我們沒有進(jìn)行重定向>,看到了4條消息,stdout默認(rèn)使用的是行刷新,在進(jìn)程fork之前,三條C函數(shù)已經(jīng)將數(shù)據(jù)打印輸出到顯示器上(外設(shè)),你的FILE內(nèi)部進(jìn)程內(nèi)部就不存在對(duì)應(yīng)的數(shù)據(jù)了。
2.如果我們進(jìn)行了重定向>,寫入文件不在是顯示器,而是普通文件,采用的刷新策略是全緩沖,之前的3條C函數(shù)雖然帶了\n,但是不足以將stdout緩沖區(qū)寫滿,所以數(shù)據(jù)并沒有刷新! 在執(zhí)行fork的時(shí)候,stdout屬于父進(jìn)程,fork創(chuàng)建子進(jìn)程緊接著就是進(jìn)程退出,誰先退出就要進(jìn)行緩沖區(qū)刷新,刷新的本質(zhì)就是修改,修改的時(shí)候發(fā)生寫時(shí)拷貝!所以數(shù)據(jù)最終會(huì)顯示兩份!上面的過程都和write無關(guān),write沒有FILE,而用的是fd,就沒有C提供的緩沖區(qū)!
簡(jiǎn)單總結(jié)來說:重定向?qū)е滤⑿虏呗园l(fā)生了改變(由行緩沖變成了全緩沖)。同時(shí)發(fā)生了寫時(shí)拷貝,父子進(jìn)程各自刷新
三、理解緩沖區(qū)
對(duì)于緩沖區(qū)的理解我們可以自己通過代碼來簡(jiǎn)單實(shí)現(xiàn):
FILE_結(jié)構(gòu)體的設(shè)計(jì),這里為了避免與FILE發(fā)生沖突,我們命名為FILE_:
#define SIZE 1024
typedef struct _FILE
{
int flags;//刷新方式
int fileno;//文件描述符
int cap;//buffer的總?cè)萘? int size;//buffer當(dāng)前使用量
char buffer[SIZE];//緩沖區(qū) }FILE_;
主函數(shù):
int main()
{
//打開
FILE_ *fp = fopen_("./log.txt","w");
//打開失敗
if(fp==NULL)
{
return 1;
}
int cnt = 10;
const char*masg = "hello world ";
while(1)
{
//寫入
fwrite_(masg,strlen(masg),fp);
//刷新
fflush_(fp);
//睡眠
sleep(1);
printf("count:%d\n",cnt);
cnt--;
if(cnt==0) break;
}
//關(guān)閉
fclose_(fp);
return 0;
}
對(duì)于C語言來說,文件接口一旦打開成功,其余接口要帶上FILE*,因?yàn)镕ILE結(jié)構(gòu)體里包含了各種數(shù)據(jù):
下面是我們需要自己實(shí)現(xiàn)的文件接口:
//打開
FILE_ * fopen_(const char*path_name,const char*mode);
//以下的接口都需要帶上FILE_*
void fwrite_(const void *ptr,int num, FILE_*fp);
void fflush_(FILE_*fp);
void fclose_(FILE_* fp);
fopen_:打開我們需要去判斷具體是按什么方式打開:
FILE_ *fopen_(const char *path_name, const char *mode)
{
int flags = 0;
int defaultMode=0666; //設(shè)置默認(rèn)權(quán)限
//讀方式
if(strcmp(mode, "r") == 0)
{
flags |= O_RDONLY;
}
//寫方式
else if(strcmp(mode, "w") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_TRUNC);
}
//追加方式
else if(strcmp(mode, "a") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_APPEND);
}
//其他
else
{
//其他方式這里就不展開了
}
int fd = 0;
if(flags & O_RDONLY) fd = open(path_name, flags);
else fd = open(path_name, flags, defaultMode);
//處理打開失敗
if(fd < 0)
{
const char *err = strerror(errno);
write(2, err, strlen(err));
return NULL;
}
//打開成功
FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
assert(fp);
fp->flags = SYNC_LINE; //默認(rèn)設(shè)置成為行刷新
fp->fileno = fd;
fp->cap = SIZE;
fp->size = 0;
memset(fp->buffer, 0 , SIZE);
return fp;
}
fwrite_:
void fwrite_(const void *ptr, int num, FILE_ *fp)
{
// 寫入到緩沖區(qū)中
memcpy(fp->buffer+fp->size, ptr, num); //不考慮緩沖區(qū)溢出的問題
fp->size += num;
// 判斷是否刷新
if(fp->flags & SYNC_NOW)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0; //清空緩沖區(qū)
}
else if(fp->flags & SYNC_FULL)
{
if(fp->size == fp->cap)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else if(fp->flags & SYNC_LINE)
{
if(fp->buffer[fp->size-1] == '\n') // abcd\nefg在這個(gè)地方不考慮
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else{
}
}
fclose:文章來源:http://www.zghlxwxcb.cn/news/detail-819040.html
void fclose_(FILE_ * fp)
{
fflush_(fp);
close(fp->fileno);
}
fflush_:這里將數(shù)據(jù)強(qiáng)制要求操作系統(tǒng)進(jìn)行外設(shè)刷新要用到fsync:文章來源地址http://www.zghlxwxcb.cn/news/detail-819040.html
void fflush_(FILE_ *fp)
{
if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
fsync(fp->fileno); //將數(shù)據(jù),強(qiáng)制要求OS進(jìn)行外設(shè)刷新!
fp->size = 0;
}
到了這里,關(guān)于【Linux】理解緩沖區(qū)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!