理解"文件"
1-1 狹義理解
- 文件在磁盤里
- 磁盤是永久性存儲介質,因此文件在磁盤上的存儲是永久性的
- 磁盤是外設(即是輸出設備也是輸入設備)
- 磁盤上的文件 本質是對文件的所有操作,都是對外設的輸入和輸出簡稱IO
?1-2 廣義理解
- Linux 下?切皆文件(鍵盤、顯示器、網卡、磁盤…… 這些都是抽象化的過程)
1-3 文件操作的歸類認知?
- 對于 0KB 的空文件是占用磁盤空間的,文件創建時間,屬性,權限....都是需要存儲的
- 文件是文件屬性(元數據)和文件內容的集合(文件 = 屬性(元數據)+ 內容)
- 所有的文件操作本質是文件內容操作和文件屬性操作
1-4 系統角度?
訪問文件,需要先打開文件!誰打開文件??進程打開文件!對文件的操作,本質是:進程對文件的操作!
- 對文件的操作本質是進程對文件的操作
- 磁盤的管理者是操作系統
- 文件的讀寫本質不是通過 C 語言?/ C++ 的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的
?回顧C文件接口
?
1 #include<stdio.h>2 #include<string.h>3 int main()4 {5 FILE *fp=fopen("log.txt","w");6 7 if(fp==NULL)8 {9 perror("fopen");10 return 1;11 }12 const char *msg="hello bit";13 int cnt=1;14 while(cnt<=10)15 {16 char buffer[1024];17 snprintf(buffer,sizeof(buffer),"%s%d\n",msg,cnt++); 18 fwrite(buffer,strlen(buffer),1,fp);19 }20 21 fclose(fp);22 23 return 0;24 }
?
?稍作修改,實現簡單cat命令:
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("argv error!\n");
return 1;
}
FILE *fp = fopen(argv[1], "r");
if(!fp){
printf("fopen error!\n");
return 2;
}
char buf[1024];
while(1){
int s = fread(buf, 1, sizeof(buf), fp);
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
?輸出信息到顯示器三種方法
c++還有cout,其實這些都是封裝了最原始的writ
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
?2-5 stdin & stdout & stderr
- C默認會打開三個輸?輸出流,分別是stdin, stdout, stderr
- 仔細觀察發現,這三個流的類型都是FILE*, fopen返回值類型,文件指針
- ?stdin 標準輸入 鍵盤文件
- stdout 標準輸出 顯示器文件
- stderr標準錯誤 顯示器文件
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
2-6 打開文件的方式
r
:以只讀模式打開文本文件,文件指針位于文件開頭。r+
:以讀寫模式打開文件,文件指針位于文件開頭。可以讀取和寫入文件內容。w
:若文件存在則將其內容清空(截斷為零長度),若文件不存在則創建一個新的文本文件用于寫入,文件指針位于文件開頭。w+
:以讀寫模式打開文件。若文件不存在則創建,若存在則清空內容,文件指針位于文件開頭。a
:以追加模式打開文件,若文件不存在則創建。文件指針位于文件末尾,寫入的內容會追加到文件現有內容之后。a+
:以讀寫和追加模式打開文件。若文件不存在則創建,讀取時文件指針位于文件開頭,寫入時內容總是追加到文件末尾
r
Open text file for reading.
The stream is positioned at the beginning of the file.
r+
Open for reading and writing.
The stream is positioned at the beginning of the file.
w
Truncate(縮短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+
Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a
Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+
Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.
系統文件I/O
?
打開文件的方式不僅僅是fopen,ifstream等流式,語言層的方案,其實系統才是打開文件最底層的方案。不過,在學習系統文件IO之前,先要了解下如何給函數傳遞標志位,該方法在系統文件IO接口中會使用到:
?一種傳遞標志位的方法(位圖)
#include<stdio.h>2 3 #define ONE_FLAG (1<<0) //0000 0000 0000...0000 00014 #define TWO_FLAG (1<<1) //0000 0000 0000...0000 00105 #define THREE_FLAG (1<<2) //0000 0000 0000...0000 0100 6 #define FOUR_FLAG (1<<3) //0000 0000 0000...0000 0100 7 8 void Print(int flags)9 {10 if(flags & ONE_FLAG)11 {12 printf("One!\n");13 }14 if(flags & TWO_FLAG)15 {16 printf("Two!\n");17 }18 if(flags & THREE_FLAG)19 {20 printf("Three!\n");21 }22 if(flags & FOUR_FLAG)23 {24 printf("Four!\n");25 }26 }27 int main()28 {29 30 Print(1);31 printf("\n");32 Print(1|2);33 printf("\n"); 34 Print(1|2|4); 35 return 0; 36 }
?hello.c寫文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len);//fd: 后?講, msg:緩沖區?地址, len: 本次讀取,期望寫
?多少個字節的數據。 返回值:實際寫了多少字節數據
}
close(fd);
return 0;
}
hello.c讀文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello bit!\n";
char buf[1024];
while(1){
ssize_t s = read(fd, buf, strlen(msg));//類?write
if(s > 0){
printf("%s", buf);
}else{
break;
}
}
close(fd);
return 0;
}
open
- 讀 int open(const char *pathname,int flags);
- 寫 int open (const char *pathname,int flags,mode_t mode );
pathname: 要打開或創建的?標?件
flags: 打開?件時,可以傳?多個參數選項,?下?的?個或者多個常量進?“或”運算,構成
flags。
參數:
O_RDONLY: 只讀打開
O_WRONLY: 只寫打開
O_RDWR : 讀,寫打開
這三個常量,必須指定?個且只能指定?個
O_CREAT : 若?件不存在,則創建它。需要使?mode選項,來指明新?件的訪問
權限
O_APPEND: 追加寫
返回值:
成功:新打開的?件描述符
失敗:-1
open 函數具體使用哪個,和具體應用場景相關,如目標文件不存在,需要open創建,則第三個參數表示創建文件的默認權限,否則,使用兩個參數的open。
#include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 int main()6 {7 8 int fd=open("log.txt",O_CREAT|O_WRONLY,0666); 9 if(fd<0)10 {11 perror("open");12 return 1;13 }14 15 return 0;16 }
?下面,權限設的是666但是這里是664,umask掩碼給屏蔽了,open是系統調用,權限掩碼的影響是在系統內部,只要設置umsk(0);就能解決,umsk是設置文件創建時的掩碼可以屏蔽掉系統內部權限的影響,用戶設置說明就是什么。write read close lseek ,類比C文件相關接口。
?
fopen fclose fread fwrite 都是C標準庫當中的函數,我們稱之為庫函數(libc)。而open close read write lseek 都屬于系統提供的接口,稱之為系統調用接口。
read
ssize_t read(int fd,void *buf,size_t count);
從指定文件描述符讀取count個緩沖區的大小,返回讀取成功的字節大小數,小于零表示失敗,等于零都到結尾
回憶一下講操作系統概念時,畫的張圖:
系統調用接口和庫函數的關系,一目了然。
所以,可以認為, f# 系列的函數,都是對系統調用的封裝,方便二次開發。?
文件描述符fd
0 & 1 & 2
Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯
誤2.
? 0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器
所以輸入輸出還可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
1
2
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
?而現在知道,文件描述符就是從0開始的小整數。當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。于是就有了file結構體。表示一個已經打開的文件對象。而進程執行open系統調用,所以必須讓進程和文件關聯起來。每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包含一個指針數組,每個元素都是一個指向打開文件的指針!所以,本質上,文件描述符就是該數組的下標。所以,只要拿著文件描述符,就可以找到對應的文件。對于以上原理結論我們可通過內核源碼驗證:
文件描述符的分配規則
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
輸出發現是 fd: 3
關閉0或者2,在看?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
?發現是結果是: fd: 0 或者 fd 2 ,可見,文件描述符的分配規則:在files_struct數組當中,找到
當前沒有被使用的最小的?個下標,作為新的文件描述符。
?進程里面有一個stuct*file能找到文件描述符表, file是一個結構體,里面含有文件的各種屬性,fd是指針數組,里面通過下標能訪問到各自file,文件被打開時,創建file文件,再把file存入文件描述符表中最小的空的fd里面,fd通過地址文件能找到file能訪問文件。
?
進程在調用read等接口時操作系統拿著fd索引來到文件描述符表找到該fd內的file地址,每一個文件都要有自己的文件緩沖區,操作系統預加載,file找到緩沖區,將緩沖區的內容拷貝給read等接口自己的緩沖內,所以讀寫的本質就是拷貝!
重定向
那如果關閉1呢?看代碼:
#include <stdio.h>2 #include <sys/types.h>3 #include <sys/stat.h>4 #include <fcntl.h>5 #include <stdlib.h>6 int main()7 {8 close(1);9 int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);10 11 printf("fd: %d\n", fd); 12 }
把本來應該寫入顯示器的內容居然寫進了文件里!
為什么不顯示?因為把標準輸出關了。
為什么又寫進了文件里?因為打開了這個文件。?
此時,我們發現,本來應該輸出到顯示器上的內容,輸出到了文件 myfile 當中,其中,fd=1。這
種現象叫做輸出重定向。常見的重定向有: > , >> , <
那重定向的本質是什么呢?
?文件描述符表包含了fd_array[],數組下標就是對應打開的文件,系統默認打開標準輸入(0),標準輸出(1),標準錯誤(2),而我要打開新的文件log.txt之前把標準輸出關了,此時文件描述符表下標為1的指向就不在是標準輸出,后來我open打開了一個文件log.txt,根據文件描述符分配規則,又因為最小未被使用的下標恰好就是剛才釋放的下標為1的文件描述符,1的地址就是log.txt的地址,把1返回給上層用戶,printf就是往stdout內打印的,它是封裝了標準輸出fd=1,上述操作是在操作系統內部實現的,而用戶層printf只認文件描述符1,通過文件描述符1找到對應的文件寫入內容。
所以,這種在操作系統內部更改內容指向,和用戶層沒關系,這種叫重定向,是在內核上做的貍貓換太子!
使用?dup2 系統調用
#include <unistd.h>
int dup2(int oldfd, int newfd);
?輸出重定向
從文件輸出顯示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
if(fd<0)return 1;
dup2(fd,1);
printf("fd: %d\n", fd);
printf("hello bit\n");
printf("hello bit\n");
fprintf(stdout,"hello stdout\n");}
輸入重定向?
從文件輸入進顯示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if(fd<0)return 1;
dup2(fd,0);
while(1)
{char buffer[64];if(!fgets(buffer,sizeof(buffer),stdin))break;printf("%s",buffer);}}
?
?printf是C庫當中的IO函數,一般往 stdout 中輸出,但是stdout底層訪問文件的時候,找的還是fd:1,但此時,fd:1下標所表示內容,已經變成了myfifile的地址,不再是顯示器文件的地址,所以,輸出的任何消息都會往文件中寫入,進而完成輸出重定向。那追加和輸入重定向如何完成呢?
?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include<unistd.h>
#include<string.h>
int main(int argc,char* argv[])
{if(argc!=2)exit(1);
int fd = open(argv[1], O_RDONLY);
if(fd<0)return 1;
dup2(fd,0);
while(1)
{char buffer[64];if(!fgets(buffer,sizeof(buffer),stdin))break;printf("%s",buffer);}}
?