1. 回顧C檔案介面
1.1 介面介紹
打開檔案的方式:
FILE *fopen( const char *path, const char *mode );
- path:需要打開的檔案,需要加上檔案的路徑,可以是一個相對路徑(相對于程式而言),也可以是一個絕對路徑
- mode:以什么方式打開檔案
- r:以讀模式打開檔案,如果說檔案不存在,則報錯
- r+:以讀寫模式打開檔案,如果檔案不存在,則報錯
- w:以寫模式打開檔案,如果 檔案不存在則創建檔案,如果檔案存在,則清空檔案內容(截斷檔案),檔案流指標指向檔案的頭部
- w+:以讀寫模式打開,如果檔案不存在則創建檔案,如果檔案存在,則清空檔案內容(截斷檔案),檔案流指標指向檔案的頭部
- a:以追加模式打開,如果檔案不存在則創建;并不能對檔案進行讀,只能在檔案的末尾進行追加寫
- a+:以追加模式打開,如果檔案不存在則創建;支持讀檔案,在檔案的末尾進行寫
- 回傳值:回傳了一個檔案流指標
讀檔案的方式:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr:將讀到的內容保存到ptr當中
- size:塊的大小,單位是位元組含義:表示一次讀幾個位元組
- nmemb:塊的個數,期望讀到的塊數
- size *nmemb,單位是位元組,用來衡量總共讀到的位元組數量
- 注意:如果size不為1,塊的大小不為1,則 能用回傳值來講讀到了多少個位元組
- 常見的用法:將size置為1,每一個塊的大小就是1;要從檔案當中讀多少位元組,就設定多少塊就可以了
- stream:檔案流指標,從哪里讀
- 回傳值:回傳成功讀到的塊的個數,實際讀到的塊數
寫檔案的方式:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE*stream);
- ptr:要往檔案當中寫的內容
- size:塊的大小,意味著一次寫多少位元組
- nmemb:塊的個數
- 判斷寫了多少位元組= size * nmemb
- 一般的用法,將size設定成為1,意味著塊的個數,就相當于寫多少位元組
- stream:檔案流指標,往哪里寫
- 回傳值:回傳成功寫入塊的個數
偏移檔案流指標的方式:
int fseek(FILE *stream, long offset, int whence)
- stream:檔案流指標(要操作的檔案流指標)
- offset.:偏移量,針對于whence而言的
- whence:偏移到哪里去
- SEEK_SET:將檔案流指標重置會檔案的頭部
- SEEK_END:將檔案流指標重置到檔案的尾部
- SEEK_CUR:將檔案流指標重置到當前位置
關閉檔案的方式:
int fclose(FILE* fp);
stdin & stdout & stderr
C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr,
仔細觀察發現,這三個流的型別都是FILE*,fopen回傳值型別,檔案指標,
1.2 示例
hello.c寫檔案:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if (!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while (count--){
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);// 關閉檔案流指標
return 0;
}
hello.c讀檔案:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "r");
if (!fp){
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello bit!\n";
while (1){
//注意回傳值和引數,此處有坑,仔細查看man手冊關于該函式的說明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if (s > 0){
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
2. 系統檔案I/O
2.1 介面介紹
打開檔案的方式:
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
寫檔案的方式:
ssize_t write(int fd, const void* buf, size_t count)
- fd:檔案描述符
- buf:寫入的資料
- count:寫入的資料大小(單位是位元組)
讀檔案的方式:
ssize_t read(int fd, void *buf, size_t count)
- fd:檔案描述符,從哪里進行讀
- buf:讀到哪里去(程式員在代碼當中定義的緩沖區)
- count:最大可以讀多少位元組
注意的點:一定要預留\0的位置
偏置檔案流指標的方式:
off_t lseek(int fd, off_t offset, int whence)
- fd:需要操作的檔案描述符
- offset:偏移量
- whence:偏移到哪里去
- SEEK_SET:將檔案流指標重置會檔案的頭部
- SEEK_END:將檔案流指標重置到檔案的尾部
- SEEK_CUR:將檔案流指標重置到當前位置
關閉檔案的方式:
int close(int fd)
2.2示例
hello.cpp 寫檔案:
#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.cpp 讀檔案:
#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;
}
3. open函式回傳值
- fopen fclose fread fwrite 都是C標準庫當中的函式,我們稱之為庫函式(libc)
- open close read write lseek 都屬于系統提供的介面,稱之為系統呼叫介面

系統呼叫介面和庫函式的關系,一目了然,
所以,可以認為,f#系列的函式,都是對系統呼叫的封裝,方便二次開發,
4. 檔案描述符fd
0 & 1 & 2
Linux行程默認情況下會有3個預設打開的檔案描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2.
0,1,2對應的物理設備一般是:鍵盤,顯示幕,顯示幕 所以輸入輸出還可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#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;
}

- 檔案描述符就是內核當中維護的fd_array陣列的下標,下標是從0開始的,所以檔案描述符是一個正整數;
- 當程式員操作檔案的時候,其實就是通過檔案描述符,找到fd_array陣列當中對應的元素,每一個元素都對應一個檔案資訊,內核通過操作陣列元素對應的檔案資訊,找到對應的檔案,從而實作檔案操作,
檔案描述符就是從0開始的小整數
- 當我們打開檔案時,作業系統在記憶體中要創建相應的資料結構來描述目標檔案,
- 于是就有了file結構體,表示一個已經打開的檔案物件,而行程執行open系統呼叫,所以必須讓行程和檔案關聯起來,
- 每個行程都有一個指標*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指標陣列,每個元素都是一個指向打開檔案的指標!
- 本質上,檔案描述符就是該陣列的下標,
- 所以,只要拿著檔案描述符,就可以找到對應的檔案,
檔案描述符的分配規則
在files_struct陣列當中,找到當前沒有被使用的最小的一個下標,作為新的檔案描述符,最小未占用規則,只要fd_array陣列當中從小到大,哪個位置沒有占用,則新打開的檔案就會占用該位置,
5. 重定向
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
if (fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此時,我們發現,本來應該輸出到顯示幕上的內容,輸出到了檔案myfile 當中,其中,fd=1,這種現象叫做輸出重定向,常見的重定向有:>, >>, <,
使用 dup2 系統呼叫:
函式原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd和newfd都是檔案描述符
eg:想要標準輸出變成一個程式員打開的檔案dup2(2,3);
注意事項:
- newfd拷貝oldfd,
- 如果oldfd是一個無效的檔案描述符,則dup2什么事情都不干,newfd還是指向原來的檔案,
- 如果newfd和oldfd是同樣的值,則dup2什么事情都不干,
- 錯誤,回傳小于0的值,
6. FILE檔案流指標
因為IO相關函式與系統呼叫介面對應,并且庫函式封裝系統呼叫,所以本質上,訪問檔案都是通過fd訪問的,
- 所以C庫當中的FILE結構體內部,必定封裝了fd,
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0 = "hello printf\n";
const char *msg1 = "hello fwrite\n";
const char *msg2 = "hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
運行結果:
hello printf
hello fwrite
hello write
但如果對行程實作輸出重定向呢? ./hello > file , 我們發現結果變成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我們發現printf 和fwrite (庫函式)都輸出了2次,而write 只輸出了一次(系統呼叫),為什么呢?肯定和fork有關!
- 一般C庫函式寫入檔案時是全緩沖的,而寫入顯示幕是行緩沖,
- printf fwrite 庫函式會自帶緩沖區(進度條例子就可以說明),當發生重定向到普通檔案時,資料的緩沖方式由行緩沖變成了全緩沖,
- 而我們放在緩沖區中的資料,就不會被立即重繪,甚至fork之后
- 但是行程退出之后,會統一重繪,寫入檔案當中,
- 但是fork的時候,父子資料會發生寫時拷貝,所以當你父行程準備重繪的時候,子行程也就有了同樣的一份資料,隨即產生兩份資料,
- write 沒有變化,說明沒有所謂的緩沖,
綜上: printf fwrite 庫函式會自帶緩沖區,而write 系統呼叫沒有帶緩沖區,另外,我們這里所說的緩沖區,都是用戶級緩沖區,其實為了提升整機性能,OS也會提供相關內核級緩沖區,不過不再我們討論范圍之內,
那這個緩沖區誰提供呢? printf fwrite 是庫函式, write 是系統呼叫,庫函式在系統呼叫的“上層”, 是對系統呼叫的“封裝”,但是write 沒有緩沖區,而printf fwrite 有,足以說明,該緩沖區是二次加上的,又因為是C,所以由C標準庫提供,

7. 動態庫和靜態庫
靜態庫(.a):程式在編譯鏈接的時候把庫的代碼鏈接到可執行檔案中,程式運行的時候將不再需要靜態庫
- 動態庫(.so):程式在運行的時候才去鏈接動態庫的代碼,多個程式共享使用庫的代碼,
- 一個與動態庫鏈接的可執行檔案僅僅包含它用到的函式入口地址的一個表,而不是外部函式所在目標檔案的整個機器碼
- 在可執行檔案開始運行以前,外部函式的機器碼由作業系統從磁盤上的該動態庫中復制到記憶體中,這個程序稱為動態鏈接(dynamic linking)
- 動態庫可以在多個程式間共享,所以動態鏈接使得可執行檔案更小,節省了磁盤空間,作業系統采用虛擬記憶體機制允許物理記憶體中的一份動態庫被要用到該庫的所有行程共用,節省了記憶體和磁盤空間,
生成靜態庫
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成靜態庫
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu歸檔工具,rc表示(replace and create)
查看靜態庫中的目錄串列
[root@localhost linux]# ar -tv libmymath.a
rw-r–r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r–r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出靜態庫中的檔案
v:verbose 詳細資訊
[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定庫路徑
-l 指定庫名
測驗目標檔案生成后,靜態庫刪掉,程式照樣可以運行,
庫搜索路徑
- 從左到右搜索-L指定的目錄,
- 由環境變數指定的目錄 (LIBRARY_PATH)
- 由系統指定的目錄
- /usr/lib
- /usr/local/lib
生成動態庫
shared: 表示生成共享庫格式
fPIC:產生位置無關碼(position independent code)
庫名規則:libxxx.so
示例:
[root@localhost linux]# gcc -fPIC -c sub.c add.c
[root@localhost linux]# gcc -shared -o libmymath.so*.o
[root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
使用動態庫
編譯選項
l:鏈接動態庫,只要庫名即可(去掉lib以及版本號)
L:鏈接庫所在的路徑.
示例: gcc main.o -o main –L. -lhello
運行動態庫
1、拷貝.so檔案到系統共享庫路徑下, 一般指/usr/lib
2、更改LD_LIBRARY_PATH
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
使用外部庫
系統中其實有很多庫,它們通常由一組互相關聯的用來完成某項常見作業的函式構成,
8. 理解檔案系統
我們使用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
inode
為了能解釋清楚inode我們先簡單了解一下檔案系統,

Linux ext2檔案系統,上圖為磁盤檔案系統圖(內核記憶體映像肯定有所不同),磁盤是典型的塊設備,硬碟磁區被劃分為一個個的block,一個block的大小是由格式化的時候確定的,并且不可以更改,
例如mke2fs的-b選項可以設定block大小為1024、2048或4096位元組,
而上圖中啟動塊(Boot Block)的大小是確定的,
- Block Group:ext2檔案系統會根據磁區的大小劃分為數個Block Group,而每個Block Group都有著相同的結構組成,政府管理各區的例子
- 超級塊(Super Block):存放檔案系統本身的結構資訊,記錄的資訊主要有:bolck 和 inode的總量,未使用的block和inode的數量,一個block和inode的大小,最近一次掛載的時間,最近一次寫入資料的時間,最近一次檢驗磁盤的時間等其他檔案系統的相關資訊,Super Block的資訊被破壞,可以說整個檔案系統結構就被破壞了
- GDT,Group Descriptor Table:塊組描述符,描述塊組屬性資訊
- 塊位圖(Block Bitmap):Block Bitmap中記錄著Data Block中哪個資料塊已經被占用,哪個資料塊沒有被占用
- inode位圖(inode Bitmap):每個bit表示一個inode是否空閑可用,
- i節點表:存放檔案屬性 如 檔案大小,所有者,最近修改時間等
- 資料區:存放檔案內容
創建一個新檔案主要有一下4個操作:
- 存盤屬性
內核先找到一個空閑的i節點(這里是263466),內核把檔案資訊記錄到其中, - 存盤資料
該檔案需要存盤在三個磁盤塊,內核找到了三個空閑塊:300,500,800,將內核緩沖區的第一塊資料復制到300,下一塊復制到500,以此類推, - 記錄分配情況
檔案內容按順序300,500,800存放,內核在inode上的磁盤分布區記錄了上述塊串列, - 添加檔案名到目錄
新的檔案名abc,linux如何在當前的目錄中記錄這個檔案?內核將入口(263466,abc)添加到目錄檔案,檔案名和inode之間的對應關系將檔案名和檔案的內容及屬性連接起來,
硬鏈接:
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i
abc def 263466 abc 263466 def
創建:
In [源檔案] [待創建 出來的硬鏈接檔案名稱]
注意:
源檔案和硬鏈接檔案兩者具有同樣的inode節點資訊
軟鏈接:
類比:類似于win環境當中創建快捷方式
263563 -rw-r–r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r–r--. 2 root root 0 9月 15 17:45 def
創建:In -s [源檔案] [待創建出來的軟鏈接檔案]
“-” 代表普通檔案,軟鏈接檔案–>,“l”代表軟鏈接檔案
注意:
1.創建出來的軟鏈接檔案,指向源檔案,檔案型別是“l”,不論是修改軟鏈接檔案還是修改源檔案是一回事!
2.軟鏈接檔案和源檔案具有不同的inode節點號,相當于 兩個inode節點,都保存了檔案在磁盤當中的存盤位置
3.如果使用“ll”命令看到的軟鏈接檔案指向的源檔案一直在閃爍,說明源檔案不存在
4.警告!!如果要洗掉源檔案,一定記著把軟鏈接檔案也一起洗掉掉
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/174469.html
標籤:其他
