公眾號文章匯總
870
2025-04-01
@TOC
零、前言
本章主要講解學習Linux基礎IO流的知識
一、C語言文件IO
1、C庫函數介紹
具體詳解博文: 文件操作超詳解CSDN博客
打關文件fopen/fclose:
FILE * fopen(const char* filename, const char* mode); int fclose (FILE* stream );
文件打開方式:
讀寫函數fread/fwrite:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
格式化讀寫fscanf/fprintf:
int fscanf( FILE *stream, const char *format [, argument ]... ); int fprintf( FILE *stream, const char *format [, argument ]...);
示例1:輸出使用
#include
示例2:文件讀寫
#include
結果:
2、stdin/stdout/stderr
文件原型:
extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
概念:
任何C程序運行都會默認打開三個輸入輸出流,分別是:stdin, stdout, stderr
分三個文件流分別對應鍵盤文件,顯示器文件,顯示器文件
為什么這里的文件流和外設關聯上了:
對于所有外設硬件來說,其本質對應的操作不外乎是讀操作和寫操作,對于不同外設也就有不同的讀寫方式
OS要管理硬件設備無非是先描述再組織,由此將屬性以及讀寫操作構成一個結構體,而文件其本身也是屬性加讀寫操作,這樣就由文件結構體同一管理文件(包括外設)
在C語言中雖然沒有多態,但是結構體中可以儲存函數指針,初始化結構體時,將屬性寫入的同時也將對應的讀寫函數給寫入;對于外設來說,通過對應的文件結構體使用函數指針調用對應的讀寫函數,也就將數據刷新到對于設備上/從設備上讀取數據
由此將普通文件和硬件設備管理組織好,所以對于Linux來說:一切皆文件
為什么C語言默認打開這三個輸入輸出流:
不僅僅是C語言會默認打開這三個輸入輸出流文件,幾乎是任何語言都會這樣,而這就不僅僅是語言層面上的功能了,也是由操作系統所支持的
對于任何語言來說,都有輸入輸出的需求,而不打開這三個輸入出輸出流文件,則無法使用這些接口
二、系統文件IO
1、系統調用介紹
操作文件,除了上述C接口(當然C++也有接口,其他語言也有),還可以使用系統接口
open接口:
#include
參數解釋:
pathname: 要打開或創建的目標文件
flags: 打開文件時,可以傳入多個參數選項,用下面的一個或者多個常量進行“或”運算,構成flags
mode_t:如果沒有對應文件需要進行創建的話,就需要指定創建文件的八進制訪問權限值
注:這里的參數選項是依靠不同的比特位來標識對應的功能設定,所以這里的異或操作就是將對應比特位置為1,同時函數也是通過對每個比特位進行與操作檢查是否該比特位置為了1
原型示例:
#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 #define O_CREAT 0100
注:open 函數具體使用哪個,和具體應用場景相關,如目標文件不存在,需要open創建,則第三個參數表示創建文件的默認權限,否則,使用兩個參數的open
其他接口:
int close(int fd); //使用close函數時傳入需要關閉文件的文件描述符fd即可,若關閉文件成功則返回0,若關閉文件失敗則返回-1 ssize_t write(int fd, const void *buf, size_t count); //使用write函數,將buf位置開始向后count字節的數據寫入文件描述符為fd的文件當中 //如果數據寫入成功,實際寫入數據的字節個數被返回;如果數據寫入失敗,-1被返回 ssize_t read(int fd, void *buf, size_t count); //使用read函數,從文件描述符為fd的文件讀取count字節的數據到buf位置當中 //如果數據讀取成功,實際讀取數據的字節個數被返回;如果數據讀取失敗,-1被返回
示例:文件讀寫
#include
結果:
2、系統調用和庫函數
概念:
對于上面的 fopen fclose fread fwrite 都是C標準庫當中的函數,我們稱之為庫函數(libc);而 open close read write lseek 都屬于系統提供的接口,稱之為系統調用接口
對于系統調用來說,接近底層,使用成本較高,并且不具備可移植性,只在本系統下可以,其他系統不行
對于庫函數來說,是在系統暴露的接口上的一個二次開發(最終調用系統調用),在兼容自己語法的特性的同時,具有可移植性(自動根據平臺選擇自己底層對應的接口)
即可以認為庫函數是對系統調用的封裝,減低人工學習成本,方便二次開發
示圖:
三、文件描述符
1、open返回值
文件描述符fd:
文件描述符就是一個小整數
0 & 1 & 2:
Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2
示例1:
#include
結果:
示例2:
#include
結果:
注:從示例中可見,文件描述符就是從0開始的小整數:默認打開0,1,2,再打開則是從后遞增
分析:
當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件
于是就有了file結構體,表示一個已經打開的文件對象。而進程執行open系統調用,所以必須讓進程和文件關聯起來。
每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指針數組,每個元素都是一個指向打開文件的指針
所以本質上,文件描述符就是該數組的下標。只要拿著文件描述符,就可以通過PCB到file_struct的指針數組找到對應的文件結構體地址
示圖:
2、fd分配規則
文件描述符分配規則:
在files_struct數組當中,找到當前沒有被使用的最小的一個下標,作為新的文件描述符
示例1:
#include
結果:輸出3
示例2:
#include
結果:關閉0輸出0,關閉2輸出2
四、重定向
1、概念及演示
Linux 中標準的輸入設備默認指的是鍵盤,標準的輸出設備默認指的是顯示器
輸入/輸出重定向:
輸入重定向:指的是重新指定設備來代替鍵盤作為新的輸入設備
輸出重定向:指的是重新指定設備來代替顯示器作為新的輸出設備
注:通常是用文件或命令的執行結果來代替鍵盤作為新的輸入設備,而新的輸出設備通常指的就是文件
常用重定向:
示例:
#include
結果:
注:本來應該輸出到顯示器上的內容,輸出到了文件 myfile 當中,其中fd=1,這種現象叫做輸出重定向
重定向本質:
從上述示例來看,輸出重定向是將進程中的文件指針數組中的標準輸出stdout文件給關閉(并非真正關閉,而是將指針數組對應下標的內容置空),再將新打開文件分配到標準輸出文件對應的下標上,再輸出時,系統不知道文件已經替換,依舊輸出到stdout文件對應的數組下標為1的文件上,但是此時文件已經被替換了
示圖:
2、dup2系統調用
函數原型:
#include
示例:
#include
結果:
3、重定向的原理
注:重定向與程序替換是可以同時進行,重定向改變的是進程PCB中的文件指針數組中的文件地址信息,而程序替換則是觸發寫時拷貝將進程地址空間的代碼和數據進行替換,這之間沒有影響
輸出重定向示例:命令 cat test.c > myfile
系統創建子進程exec替換程序執行cat test.c命令之前,先將標準輸出文件關閉,并打開myfile文件(如果不存在則創建,對應的open選項則是O_WRONLY|O_CREAT)
追加重定向示例:命令 cat test.c >> myfile
這里大致和輸出重定向一樣,只不過open的選項改為O_APPEND|O_CREAT
輸入重定向示例:命令 mycmd > test.c
系統創建子進程exec替換程序執行 test.c 命令之前,先將標準輸入文件關閉,并打開 mycmd 文件(對應的open選項則是O_RDONLY)
4、緩沖區和刷新策略
示例:
#include
結果:
解釋:
這里明明將輸出結果重定向到文件myfile中,但是myfile文件并沒有內容,與上面示例的區別是在文件關閉之前并沒有將結果給強制刷新
對于文件結構體來說,里面除了讀寫方法外,還存在著緩沖區,再正式刷新到磁盤上對應的文件之前,數據先是由文件緩沖區保存著
對于標準輸出的刷新策略是行緩沖,當遇到\n時觸發刷新機制,對于普通文件來說則是全緩沖,當緩沖滿時就進行刷新,而強制刷新以及進程結束刷新對兩者都有效
這里輸出重定向之后指針數組對應的原標準輸出文件的替換成了普通文件,數據寫到對應文件緩沖區里,同時對應刷新策略也改變成全緩沖,關閉文件之前沒有強制刷新,則數據也就沒寫到對應磁盤上的文件里
刷新策略:
無緩沖:無緩沖的意思是說,直接對數據進行操作,無需經過數據緩沖區緩存,系統調用接口采用此方式
行緩存:緩沖區的數據每滿一行即對數據進行操作,而通常情況下向屏幕打印數據就是行緩存方式
全緩沖:緩沖區的數據滿時才對數據進行操作,通常向文件中寫數據使用的就是全緩沖方式
五、文件及文件系統
1、FILE
概念:
因為IO相關函數與系統調用接口對應,并且庫函數封裝系統調用,所以本質上,訪問文件都是通過fd訪問的,所以C庫當中的FILE結構體內部,必定封裝了fd
示例:
#include
運行出結果:
hello printf hello fwrite hello write
輸出重定向結果: ./hello > file
hello write hello printf hello fwrite hello printf hello fwrite
區別:這里 printf 和 fwrite (庫函數)都輸出了2次,而 write 只輸出了一次(系統調用),而這與就和fork有關
解釋:
printf fwrite 庫函數是C語言上的函數,這些庫函數在實現輸出時必定通過調用C語言的文件IO函數實現。C語言文件IO函數的返回類型是FILE*,這里的FILE是C語言上的文件結構體,其中為了實現語言與系統層面的相連,FILE結構體里也存在著_fileno(對應fd)以及用戶層面的緩沖區,所以庫函數輸出數據是先輸出到FILE文件結構體里的緩沖區
如果是直接運行,即沒有發生輸出重定向時,向顯示屏文件的刷新機制是行緩沖(遇到\n則刷新),即立即將緩沖數據給刷新,fork之后沒有什么作用
當發生重定向到普通文件時,數據的緩沖方式由行緩沖變成了全緩沖(普通文件是全緩沖的,緩沖滿則刷新),即FILE中緩沖區存有數據,當fork之后,子進程會與父進程代碼共享,數據各有一份(刷新就是寫入,發生寫時拷貝),程序結束退出時強制刷新數據,所以庫函數調用的都輸出了兩次
write 為系統接口無緩沖機制,就直接將數據刷新
注意:
OS內核區實際上也是有緩沖區的,當我們刷新用戶緩沖區的數據時,并不是直接將用戶緩沖區的數據刷新到磁盤或是顯示器上,而是先將數據刷新到操作系統緩沖區,然后再由操作系統將數據刷新到磁盤或是顯示器上
操作系統也有自己的刷新機制,這樣的分用戶層面和內核層面的緩沖區,便于用戶層面與內核層面進行解耦
FILE結構體:
//在/usr/include/stdio.h typedef struct _IO_FILE FILE; //在/usr/include/libio.h 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 di rectly. */ 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 };
2、文件系統
命令 ls -l 查看文件信息:
[root@localhost linux]# ls -l 總用量 12 -rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out -rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
每行包含7列:模式;硬鏈接數;文件所有者;組;大小;最后修改時間 ;文件名
命令 stat 查看文件信息:
[root@localhost linux]# stat test.c File: "test.c" Size: 654 Blocks: 8 IO Block: 4096 普通文件 Device: 802h/2050d Inode: 263715 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2017-09-13 14:56:57.059012947 +0800 Modify: 2017-09-13 14:56:40.067012944 +0800 Change: 2017-09-13 14:56:40.069012948 +0800
注意:
Access最近訪問文件時間(不會立即刷新,訪問是一個比較頻繁的行為,立即刷新則會減緩效率)
Modify:最近修改文件的時間(主要是文件的內容,立即更新)
Change:最近修改文件屬性的時間(修改文件內容可能會造成文件屬性的修改,立即更新)
如何讀取文件信息:
通過讀取存儲在磁盤上的文件信息,然后顯示出來
示圖:
文件系統概念:
對于文件操作來說,我們操作的都是在內存打開的文件,而大多數文件都是未打開的文件并且儲存在磁盤上,而對于磁盤上的文件OS也需要進行管理,由此就需要文件系統
示圖:
確定磁盤的讀寫文件:
確定讀寫信息在磁盤的哪個盤面/柱面/扇區,但是這樣的方式并不便于移植,由此我們將磁盤抽象成數組,數組的下標是單調遞增不重復的數字,可以直接確定要讀寫的文件
分區管理:
磁盤分區就是使用分區編輯器在磁盤上劃分幾個邏輯部分,盤片一旦劃分成數個分區,不同的目錄與文件就可以存儲進不同的分區,分區越多,就可以將文件的性質區分得越細,按照更為細分的性質,存儲在不同的地方以管理文件
磁盤是典型的塊設備,硬盤分區被劃分為一個個block,一個block的大小是由格式化的時候確定的,并且不可以更改
如何進行管理:
示圖:
說明:
Boot Block:該區域磁盤文件的驅動文件,如果驅動損壞,那么則無法進行讀取對應區域的文件信息及數據
Block Group:ext2文件系統會根據分區的大小劃分為數個Block Group,而每個Block Group都有著相同的結構組成
Super Block:存放文件系統本身的結構信息,記錄的信息主要有:bolck 和 inode的總量,未使用的block和inode的數量,一個block和inode的大小,最近一次掛載的時間,最近一次寫入數據的時間,最近一次檢驗磁盤的時間等其他文件系統的相關信息。Super Block的信息被破壞,可以說整個文件系統結構就被破壞了
Group Descriptor Table:塊組描述符,描述塊組屬性信息,整體group的空間使用信息,以及其他信息
Block Bitmap:Block Bitmap中記錄著Data Block中哪個數據塊已經被占用,哪個數據塊沒有被占用
inode Bitmap: inode位圖當中記錄著每個inode是否空閑可用
inode Table:存放文件屬性,即每個文件的inode,每個文件對應一個inode,而inode才是標識文件的唯一方式
Data Blocks:存放inode對應的文件數據
注:其他塊組當中可能會存在冗余的Super Block,當某一Super Block被破壞后可以通過其他Super Block進行恢復;磁盤分區并格式化后,每個分區的inode個數就確定了
如何理解創建一個文件:
通過遍歷inode位圖的方式,找到一個空閑的inode,在inode表當中找到對應的inode,并將文件的屬性信息填充進inode結構中,并將該文件的文件名和inode的映射關系添加到目錄文件的數據塊中,如果寫入內容,需要通過Block Bitmap找到閑置的數據塊,將數據寫入數據塊,并將映射關系寫到inode結構中
如何理解對文件寫入信息:
通過目錄文件中的數據塊找到文件名及其inode的映射,再找到對應的inode結構,再通過inode結構找到存儲該文件內容的數據塊,并將數據寫入數據塊;若不存在數據塊或申請的數據塊已被寫滿,則通過遍歷塊位圖的方式找到一個空閑的塊號,并在數據區當中找到對應的空閑塊,再將數據寫入數據塊,最后還需要建立數據塊和inode結構的對應關系
如何理解刪除一個文件:
將該文件對應的inode在inode位圖當中置為無效,將該文件申請過的數據塊在塊位圖當中置為無效,并不真正將文件對應的信息刪除,而只是將其inode號和數據塊號置為了無效,所以當我們刪除文件后短時間內是可以恢復的,如果再次創建文件及數據,可能將對應的數據塊給覆蓋,原來的數據也就沒有了
如何理解目錄:
目錄也是文件,有自己的屬性信息,目錄的inode結構當中存儲的就是目錄的屬性信息,比如目錄的大小、目錄的擁有者等;目錄也有自己的內容,目錄的數據塊當中存儲的就是該目錄下的文件名以及對應文件的inode指針
注: 每個文件的文件名并沒有存儲在自己的inode結構當中,而是存儲在該文件所處目錄文件的文件內容當中。計算機只關注文件的inode號,只有用戶才關注,用戶需要看到文件名,所以將文件名和文件的inode指針存儲在其目錄文件的文件內容當中后,目錄通過文件名和文件的inode指針即可將文件名和文件內容及其屬性連接起來
3、軟硬鏈接
軟鏈接概念:
軟鏈接又叫做符號鏈接,軟鏈接文件相對于源文件來說是一個獨立的文件,該文件有自己的inode號,但是該文件只包含了源文件的路徑名,所以軟鏈接文件的大小要比源文件小得多。軟鏈接就類似于Windows操作系統當中的快捷方式
但是軟鏈接文件只是其源文件的一個標記,當刪除了源文件后,鏈接文件不能獨立存在,雖然仍保留文件名,但卻不能執行或是查看軟鏈接的內容了
命令ln -s 創建軟連接:
硬鏈接概念:
硬鏈接文件就是源文件的一個別名,一個文件有幾個文件名,該文件的硬鏈接數就是幾
當硬鏈接的源文件被刪除后,硬鏈接文件仍能正常執行,只是文件的鏈接數減少了一個
使用命令ln 創建硬連接:
注:硬鏈接文件的inode號與源文件的inode號是相同的,并且硬鏈接文件的大小與源文件的大小也是相同的,特別注意的是,當創建了一個硬鏈接文件后,該硬鏈接文件和源文件的硬鏈接數都變成了2
為什么創建的目錄的硬鏈接數是2:
創建一個普通文件,該普通文件的硬鏈接數是1,因為此時該文件只有一個文件名。而目錄創建后,該目錄下默認會有兩個隱含文件.和…,它們分別代表當前目錄和上級目錄,因此這里創建的目錄有兩個名字,一個是dir另一個就是該目錄下的.,所以剛創建的目錄硬鏈接數是2
示圖:
注:通過命令我們也可以看到dir和該目錄下的.的inode號是一樣的,也就可以說明它們代表的實際上是同一個文件
軟硬鏈接的區別:
軟鏈接是一個獨立的文件,有獨立的inode,而硬鏈接沒有獨立的inode
軟鏈接相當于快捷方式,硬鏈接本質沒有創建文件,只是建立了一個文件名和已有的inode的映射關系,并寫入當前目錄
六、動靜態庫
概念:
靜態庫(.a):程序在編譯鏈接的時候把庫的代碼鏈接到可執行文件中。程序運行的時候將不再需要靜態庫
動態庫(.so):程序在運行的時候才去鏈接動態庫的代碼,多個程序共享使用庫的代碼
一個與動態庫鏈接的可執行文件僅僅包含它用到的函數入口地址的一個表,而不是外部函數所在目標文件的整個機器碼。在可執行文件開始運行以前,外部函數的機器碼由操作系統從磁盤上的該動態庫中復制到內存中,這個過程稱為動態鏈接
動態庫可以在多個程序間共享,所以動態鏈接使得可執行文件更小,節省了磁盤空間。操作系統采用虛擬內存機制允許物理內存中的一份動態庫被要用到該庫的所有進程共用,節省了內存和磁盤空間,缺點是一旦庫缺失,所以依賴的程序都不可運行
靜態庫鏈接方式生成的可執行程序體積比較大,因為他會將庫里面的代碼拷貝至可執行程序,缺點是程序的體積比較大,浪費系統空間資源,但是如果庫缺失不影響程序運行
示例:
注:編譯時默認是動態編譯,加上-static選項則是靜態編譯
庫文件名稱和引入庫的名稱:
如:libc.so -> c庫,去掉前綴lib,去掉后綴.so,.a
1、制作使用靜態庫
示例:
注意:
制作靜態庫指令:ar -rc
ar是gnu歸檔工具;rc表示(replace and create)
查看靜態庫中的目錄列表: ar -tv libmymath.a
t:列出靜態庫中的文件;v:verbose 詳細信息
指定鏈接靜態庫:gcc main.c -L. -lmymath
-L 指定庫路徑;-l 指定庫名
注:測試目標文件生成后,靜態庫刪掉,程序照樣可以運行
庫搜索路徑:
從左到右搜索-L指定的目錄
由環境變量指定的目錄 (LIBRARY_PATH)
由系統指定的目錄(/usr/lib;/usr/local/lib)
2、制作使用動態庫
示例:
注意:
生成動態庫選項:
shared: 表示生成共享庫格式;fPIC:產生位置無關碼(position independent code)
動態庫是文件,先從磁盤加載到內存上的共享區,并與進程的程序地址空間建立映射關系,由此映射的位置不能影響到進程就需要fPIC
編譯選項:
-I:指定頭文件搜索路徑;-L:指定庫文件搜索路徑;-l:指明需要鏈接庫文件路徑下的哪一個庫
運行動態庫方法:
拷貝動態庫.so文件到系統共享庫路徑下, 一般指/usr/lib
添加庫路徑到 LD_LIBRARY_PATH
ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
具體操作:首先將庫文件所在目錄的路徑存入一個以.conf為后綴的文件當中;然后將該.conf文件拷貝到/etc/ld.so.conf.d/目錄下;使用ldconfig命令將配置文件更新
Linux
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。