文件
在之前學習C
語言文件操作時,我們了解過什么是文件,這里簡單回顧一下:
文件存在磁盤中,文件有分為程序文件、數據文件;二進制文件和文本文件等。
詳細描述見文章:文件操作——C語言
- 文件在磁盤里,磁盤是永久性存儲介質,因為文件在磁盤上的存儲是永久性的;
- 磁盤是外設(是輸入設備也是輸出設備);
- 本質上對磁盤是文件的所有操作,都是對外設的輸入和輸出;也就是
I/O
。
對于文件的認識:
- 文件 = 文件內容 + 文件屬性
- 文件大小是
0
KB的文件也是占用磁盤空間的- 所有對于文件的相關操作都是對文件內容和文件屬性就行操作。
在系統層面理解
- 我們操作文件(打開,關閉文件),本質上是進程對于文件的操作;
- 磁盤的管理者是操作系統;
- 我們在C/C++使用庫函數來對文件進行讀寫操作,本質上是通過文件相關的系統調用去完成的。
C
文件操作
打開和關閉文件
在C
語言當中,我們通過fopen
來打開文件,fclose
來關閉文件;
fopen
:打開文件,如果打開成功返回一個FILE*
類型的指針,執行被打開的文件;打開失敗則返回NULL
;fclose
:關閉文件,傳參FILE*
類型的指針,關閉指定文件。
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以讀方式打開,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; } //.... fclose(fp);//關閉文件 return 0;
}
打開文件的方式
我們知道在C
語言的fopen
接口打開文件是有很多打開方式
r
:以讀方式打開、w
以寫方式打開、a
以追加方式打開。
r
方式,當文件不存在時就打開文件失敗;
w
方式,當文件不存在時就新建文件(在當前工作路徑下創建,進程
當中存放了當前工作路徑);如果文件存在會清空當前文件的內容;然后在進入寫入。
a
方式,追加,當文件不存在就新建文件;如果文件已經存在,打開時不會清空文件內容,而是在文件末尾進行寫入
寫文件
當我們以w
/r
方式打開一個文件,我們要將內容寫到文件當中;
我們可以使用fputc
、fputs
、fwrite
、fprintf
進行文件的寫入;
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以讀方式打開,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; } for(char ch = 'a';ch <= 'z';ch++){ fputc(ch,fp); } char* str = (char*)"I love you\n"; fputs(str,fp); int x = 100; fprintf(fp,"x = %d\n",x); fclose(fp);//關閉文件 return 0;
}
文件讀取
我們以r
方式打開一個文件,我們要像讀取這個文件的內容,我們可以使用fgetc
、fgets
、fscanf
進入文件內容的讀取操作:
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以讀方式打開,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; }for(int i = 0;i<26;i++){ printf("%c",fgetc(fp)); } printf("\n"); char buff[20]; fgets(buff,12,fp); //buff[11] = '\0'; printf("%s",buff); int x; fscanf(fp,"x = %d",&x); printf("x = %d\n", x); fclose(fp);//關閉文件 return 0;
}
stdin/stdout/stderr
在我們程序運行時,C
語言它會默認打開三個文件:stdin
、stdout
和stderr
;
我們可以發現這三個都是文件類型指針
系統文件I/O
在上述C
語言的文件操作fopen
、fclose
都是語言層提供給我們的文件操作接口;以及C
語言的stdin
、stdout
、stderr
;C++中的cin
、cout
、cerr
都是語言層提供給我們的方案;
我們知道文件的管理者是操作系統,所以我們對文件操作都要經過操作系統;
那也就是說語言層的文件操作接口,其底層都封裝了系統調用。
1. 傳遞多個標志位的方法
在之前的習慣中,我們通常使用一個參數來作為一個標記位;這樣我們在傳遞多個標志位時就需要傳遞多個參數。
現在來了解一種使用一個參數來傳遞多個標志位的方法:
使用一個
bit
為來作為一個標識符,這樣使用一個參數就可以表示多個標志位了。
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flag){ if(flag & ONE) printf("ONE "); if(flag & TWO) printf("TWO "); if(flag & THREE) printf("THREE "); printf("\n");
}
int main()
{ func(ONE); func(ONE | TWO); func(ONE | THREE); func(ONE | TWO | THREE); return 0;
}
這樣就可以使用一個參數,傳遞多個標志位了。
Linux
操作系統open
接口就使用了一個參數來傳遞多個標志位。
2. 打開文件open
在語言層面,我們使用的fopen
,它本質上就是對系統調用的封裝;
可以看到
open
是一個系統調用;它的作用就是打開一個文件,也可能會創建一個新的文件。
我們可以看到open
函數它存在一個兩個參數的,也存在一個三個參數的;
pathname
:表示要打開文件的文件名(不帶路徑就默認在當前工作路徑下)flags
:表示文件的打開方式,存在多個標志位mode
:表示新建文件時,文件的默認權限
文件名pathname
這個想必就不用多說了,表示要打開文件的文件名;
不帶路徑時,就表示在當前工作路徑下打開文件。(進程中存在當前工作路徑cwd
)
標志位flags
通過查看open
函數說明可以看到,flags
存在非常多的標志位;這里列舉一些常用的選項
O_RDONLY
只讀、O_WRONLY
只寫、O_RDWR
讀寫;
O_CREAT
:文件不存在時就新建文件
O_TRUNC
:打開文件時,文件存在就清空文件內容
O_APPEND
:打開文件時,以追加形式打開。
這里flags
表示的就是文件的打開方式;
首先就是:只讀、只寫和讀寫;在我們打開文件時必須指定一個且只能指定一個。
O_CREAT
在我們打開一個文件時,如果這個文件不存在,那open
函數就會直接返回-1
表示文件打開失敗;
而我們帶了O_CREAT
選項,就表明當文件不存在時,就新建文件;(這里新建文件要指明新建文件的權限,否則創建出來文件的權限就是亂碼)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY,0666); if(fd < 0){ perror("open"); return 1; } return 0;
}
一般情況下,在以寫方式打開文件,文件不存在就新建,就要指明文件的權限。
(以只讀讀方式
O_RDONLY
,文件不存在新建出的文件是空的,沒有什么意義)
O_TRUNC
當我們打開一個文件時,如果這個文件已經存在了,那就打開這個已有的文件;
如果我們帶了O_TRUNC
選項,就表示清空這個文件的內容;
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); if(fd < 0){ perror("open"); return 1; } return 0;
}
O_APPEND
在C語言的文件操作中,fopen
打開文件,w
就是以寫方式打開、文件不存在就新建、文件存在就清空文件的內容;(這就對應了上述選項的O_WRONLY
、O_CREAT
、O_TRUNC
)
但是我們fopen
還可以以a
方式打開文件,也就是追加方式;這里的O_APPEND
就是以追加的方式打開文件。
這里我們先看一種現象:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY,0666); if(fd < 0){ perror("open"); return 1; }char buff[] = "abcdef";write(fd,buff,strlen(buff)); return 0;
}
可以看到,我們不帶
O_APPEND
選項,寫入的時候是在文件的開頭位置進行寫入的。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY | O_APPEND,0666); if(fd < 0){ perror("open"); return 1; }char buff[] = "abcdef";write(fd,buff,strlen(buff)); return 0;
}
文件權限mode
當打開文件,文件不存在,我們還帶了O_CREAT
選項時;如果我們不帶文件的權限,那新建文件的權限就是亂碼;
而新建的文件的權限:文件權限 = 默認權限 &(~umask)
3. 關閉文件
關閉文件的系統調用close
,根據文件描述符,關閉指定文件。
對于這里的
fd
,它指的是文件描述符,當我們打開文件成功時,會返回給我們該文件的文件描述符;我們對指定文件的讀寫操作,以及關閉文件都要使用指定文件的文件描述符。
4. 文件寫入write
在C語言中,我們進行文件寫入時,我們有非常多的接口;
但是在系統層面,我們對文件進行寫入只有一個接口,就是write
。
write
有三個參數,分別是fd
、buf
、和count
;
其中
fd
指的是文件描述符,表示我們要向哪一個文件進行寫入;
buf
表示我們要進行寫入的內容,它是void*
類型的指針,可以寫入任何數據
count
就表示,我們要寫入內容的長度。
這里值的注意的是:這里write
主要用于字符寫入
當然,我們之前聽過文本寫入和二進制寫入,那都是語言層的概念;
再這里我們操作系統不管這些,
write
就將給定的內容寫入到文件緩沖區中。
5. 文件讀取read
和寫入一樣,在語言層我們有非常多的函數接口,但是在系統層面,就只有read
。
這樣存在三個參數;
fd
文件描述符,表示要從哪一個文件讀取數據;
buf
,表示我們要將文件中的數據讀取到buf
中;
count
表示要讀取內容的長度。
文件描述符
在上面描述中,我們了解了文件操作相關的系統調用;
在open
、write
、read
、close
這些系統調用中,都用到了一個fd
來指明一個文件;那這個fd
究竟是什么呢?
open
函數的返回值
在上述使用open
函數時,我們并沒有關心open
函數的返回值,也沒有說明文件描述符到底是什么?
當我們使用
open
打開一個文件時:如果打開成功,那就返回這個新打開文件的文件描述符;
如果打開失敗,就返回
-1
,并且設置錯誤碼。
什么是文件描述符
那我們知道了,文件描述符就是標識一個文件的整數,每一個文件的文件描述符都不一樣;
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);if(fd < 0) return -1;printf("fd : %d\n",fd);close(fd);return 0;
}
我們可以看到打開的一個文件的文件描述符是3
;
這里打開一個文件我們看不出來什么,我們多打開一些文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd1 = open("log1.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd2 = open("log2.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd3 = open("log3.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd4 = open("log4.txt",O_CREAT | O_WRONLY | O_APPEND,0666);//這里就不做錯誤判斷了printf("fd1 : %d\n",fd1);printf("fd2 : %d\n",fd2);printf("fd3 : %d\n",fd3);printf("fd4 : %d\n",fd4);//不關閉文件,進程結束會自動關閉return 0;
}
這里,我們可以發現我們打開多個文件,這些文件的文件描述符是線性增長的。
文件描述符0/1/2
在上述中,我們發現,我們的文件描述符它是線性增長的,而且我們打開第一個文件它的文件描述符是3
;
那0
、1
、2
文件描述符去哪里了呢?
還記得在程序運行時,
C
語言會默認給我們打開三個文件stdin
、stdout
和strerr
;在
Linux
操作系統中,進程默認情況下會有三個缺省打開的文件描述符(0
、1
、2
),分別是標準輸入、標準輸出和標準錯誤。那這里進程默認打開的標準輸入、標準輸出和標準錯誤和C語言中的
stdin
、stdout
和stderr
有什么區別呢?
這里0
標準輸入、1
標準輸出、2
標準錯誤;
一般情況下0,1,2
對應的物理設備是鍵盤、顯示器、顯示器。
所以,我們從鍵盤中讀取數據就是從0
文件中讀取數據;將數據輸出到顯示器中就是將數據輸出到1
文件當中。
文件描述符的分配規則
當我們打開一個文件時,它的文件描述符是3
;我們打開多個文件時,我們可以發現它的文件描述符是線性增長的;
那文件描述符是如何分配的呢?是一直在增長的嗎?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{ int fd1 = open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd1 < 0) return -1; printf("fd1 : %d\n",fd1); close(0); close(2); int fd2 = open("log2.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd2 < 0) return -1; printf("fd2 : %d\n",fd2); int fd3 = open("log3.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd3 < 0) return -1; printf("fd3 : %d\n",fd3);return 0;
}
可以看到,我們關閉
0
、2
文件之后,新打開文件的文件描述符是0
和2
所以,這里我們文件描述符分配時,會從0
開始去找到一個最小且沒有被使用的文件描述符,再進行分配。
操作系統對于打開的文件的管理
對于上面的文件描述符,可以說很抽象,為什么可以使用一個整數來表示一個打開的文件呢?
現在來看操作系統是如何對被打開的文件進行管理的。
要想進行管理,那一定是先描述,再組織
**先描述:**操作系統要像對被打開文件進行管理,那就要先描述這些被打開文件;在Linux
操作系統中就存在struct file
結構體來描述這些被打開的文件(struct file
里就可能有文件描述符等等一些關于文件的信息)。
所以,在打開一個文件時,就只需根據文件的相關屬性構建相對應的
struct file
即可(還存在文件緩沖區,也會將部分內容拷貝到文件緩沖區中)
再組織: 操作系統要將這些被打開文件(struct file
)進行管理;在Linux
操作系統中就存在一個全局鏈表,所以被打開文件的struct file
都在這個鏈表當中。
這樣操作系統就將所有被打開的文件管理起來的,但是現在還有一個問題,程序打開文件也就是進程要打開文件,那進程是如何打開的呢?
這個問題就比較簡單了,我們的進程不是有對應的task_struct
嗎,那我們進程打開了哪些文件就肯定在tasj_struct
中存儲著相對應的信息。
在
task_struct
中,存在著一個指針struct file_struct* files
,這個指針指向進程相對應的文件描述符表
;而在
文件描述符表
中,存在著一個數組struct file* fd_array
;而我們的文件描述符就和這個
fd_array
數組的下標一一對應。
那么,也就是說我們文件描述符表在的fd_array
數組,下標0
、1
…和我們文件描述符一一對應。
那么,在進程執行的過程中,只需要訪問
task_struct
中的文件描述符表中的fd_array
數組,就可以知道我們進程打開了哪些文件。
了解了操作系統對于被打開文件的管理,現在再來看文件描述符
它就是進程文件描述符表中
fd_array
的數組下標;在新建文件時,操作系統就會遍歷進程的描述符表中
fd_array
數組,找到一個最小且沒有被使用的數組下標來作為新打開文件的文件描述符。
stdin/stdout/stderr
和文件描述符0/1/2
的區別
我們知道,在C
語言程序啟動時,它會默認打開三個文件stdin/stdout/stderr
,這是語言層面的概念;
而文件描述符0/1/2
這是操作系統層面的概念。
在C語言中stdin/stdout/stderr
的類型都是FILE*
,我們只知道FILE*
是文件類型的指針,但是FILE
它是什么呢?
其他的我們不懂,但是這里我們可以肯定在FILE
中肯定存在文件對應的文件描述符;(因為C語言文件操作是封裝了系統調用,而系統調用是使用文件描述符來訪問文件的,所以在FILE
肯定存在文件所對應的文件描述符)
并且我們也可以確定,stdin
對應的文件描述符肯定是0
、stdout
對應的文件描述符肯定是1
、stderr
對應的文件描述符肯定是2
。
這里提出一個問題?
我們知道文件描述符的分配規則是找一個最小且沒有被使用的文件描述符進行分配。
那如果我們關閉了
0/1/2
文件,再創建文件那0/1/2
還是標準輸入、標準輸出和標準錯誤嗎?
答案是的,在語言層我們標準輸入stdin
中的文件描述符就是0
、標準輸出stdout
中的文件描述符就是1
、標準錯誤stderr
中的文件描述符就是2
。
我們關閉
0/1/2
文件,這一操作是系統層的操作,我們語言層C
它并不知道;那也就是說,在程序往標準輸出中輸出數據時,它只會拿著
stdout
中的文件描述符1
去調用系統調用;那也就是說在系統層:文件描述符
0
指向哪個文件,哪個文件就是標準輸入;文件描述符1
指向哪個文件,哪個文件就是標準輸出;文件描述符2
指向哪個文件,哪個文件就是標準錯誤。
本篇文章到這里就結束了,感謝各位的支持。
繼續加油!!!