真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

系統(tǒng)文件IO/文件描述符/重定向/FILE/緩沖區(qū)的理解-創(chuàng)新互聯(lián)

本文目標(biāo):
認(rèn)識(shí)文件相關(guān)系統(tǒng)調(diào)用接口
認(rèn)識(shí)文件描述符,理解重定向
對(duì)比f(wàn)d和FILE,理解系統(tǒng)調(diào)用和庫(kù)函數(shù)的關(guān)系

成都創(chuàng)新互聯(lián)專注于浪卡子企業(yè)網(wǎng)站建設(shè),自適應(yīng)網(wǎng)站建設(shè),商城系統(tǒng)網(wǎng)站開發(fā)。浪卡子網(wǎng)站建設(shè)公司,為浪卡子等地區(qū)提供建站服務(wù)。全流程按需設(shè)計(jì)網(wǎng)站,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)

來(lái)來(lái)來(lái),學(xué)起來(lái)!動(dòng)起來(lái)!熱愛計(jì)算機(jī)的我們必然可以克服種種困難去達(dá)成我們的目標(biāo)!

談文件:

對(duì)于文件,有以下共識(shí):

①空文件,也是要在磁盤中占據(jù)空間

②文件 = 內(nèi)容 + 屬性

③文件操作?=? 對(duì)文件內(nèi)容的操作 + 對(duì)文件屬性的操作

④標(biāo)定一個(gè)問(wèn)題,必須是文件路徑+文件名【唯一性】

⑤如果沒(méi)有指明路徑,那么默認(rèn)是在當(dāng)前路徑(進(jìn)程當(dāng)前路徑)進(jìn)行文件操作

⑥當(dāng)我們?cè)贑語(yǔ)言中,把fopen、fclose、fread、fwrite等接口寫完后,然后通過(guò)代碼編譯、形成二進(jìn)制可執(zhí)行程序之后,但是沒(méi)有運(yùn)行程序,此時(shí)的文件并沒(méi)有被操作起來(lái),這意味著,對(duì)文件的操作,其本質(zhì)是進(jìn)程對(duì)文件的操作!因?yàn)槌绦蜻\(yùn)行起來(lái),創(chuàng)建了進(jìn)程,文件才被操作。

⑦文件要被訪問(wèn),那就必須被打開。是被用戶進(jìn)程和OS一起打開的,用戶進(jìn)程負(fù)責(zé)對(duì)接口的調(diào)用,OS則是負(fù)責(zé)對(duì)文件的打開。并且,雖然文件要被打開才能被訪問(wèn),但是磁盤上的文件并不是全都被打開的,因此文件我們可以狹隘地分被打開的和未被打開的兩種文件。

結(jié)合以上,我們得出結(jié)論:文件操作的本質(zhì)就是進(jìn)程與被打開的文件的關(guān)系!我們研究文件操作,就是在研究?jī)烧叩年P(guān)系!

對(duì)于文件操作系統(tǒng),C語(yǔ)言有,C++有,Java等等的計(jì)算機(jī)語(yǔ)言都有,雖然它們的接口不一樣,但是,它們的底層,都是調(diào)用了操作系統(tǒng)提供的文件操作的接口。因?yàn)閷?duì)于文件來(lái)說(shuō),文件是放在磁盤的,而磁盤是硬件,只有操作系統(tǒng)有資格去訪問(wèn)硬件,因此要對(duì)文件進(jìn)行操作,就必須通過(guò)OS,OS提供系統(tǒng)級(jí)別的系統(tǒng)調(diào)用接口,而操作系統(tǒng)只有一個(gè),因此,底層就是相同的啦!

也就是說(shuō):不管上層語(yǔ)言如何變化,庫(kù)函數(shù)底層必須調(diào)用系統(tǒng)調(diào)用接口。

系統(tǒng)文件IO:

操作文件,除了調(diào)用計(jì)算機(jī)語(yǔ)言:C,C++等待的庫(kù)函數(shù)接口以外,那就是可以調(diào)用系統(tǒng)接口。

接口介紹:

open:

#include#include#includeint open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname: 要打開或創(chuàng)建的目標(biāo)文件,就跟C語(yǔ)言的一樣,選擇路徑,默認(rèn)當(dāng)前路徑
flags: 打開文件時(shí),可以傳入多個(gè)參數(shù)選項(xiàng),用下面的一個(gè)或者多個(gè)常量進(jìn)行“或”運(yùn)算,構(gòu)成flags。
mode:權(quán)限,就是創(chuàng)建的文件的權(quán)限是啥,得告訴接口函數(shù)

參數(shù):
O_RDONLY: 只讀打開
O_WRONLY: 只寫打開
O_RDWR : 讀,寫打開
這三個(gè)常量,必須指定一個(gè)且只能指定一個(gè)
O_CREAT : 若文件不存在,則創(chuàng)建它。需要使用mode選項(xiàng),來(lái)指明新文件的訪問(wèn)權(quán)限
O_APPEND: 追加寫
O_TRUNC:清空文件內(nèi)容

返回值:
成功:新打開的文件描述符
失?。?1

對(duì)于flags的參數(shù)選項(xiàng),是OS通過(guò)比特位來(lái)傳遞選項(xiàng)的,看下面代碼:

//每一個(gè)宏,對(duì)應(yīng)的數(shù)值,只有一個(gè)比特位是1,彼此的位置不重疊
#define ONE (1<<0)  //0x1   0001
#define TWO (1<<1)   //0x2  0010
#define THREE (1<<2)  //0x4  0100
#define FOUR (1<<3)  //0x8   1000

void show(int flags)
{
	if (flags & ONE)printf("one\n");
	if (flags & TWO)printf("two\n");
	if (flags & THREE)printf("three\n");
	if (flags & FOUR)printf("four\n");
}

int main()
{
	show(ONE);
	show(TWO);
	show(ONE | TWO);
	show(ONE | TWO | THREE);
	show(ONE | TWO | THREE | FOUR);
	return 0;
}

因?yàn)槊總€(gè)比特位都對(duì)應(yīng)這一個(gè)選項(xiàng),而且是不能重疊的,因此,選項(xiàng)對(duì)應(yīng)的比特位都是單獨(dú)一個(gè)1.不能出現(xiàn)3(0011)這樣的值。這里也就解釋了關(guān)于flags參數(shù)的使用方法了。

下面是open接口、write接口和read接口的使用:

三個(gè)參數(shù)的:

以寫的方式:

O_WRONLY:只寫打開,但是在沒(méi)有文件存在的時(shí)候,會(huì)打開失敗,可以或上O_CREAT,默認(rèn)權(quán)限為666。對(duì)應(yīng)關(guān)閉文件的是close。

int main()
{

	int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
	if (fd< 0)
	{
		perror("fd faile");
		return 1;
	}
	close(fd);
	return 0;
}

兩個(gè)參數(shù)的:

int fd = open(FILE_NAME, O_WRONLY);

write:

#includesize_t write(int fd, const void* buf, size_t count);

fd:想要往哪個(gè)文件寫
buf:緩沖區(qū)對(duì)應(yīng)的數(shù)據(jù)
count:緩沖區(qū)的字節(jié)個(gè)數(shù)

返回值:寫了的字節(jié)個(gè)數(shù)

write接口比較簡(jiǎn)單粗暴,buf的類型是const void*,因?yàn)閷?duì)于文件,它的文本類跟二進(jìn)制類都是語(yǔ)言本身提供的分類。但是對(duì)于操作系統(tǒng)來(lái)說(shuō),它們都是二進(jìn)制的。

代碼如下:

int main()
{

	int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
	if (fd< 0)
	{
		perror("fd faile");
		return 1;
	}

	char outbuff[64];
	int cnt = 5;
	while (cnt)
	{
		sprintf(outbuff, "%s:%d\n", "hello Linux", cnt--);
		write(fd, outbuff, strlen(outbuff));
	}
	close(fd);
	return 0;
}

這里的strlen(outbuff)不能減一,不要想著里面的'\0'。對(duì)于C語(yǔ)言有規(guī)定'\0'作為字符串的結(jié)尾,但是這關(guān)我文件什么事?我文件的內(nèi)容結(jié)尾又不需要'\0'作結(jié)尾。所以不要加1,如果加1了,我們的文件內(nèi)容就不是我們預(yù)期的那樣子了。

當(dāng)我們將代碼修改一下:hello Linux改成aaaa,然后執(zhí)行程序。

sprintf(outbuff, "%s:%d\n", "aaaa", cnt--);

這樣會(huì)導(dǎo)致結(jié)果出現(xiàn)異常,其原因是我們沒(méi)有清空原來(lái)的文件內(nèi)容,所以需要加上O_TRUNC.

int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);

所以,當(dāng)我們?cè)趯慍語(yǔ)言的文件操作的代碼的時(shí)候,我們寫入"w",其實(shí)就是在調(diào)用了open,然后操作系統(tǒng)自動(dòng)給我們傳入了FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666等等的參數(shù)!這就是C語(yǔ)言文件接口和系統(tǒng)調(diào)用接口的關(guān)系。

對(duì)于追加,C語(yǔ)言中的"a",也是這里的O_APPEND

int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);

open以讀的方式:

需用用到read接口:

#includessize_t read(int fd, void* buf, size_t count);
ssize_t:系統(tǒng)定制的類型:有符號(hào)整數(shù),可以大于0,等于0,小于0.
fd:期望讀的文件
buf:讀去的緩沖區(qū),它的void*也是一樣,表示讀過(guò)去的文件類型不管是什么類型,來(lái)到這里都是二進(jìn)制
count:讀的字節(jié)個(gè)數(shù)

返回值:成功,就返回讀到的字節(jié)個(gè)數(shù)
40     int fd = open(FILE_NAME,O_RDONLY);                                                                                                   
 41     if(fd<0)                                                                                                                             
 42     {                                                                                                                                    
 43         perror("fd faile");                                                                                                              
 44         return 1;                                                                                                                        
 45     }                                                                                                                                    
 46                                                                                                                                          
 47     char buffer[1024];                                                                                                                   
 48     ssize_t num = read(fd,buffer,sizeof(buffer)-1);//因?yàn)榻Y(jié)尾是'\0',不讀'\0'                                                            
 49     if(num >0)                                                                                                                          
 50     {                                                                                                                                    
 51         buffer[num] = 0;// 當(dāng)文件中的數(shù)據(jù)讀取到了buffer中,然而buffer和打印的函數(shù)是C語(yǔ)言接口,需要加上0;num >0表示讀取成功,是讀取的字節(jié)個(gè)數(shù)   
 52     }                                                                                                                   
 53     printf("%s",buffer);

如上:系統(tǒng)調(diào)用的文件操作接口:open、close、write、read.當(dāng)然,還有l(wèi)seek。

對(duì)應(yīng)的C語(yǔ)言的接口:

系統(tǒng)調(diào)用C語(yǔ)言庫(kù)函數(shù)接口
openfoen
closefclose
writefwrite
readfread
lseekflseek

文件描述符fd:

文件操作的本質(zhì),是進(jìn)程和被打開文件的關(guān)系,而進(jìn)程是可以打開多個(gè)文件,那么系統(tǒng)中就一定會(huì)存在大量被打開的文件,這些文件都需要OS通過(guò)內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct_file{}來(lái)進(jìn)行標(biāo)識(shí)文件,來(lái)管理文件。那么進(jìn)程和這些被打開的文件之間的關(guān)系是通過(guò)文件描述符來(lái)維護(hù)的。

來(lái)看看文件描述符fd:

通過(guò)open接口,其返回值就是文件描述符fd。創(chuàng)建五個(gè)文件,分別返回其描述符,得到的結(jié)果是3,4,5,6,7.

我們看到,它是從3開始的,那么0,1,2呢?

我們使用C語(yǔ)言寫文件的時(shí)候,F(xiàn)ILE其實(shí)是一個(gè)結(jié)構(gòu)體,因?yàn)閹?kù)函數(shù)中的fopen調(diào)用的系統(tǒng)接口open,返回的是fd,那么FILE結(jié)構(gòu)體里面必有一個(gè)字段,那就是文件描述符!因此,我們可以使用FILE結(jié)構(gòu)體的字段,將標(biāo)準(zhǔn)輸入輸出流的文件描述符打印出來(lái),就可以知道了:0,1,2對(duì)應(yīng)的物理設(shè)備一般是:鍵盤(stdin),顯示器(stdout),顯示器(stderr),也就是0表示標(biāo)準(zhǔn)輸入, 1表示標(biāo)準(zhǔn)輸出, 2表示標(biāo)準(zhǔn)錯(cuò)誤。所以,從3開始的原因就是0,1,2被占用了。

那么為什么是從0開始,0,1,2,3,4...這樣的順序呢?

如上圖,由于當(dāng)一個(gè)文件被加載到內(nèi)存時(shí),會(huì)有許多個(gè)被打開的文件存在,這是負(fù)責(zé)打開這個(gè)文件的進(jìn)程一看,那么多文件,選誰(shuí)好?此時(shí)就會(huì)創(chuàng)建一個(gè)結(jié)構(gòu)體,里面有一個(gè)指針數(shù)組,用來(lái)存放這些文件,而這個(gè)數(shù)組,也稱為文件描述符表。此時(shí),當(dāng)我們需要打開一個(gè)文件的時(shí)候,會(huì)通過(guò)這個(gè)數(shù)組來(lái)訪問(wèn)它,并且返回這個(gè)數(shù)組的下標(biāo),這個(gè)下標(biāo)就是文件描述符!所以,文件描述符的本質(zhì),就是一個(gè)數(shù)組的下標(biāo)。

文件描述符的分配規(guī)則

如果將文件描述符為0,1,2的文件關(guān)掉,然后新建一個(gè)文件,并打印它的文件描述符,那么此時(shí)它的fd又是如何的呢?請(qǐng)看下面代碼:

首先,沒(méi)有關(guān)閉0,1,2任一文件時(shí),fd為3

#include#include#include#include#includeint main()
{

	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd< 0)
	{
		perror("open");
		return 1;
	}
	printf("fd %d\n", fd);

	close(fd);
	return 0;
}

關(guān)閉0:即在新創(chuàng)建文件或打開文件前,先關(guān)閉文件描述符為0的文件:

close(0);
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    //......

此時(shí)的結(jié)果:

關(guān)閉2:此時(shí)的結(jié)果是fd為2。如果把0和2同時(shí)關(guān)掉,那么fd為0。

因此:文件描述符的分配規(guī)則:在files_struct數(shù)組當(dāng)中,找到當(dāng)前沒(méi)有被使用的最小的一個(gè)下標(biāo),作為新的文件描述符

重定向:
close(1);
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    //......

關(guān)閉1:什么都沒(méi)顯示,即不會(huì)顯示預(yù)期中的1。那是因?yàn)槲覀冴P(guān)掉的1,是標(biāo)準(zhǔn)輸出的文件描述符,是固定的,然后創(chuàng)建出來(lái)的文件會(huì)被存進(jìn)下標(biāo)為1的數(shù)組空間中!那么此時(shí),fd為1,是這個(gè)文件的文件描述符了,但是標(biāo)準(zhǔn)輸出的文件描述符依然是1,只不過(guò)在下標(biāo)為1的這個(gè)空間里,變成了新建的那個(gè)文件。并且,我們打開文件一看,本來(lái)應(yīng)該打印出來(lái)的那個(gè)fd為1的信息,此時(shí)是被寫到了文件里面去了!這種特性,就叫做重定向!

當(dāng)子進(jìn)程重定向后,是不會(huì)影響到父進(jìn)程的,因?yàn)檫M(jìn)程具有獨(dú)立性

重定向的本質(zhì)就是長(zhǎng)層的fd不變,在內(nèi)核中修改fd對(duì)于的struct file*的地址。

dup2系統(tǒng)調(diào)用:

重定向的功能可以用dup2接口來(lái)實(shí)現(xiàn)。

#includeint dup2(int oldfd, int newfd);
:拷貝指定文件描述符的文件

oldfd:被拷貝的fd
newfd:要將fd的內(nèi)容拷貝到指定的fd中

返回值:成功,就會(huì)返回新的文件描述符,也就是fd。失敗,返回-1

看下面代碼:

#include#include#include#include#includeint main()
{
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd< 0)
	{
		perror("open");
		return 1;
	}

	dup2(fd, 1);//將fd中的文件拷貝到文件描述符為1數(shù)組空間中

	printf("fd %d\n", fd);
	close(fd);
	return 0;
}

結(jié)果如下:

對(duì)于重定向,可以操作的由追加重定向,輸入重定向,輸出重定向。

追加重定向:

重定向的同時(shí),追加文本內(nèi)容:

#include#include#include#include#includeint main()
{

	//close(0);
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
	if (fd< 0)
	{
		perror("open");
		return 1;
	}
	dup2(fd, 1);
	printf("fd %d\n", fd);

	const char* msg = "hello Linux";
	write(1, msg, strlen(msg));//寫入標(biāo)準(zhǔn)輸出
	close(fd);
	return 0;
}

輸入重定向:

本來(lái)是從stdin(0)上輸入的內(nèi)容,變?yōu)閺闹付ㄎ募陷斎搿?/p>

代碼如下:

#include#include#include#include#includeint main()
{

	//close(0);
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd< 0)
	{
		perror("open");
		return 1;
	}

	dup2(fd, 0);

	char line[64];
	while (1)
	{
		printf(">");
		if (fgets(line, sizeof(line), stdin) == NULL)break;
		printf("%s", line);
	}

	return 0;
}

若沒(méi)有重定向,沒(méi)有使用dup2(fd,0);的指令,那么就是從鍵盤(標(biāo)準(zhǔn)輸入)輸入,并打印

但在重定向后,便從指定文件中獲取信息并打印。

常見的重定向有:>>><:

這些重定向指令在命令行上使用。>為輸出重定向,<為輸入重定向,>>為追加重定向。

下面,我將模擬實(shí)現(xiàn)簡(jiǎn)易版的shell,并且是添加了重定向功能的!代碼如下:

#include#include#include#include#include#include#include#include#include#include 
#include#define NUM 1024
#define OPT_NUM 64

#define NONE_REDIR   0  //無(wú)
#define INPUT_REDIR  1  //輸出
#define OUTPUT_REDIR 2  //輸出
#define APPEND_REDIR  3 //出錯(cuò)

#define trimSpace(start) do{\
            while(isspace(*start)) ++start;\
        }while(0)

char lineCommand[NUM];
char* myargv[OPT_NUM]; //指針數(shù)組
int  lastCode = 0;
int  lastSig = 0;

int redirType = NONE_REDIR;
char* redirFile = NULL;

// "ls -a -l -i >myfile.txt" ->"ls -a -l -i" "myfile.txt" ->void commandCheck(char* commands)
{
    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);

    while (start< end)
    {
        if (*start == '>')
        {
            *start = '\0';
            start++;
            if (*start == '>')
            {
                // "ls -a >>file.log"
                redirType = APPEND_REDIR;
                start++;
            }
            else
            {
                // "ls -a >file.log"
                redirType = OUTPUT_REDIR;
            }
            trimSpace(start);
            redirFile = start;
            break;
        }
        else if (*start == '<')
        {
            //"cat<      file.txt"
            *start = '\0';
            start++;
            trimSpace(start);
            // 填寫重定向信息
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else
        {
            start++;
        }
    }
}


int main()
{
    while (1)
    {
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;

        // 輸出提示符
        printf("用戶名@主機(jī)名 當(dāng)前路徑# ");
        fflush(stdout);

        // 獲取用戶輸入, 輸入的時(shí)候,輸入\n
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL);
        (void)s;
        // 清除最后一個(gè)\n , abcd\n
        lineCommand[strlen(lineCommand) - 1] = 0; // ?

        // "ls -a -l -i >>myfile.txt" ->"ls -a -l -i" "myfile.txt" ->commandCheck(lineCommand);
        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

        // 如果沒(méi)有子串了,strtok->NULL, myargv[end] = NULL
        while (myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令,不需要?jiǎng)?chuàng)建子進(jìn)程,讓shell自己執(zhí)行對(duì)應(yīng)的命令,本質(zhì)就是執(zhí)行系統(tǒng)接口
        // 像這種不需要讓我們的子進(jìn)程來(lái)執(zhí)行,而是讓shell自己執(zhí)行的命令 --- 內(nèi)建/內(nèi)置命令
        if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if (myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        if (myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if (strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }
        // 測(cè)試是否成功, 條件編譯
#ifdef DEBUG
        for (int i = 0; myargv[i]; i++)
        {
            printf("myargv[%d]: %s\n", i, myargv[i]);
        }
#endif
        // 內(nèi)建命令 -->echo

        // 執(zhí)行命令
        pid_t id = fork();
        assert(id != -1);

        if (id == 0)
        {
            // 因?yàn)槊钍亲舆M(jìn)程執(zhí)行的,真正重定向的工作一定要是子進(jìn)程來(lái)完成
            // 如何重定向,是父進(jìn)程要給子進(jìn)程提供信息的
            // 這里重定向不會(huì)影響父進(jìn)程,進(jìn)程具有獨(dú)立性
            switch (redirType)
            {
            case NONE_REDIR:
                // 什么都不做
                break;
            case INPUT_REDIR:
            {
                int fd = open(redirFile, O_RDONLY);
                if (fd< 0) {
                    perror("open");
                    exit(errno);
                }
                // 重定向的文件已經(jīng)成功打開了
                dup2(fd, 0);
            }
            break;
            case OUTPUT_REDIR:
            case APPEND_REDIR:
            {
                umask(0);
                int flags = O_WRONLY | O_CREAT;
                if (redirType == APPEND_REDIR) flags |= O_APPEND;
                else flags |= O_TRUNC;
                int fd = open(redirFile, flags, 0666);
                if (fd< 0)
                {
                    perror("open");
                    exit(errno);
                }
                dup2(fd, 1);
            }
            break;
            default:
                printf("bug?\n");
                break;
            }

            execvp(myargv[0], myargv); // 執(zhí)行程序替換的時(shí)候,不會(huì)影響曾經(jīng)進(jìn)程打開的重定向的文件
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret >0);
        (void)ret;
        lastCode = ((status >>8) & 0xFF);
        lastSig = (status & 0x7F);
    }
}

于是,我們可以像是正常地在命令行上寫入指令。

理解Linux下一切皆文件? ?

在馮諾依曼體系中,硬件都屬于外設(shè),對(duì)于外設(shè)的數(shù)據(jù)處理,都是先把數(shù)據(jù)讀到內(nèi)存,當(dāng)處理完后,再將數(shù)據(jù)刷新到外設(shè)中,這就稱作IO。

而對(duì)于硬件來(lái)說(shuō),是操作系統(tǒng)來(lái)管理它們,操作系統(tǒng)為了方便管理,就會(huì)對(duì)不同的外設(shè)硬件創(chuàng)建出對(duì)應(yīng)的結(jié)構(gòu)體,每一個(gè)結(jié)構(gòu)體里面包含了相應(yīng)的屬性信息,都有屬于自己的讀寫方法,這就意味著每種硬件的方法方法是一定不一樣的??墒窃趺磥?lái)表示這些硬件的方法呢?

Linux做了一件事,就是提取硬件的屬性,創(chuàng)建一個(gè)結(jié)構(gòu)體struct file(){}來(lái)訪問(wèn)硬件,并且這個(gè)結(jié)構(gòu)體里面擁有讀寫的方法的函數(shù)指針,最后將每種硬件對(duì)應(yīng)的結(jié)構(gòu)體連起來(lái),組織起來(lái),當(dāng)需要調(diào)用硬件的方法的時(shí)候,只需要去結(jié)構(gòu)體里面找,然后調(diào)用方法即可。于是,站在struct file上層去看,所有的文件和設(shè)備,統(tǒng)一都是struct file,即內(nèi)核數(shù)據(jù)結(jié)構(gòu),這部分也稱為虛擬文件(vfs)。?這就是所謂的Linux一切皆文件!

FILE/緩沖區(qū)問(wèn)題

上面我們提到,因?yàn)镮O相關(guān)函數(shù)與系統(tǒng)調(diào)用接口對(duì)應(yīng),并且?guī)旌瘮?shù)封裝系統(tǒng)調(diào)用,所以本質(zhì)上,訪問(wèn)文件都是通過(guò)fd訪問(wèn)的。所以C庫(kù)當(dāng)中的FILE結(jié)構(gòu)體內(nèi)部,必定封裝了fd。

先看下面代碼及其現(xiàn)象:

#include#include#include#include#include#includeint main()
{
	//C接口                                                                                                                                    
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	fputs("hello fputs\n", stdout);

	// system call                                                                                                                             
	const char* msg = "hello write\n";
	write(1, msg, strlen(msg));
    
    //fork();
	return 0;
}

運(yùn)行了代碼之和,結(jié)果和預(yù)期中的一樣,四個(gè)打印都出來(lái)了。

接著,我們重定向輸入到文件中,那么,文件里面的內(nèi)容,也跟預(yù)期中的一樣,是這四個(gè)打印的內(nèi)容。

然而,當(dāng)我們把代碼中被注釋掉的fork()放出來(lái),按照同樣的測(cè)試,不重定向的話,打印出來(lái)的結(jié)果,也是這四個(gè),但是一旦重定向,就會(huì)有以下現(xiàn)象:

凡是C語(yǔ)言接口的,都被打印了兩次!看著以上的操作,可以判斷是fork()函數(shù)帶來(lái)的原因。

第一:如果printf調(diào)用成功,即信息輸入到了顯示器上,就說(shuō)明是寫到外設(shè)上了,就不屬于這個(gè)父進(jìn)程了,但如果數(shù)據(jù)沒(méi)有被寫到顯示器上,此時(shí)這個(gè)數(shù)據(jù)依舊屬于這個(gè)父進(jìn)程。

第二:如果printf調(diào)用成功,數(shù)據(jù)不一定會(huì)到了顯示器上,也就是在馮諾依曼體系中,沒(méi)有從內(nèi)存到外設(shè)這一步,因此依舊屬于當(dāng)前進(jìn)程,然后當(dāng)調(diào)用fork(),最后進(jìn)程結(jié)束,需要刷新緩沖區(qū),將數(shù)據(jù)從內(nèi)存刷新到外設(shè)。

理解緩沖區(qū)問(wèn)題:

緩沖區(qū)的本質(zhì)就是一段內(nèi)存,那么就會(huì)有以下問(wèn)題:

這段內(nèi)存是誰(shuí)申請(qǐng)的?

這段內(nèi)存是屬于誰(shuí)的?

這段內(nèi)存存在的意義?

緩沖區(qū)就是相當(dāng)于現(xiàn)實(shí)生活中的快遞公司,有了快遞公司,當(dāng)我們想把某樣?xùn)|西送給遠(yuǎn)方的親戚朋友時(shí),就不需要我們自己還得坐飛機(jī)坐火車那樣花時(shí)間送過(guò)去了,只需要把東西交給快遞公司,快遞公司就會(huì)幫我們送到。所以說(shuō)快遞存在的意義就是節(jié)省我們的時(shí)間。

同理,緩沖區(qū)就是如此,進(jìn)程在與外設(shè)進(jìn)行數(shù)據(jù)處理的時(shí)候,會(huì)通過(guò)緩沖區(qū)來(lái)進(jìn)行數(shù)據(jù)的交流,即將數(shù)據(jù)從內(nèi)存拷貝到緩沖區(qū)中,從而達(dá)到節(jié)省進(jìn)程進(jìn)行數(shù)據(jù)IO的時(shí)間!這就是緩沖區(qū)存在的意義!

數(shù)據(jù)從內(nèi)存拷貝到緩沖區(qū)時(shí),通過(guò)fwrite函數(shù)來(lái)進(jìn)行拷貝,因此我們與其將fwrite函數(shù)理解為是寫入到文件的函數(shù),倒不如說(shuō)它是拷貝函數(shù),將數(shù)據(jù)從進(jìn)程進(jìn)行拷貝到緩沖區(qū)或外設(shè)。

緩沖區(qū)刷新策略:

緩沖區(qū)會(huì)結(jié)合具體的設(shè)備,定制相應(yīng)的刷新策略:

a.立即刷新,這種是無(wú)緩沖,拿到數(shù)據(jù)后直接刷新

? :比如說(shuō)fflush();刷新緩沖區(qū),直接刷新。

b.行刷新,也就是行緩沖,顯示器就是這種。

? :因?yàn)轱@示器是給人看的,正常來(lái)說(shuō),我們都是從左向右一行一行地看。而刷新效率是一下子全刷新的話,效率是最高的。因此在保證刷新效率的同時(shí),也要保證體驗(yàn)效果,就有了行緩沖。

?????

c.緩沖區(qū)滿,也就是全緩沖,對(duì)應(yīng)的是磁盤文件等等。

? :全緩沖的效率是最高的,因?yàn)榈染彌_區(qū)滿了之和,一下子全刷新,IO的時(shí)間只需一次。

另外兩種刷新情況:

1.進(jìn)程退出后,緩沖區(qū)一般都要刷新

2.用戶強(qiáng)制刷新

緩沖區(qū)在哪?指的是什么緩沖區(qū)?

看回上面的代碼,我們發(fā)現(xiàn)程序執(zhí)行后,打印出來(lái)是4次,但是如果輸入到文件中,發(fā)現(xiàn)是7次,而且凡是C語(yǔ)言的接口所打印的,都多了1次。那么這里說(shuō)明了,這種現(xiàn)象一定是跟緩沖區(qū)有關(guān)!并且,緩沖區(qū)一定不在內(nèi)核當(dāng)中!如果在的話,那么write也會(huì)打印兩次。

所以,我們討論的緩沖區(qū),都是用戶級(jí)別語(yǔ)言層面給我們提供的緩沖區(qū)。我們?cè)谧x寫文件的時(shí)候,都會(huì)用stdout、stdin、stderr或者是別的文件,它們對(duì)應(yīng)的類型是FILE*,而FILE是一個(gè)結(jié)構(gòu)體,里面包含了fd,還有緩沖區(qū)!

當(dāng)我們使用C語(yǔ)言,寫出fflush,fwrit的接口時(shí)候,是要傳文件指針的,而文件指針就是FILE*,包含了緩沖區(qū)!FILE里面封裝了fd,所以C語(yǔ)言會(huì)在合適的時(shí)候,將我們的數(shù)據(jù)刷新到外設(shè)。

解釋上面程序結(jié)果現(xiàn)象:

fork()函數(shù)是在代碼結(jié)束之前的,也就是在代碼結(jié)束之前,子進(jìn)程被創(chuàng)建。

①如果我們沒(méi)有進(jìn)行重定向,會(huì)看到四條打印結(jié)果。因?yàn)閟tdout是行刷新,在進(jìn)程fork()之前,三條C語(yǔ)言函數(shù)已經(jīng)將數(shù)據(jù)打印到標(biāo)準(zhǔn)輸出(顯示器外設(shè)),此時(shí)的FILE內(nèi)部,或者說(shuō)是進(jìn)程內(nèi)部,已經(jīng)沒(méi)有了這些數(shù)據(jù)了

②如果我們進(jìn)行了重定向,是寫入文件,而文件是屬于磁盤文件,一般C庫(kù)函數(shù)寫入文件時(shí),是全緩沖的,因此采用的刷新策略是全緩沖,之前的三條C語(yǔ)言函數(shù),雖然帶了'\n',但是不足以讓stdout緩沖區(qū)寫滿,數(shù)據(jù)并沒(méi)有被刷新!

③在執(zhí)行fork()的時(shí)候,stdout屬于父進(jìn)程,創(chuàng)建子進(jìn)程時(shí),緊接著是進(jìn)程退出,誰(shuí)先退出,誰(shuí)就要刷新緩沖區(qū),將數(shù)據(jù)拿走,放到外設(shè)中,所以刷新的本質(zhì)就是修改,修改的時(shí)候,就會(huì)發(fā)生寫實(shí)拷貝!此時(shí),父子進(jìn)程都有一份一樣的數(shù)據(jù),不管誰(shuí)先退出,那就先刷新唄,你退出,刷新完,到我退出刷新。最后就導(dǎo)致有兩份的數(shù)據(jù)被刷新到外設(shè)了。

④write是系統(tǒng)調(diào)用,上面的過(guò)程與write無(wú)關(guān),write沒(méi)有FILE,用的是fd,因此也就沒(méi)有C語(yǔ)言提供的緩沖區(qū),再怎么寫時(shí)拷貝,跟它無(wú)關(guān)。

緩沖區(qū)與OS的關(guān)系:

當(dāng)數(shù)據(jù)需要刷新到外設(shè)的時(shí)候,進(jìn)程會(huì)創(chuàng)建一個(gè)struct file的結(jié)構(gòu)體,還有一個(gè)內(nèi)核緩沖區(qū)。

我們上面所說(shuō)的緩沖區(qū)刷新策略,是在用戶層面,C語(yǔ)言的FILE自己的緩沖策略,而操作系統(tǒng)可不會(huì)理這些策略,它有自己的判斷,即OS可以自主決定,是否將內(nèi)核緩沖區(qū)中的數(shù)據(jù)刷新到外設(shè)上。比如內(nèi)核緩沖區(qū)空間不夠了,那么OS就會(huì)強(qiáng)制刷新等等。用戶也能強(qiáng)制讓內(nèi)核緩沖區(qū)里面的數(shù)據(jù)之間刷新到外設(shè)中,使用fsync(int fd)函數(shù)。

綜上: printf fwrite 庫(kù)函數(shù)會(huì)自帶緩沖區(qū),而 write 系統(tǒng)調(diào)用沒(méi)有帶緩沖區(qū)。另外,我們這里所說(shuō)的緩沖區(qū),都是用戶級(jí)緩沖區(qū)。其實(shí)為了提升整機(jī)性能,OS也會(huì)提供相關(guān)內(nèi)核級(jí)緩沖區(qū)。那這個(gè)緩沖區(qū)誰(shuí)提供呢? printf fwrite 是庫(kù)函數(shù), write 是系統(tǒng)調(diào)用,庫(kù)函數(shù)在系統(tǒng)調(diào)用的“上層”, 是對(duì)系統(tǒng)調(diào)用的“封裝”,但是 write 沒(méi)有緩沖區(qū),而 printf,fwrite 有,足以說(shuō)明,該緩沖區(qū)是二次加上的,又因?yàn)槭荂,所以由C標(biāo)準(zhǔn)庫(kù)提供。

本篇完,但文件還沒(méi)完,下一篇:文件系統(tǒng)!沖!

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級(jí)服務(wù)器適合批量采購(gòu),新人活動(dòng)首月15元起,快前往官網(wǎng)查看詳情吧


當(dāng)前題目:系統(tǒng)文件IO/文件描述符/重定向/FILE/緩沖區(qū)的理解-創(chuàng)新互聯(lián)
當(dāng)前路徑:http://weahome.cn/article/jchsd.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部