一.緩沖區(qū)
int main()
{
printf("hello linux");
sleep(2);
return 0;
}
對于這樣的代碼,首先可以肯定的是printf
語句先于sleep
執(zhí)行,既然如此那么就應該是先打印語句然后進行休眠,下面看看結(jié)果:
但這里卻是先休眠以后再打印語句,這是因為存在一個叫緩沖區(qū)的東西,當我們要向外設寫入數(shù)據(jù)(讓顯示器顯示就是向顯示器寫入數(shù)據(jù))時會將數(shù)據(jù)暫存至緩沖區(qū),然后在根據(jù)緩沖區(qū)的刷新策略刷新。
先休眠再顯示數(shù)據(jù)是因為我們并不是直接向外設寫入數(shù)據(jù),而休眠以后還能刷出數(shù)據(jù)是因為有緩沖區(qū)暫存數(shù)據(jù)。下面就來談談緩沖區(qū)。
1.什么是緩沖區(qū)
緩沖區(qū)的本質(zhì)就是一塊內(nèi)存(物理內(nèi)存)
2.緩沖區(qū)的意義
我是一個奇思妙想的手藝人,我有一個好朋友叫泰褲辣。每當我打造出一個東西的時候我都會騎著自行車跨越一百多公里去送給他。后來有一天,快遞行業(yè)興起了,我有新發(fā)明就不用再自己騎著自行車跨越山和大海去給他送了,我只要將我的東西交給快遞點,就可以繼續(xù)回家搞發(fā)明,東西有快遞公司去給我送,這樣就節(jié)省了我大量的時間。
那么我就是進程,我的好朋友泰褲辣就是文件,而我的新發(fā)明就是數(shù)據(jù),緩沖區(qū)就是快遞點。所以說緩沖區(qū)最大意義就在于節(jié)省發(fā)送者的時間,也就是節(jié)省進程的時間。因為外設是一個很慢的東西,當我們訪問外設的時候大部分時間都是在等外設準備好,真正寫入的時間占比很少。如果有緩沖區(qū)的存在,那么進程只要將數(shù)據(jù)交給緩沖區(qū)以后就可以返回去執(zhí)行后續(xù)的代碼,緩沖區(qū)幫進程承擔了等外設準備好的時間代價。
3.緩沖區(qū)的刷新策略
但我去寄快遞,往往都是我將東西交給快遞點一段時間后我的東西才被快遞點發(fā)出,因為如果一有人寄東西快遞點就派車去送這樣效率太低百分百虧錢。但是如果是在淡季,等了很長也沒有多少人寄快遞,快遞點也不會說將你的東西留在他那里好幾個月。而且如果你是寄一輛轎車大小或者等級的東西,快遞點也是會根據(jù)你這個情況單次的將你的快遞發(fā)出。所以雖然快遞公司正常情況下是等貨物累計要一定數(shù)量才發(fā)送,但是也會有特殊情況。
同理,緩沖區(qū)刷新也是一樣,雖然效率最高的是緩沖區(qū)滿了以后再一次將整個緩沖區(qū)中的數(shù)據(jù)刷新出去(又稱全緩沖),但是這個刷新方式只在將數(shù)據(jù)刷新到磁盤文件上的時候才使用。
向顯示器寫入數(shù)據(jù)時,緩沖區(qū)采用的方式是行刷新(行緩沖)。這是因為顯示器是給用戶看的,而我們?nèi)说拈喿x習慣是按行從左到右讀取,計算機本質(zhì)就是給人使用的工具,所以在給顯示器刷新的時候采用行刷新。
除了全緩沖和行緩沖以外,還有一種很少見的刷新方式叫無緩沖,也就是說一有數(shù)據(jù)寫入就立馬刷新出去。比如printf立馬fflush
此外還有兩種特殊的刷新方式:
1.用戶強制刷新
2.進程退出;進程在退出之前為了防止緩沖區(qū)還有數(shù)據(jù)沒被刷新出去導致數(shù)據(jù)丟失會再刷新一次緩沖區(qū)
4.我們目前談論的緩沖區(qū)在哪里
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
//先寫一批C語言函數(shù)接口
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
//再寫一個系統(tǒng)調(diào)用
const char*s="hello write\n";
write(1,s,strlen(s));
fork();
return 0;
}
上面的代碼在直接將結(jié)果顯示到屏幕中和將結(jié)果重定向到文件中是兩種不同的結(jié)果:
根據(jù)上圖可以看到,當我們直接將結(jié)果輸出到屏幕上,一共打印了四條語句這很符合我們的推測。但是一旦將這個輸出結(jié)果重定向到文件中,就變成了打印七條語句,其中C語言的函數(shù)接口被打印了兩次。首先這個現(xiàn)象的原因和緩沖區(qū)有關,其次和fork有關。
上述現(xiàn)象可以說明我們目前為止在談論的緩沖區(qū)不在內(nèi)核中,否則系統(tǒng)調(diào)用write
也要被打印兩次,那么它就只能在用戶層。要訪問一個文件首先要有這個文件的fd
,所以C語言所用的FILE結(jié)構(gòu)體中一定要包含fd
,那么今天可以知道FILE結(jié)構(gòu)體中肯定也是有緩沖區(qū)的,否則為什么我們調(diào)用fflush函數(shù)都是傳FILE*
呢?上面談論的各種刷新策略也針對的是FILE結(jié)構(gòu)體中的緩沖區(qū)。
上述情況的解釋:
1.因為顯示器是給用戶看的外設,所以必須要符合用戶按行從左到右的閱讀習慣,也就是說向顯示器文件中寫入時采用的是行刷新,一旦遇到\n
就果斷刷新,而向文件中刷新數(shù)據(jù)為了效率采用的是全緩沖,雖然四條輸出語句都帶了\n
,但是仍然不足以將緩沖區(qū)寫滿。
2.fork創(chuàng)建的子進程是對父進程的一種拷貝,它們共享代碼和數(shù)據(jù)(包括FILE中的緩沖區(qū)),fork之后馬上就退出了,進程一旦退出為了防止進程丟失會刷新一次緩沖區(qū),而刷新緩沖區(qū)就是將緩沖區(qū)清空,這本質(zhì)上是一種修改,因為進程具有獨立性,為了不然子進程的行為影響父進程就會發(fā)生寫時拷貝,即子進程復制父進程緩沖區(qū)的數(shù)據(jù)并將其刷新到文件中,隨后父進程退出再將數(shù)據(jù)刷新到文件中。
3.系統(tǒng)調(diào)用用的是fd,沒有FILE結(jié)構(gòu)體,也就沒有FILE所提供的緩沖區(qū)。
5.仿寫FILE
紙上得來終覺淺,絕知此事要躬行。接下來我們就自己通過使用系統(tǒng)調(diào)用接口,來嘗試封裝一下FILE結(jié)構(gòu)體:
5.1myStdio.h
#pragma once #include<unistd.h> #include<assert.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h>
#include<assert.h> #include<stdlib.h>
#include<string.h> #include<stdio.h>
//FILE中有緩沖區(qū),刷新方式,以及fd
//定義緩沖區(qū)大小
#define SIZE 1024 //定義刷新方式
#define SYNC_NOW 1 #define SYNC_LINE 2 #define SYNC_ALL 3 typedef struct FILE_
{
int flag;//刷新方式 int feilno;//fd int cap;//記錄緩沖區(qū)容量 int size;//記錄緩沖區(qū)使用
char buff[];//緩沖區(qū) }FILE_;
//實現(xiàn)四個函數(shù):fopen,fflush,fwrite,fclose
FILE_*fopen_(const char*path,const char*mode); void fflush_(FILE_*fp); void fwrite_(const char*ptr,size_t num,FILE_*fp); void fclose_(FILE_*fp);
5.2myStdio.c
#include"myStdio.h"
//實現(xiàn)四個函數(shù)
FILE_ *fopen_(const char*path,const char*mode)
{
int flags=0;//設置文件打開的方式
int defaultmode=0666;//設置文件打開的默認權(quán)限
if(strcmp(mode,"r")==0)
{
flags|=O_RDWR;
}
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;
}
int fd=0;
if(flags&O_RDWR)
fd=open(path,flags);
else
fd=open(path,flags,defaultmode);
if(fd<0)//文件打開失敗
{
perror("open");
return NULL;
}
FILE_*fp=(FILE_*)malloc(sizeof(FILE_));//為FILE_結(jié)構(gòu)體開辟空間
assert(fp);
//初始化FILE_
fp->cap=SIZE;
fp->feilno=fd;
fp->flag=SYNC_LINE;//默認設為行刷新
fp->size=0;
memset(fp->buff,0,SIZE);
return fp;
}
void fwrite_(const char*ptr,size_t num,FILE_*fp)
{
//將字符串拷貝到緩沖區(qū)
memcpy(fp->buff+fp->size,ptr,num);
//更新緩沖區(qū)使用量
fp->size+=num;
//按照刷新方式刷新
if(fp->flag&SYNC_NOW)
{
write(fp->feilno,fp->buff,fp->size);
fp->size=0;
}
else if(fp->flag&SYNC_ALL)
{
if(fp->size==fp->cap)
{
write(fp->feilno,fp->buff,fp->size);
fp->size=0;
}
}
else if(fp->flag&SYNC_LINE)
{
if(fp->buff[fp->size-1]=='\n')//如果最后一個字符是\n
{
write(fp->feilno,fp->buff,fp->size);
fp->size=0;
}
}
}
void fflush_(FILE_*fp)
{
//所謂刷新,不過就是將緩沖區(qū)中的內(nèi)容刷新到外設中,有內(nèi)容才刷新
if(fp->size>0)
write(fp->feilno,fp->buff,fp->size);
fsync(fp->feilno);//強制刷新到磁盤
//刷新完以后緩沖區(qū)就沒數(shù)據(jù)了,要將緩沖區(qū)置空
fp->size=0;
}
void fclose_(FILE_*fp)//在關閉文件之前,還要刷新緩沖區(qū)
{
fflush_(fp);
close(fp->feilno);
}
6.操作系統(tǒng)的緩沖區(qū)
不止用戶層有緩沖區(qū),內(nèi)核中也有一個內(nèi)核緩沖區(qū)。當我們使用C語言文件操作函數(shù)寫入數(shù)據(jù)時,首先將數(shù)據(jù)拷貝到FILE結(jié)構(gòu)體的緩沖區(qū)中,并按照無緩沖/行緩沖/全緩沖的刷新策略將數(shù)據(jù)刷新到內(nèi)核緩沖區(qū)中,最后由操作系統(tǒng)自主將內(nèi)核緩沖去中的數(shù)據(jù)刷新到磁盤中。
與其將fwrite等函數(shù)理解成寫入函數(shù),不如將其理解成拷貝函數(shù)
文章來源:http://www.zghlxwxcb.cn/news/detail-425269.html
如果你要強制將內(nèi)核緩沖區(qū)中的數(shù)據(jù)刷新到外設中,可以使用系統(tǒng)調(diào)用fsync
。文章來源地址http://www.zghlxwxcb.cn/news/detail-425269.html
到了這里,關于理解緩沖區(qū)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!