目錄
- 理解"文件"
- 狹義理解
- 廣義理解
- 文件操作的歸類認知
- 系統角度
- 文件類別
- 回顧C文件接口
- 打開文件
- 寫文件
- 讀文件
- 稍作修改,實現簡單cat命令
- 輸出信息到顯示器,你有哪些方法
- stdin & stdout & stderr
- 打開文件的方式
- 系統?件I/O
- ?種傳遞標志位的方法
- 系統調用
- 理解文本寫入和二進制寫入
- FILE介紹
- open函數返回值
- 文件描述符fd
- ?件描述符的分配規則
- 重定向
- 使用 dup2 系統調用
- 為什么會有標準錯誤和標準輸錯呢?還指向同一個顯示器(文件)?
- 理解“?切皆文件”
- 緩沖區
- 什么是緩沖區
- 為什么要引入緩沖區機制
- FILE
- 緩沖類型
理解"文件"
狹義理解
? ?件在磁盤?
? 磁盤是永久性存儲介質,因此?件在磁盤上的存儲是永久性的
? 磁盤是外設(即是輸出設備也是輸?設備)
? 磁盤上的?件 本質是對?件的所有操作,都是對外設的輸?和輸出簡稱 IO
廣義理解
Linux 下?切皆?件(鍵盤、顯?器、?卡、磁盤…… 這些都是抽象化的過程)
文件操作的歸類認知
? 對于 0KB 的空?件是占?磁盤空間的
? ?件是?件屬性(元數據)和?件內容的集合(?件 = 屬性(元數據)+ 內容)
? 所有的?件操作本質是?件內容操作和?件屬性操作
系統角度
? 對?件的操作本質是進程對?件的操作
? 磁盤的管理者是操作系統
? ?件的讀寫本質不是通過 C 語? / C++ 的庫函數來操作的(這些庫函數只是為??提供?便),?是通過?件相關的系統調?接?來實現的.
文件類別
"內存級(被打開)"文件
磁盤級文件
回顧C文件接口
打開文件
打開的log.txt?件在哪個路徑下?
在程序的當前路徑下,那系統怎么知道程序的當前路徑在哪?呢?(會把當前路徑和文件名進行拼接,來創建文件)
可以使? ls /proc/[進程id] -l 命令查看當前正在運?進程的信息:
打開?件,本質是進程打開,所以,進程知道??在哪?,即便?件不帶路徑,進程也知道。由此OS就能知道要創建的?件放在哪?。
寫文件
讀文件
稍作修改,實現簡單cat命令
輸出信息到顯示器,你有哪些方法
stdin & stdout & stderr
- C默認會打開三個輸?輸出流,分別是
stdin
,stdout
,stderr
- 仔細觀察發現,這三個流的類型都是FILE*, fopen返回值類型,文件指針
#include <stdio.h>
extern FILE *stdin;//標準輸入 -- 鍵盤文件
extern FILE *stdin;//標準輸出 -- 顯示器文件
extern FILE *stderr;//標準錯誤 -- 顯示器文件
程序啟動時,默認會打開這三個文件(編譯器做的),為什么呢?
因為我們的程序是做數據處理的,打開stdin,stdin是為了給程序提供默認的數據源和數據結果。這三個默認打開的文件,自己是可以手動關掉的。
打開文件的方式
如上,是我們之前學的?件相關操作。還有 fseek
,ftell
,rewind
的函數
細節注意:不要把\0
寫入文件,它是C語言的規定,和文件沒有關系,有時候會出現亂碼。
系統?件I/O
打開?件的?式不僅僅是fopen,ifstream等流式,語?層的?案,其實系統才是打開?件最底層的?案。不過,在學習系統?件IO之前,先要了解下如何給函數傳遞標志位,該?法在系統?件IO接?中會使?到。
?種傳遞標志位的方法
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100void func(int flags) {if (flags & ONE) printf("flags has ONE! ");if (flags & TWO) printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
}int main() {func(ONE);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);return 0;
}
以位圖的形式傳遞。
操作?件,除了上?節的C接?(當然,C++也有接?,其他語?也有),我們還可以采?系統接?來進??件訪問, 先來直接以系統代碼的形式,實現和上??模?樣的代碼:
系統調用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);//打開已經創建的文件
int open(const char *pathname, int flags, mode_t mode);//打開未創建的文件flags(這里挑選幾個常用的標記位)
O_CREAT:不存在就創建
O_RDNOIY:只讀
O_WRNOLY:只寫(不會清空內容)
O_APPEND:追加
O_TRUNC:清除
#include <unistd.h>int close(int fd);//fd是open函數的返回值
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);//返回值是寫入的字節數
理解文本寫入和二進制寫入
文本寫入:將數據以文本形式存儲,通常是以人類可讀的字符形式(如ASCII或UTF-8編碼)寫入文件。例如,普通的文本文件(如.txt文件)就是以文本方式存儲的。數據以字符編碼的形式存儲。例如,字符’A’在ASCII編碼中對應十進制的65,存儲時會以01000001的形式寫入文件。
二進制寫入:將數據以二進制形式存儲,即直接將數據的二進制表示寫入文件。這種方式通常用于存儲程序的可執行文件、庫文件、圖像文件等,這些文件的內容對人類來說是不可直接閱讀的,但計算機可以直接解析和處理。數據以原始的二進制形式存儲,不經過字符編碼轉換。例如,一個整數65在內存中以二進制形式存儲為01000001,直接將這個二進制形式寫入文件。
總結:文本寫入和二進制寫入都是語言層面的概念,系統只認二進制,格式化輸出時,只是把二進制轉成你想要的格式,你想要輸出什么格式,需要你自己轉。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);//返回值是讀到的字節數
FILE介紹
FILE
:是C語言標準庫中定義的一個結構體類型(struct),用來表示一個“文件流”,結構體里面封裝了文件描述符fd,在操作系統的接口中,只認文件操作符fd。
open函數返回值
在認識返回值之前,先來認識?下兩個概念: 系統調? 和 庫函數
? 上?的 fopen fclose fread fwrite 都是C標準庫當中的函數,我們稱之為庫函數(libc)。
? ? open close read write lseek 都屬于系統提供的接?,稱之為系統調?接?
? 回憶?下我們講操作系統概念時,畫的?張圖:
系統調?接?和庫函數的關系,??了然。
所以,可以認為, f# 系列的函數,都是對系統調?的封裝,?便?次開發。
文件描述符fd
通過對open函數的學習,我們知道了?件描述符就是?個?整數
- Linux進程默認情況下會有3個缺省打開的?件描述符,分別是標準輸?0, 標準輸出1, 標準錯
誤2.- 0,1,2對應的物理設備?般是:鍵盤,顯?器,顯?器
所以輸?輸出還可以采?如下?式:
進程執?open系統調?,所以必須讓進程和?件關聯起來。每個進程都有?個指針*files, 指向?張表files_struct,該表最重要的部分就是包含?個指針數組,每個元素都是?個指向打開?件的指針!所以,本質上,?件描述符就是該數組的下標。所以,只要拿著?件描述符,就可以找到對應的?件。
?件描述符的分配規則
可?,?件描述符的分配規則:在files_struct數組當中,找到當前沒有被使用的最小的?個下標,作為新的?件描述符。
重定向
那重定向的本質是什么呢?更改文件描述符表
使用 dup2 系統調用
#include<unistd.h>
int dup2(int oldfd,int newfd);
為什么會有標準錯誤和標準輸錯呢?還指向同一個顯示器(文件)?
這樣可以通過重定向的能力,把常規消息和錯誤消息進行分離。
理解“?切皆文件”
- ?先,在windows中是?件的東西,它們在linux中也是?件;其次?些在windows中不是?件的東西,?如進程、磁盤、顯?器、鍵盤這樣硬件設備也被抽象成了?件,你可以使?訪問?件的?法訪問它們獲得信息;甚?管道,也是?件;將來我們要學習?絡編程中的socket(套接字)這樣的東西,使?的接?跟?件接?也是?致的。
- 這樣做最明顯的好處是,開發者僅需要使??套 API 和開發?具,即可調取 Linux 系統中絕?部分的資源。舉個簡單的例?,Linux 中?乎所有讀(讀?件,讀系統狀態,讀PIPE)的操作都可以?read 函數來進?;?乎所有更改(更改?件,更改系統參數,寫PIPE)的操作都可以? write 函數來進?。
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表?打開?件的引?計數,如果有多個?件指針指向
它,就會增加f_count的值。
unsigned int f_flags; // 表?打開?件的權限
fmode_t f_mode; // 設置對?件的訪問模式,例如:只讀,只寫等。所有
的標志在頭?件<fcntl.h> 中定義
loff_t f_pos; // 表?當前讀寫?件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
當打開?個?件時,操作系統為了管理所打開的?件,都會為這個?件創建?個file結構體。
值得關注的是 struct file 中的f_op
指針指向了?個file_operations
結構體,這個結構體中的成員除了struct module* owner
其余都是函數指針。
file_operation
就是把系統調?和驅動程序關聯起來的關鍵數據結構,這個結構的每?個成員都對應著?個系統調?。讀取 file_operation
中相應的函數指針,接著把控制權轉交給函數,從?完成了Linux設備驅動程序的?作。
struct file_operations {
struct module *owner;
//指向擁有該模塊的指針;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ?法?作改變?件中的當前讀/寫位置, 并且新位置作為(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//?來從設備中獲取數據. 在這個位置的?個空指針導致 read 系統調?以 -
EINVAL("Invalid argument") 失敗. ?個?負返回值代表了成功讀取的字節數( 返回值是?個
"signed size" 類型, 常常是?標平臺本地的整數類型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//發送數據給設備. 如果 NULL, -EINVAL 返回給調? write 系統調?的程序. 如果?負, 返
回值代表成功寫的字節數.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化?個異步讀 -- 可能在函數返回前不結束的讀操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化設備上的?個異步寫.
int (*readdir) (struct file *, void *, filldir_t);
//對于設備?件這個成員應當為 NULL; 它?來讀取?錄, 并且僅對**?件系統**有?.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);//mmap ?來請求將設備內存映射到進程的地址空間. 如果這個?法是 NULL, mmap 系統調?返
回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打開?個?件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在進程關閉它的設備?件描述符的拷?時調?;
int (*release) (struct inode *, struct file *);
//在?件結構被釋放時引?這個操作. 如同 open, release 可以為 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//??調?來刷新任何掛著的數據.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ?法?來實現?件加鎖; 加鎖對常規?件是必不可少的特性, 但是設備驅動?乎從不實現
它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
緩沖區
什么是緩沖區
緩沖區是內存空間的?部分。也就是說,在內存空間中預留了?定的存儲空間,這些存儲空間?來緩沖輸?或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸?設備還是輸出設備,分為輸?緩沖區和輸出緩沖區。
總結:緩沖區就是一段內存空間。
為什么要引入緩沖區機制
- 讀寫?件時,如果不會開辟對?件操作的緩沖區,直接通過系統調?對磁盤進?操作(讀、寫等),那么每次對?件進??次讀寫操作時,都需要使?讀寫系統調?來處理此操作,即需要執??次系統調?,執??次系統調?將涉及到CPU狀態的切換,即從??空間切換到內核空間,實現進程上下?的切換,這將損耗?定的CPU時間,頻繁的磁盤訪問對程序的執?效率造成很?的影響。
- 為了減少使?系統調?的次數,提?效率,我們就可以采?緩沖機制。?如我們從磁盤?取信息,可以在磁盤?件進?操作時,可以?次從?件中讀出?量的數據到緩沖區中,以后對這部分的訪問就不需要再使?系統調?了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作? 快于對磁盤的操作,故應?緩沖區可? 提?計算機的運?速度。
- ??如,我們使?打印機打印?檔,由于打印機的打印速度相對較慢,我們先把?檔輸出到打印機相應的緩沖區,打印機再??逐步打印,這時我們的CPU可以處理別的事情。可以看出,緩沖區就是?塊內存區,它?在輸?輸出設備和CPU之間,?來緩存數據。它使得低速的輸?輸出設備和?速的CPU能夠協調?作,避免低速的輸?輸出設備占?CPU,解放出CPU,使其能夠?效率?作。
FILE
- 因為IO相關函數與系統調?接?對應,并且庫函數封裝系統調?,所以本質上,訪問?件都是通過fd訪問的。
- 所以C庫當中的FILE結構體內部,必定封裝了fd。
緩沖類型
標準I/O提供了3種類型的緩沖區。
- 全緩沖區:這種緩沖?式要求填滿整個緩沖區后才進?I/O系統調?操作。對于磁盤?件的操作通常使?全緩沖的?式訪問。
- ?緩沖區:在?緩沖情況下,當在輸?和輸出中遇到換?符時,標準I/O庫函數將會執?系統調?操作。當所操作的流涉及?個終端時(例如標準輸?和標準輸出),使??緩沖?式。因為標準I/O庫每?的緩沖區?度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換?符,也會執?I/O系統調?操作,默認?緩沖區的??為1024。
- ?緩沖區:?緩沖區是指標準I/O庫不對字符進?緩存,直接調?系統調?。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯示出來。
我們發現 printf ,fprintf和 fwrite (庫函數)都輸出了2次,? write 只輸出了?次(系統調?)。為什么呢?肯定和fork有關!
- ?般C庫函數寫??件時是全緩沖的,?寫?顯示器是?緩沖。
- fprintf printf fwrite 庫函數+會?帶緩沖區,當發?重定向到普通?件時,數據的緩沖?式由?緩沖變成了全緩沖。
- ?我們放在緩沖區中的數據,就不會被?即刷新,甚?fork之后
- 但是進程退出之后,會統?刷新,寫??件當中。
- 但是fork的時候,??數據會發?寫時拷?,所以當你?進程準備刷新的時候,?進程也就有了同樣的?份數據,隨即產?兩份數據。
- write 沒有變化,說明沒有所謂的緩沖。
typedef _IO_FILE FILE;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//緩沖區相關
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer *///這些就是緩沖區
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;//管理文件緩沖區的鏈表
int _fileno; //封裝的?件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};