目錄
C語言關于文件操作的函數
Linux關于文件操作的系統調用
完善myshell
C語言緩沖區
其實我們在C語言就學過文件操作,但是從語言的角度,我們只是說會用了關于文件的一些操作和函數,但其實它究竟是怎么回事我們其實并不明白,那么當我們學習到Linux操作系統的時候,我們才能更加深入的去了解這文件究竟是怎么回事
那么我們需要首先明確一些概念:文件=內容+屬性,不能說我這個文件是空,那么就不占空間;訪問文件都得先打開,然后通過執行代碼的方式去修改文件內容,也就是文件要被加載到內存中;是進程打開的文件并且一個進程可以打開多個文件;一個時間段內,可能有多個進程,操作系統要管理這些進程,同時也可能有多個被打開的文件,操作系統也要管理這些文件,那么如何管理呢?先描述,再組織,就是說,操作系統要給每個文件創建一個結構體對象,對文件的管理就變成了對于結構體的管理。
C語言關于文件操作的函數
下面只是介紹了一小部分,如果想詳細了解,可以去我的另一篇博客:
C語言文件操作詳談
那么下面先回憶一下之前C語言用的一些函數:
首先就是fopen,并且我記得當時選項w最為奇怪,因為每一個用,那么之前的數據都會不見了,這不是跟我們的輸出重定向(>)很像嗎,因為它們都是每次使用之前的內容就會被清空
這段話的意思是:截斷文件到長度0或者如果沒有那么創建新文件
我們原來都是這么用的:
我們說如果w選項,當前路徑下沒有這個文件,那么就會在當前路徑下去創建,那么進程怎么知道當前在那個路徑下呢?我們說過進程啟動時,會記錄下自己的路徑
所以進程就會在這個路徑下創建新文件
并且我們還有一個選項a,叫做append(附加),這不是跟追加重定向(>>)很像嗎
下面我們再看一下fwrite這個函數
基本的使用就是這樣
我們可以確定的是,輸入或輸出一些東西必須要指定文件,就連鍵盤和顯示器也不例外,因為Linux下一切皆文件,那么平時用printf/scanf的時候也沒打開鍵盤和顯示器文件并且也沒有指定文件啊(我們這么想是因為把鍵盤和顯示器看成和普通文件一樣了),那是因為首先stdout,stdin和stderror是進程默認打開的,不用手動打開,可以直接使用
其次為什么printf不用指定文件呢?因為它為了方便使用,是不用指定文件的,但是底層實現是封裝了fprintf
fprintf是要加stdout的,所以printf就是這么實現的
Linux關于文件操作的系統調用
我們知道,對硬件進行修改只能通過操作系統,所以操作系統就必須提供系統調用接口,像fopen這樣的庫函數是語言層面的概念,為了實現語言的跨平臺性和可移植性,所以它要封裝系統調用,并且在不同的操作系統要封裝各自的系統調用。所以我們下面就介紹一下Linux操作系統關于文件操作的系統調用open和close
open的第一個參數就是文件所處的路徑,第二個參數就是之前說的類似于“w“、“a”的一些選項,常見的打開標志有:
O_RDONLY:以只讀方式打開文件
O_WRONLY:以只寫方式打開文件
O_CREAT:沒有這個文件就創建
O_TRUNC:打開文件前會清空文件
O_APPEND:在文件尾追加數據
第三個參數就是我剛創建好一個文件,此時要給文件設置的權限,返回值就是文件描述符,其實就是一個整數,通過這個整數,就可以確定這個文件,具體是怎么確定的,這就跟底層實現有關了,我們后面會介紹一下
當我們用第一個open時,并且路徑中沒有,它需要創建,這時我們可以看到文件的權限是亂碼
所以我們一般使用第二個,權限計算還是給定的權限減去權限掩碼,我們把它們當成八進制數,比如:
666-002=664
并且通過umask()函數我們還可以在當前程序中設置權限掩碼,并且這個權限掩碼只在當前程序下生效
666-666=000
我們上面介紹了第二個參數,第二個參數要給一個整數,其實下面的一些“選項”就是一些宏,這些宏可以通過位運算決定整數的比特位,其實就是位圖,從而達到不同的下面的選項,知道了這些宏的意義,我們就可以和之前fopen的不同選項的功能對應上了,其實“w”選項不就是這三個選項的疊加嗎
其實如果只有前兩個的話原始文件中的內容并不會清空,而新內容就從頭開始進行覆蓋,就是這樣一種現象
選項“a”不就是這三個選項的疊加嗎
并且追加內容時會在下一行追加
所以我們說fopen底層就是封裝了open,并且不同的選項底層就對應了不同的宏,我們上面說stdin、stdout、stderr每個進程都會默認打開,并且它們的類型是FILE*,這是C語言層面的類型,本質就是一個結構體,既然Linux要通過文件描述符來確定一個文件,C語言要通過FILE*的對象,所以FILE結構體中肯定有文件描述符
為了探究文件描述符到底是什么,我們可以連續創建一些文件看看它們的文件描述符之間有什么規律
文件描述符是從3開始依次增長,那0,1,2呢?其實0,1,2就分別是程序默認打開的stdin,stdout和stderr,并且這個數字其實就是數組下標,什么意思呢?
我們知道,操作系統不僅要進行進程管理,還要進行文件管理,對于進程的管理我們知道是通過PCB,就是一個結構體對象;同樣,對于打開的文件(不管以什么樣的方式打開),操作系統也是要創建一個結構體對象(比如我們叫做struct file)進行管理的,我們可以把它們之間的關系大致理解為這樣:
就是說:一個進程的PCB中存著此進程打開的文件列表的指針,通過這個指針可以找到打開的文件列表,而這個文件列表中也是存著每個文件管理的指針,通過這個指針就可以找到操作系統對文件進行管理的結構體了
這就是為什么我們可以通過文件描述符(一個整數),來確定唯一一個文件了
并且要注意,我們圖中的緩沖區是操作系統給每個打開的文件提供的,和下面說的C語言庫函數提供的緩沖區不是一個概念
既然stdout等是自動打開的,我們可以關閉stdout試一試,還不能關這個,因為一旦關了就打印不出東西來了,我們可以關掉stdin,于是新打開的文件的文件描述符就會從最小的沒用到的0開始
這個_fileno就是FILE*結構體中文件描述符變量的名字
所以我們就可以利用上面的規則實現重定向,比如printf默認向stdout中打印,其實它認識的是文件描述符為1,所以我們就可以這樣,將想打印的內容打印到一個文件中而不是顯示器
這樣也確實比較麻煩,并且還要了解文件描述符的分配規則,其實也不知可以這么做,系統還提供了系統調用來供我們使用,這個就是通過拷貝指針來實現目的,就是上面圖中中間那個圖里邊的指針
于是我們就可以這么用:
就是讓數組下標為1的里邊的內容變成數組下標為fd的里邊的內容,這樣printf打印肯定還是向數組下標為1的里邊的指針指向的文件打印,這時文件就變成log.txt
完善myshell
既然已經明白了重定向的原理:就是通過文件描述符實現向任意一個文件中寫入,本質上就是改變文件指針數組中的指針就可以實現向指定的文件中寫入,于是我們就可以完善一下我們之前寫的myshell
可以移步到下面這篇博客中:自定義bash進程
C語言緩沖區
我們這里說的緩沖區其實就是C語言庫中提供的一個緩沖區,這么說可能有點抽象,其實就是在我們調用文件相關函數的時候,需要用到FILE這個類型,這個類型其實就是一個結構體,里邊就存著緩沖區。
也就是說:不管是log.txt這樣的普通文件的文件指針,又或者是stdout這樣的進程自動打開的文件指針,它們都是一個結構體對象,這個對象中就存著緩沖區,那么這樣好處是什么呢?
1.首先我們要知道,調用系統調用是有成本的,所以我們需要盡可能的少的調用,那么我先暫時把數據寫到C語言的緩沖區中,最后統一的調用系統調用把數據給操作系統,這樣系統調用的次數就減少了
2.其次在寫代碼的人看來,執行完這句代碼,數據就好像已經寫入到了文件中,但實際上是寫入到了緩沖區中,這種在內存中的拷貝可比把數據寫入到磁盤當中快多了,這樣表現出來的就是C語言的速度很快,可以提高寫代碼的人的體驗
我剛才說緩沖區就在FILE的結構體對象中,那么它跟我們之前說的進程地址空間是什么關系呢?
其實你得看FILE對象在哪,FILE對象可以在調用fopen函數時在函數內部去堆上申請,所以緩沖區就在堆上,我們下邊也會模擬實現一下緩沖區的工作原理。而堆不就是進程地址空間的一部分嘛。
我們在語言層面上有這么幾種處理不同文件的緩沖區的策略
1.無緩沖區,不用刷新
2.行刷新,比如要寫入到顯示器中
3.全緩沖,全部刷新,就是一般文件只有當緩沖區寫滿時才刷新
當然我們也可以主動刷新,比如調用fflush函數,或者進程結束時,緩沖區會自動刷新
那么,上面說的緩沖區的位置,處理不同文件的緩沖區的策略你怎么證明呢?下面我們就來寫一個代碼驗證一下
我們運行這個代碼,無論是直接運行,或者是打印到一個文件中,都是正常的:
但是當我們僅在代碼尾部加了一個fork后,結果就變了
可以看到,打印到顯示器上是正常的,但是打印到文件中,系統調用打印了一回,庫函數打印了兩回。這是因為系統調用沒有緩沖區,直接寫到操作系統中;而庫函數因為是寫入到文件中,所以不是行刷新,程序結束才會刷新,而在程序沒結束時,子進程被創建,父子進程共享進程地址空間,而進程地址空間中就存著緩沖區,緩沖區中有內容,程序結束時,父子進程緩沖區中的內容都要被刷新出來,刷新緩沖區也是一種修改數據,所以發生寫時拷貝。
知道了上面的理論,我們可以自己創建一個源代碼文件來實現FILE結構體并且實現相關函數,我們實現的大體思路是首先用法要和庫中的保持一致,我們內部實現要加上緩沖區,并且對于不同種類的文件要有不同的刷新方式,我們只要是通過自己實現知道C語言的緩沖區具體在哪里就可以
//myFILE.h
#pragma once
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/types.h>
enum MODEBUFF
{NO_BUFF,ROW_BUFF,FULL_BUFF,
};
typedef struct MYFILE
{int fileno;char buffer[64];int pos;int mode;
}MYFILE;MYFILE* fopen(const char*path,const char*option);int mywrite(const char*str,int size,int n,MYFILE*fp);int fclose(MYFILE*fp);
//myFILE.c
#include"myFILE.h"MYFILE* fopen(const char*path,const char*option)
{MYFILE* tmp=(MYFILE*)malloc(sizeof(MYFILE));tmp->pos=0;tmp->mode=FULL_BUFF;if(option[0]=='w'){tmp->fileno=open(path,O_WRONLY|O_CREAT|O_TRUNC,0666);}else if(option[0]=='a'){tmp->fileno=open(path,O_WRONLY|O_CREAT|O_APPEND|0666);}else if(option[0]=='r'){tmp->fileno=open(path,O_RDONLY);}else return NULL;return tmp;
}int mywrite(const char*str,int size,int n,MYFILE*fp)
{while(n--){ if(fp->mode==FULL_BUFF){strncpy(fp->buffer+fp->pos,str,size);fp->pos=strlen(fp->buffer);}else{write(fp->fileno,str,size);}}
return size;
}
int fclose(MYFILE*fp)
{if(fp->pos!=0)write(fp->fileno,fp->buffer,fp->pos);close(fp->fileno);free(fp);fp=NULL;return 0;
}
//test.c
#include"myFILE.h"int main()
{MYFILE* fp=fopen("./tmp1","w");mywrite("abcbcbbc",5,1,fp);fork();fclose(fp);return 0;
}
//makefile
myFILE:myFILE.c test.cgcc -o $@ $^
.PHONY:clean
clean:rm myFILE