文件的輸入輸出在c語言之前瞭解過。比如標準庫的IO接口
回顧C語言的庫函數
fopen,fread,fwrite,fseek,fclose這幾個函數。關鍵在於參數的使用。
例如fopen,FILE *fopen(const char *path, const char *mode)
,在不同的模式下打開文件所能作的操作也不同。
文件使用方式 | 含義 | 如果指定文件不存在 |
---|---|---|
r–只讀 | 爲了輸入數據,打開一個已經存在的文本 | 文件出錯 |
w–只寫 | 爲了輸出數據,打開一個文本文件 | 建立一個新文件 |
a–追加 | 向文本文件尾添加數據 | 出錯 |
rb–只讀 | 爲了輸入數據,打開一個二進制文件 | 出錯 |
wb–只寫 | 爲了輸出數據,打開一個二進制文件 | 建立一個新文件 |
ab–追加 | 向一個二進制文件尾添加數據 | 出錯 |
r±-讀寫 | 爲了讀和寫,打開一個文本文件 | 出錯 |
w±-讀寫 | 爲了讀和寫,建立一個新的文件 | 建立一個新文件 |
a±-讀寫 | 打開一個文件,在文件尾進行讀寫 | 建立一個新的文件 |
rb±-讀寫 | 爲了讀和寫打開一個二進制文件 | 出錯 |
wb±-讀寫 | 爲了讀和寫,新建一個新的二進制文件 | 建立一個新的文件 |
ab±-讀寫 | 打開一個二進制文件,在文件尾進行讀和寫 | 建立一個新的文件 |
先回顧一下C語言中的文件操作
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
int ret;
FILE *fp = NULL;
fp = fopen("./tmp.txt","r+");
if(fp == NULL){
perror("打開失敗");
return -1;
}
fseek(fp, 5, SEEK_END);
char *ptr = "nihao---\n";
ret = fwrite(ptr, 1, strlen(ptr), fp);
printf("write item:%d\n",ret);
fseek(fp, 0, SEEK_SET);
char buf[1024] = {0};
ret = fread(buf, 1, 1023, fp);
perror("讀取失敗");
printf("read buf[%s]-[%d]",buf,ret);
fclose(fp);
return 0;
}
先打開當前路徑下的tmp.txt文件以讀寫的方式打開,之後利用fseek函數跳轉讀寫位置從當前文件位置的末尾開始向後五個位置跳轉。然後寫入數據,一個字節寫入,寫入長度爲*ptr的字符串長度,然後再跳轉到文件的開頭開始讀取數據讀取到buf中。fread返回值是讀取的數據長度。最後關閉文件。
這是C語言庫函數中的使用方法。
系統調用IO接口
文件描述符和文件流指針
標準庫接口使用文件流指針 *FILE
系統調用接口使用文件描述符 比如 int fd
進程中使用open函數打開某個文件,前提是需要我們將進程與文件聯繫起來。所以進程PCB中就有一個 *FILE
指針,這個*FILE
指針指向一個數組files_struct,這個數組內每個元素都對應了一個文件指針,文件指針指向各個FILE結構體。
文件流指針這個結構體中就包含了文件描述符,當使用標準庫接口進行io,則最終是通過文件流指針找到文件描述符進而對文件進行操作
文件描述符是一個正整型數字。文件描述符實際上就是一個數組下標,當進程每打開一個文件,都會使用struct file描述這個文件,並且將描述信息添加到struct file這個結構中的file結構體數組中,並且向用戶返回數組下標作爲文件描述符,用戶通過文件描述符對文件進行操作,再內核實際上是通過文件描述符找到文件描述信息,進而操作文件。
標準輸入 標準輸出 標準錯誤
stdin stdout stderr
0 1 2
文件描述符分配規則:最小未使用
系統調用函數
open write read close lseek
int open(const char *pathname, int flags, mode_t mode);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
off_t lseek(int fd, off_t offset, int whence);
int open(const char *pathname, int flags, mode_t mode);
pathname:文件路徑名
flags:選項標誌
必選項:
O_RDONLY 只讀
O_WRONLY 只寫
O_REWR 可讀可寫
可選項:
O_CREAT 文化不存在則創建,存在則打開
O_EXCL 與O_CREAT同用時,若文件存在則報錯
O_APPEND 寫追加模式
mode:創建文件時給定權限 (八進制數字)
mode & (~umask)
返回值:文件描述符-正整數 錯誤:-1
ssize_t write(int fd, const void *buf, size_t count);
fd:打開文件所返回的文件描述符
buf:要向文件寫入數據
count:要寫入的數據長度
返回值:實際的寫入字節數 錯誤:-1
ssize_t read(int fd, void *buf, size_t count);
fd:打開文件所返回的文件描述符
buf:對讀取到的數據進行存儲的位置
count:要讀取的數據長度
返回值:實際的讀取字節數 錯誤:-1
off_t lseek(int fd, off_t offset, int whence);
fd:打開文件所返回的文件描述符
offset:偏移量
whence:偏移位置
SEEK_SET
SEEK_CUR
SEEK_END
返回值:返回當前位置到文件起始位置的偏移量
掌握知識的最好辦法還是結合代碼!!!
#include <stdio.h>
#inlcude <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
int main(){
//mode_t umask
//修改調用進程的文件創建權限掩碼
umask(0);
int fd = open("./tmp.txt",O_RDWR | O_CREAT | O_APPEND,0777);
if(fd < 0){
perror("open error");
return -1;
}
char buf[1024] = "nihaoa~~!!";
int ret = write(fd, buf, strlen(buf));
if(ret < 0){
perror("write error");
return -1;
}
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//對buf數組所在的內存空間全部初始化爲0,初始化的長度爲1024
ret = read(fd, buf, 1023);
if(ret < 0){
perror("read error");
return -1;
}
printf("read buf:[%s]\n", buf);
close(fd);
return 0;
}
運行如圖,系統調用函數還是比較簡單的。
標準輸入輸出的重定向
這裏要學習一個dup2函數。
函數dup和dup2提供了複製文件描述符的功能。他們通常用於stdin,stdout或進程的stderr的重定向。
int dup2(int oldfd, int newfd);
dup2用來複制參數oldfd所指的文件描述符,並將oldfd拷貝到參數newfd後一起返回。若參數newfd爲一個打開的文件描述符,則newfd所指的文件會先被關閉,若newfd等於oldfd,則返回newfd,而不關閉newfd所指的文件。dup2所複製的文件描述符與原來的文件描述符共享各種文件狀態。共享所有的鎖定,讀寫位置和各項權限或flags。
返回值:如果成功則返回新的文件描述符,否則出錯返回-1.
由dup2函數返回的新文件描述符一定是當前文件描述符可用的最小值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main(){
int fd = open("./tmp.txt",O_RDWR,0777);
dup2(fd, 1);
printf("%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
此時輸出的結果爲1,因爲oldfd爲打開文件的文件描述符,本應該爲3,但是調用了dup2函數,此時stdout關閉了,將oldfd複製進去此時在返回新的文件描述符即爲1。
模擬實現minishell重定向
1、接收標準輸入數據
2、解析命令(判斷是否包含重定向符號 > \ >>)
3、如果包含,則認爲需要輸出重定向,這時候獲取重定向符號後邊的文件名將重定向符號替換成’\0’
4、在子進程中打開文件,將標準輸出重定向到這個文件,進行程序替換
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <wait.h>
//之前寫過一個關於minishell的文件,先對之前的一些進行封裝函數
char buf[1024] = {0};
char *argv[32];
int argc = 0;
void do_face(){
printf("[liuyucheng@localhost]$ ");
fflush(stdout);
memset(buf, 0x00, 1024);
if(scanf("%[^\n]",buf) != 1){
getchar();
}
}
void do_parse(){
char *ptr = buf;
argc = 0;
while(*ptr != '\0'){
if(!isspace(*ptr)){
argv[argc++] = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
}else{
*ptr = '\0';
ptr++;
}
}
argv[argc] = NULL;
return;
}
int main(){
while(1){
do_face();
int redirect = 0;
char *file = NULL;
char *ptr = buf;
while(*ptr != '\0'){
if(*ptr == '>'){
redirect = 1;//清空重定向
*ptr++ = '\0';
if(*ptr == '>'){
redirect = 2;
*ptr++ = '\0';
}
while(isspace(*ptr) && *ptr != '\0'){
ptr++;
}
file = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
*ptr = '\0';
}
ptr++;
}
do_parse();
int pid = fork();
if(pid < 0){
exit(-1);
}else if(pid == 0){
if(redirect == 1){
int fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
}else if(redirect == 2){
int fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
}
execvp(argv[0], argv);
exit(0);
}
wait(NULL);
}
return 0;
}
Linux ext2文件系統
注:圖中應爲inode。
上圖是一個磁盤文件系統的內置。
每個分區都有一個文件系統,不同的分區擁有不同的文件系統。
什麼是inode?inode裏面包含了大小,權限,用戶,時間,塊位置
利用指令ls -i
可以查看inode節點號
存儲文件的流程
通過inode bitmap在inode table(表結構)找到空閒的inode節點,通過data bitmap在數據塊區域找到空閒數據塊,將數據塊位置信息,記錄到inode節點中,將文件數據寫入到數據塊中;將文件名和inode節點名寫入父目錄中。
目錄文件中:存放了一張目錄下有什麼文件的表,表中記錄了文件名。inode節點號—>目錄項
當我們 cat ./a.txt
輸出文件內容時,在當前目錄文件中查找文件名信息,通過文件名獲取inode節點號,通過inode節點號,找到inode節點,進而訪問數據塊,讀取數據進行打印。
軟硬鏈接
創建硬鏈接:ln a.txt b.txt
創建軟鏈接:ln -s a.txt a.soft
硬鏈接是一個文件的另一個名字,跟源文件並沒有什麼區別,----inode節點號相同
軟鏈接是一個獨立的文件,像是一個文件的快捷方式,----inode節點號不同
刪除源文件,軟鏈接失效---->通過記錄的源文件名路徑查找源文件數據;
硬鏈接無影響---->通過inode節點找文件只是鏈接數-1
軟鏈接可以針對目錄進行創建,硬鏈接不可以
軟鏈接可以跨分區建立,硬鏈接不可以
靜態庫與動態庫
靜態庫:名字一般爲libxxx.a,編譯時會整合到可執行程序中。程序在編譯鏈接的時候把庫的代碼鏈接到可執行文件中。程序運行的時候將不再需要靜態庫 。優點是運行時不需要外部函數庫支持,缺點是編譯後程序較大,一旦靜態庫改變,程序需要重新編譯。
動態庫:名字一般爲libxxx.M.N.so,M爲主版本號,N爲副版本號 ,程序在運行的時候纔去鏈接動態庫的代碼,多個程序共享使用庫的代碼。優點是運行時,有需要時才動態調用外部庫中的函數,節省空間,缺點是運行環境中必須提供相應的庫,動態庫更新升級方便。
建立靜態庫
gcc -fPIC -c b.c -o b.o
產生位置無關代碼
生成靜態庫gcc -c b.c -o b.o
ar -cr libmytest.a b.o
生成靜態庫
gcc選項:
-fPIC:產生位置無關代碼
--share:生成一個共享庫而不是可執行程序
ar:靜態庫打包所有命令
-c 創建
-r 替換
建立動態庫
生成動態庫gcc -fPIC -c b.c -o b.o
gcc --share b.o -o libmytest.so
生成動態庫
庫的使用
動態庫—>libmytest.so 靜態庫—>libmytest.a
同名動態和靜態庫,先鏈接動態庫
鏈接庫的時候:gcc a.c -o main -lmytest。如果報錯,找不到庫(鏈接庫的查找路徑----庫的查找路徑)
庫鏈接的時候和運行加載的時候都需要在指定目錄下
庫文件的默認查找路徑/lib64 /usr/lib64
設置環境變量:LIBRARY_PATH=.(庫的鏈接路徑)------>使用選項 ’-L‘
因爲gcc默認是動態鏈接—因此優先使用動態庫生成可執行程序
注意!
通常我們自己鏈接靜態庫生成可執行程序的時候,並不是使用-static靜態鏈接,而是將靜態庫放到指定路徑下,然後直接使用gcc -L選項指定庫的鏈接路徑鏈接靜態庫生成可執行程序
-static:作用是可執行程序使用靜態鏈接生成,不依賴任何動態庫
關於基礎IO的總結
最重要的還是區分系統調用和庫函數調用的區別和用法。在不同的語言中有着不同的用法,Linux下的使用可能還要考慮到進程的創建,程序替換等操作。
還有就是文件系統的理解與inode節點的作用,磁盤的分配有着獨特的規則,通過inode來調用分配空間的使用。