首先需要複習三個知識點:
一、通過系統層析結構圖瞭解系統調用所處的位置:以下所總結的函數經常被稱爲不帶緩衝的I/O,也就是所謂的每個read和write都調用內核中的一個系統調用。
在linux中,查看系統調用文件的幫助文檔需要在man指令後加上2,指定在系統調用章節查看。
二、文件描述符:
在linux中,進程是通過文件描述符(file descriptors 簡稱fd)來訪問文件的,文件描述符實際上是一個非負整數。當打開一個現有文件或創建一個新文件時,內核向進程返回一個文件描述符。當讀或者寫一個文件時,也需要將對應的文件描述符作爲參數傳給對應函數。
在程序剛啓動的時候,默認有三個文件描述符,分別是:0(代表標準輸入),1(代表標準輸出),2(代表標準出錯)。如果不修改已經打開文件,再打開一個新的文件的話,它的文件描述符就是3。
注意:POSIX標準規定,由open返回的文件描述符一定是最小的未用描述符數值。
三、內核所用的I/O的數據結構:
內核使用三種數據結構表示打開的文件:
1. 每個進程再進程表中都有一個記錄項,記錄項中包含有一張打開文件描述符表,可將其看做一個矢量,每個描述符佔用一項。與每個文件描述符關聯的是:
a)文件描述符標誌;
b)指向一個文件表項的指針;
2. 內核爲所有打開文件維持一張文件表。每個文件表項包含:
a)文件狀態標誌;
b)當前文件偏移量;
c)指向該文件V節點表項的指針。
3. 每個打開文件都有一個i節點(i_node)結構。i節點包含了文件類型和對比文件進行各種操作的函數的指針。
當我們執行open操作時,就必須使得進程和文件關聯起來。按照上面的描述:在每個進程的task_struct中,有一個指向files_struct的指針*files,在files_struct中,包含一個指針數組,而每個元素都是一個指向打開文件的指針,而文件描述符則是對應指針在該指針數組的下標。所以拿到文件描述符,就可以找到在該數組的對應位置,從而找到對應的打開文件。簡化的畫一下就是下面這樣
下面開始正式介紹一些系統文件操作的系統調用接口
open:(打開或創建一個文件)
man 手冊第二章中對open的描述:
參數中的pathname代表要打開或創建的目標文件;flags表示打開文件時傳入的參數選項:可同時使用多個參數進行“或”運算;mode是在當創建一個文件時賦予的權限。
常用參數:
O_RDONLY:只讀打開;(read only)
O_WRONLY:只寫打開;(write only)
O_RDWR:讀,寫打開。
(以上三種打開方式必須制定一個且同時只能指定一個)
O_CREAT:若文件不存在,則創建一個新的。創建同時需要使用mode 選項來設置文件的權限
O_APPEND:追加寫返回值:
打開成功返回對應文件的描述符;
失敗返回1。
例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1); //close stdout
//以只寫並創建的方式打開文件,並將權限設置爲644
int fd = open("myfile",O_WRONLY | O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
printf("fid = %d\n",fd);
close(fd);
return 0;
}
輸出結果:
由於操作系統會默認首先打開stdin,stdout,stderr三個文件,所以我們一進來先關閉stdout,再使用open用讀加上創建的參數打開一個文件,並將權限設置爲644,打開成功後,打印fd的值,但是由於此時stdout已經關閉,myfile使用了下標爲1的位置,而printf則是默認向stdout中寫入數據,此時就變成了輸出到了myfile文件中。一定要注意文件描述符的分配規則:用當前進程中最小可用的文件描述符。
creat:(創建文件)
man 2 creat:
int creat(const char *pathname, mode_t mode);
//返回值:成功返回爲只寫打開的文件描述符,失敗爲-1;
//該函數等價於open(pathname,O_WRONLY | O_CREAT | O_TRUNC , mode)
注:早期由於open函數第二個參數有限,不能打開一個尚未存在的文件,所以需要creat,但是現在完全可以用open函數取代creat。
close:(關閉文件)
man 2 close結果:
返回值:成功返回0,失敗返回-1;
注:打開文件後若不再使用一定要記得關閉,系統中對於同一時間打開文件數目有上限,一旦達到上限,將無法再打開新的文件。但是當一個進程終止時,內核會自動關閉它所有打開的文件。
write:(寫文件)
man 2 write結果:
參數介紹:
write第一個參數fd爲要寫入文件的文件描述符;
第二個參數buf爲寫的內容的首地址;
第三個count爲本次要寫入數據的字節個數。
返回值:
調用成功後,返回實際寫入數據字節數。
例:
#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("newfile",O_WRONLY | O_CREAT,0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 3;
const char* buf = "hello world\n";
size_t len = strlen(buf); //字符串長度爲12
ssize_t total = 0;
while(count--){
total += write(fd, buf, len);
}
//一共寫入三次,一次12個字節,共36字節
printf("total write num = %ld\n",total);
close(fd);
return 0;
}
輸出結果:
read:(讀文件)
man 2 write結果:
參數介紹:
第一個參數fd爲要讀取文件的文件描述符;
第二個參數buf爲讀取內容存放緩衝區首地址;
第三個count爲緩衝區最多容納多少字節數。
返回值:
調用成功後,返回實際寫入數據字節數。
例:從上面例子創建的newfile中將之前輸入的內容都讀取出來並打印到屏幕上
#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("newfile",O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
ssize_t total = 0;
const char* msg = "hello world\n";
char buf[1024] = {0};
while(1)
{
ssize_t tmp = read(fd, buf, strlen(msg));
if(tmp > 0){
total += tmp;
printf("%s",buf);
}else{
break;
}
}
printf("total read num = %ld\n",total);
close(fd);
return 0;
}
輸出結果:
注:write和read返回值類型爲ssize_t,爲有符號長整型,使用%ld格式化輸出;size_t 爲無符號長整型,%lu格式化輸出
lseek:(設置打開文件的偏移量)
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//成功返回新的文件偏移量,出錯返回-1
參數介紹:
①fd:要設置的文件對應文件描述符;
②offset:對應解釋依靠於參數三;
③whence:
當whence爲SEEK_SET時,則將該文件的偏移量設置爲距文件開始處offset個字節;
當whence爲SEEK_CUR時,則將該文件的偏移量設置爲當前值加offset,offset可正可負;
注:可通過lseek的返回值來確定所涉及的文件是否可以設置偏移量:若文件描述符引用的是一個管道、FIFO或網絡套接字,則lseek返回-1,並將errno置爲ESPIPE。
示例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
int main()
{
const char buf1[] = "aaaa";
const char buf2[] = "bbb";
int fd = open("file",O_WRONLY | O_CREAT , 0666);
if(fd < 0){
perror("open");
exit(1);
}
//open success
if(write(fd, buf1, sizeof(buf1)) != sizeof(buf1)){
perror("write");
exit(1);
}
//offset = 4
//write success
if(lseek(fd, 1, SEEK_SET) == -1){
perror("lseek");
exit(1);
}
//offset = 1
if(write(fd, buf2, sizeof(buf2)) != sizeof(buf2)){
perror("write");
exit(1);
}
return 0;
}
運行結果:
在我們沒設置偏移量之前,文件偏移量爲4,更改之後偏移量爲1,也就是說之後我們再將內容寫入文件時,將直接從相對於文件開始偏移量爲1的位置開始進行寫入。
注:部分內容參考自《unix環境高級編程》