??作者:阿潤(rùn)菜菜
??專欄:Linux系統(tǒng)編程
重新認(rèn)識(shí)文件
- 是不是只有C/C++有文件操作呢?python、java、go等文件接口操作的方法是不太一樣的,那如何理解這種現(xiàn)象?有沒有統(tǒng)一的視角去看待所有的語(yǔ)言文件操作呢?—我們今天從系統(tǒng)視角去理解 ---- 實(shí)際都是通過系統(tǒng)調(diào)用來訪問
- 文件=內(nèi)容+屬性 — 針對(duì)文件的操作:對(duì)內(nèi)容的操作,對(duì)屬性的操作,對(duì)內(nèi)容和屬性的操作
- 文件可以分為兩大類:磁盤文件 和 被打開的文件(內(nèi)存文件)
- 當(dāng)文件沒有被操作的時(shí)候,文件一般放在磁盤位置??瘴募苍诖疟P中占據(jù)空間,因?yàn)槲募傩砸彩菙?shù)據(jù),保存數(shù)據(jù)就需要空間。
- 我們?cè)谖募僮鞯臅r(shí)候,文件需要在哪里?—內(nèi)存,依據(jù)馮諾依曼體系的規(guī)定
- 所以我們?cè)谖募僮鞯臅r(shí)候,文件需要提前l(fā)oad到內(nèi)存,那load是內(nèi)容還是屬性?至少有屬性吧!那是不是只有你一個(gè)人在load呢?當(dāng)然不是,內(nèi)存中一定存在大量的不同文件的屬性
- 所以,打開文件的本質(zhì)就是將需要的文件加載到內(nèi)存中,OS內(nèi)部一定會(huì)同時(shí)存在大量的被打開的文件,那操作系統(tǒng)需不需要管理呢?怎么管理?— 先描述,在組織!
- 先描述 — 構(gòu)建在內(nèi)存中的文件結(jié)構(gòu)體 struct file (文件從磁盤中來,struct file* next連接下一個(gè)文件信息)。在組織 — struct file結(jié)構(gòu)體利用某種數(shù)據(jù)結(jié)構(gòu)鏈接起來。在OS內(nèi)部,對(duì)被打開的文件進(jìn)行管理,就轉(zhuǎn)換成了對(duì)類似鏈表的增刪查改
- 結(jié)論:文件被打開,OS要為被打開的文件,創(chuàng)建對(duì)應(yīng)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)
- 所有文件操作的本質(zhì)就是進(jìn)程和被打開文件的關(guān)系。 — struct task_struct 和 struct file
系統(tǒng)內(nèi)部的文件操作
庫(kù)函數(shù)底層必須調(diào)用系統(tǒng)調(diào)用接口,因?yàn)闊o(wú)論什么進(jìn)程想訪問文件,都必須按照操作系統(tǒng)提供的方式來進(jìn)行訪問,所以就算文件操作相關(guān)函數(shù)千變?nèi)f化,但是底層是不變的,這些函數(shù)最后都會(huì)調(diào)用系統(tǒng)調(diào)用接口,按照操作系統(tǒng)的意愿來合理的訪問磁盤上的文件。
我們不能用語(yǔ)言繞過操作系統(tǒng)去操縱硬件,所以必須通過系統(tǒng)調(diào)用通過操作系統(tǒng)來進(jìn)行文件操作!不管什么編程語(yǔ)言,只是不同語(yǔ)言對(duì)系統(tǒng)調(diào)用進(jìn)行了各自不同的封裝,所以對(duì)這些文件操作接口的理解,其實(shí)就要落實(shí)到對(duì)系統(tǒng)調(diào)用接口的理解! 也就是說所有的只要要訪問硬件或者操作系統(tǒng)內(nèi)部的資源,都要通過系統(tǒng)調(diào)用!避不開的!
我們C語(yǔ)言的文件操作
C語(yǔ)言文件操作接口主要包括以下幾類:
- 打開和關(guān)閉文件的接口,如fopen(), fclose()等。這些接口用于創(chuàng)建或打開一個(gè)文件,并返回一個(gè)FILE類型的指針,以及關(guān)閉一個(gè)已打開的文件,并釋放相關(guān)資源。
- 順序讀寫數(shù)據(jù)的接口,如fgetc(), fputc(), fgets(), fputs(), fprintf(), fscanf()等。這些接口用于從文件中讀取或?qū)懭胱址?、字符串或格式化?shù)據(jù),并自動(dòng)移動(dòng)文件指針。
- 隨機(jī)讀寫數(shù)據(jù)的接口,如fread(), fwrite(), fseek(), ftell()等。這些接口用于從文件中讀取或?qū)懭攵M(jìn)制數(shù)據(jù)塊,并根據(jù)指定位置移動(dòng)或獲取文件指針。
- 其他輔助功能的接口,如feof(), ferror(), clearerr()等。這些接口用于檢測(cè)文件是否到達(dá)末尾、是否發(fā)生錯(cuò)誤、以及清除錯(cuò)誤標(biāo)志。
文件的打開方式:
r:以只讀的方式打開文件,若文件不存在就會(huì)出錯(cuò)。
w:以只寫的方式打開文件,文件若存在則清空文件內(nèi)容重新開始寫入,若不存在則創(chuàng)建一個(gè)文件。
a:以只寫的方式打開文件,文件若存在則從文件尾部以追加的方式進(jìn)行寫入,若不存在則創(chuàng)建一個(gè)文件。
r+:以可讀寫的方式打開文件,若文件不存在就會(huì)出錯(cuò)。
w+:以可讀寫的方式打開文件,其他與w一樣。
a+:以可讀寫的方式打開文件,其他與a一樣。
fopen, fread, fwrite, fseek, fclose等函數(shù)的使用
需要注意的是,當(dāng)向文件中寫入數(shù)據(jù)后,想要重新讀取到數(shù)據(jù),要么需要關(guān)閉文件重新打開,要么就要跳轉(zhuǎn)讀寫位置到文件起始位置,然后再開始讀取文件數(shù)據(jù)。
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("./bite", "wb+");
if (fp == NULL) {
perror("fopen error");
return -1;
}
fseek(fp, 0, SEEK_SET);
char *data = "linux so easy!\n";
//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t ret = fwrite(data, 1, strlen(data), fp);
if (ret != strlen(data)) {
perror("fwrite error");
return -1;
}
fseek(fp, 0, SEEK_SET);//跳轉(zhuǎn)讀寫位置到,從文件起始位置開始偏移0個(gè)字節(jié)
char buf[1024] = {0};
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ret = fread(buf, 1, 1023, fp);//因?yàn)樵O(shè)置讀取塊大小位1,塊個(gè)數(shù)為1023因此fread返回值為實(shí)際讀取到的數(shù)據(jù)長(zhǎng)度
if (ret == 0) {
if (ferror(fp)) //判斷上一次IO操作是否正確
printf("fread error\n");
if (feof(fp)) //判斷是否讀取到了文件末尾
printf("read end of file!\n");
return -1;
}
printf("%s", buf);
fclose(fp);
return 0;
}
當(dāng)然這些也都是C庫(kù)提供的函數(shù),是對(duì)系統(tǒng)調(diào)用的上層封裝,在系統(tǒng)級(jí)別文件操作我們是通過系統(tǒng)調(diào)用實(shí)現(xiàn)的:
系統(tǒng)內(nèi)部的文件操作
文件操作系統(tǒng)調(diào)用接口是指Linux內(nèi)核提供的一組用于對(duì)文件進(jìn)行打開、讀寫、關(guān)閉等操作的函數(shù)。它們包括以下幾個(gè)常用的函數(shù):
- open:打開一個(gè)文件,返回一個(gè)文件描述符,可以指定文件的打開方式和權(quán)限。
- write:向一個(gè)已打開的文件中寫入數(shù)據(jù),返回實(shí)際寫入的字節(jié)數(shù)。
- read:從一個(gè)已打開的文件中讀取數(shù)據(jù),返回實(shí)際讀取的字節(jié)數(shù)。
- lseek:改變一個(gè)已打開文件的讀寫位置,返回新的偏移量。
- close:關(guān)閉一個(gè)已打開的文件,釋放資源。
這些函數(shù)都需要傳入一個(gè)文件描述符作為參數(shù),它是一個(gè)非負(fù)整數(shù),用于標(biāo)識(shí)不同的打開文件。每個(gè)進(jìn)程都有自己獨(dú)立的一組文件描述符,并且默認(rèn)有三個(gè)預(yù)定義的描述符:0代表標(biāo)準(zhǔn)輸入,1代表標(biāo)準(zhǔn)輸出,2代表標(biāo)準(zhǔn)錯(cuò)誤輸出。
這些函數(shù)都有可能失敗,并返回-1,并設(shè)置errno變量為相應(yīng)的錯(cuò)誤碼。因此,在調(diào)用這些函數(shù)后,需要檢查返回值和錯(cuò)誤碼來判斷是否成功。
我們主要介紹前三個(gè):
OS一般會(huì)如何讓用戶給自己傳遞標(biāo)志位的?多個(gè)標(biāo)志位怎么實(shí)現(xiàn)呢? — 位圖
其實(shí)是通過位操作實(shí)現(xiàn)的:
#include <stdio.h>
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10
// 0000 0000 0000 0000 0000 0000 0000 0000
void Print(int flags)
{
if(flags & ONE) printf("hello 1\n"); //充當(dāng)不同的行為
if(flags & TWO) printf("hello 2\n");
if(flags & THREE) printf("hello 3\n");
if(flags & FOUR) printf("hello 4\n");
if(flags & FIVE) printf("hello 5\n");
}
int main()
{
printf("--------------------------\n");
Print(ONE);
printf("--------------------------\n");
Print(TWO);
printf("--------------------------\n");
Print(FOUR);
printf("--------------------------\n");
Print(ONE|TWO);
printf("--------------------------\n");
Print(ONE|TWO|THREE);
printf("--------------------------\n");
Print(ONE|TWO|THREE|FOUR|FIVE);
printf("--------------------------\n");
return 0;
}
open:打開一個(gè)文件,返回一個(gè)文件描述符,可以指定文件的打開方式和權(quán)限
open有兩種調(diào)用方式:
一種是只傳入文件名和訪問模式,另一種是還傳入創(chuàng)建權(quán)限(如果需要?jiǎng)?chuàng)建新文件)。訪問模式有必需部分和可選部分,必需部分是 O_RDONLY(只讀)、O_WRONLY(只寫)或 O_RDWR(讀寫),可選部分有 O_APPEND(追加)、O_TRUNC(截?cái)啵_CREAT(創(chuàng)建)、O_EXCL(排他)等。創(chuàng)建權(quán)限是由幾個(gè)標(biāo)志按位或得到的,如 S_IRUSR(用戶讀)、S_IWUSR(用戶寫)、S_IXUSR(用戶執(zhí)行)等。
字符串/0 問題: 系統(tǒng)調(diào)用不需要這個(gè)!
使用 open 函數(shù)打開一個(gè)文件,如果不存在則創(chuàng)建一個(gè)新文件,并設(shè)置訪問模式為讀寫和追加,創(chuàng)建權(quán)限為用戶讀寫和組讀寫:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main() {
// 打開或創(chuàng)建一個(gè)文件
int fd = open("test.txt", O_RDWR | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (fd == -1) {
// 打開失敗,打印錯(cuò)誤信息
perror("open error");
exit(1);
}
// 打開成功,打印文件描述符
printf("open success, fd = %d\n", fd);
// 關(guān)閉文件
close(fd);
return 0;
}
創(chuàng)建目錄的命令mkdir,目錄起始權(quán)限默認(rèn)是0777,創(chuàng)建文件的命令touch,文件起始權(quán)限是0666,這些命令的實(shí)現(xiàn)實(shí)際上是要調(diào)用系統(tǒng)接口open的,并且在創(chuàng)建文件或目錄的時(shí)候要在open的第三個(gè)參數(shù)中設(shè)置文件的起始權(quán)限。
25 int main()
26 {
27 umask(0);//將進(jìn)程的umask值設(shè)置為0000
28
29 // C語(yǔ)言中的w選項(xiàng)實(shí)際上底層需要調(diào)用這么多的選項(xiàng)O_WRONLY O_CREAT O_TRUNC 0666
30 // C語(yǔ)言中的a選項(xiàng)需要將O_TRUNC替換為O_APPEND
31 int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);//設(shè)置文件起始權(quán)限為0666
32 if(fd < 0)
33 {
34 perror("open");
35 return 1;//退出碼設(shè)置為1
36 }
37 close(fd);
38 }
### write:向一個(gè)已打開的文件中寫入數(shù)據(jù),返回實(shí)際寫入的字節(jié)數(shù)
**write:向一個(gè)已打開的文件中寫入數(shù)據(jù),返回實(shí)際寫入的字節(jié)數(shù)。需要傳入文件描述符、數(shù)據(jù)緩沖區(qū)和數(shù)據(jù)長(zhǎng)度。如果返回值小于請(qǐng)求的字節(jié)數(shù),可能是因?yàn)殄e(cuò)誤或者設(shè)備驅(qū)動(dòng)程序?qū)?shù)據(jù)塊長(zhǎng)度敏感。如果返回值為 0,表示沒有寫入任何數(shù)據(jù);如果返回值為 -1,則表示出現(xiàn)錯(cuò)誤。**
使用 write 函數(shù)向一個(gè)已打開的文件中寫入一段字符串,并檢查返回值是否正確:
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 要寫入的字符串和長(zhǎng)度
char *str = "Hello world!\n";
int len = 13;
// 向標(biāo)準(zhǔn)輸出(文件描述符為1)寫入字符串
int ret = write(1, str, len);
if (ret == -1) {
// 寫入失敗,打印錯(cuò)誤信息
perror("write error");
exit(1);
}
if (ret != len) {
// 寫入字節(jié)數(shù)不正確,打印警告信息
fprintf(stderr, "write warning: expected %d bytes, but got %d bytes\n", len, ret);
}
// 寫入成功,打印返回值
printf("write success, ret = %d\n", ret);
}
read:從一個(gè)已打開的文件中讀取數(shù)據(jù),返回實(shí)際讀取的字節(jié)數(shù)
read:從一個(gè)已打開的文件中讀取數(shù)據(jù),返回實(shí)際讀取的字節(jié)數(shù)。需要傳入文件描述符、數(shù)據(jù)緩沖區(qū)和數(shù)據(jù)長(zhǎng)度。如果返回值小于請(qǐng)求的字節(jié)數(shù),可能是因?yàn)殄e(cuò)誤或者已到達(dá)文件尾。如果返回值為 0,表示沒有讀取任何數(shù)據(jù);如果返回值為 -1,則表示出現(xiàn)錯(cuò)誤。
使用 read 函數(shù)從一個(gè)已打開的文件中讀取一定長(zhǎng)度的數(shù)據(jù),并存儲(chǔ)到一個(gè)緩沖區(qū)中,并檢查返回值是否正確:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 要讀取的字節(jié)數(shù)和緩沖區(qū)大小
int len = 100;
char buf[100];
// 從標(biāo)準(zhǔn)輸入(文件描述符為0)讀取數(shù)據(jù)到緩沖區(qū)中
int ret = read(0, buf, len);
if (ret == -1) {
// 讀取失敗,打印錯(cuò)誤信息
perror("read error");
exit(1);
}
if (ret == 0) {
// 讀取到文件尾,沒有數(shù)據(jù)可讀,打印提示信息
printf("read end of file\n");
}
// 讀取成功,打印返回值和緩沖區(qū)內(nèi)容(注意添加結(jié)束符)
printf("read success, ret = %d\n", ret);
buf[ret] = '\0';
printf("buf: %s\n", buf);
}
使用這些接口時(shí),有一些事項(xiàng)需要注意:
- 在調(diào)用 open 函數(shù)時(shí),要根據(jù)文件的用途和狀態(tài)選擇合適的訪問模式和創(chuàng)建權(quán)限。如果使用了 O_CREAT 標(biāo)志,要指定創(chuàng)建權(quán)限,否則可能導(dǎo)致文件權(quán)限不正確。如果使用了 O_EXCL 標(biāo)志,要檢查返回值是否為 -1,否則可能導(dǎo)致覆蓋已有文件。如果打開的是設(shè)備文件或符號(hào)鏈接,要注意一些特殊的訪問模式,如 O_NONBLOCK、O_NOCTTY、O_NOFOLLOW 等。
- 在調(diào)用 write 函數(shù)時(shí),要保證數(shù)據(jù)緩沖區(qū)的有效性和長(zhǎng)度正確性。如果寫入的是文本文件,要注意添加換行符或結(jié)束符。如果寫入的是二進(jìn)制文件,要注意字節(jié)序和對(duì)齊問題。如果寫入的是設(shè)備文件或網(wǎng)絡(luò)套接字,要注意數(shù)據(jù)塊長(zhǎng)度和超時(shí)問題。
- 在調(diào)用 read 函數(shù)時(shí),要保證數(shù)據(jù)緩沖區(qū)的有效性和大小足夠。如果讀取的是文本文件,要注意處理?yè)Q行符或結(jié)束符。如果讀取的是二進(jìn)制文件,要注意字節(jié)序和對(duì)齊問題。如果讀取的是設(shè)備文件或網(wǎng)絡(luò)套接字,要注意數(shù)據(jù)塊長(zhǎng)度和超時(shí)問題。
- 在調(diào)用這些接口后,都要檢查返回值是否為 -1,并根據(jù) errno 變量來判斷錯(cuò)誤原因,并進(jìn)行相應(yīng)的處理或提示。有些錯(cuò)誤可能是暫時(shí)性的或可恢復(fù)的,如 EINTR、EAGAIN、EWOULDBLOCK 等;有些錯(cuò)誤可能是嚴(yán)重性的或不可恢復(fù)的,如 EACCES、EBADF、EFAULT、EINVAL 等。
綜合使用:
fopen, fread, fwrite, fseek, fclose等函數(shù)的使用
需要注意的是,當(dāng)向文件中寫入數(shù)據(jù)后,想要重新讀取到數(shù)據(jù),要么需要關(guān)閉文件重新打開,要么就要跳轉(zhuǎn)讀寫位置到文件起始位置,然后再開始讀取文件數(shù)據(jù)。
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("./bite", "wb+");
if (fp == NULL) {
perror("fopen error");
return -1;
}
fseek(fp, 0, SEEK_SET);
char *data = "linux so easy!\n";
//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t ret = fwrite(data, 1, strlen(data), fp);
if (ret != strlen(data)) {
perror("fwrite error");
return -1;
}
fseek(fp, 0, SEEK_SET);//跳轉(zhuǎn)讀寫位置到,從文件起始位置開始偏移0個(gè)字節(jié)
char buf[1024] = {0};
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ret = fread(buf, 1, 1023, fp);//因?yàn)樵O(shè)置讀取塊大小位1,塊個(gè)數(shù)為1023因此fread返回值為實(shí)際讀取到的數(shù)據(jù)長(zhǎng)度
if (ret == 0) {
if (ferror(fp)) //判斷上一次IO操作是否正確
printf("fread error\n");
if (feof(fp)) //判斷是否讀取到了文件末尾
printf("read end of file!\n");
return -1;
}
printf("%s", buf);
fclose(fp);
return 0;
}
綜合使用:
open, read, write, lseek, close等函數(shù)的使用
#include <stdio.h>
#include <unistd.h>//是close, write這些接口的頭文件
#include <string.h>
#include <fcntl.h>//是 O_CREAT 這些宏的頭文件
#include <sys/stat.h>//umask接口頭文件
int main()
{
//將當(dāng)前進(jìn)程的默認(rèn)文件創(chuàng)建權(quán)限掩碼設(shè)置為0--- 并不影響系統(tǒng)的掩碼,僅在當(dāng)前進(jìn)程內(nèi)生效
umask(0);
//int open(const char *pathname, int flags, mode_t mode);
int fd = open("./bite", O_CREAT|O_RDWR, 0664);
if(fd < 0) {
perror("open error");
return -1;
}
char *data = "i like linux!\n";
//ssize_t write(int fd, const void *buf, size_t count);
ssize_t ret = write(fd, data, strlen(data));
if (ret < 0) {
perror("write error");
return -1;
}
//off_t lseek(int fd, off_t offset, int whence);
lseek(fd, 0, SEEK_SET);
char buf[1024] = {0};
//ssize_t read(int fd, void *buf, size_t count);
ret = read(fd, buf, 1023);
if (ret < 0) {
perror("read error");
return -1;
}else if (ret == 0) {
printf("end of file!\n");
return -1;
}
printf("%s", buf);
close(fd);
return 0;
}
看看Linux內(nèi)核源代碼是怎么說的
可以看到內(nèi)核源代碼的設(shè)計(jì)內(nèi)容跟我們所說的基本一致
理解文件控制塊&&文件描述符&&文件指針的關(guān)系
在進(jìn)程中每打開一個(gè)文件,都會(huì)創(chuàng)建有相應(yīng)的文件描述信息struct file,這個(gè)描述信息被添加在pcb的struct files_struct中,以數(shù)組的形式進(jìn)行管理,隨即向用戶返回?cái)?shù)組的下標(biāo)作為文件描述符,用于操作文件
進(jìn)程可以打開多個(gè)文件,對(duì)于大量的被打開文件,操作系統(tǒng)一定是要進(jìn)行管理的,也就是先描述再組織,所以操作系統(tǒng)會(huì)為被打開的文件創(chuàng)建對(duì)應(yīng)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),也就是文件控制塊FCB,在linux源碼中是struct file{}結(jié)構(gòu)體,包含了文件的大部分屬性
#include <assert.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define FILE_NAME(number) "log.txt"#number
int main()
{
int fd0 = open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);//設(shè)置文件起始權(quán)限為0666
int fd1 = open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);//設(shè)置文件起始權(quán)限為0666
int fd2 = open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);//設(shè)置文件起始權(quán)限為0666
int fd3 = open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);//設(shè)置文件起始權(quán)限為0666
int fd4 = open(FILE_NAME(5),O_WRONLY | O_CREAT | O_TRUNC,0666);//設(shè)置文件起始權(quán)限為0666
printf("fd:%d\n",fd0);
printf("fd:%d\n",fd1);
printf("fd:%d\n",fd2);
printf("fd:%d\n",fd3);
printf("fd:%d\n",fd4);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
結(jié)果:
通過上述講解,我們知道open系統(tǒng)調(diào)用會(huì)返回文件描述符,那它為什么是從3開始呢??
其實(shí)main函數(shù)會(huì)默認(rèn)打開這三個(gè)標(biāo)準(zhǔn)文件:
這三個(gè)標(biāo)準(zhǔn)文件是:
- 標(biāo)準(zhǔn)輸入(stdin):用于從鍵盤或其他輸入設(shè)備讀取數(shù)據(jù),通常對(duì)應(yīng)文件描述符0。可以使用C語(yǔ)言的scanf、getchar等函數(shù)或者Linux的read系統(tǒng)調(diào)用來讀取標(biāo)準(zhǔn)輸入。
- 標(biāo)準(zhǔn)輸出(stdout):用于向屏幕或其他輸出設(shè)備寫入數(shù)據(jù),通常對(duì)應(yīng)文件描述符1??梢允褂肅語(yǔ)言的printf、putchar等函數(shù)或者Linux的write系統(tǒng)調(diào)用來寫入標(biāo)準(zhǔn)輸出。
- 標(biāo)準(zhǔn)錯(cuò)誤輸出(stderr):用于向屏幕或其他輸出設(shè)備寫入錯(cuò)誤信息,通常對(duì)應(yīng)文件描述符2??梢允褂肅語(yǔ)言的fprintf、perror等函數(shù)或者Linux的write系統(tǒng)調(diào)用來寫入標(biāo)準(zhǔn)錯(cuò)誤輸出。
這三個(gè)標(biāo)準(zhǔn)文件在程序啟動(dòng)時(shí)就被自動(dòng)打開,并且在程序結(jié)束時(shí)被自動(dòng)關(guān)閉,無(wú)需手動(dòng)操作。它們也可以被重定向到其他文件或設(shè)備,例如使用 > 或 < 符號(hào)。
所以為什么open文件操作后返回值 是3? 因?yàn)?0 1 2 已經(jīng)被占用了 ---- 本質(zhì)是數(shù)組下標(biāo)
內(nèi)存中文件描述符,文件描述符表,文件控制塊,進(jìn)程控制塊的關(guān)系如下圖所示,文件描述符表,說白了就是一個(gè)存儲(chǔ)指向文件控制塊的指針的指針數(shù)組,而文件描述符就是這個(gè)指針數(shù)組的索引,進(jìn)程控制塊中會(huì)有一個(gè)指向文件描述符表的指針。通過文件描述符就可以找到對(duì)應(yīng)的被打開的文件。
操作系統(tǒng)通過這些內(nèi)核數(shù)據(jù)結(jié)構(gòu),將被打開的文件和進(jìn)程聯(lián)系起來。
深度理解
文件描述符的實(shí)質(zhì):文件描述符是內(nèi)核為每個(gè)進(jìn)程維護(hù)的一個(gè)打開文件記錄表的索引值
C語(yǔ)言如何訪問系統(tǒng)? 就是通過文件描述符;同樣的C++的cin、cout等類中也必須有文件描述符!沒有文件描述符,怎么通過操作系統(tǒng)訪問(系統(tǒng)調(diào)用)外設(shè)呢! 每個(gè)編程語(yǔ)言都是如此!
通過上述的引出,我們可以知道文件描述符的實(shí)質(zhì)是:
- 文件描述符是一個(gè)非負(fù)整數(shù),用于標(biāo)識(shí)不同的已打開文件。
- 文件描述符是內(nèi)核為了高效管理已打開文件所創(chuàng)建的索引,它可以用來調(diào)用各種I/O系統(tǒng)調(diào)用函數(shù)。
- 文件描述符是進(jìn)程級(jí)別的,每個(gè)進(jìn)程都有自己獨(dú)立的一組文件描述符,并且默認(rèn)有三個(gè)預(yù)定義的描述符:0代表標(biāo)準(zhǔn)輸入,1代表標(biāo)準(zhǔn)輸出,2代表標(biāo)準(zhǔn)錯(cuò)誤輸出。
- 文件描述符可以被復(fù)制、重定向、關(guān)閉等操作,但不能被直接讀寫。要讀寫一個(gè)已打開文件,需要使用read、write等系統(tǒng)調(diào)用函數(shù),并傳入相應(yīng)的文件描述符作為參數(shù)。
文件描述符表和file結(jié)構(gòu)體之間的關(guān)系是:
- 文件描述符表是內(nèi)核用來存儲(chǔ)每個(gè)進(jìn)程的文件描述符和對(duì)應(yīng)的打開文件信息的表格。每個(gè)進(jìn)程在其進(jìn)程控制塊(PCB)中都保存著一份文件描述符表
- file結(jié)構(gòu)體是內(nèi)核用來表示已打開文件的數(shù)據(jù)結(jié)構(gòu),它包含了當(dāng)前讀寫位置、訪問模式、狀態(tài)標(biāo)志等信息,以及指向?qū)?yīng)inode對(duì)象或者i-node表項(xiàng)的指針。file結(jié)構(gòu)體也可以稱為打開文件句柄或者打開文件表項(xiàng)。
- 文件描述符表和file結(jié)構(gòu)體之間通過指針相互連接,一個(gè)文件描述符可以指向一個(gè)或多個(gè)file結(jié)構(gòu)體,一個(gè)file結(jié)構(gòu)體也可以被一個(gè)或多個(gè)文件描述符所指向。這樣可以實(shí)現(xiàn)不同進(jìn)程或同一進(jìn)程中不同文件描述符共享同一個(gè)已打開文件。
文件描述符的分配規(guī)則:系統(tǒng)在創(chuàng)建文件描述符時(shí)會(huì)尋找當(dāng)前未使用的最小下標(biāo)
關(guān)閉012文件描述符產(chǎn)生的現(xiàn)象(新打開文件的fd被賦值為0或1或2)
當(dāng)關(guān)閉0或2時(shí),打印出來的log.txt對(duì)應(yīng)的fd的值就是對(duì)應(yīng)的關(guān)閉的0或2的值,而當(dāng)關(guān)閉1時(shí),顯示器不會(huì)顯示對(duì)應(yīng)的fd的值。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <unistd.h>
6
7 int main()
8 {
9 //close(0);
10 //close(1);
11 //close(2);
12 umask(0000);
13 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//沒有指明文件路徑,默認(rèn)在當(dāng)前路徑下,也就是當(dāng)前進(jìn)程的工作目錄
14 if(fd<0)
15 {
16 perror("open");
17 return 1;
18 }
19
20 printf("open fd:%d\n",fd);
21 close(fd);
22 return 0;
23 }
測(cè)試結(jié)果:
分析:
所以實(shí)際上文件描述符在分配時(shí),會(huì)從文件描述符表中的指針數(shù)組中,從小到大按照順序找最小的且沒有被占用的fd來進(jìn)行分配,自然而然關(guān)閉0時(shí),0對(duì)應(yīng)存儲(chǔ)的地址就會(huì)由stdin改為新打開的文件的地址,所以打印新的文件的fd值時(shí),就會(huì)出現(xiàn)0。
關(guān)閉2也是這個(gè)道理,fd為2對(duì)應(yīng)的存儲(chǔ)的地址會(huì)由stderr改為新打開的文件的地址,所以在打印fd時(shí),也就會(huì)出現(xiàn)2了。
文件描述符的分配規(guī)則是:
- 當(dāng)一個(gè)進(jìn)程打開一個(gè)新的文件時(shí),系統(tǒng)會(huì)在該進(jìn)程的文件描述符表中尋找當(dāng)前未使用的最小下標(biāo),并將其分配給該文件。
- 當(dāng)一個(gè)進(jìn)程關(guān)閉一個(gè)已打開的文件時(shí),系統(tǒng)會(huì)將該文件對(duì)應(yīng)的文件描述符表項(xiàng)置為空,并釋放其占用的資源。
- 當(dāng)一個(gè)進(jìn)程復(fù)制或重定向一個(gè)已打開的文件時(shí),系統(tǒng)會(huì)在該進(jìn)程或目標(biāo)進(jìn)程的文件描述符表中尋找當(dāng)前未使用的最小下標(biāo),并將其指向同一個(gè)file結(jié)構(gòu)體。
下面是一些示例代碼:
- 打開一個(gè)新的文件:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main() {
// 打開或創(chuàng)建一個(gè)新文件
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
// 打開失敗,打印錯(cuò)誤信息并退出
perror("open error");
exit(1);
}
// 打開成功,打印分配到的文檔描述符
printf("open success, fd = %d\n", fd);
// 關(guān)閉文檔
close(fd);
return 0;
}
- 關(guān)閉一個(gè)已打開的文檔:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main() {
// 打開或創(chuàng)建一個(gè)新文檔
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
// 打開失敗,打印錯(cuò)誤信息并退出
perror("open error");
exit(1);
}
// 打開成功,打印分配到的文檔描述符
printf("open success, fd = %d\n", fd);
// 關(guān)閉文檔
int ret = close(fd);
if (ret == -1) {
// 關(guān)閉失敗,打印錯(cuò)誤信息并退出
perror("close error");
exit(1);
}
// 關(guān)閉成功,打印關(guān)閉信息
printf("close success\n");
return 0;
}
文件重定向與dup2系統(tǒng)調(diào)用 (內(nèi)核中更改fd對(duì)應(yīng)的struct file*地址)
重定向命令>和>>的含義和用法
- 重定向命令>和>>都是用來將一個(gè)命令的標(biāo)準(zhǔn)輸出或錯(cuò)誤輸出重定向到一個(gè)文件中,而不是顯示在屏幕上。
- 重定向命令>表示覆蓋模式,即如果目標(biāo)文件已經(jīng)存在,那么原來的內(nèi)容會(huì)被清空,然后寫入新的內(nèi)容。例如:echo “hello” > log.txt 表示將字符串"hello"寫入到log.txt文件中,如果log.txt文件已經(jīng)存在,那么原來的內(nèi)容會(huì)被覆蓋。
- 重定向命令>>表示追加模式,即如果目標(biāo)文件已經(jīng)存在,那么新的內(nèi)容會(huì)被追加到原來的內(nèi)容后面。例如:echo “world” >> log.txt 表示將字符串"world"追加到log.txt文件中,如果log.txt文件已經(jīng)存在,那么原來的內(nèi)容會(huì)保留。
-
重定向命令>和>>可以指定不同的文件描述符來重定向不同類型的輸出。默認(rèn)情況下,如果不指定文件描述符,那么就是1,表示標(biāo)準(zhǔn)輸出。如果要重定向錯(cuò)誤輸出,就要指定2作為文件描述符。例如:ls -l /etc/passwd /etc/abc > log1.txt 2> log2.txt 表示將ls -l 命令的標(biāo)準(zhǔn)輸出重定向到log1.txt文件中,并將錯(cuò)誤輸出重定向到log2.txt文件中。
測(cè)試如下:
我們vim一個(gè)abc文件,可以看到abc文件中的內(nèi)容如下圖所示:
然后我們使用 ls > abc 命令之后可以看到abc中的內(nèi)容已經(jīng)被清空重定向了
:追加重定向,直接在文件的尾部進(jìn)行重定向
我們對(duì)abc文件進(jìn)行追加重定向,可以看到,直接在文件的尾巴進(jìn)行了重定向
重定向原理
簡(jiǎn)單說將 fd_array 數(shù)組當(dāng)中的元素struct file* 指針的指向關(guān)系進(jìn)行修改,改變成為其它的struct file結(jié)構(gòu)體的地址—每個(gè)文件描述符都是一個(gè)內(nèi)核中文件描述信息數(shù)組的下標(biāo),對(duì)應(yīng)有一個(gè)文件的描述信息用于操作文件,而重定向就是在不改變所操作的文件描述符的情況下,通過改變描述符對(duì)應(yīng)的文件描述信息進(jìn)而實(shí)現(xiàn)改變所操作的文件
詳細(xì)說文件操作重定向的原理是通過改變文件描述符對(duì)應(yīng)的文件描述信息,從而實(shí)現(xiàn)改變所操作的文件。文件描述符是一個(gè)整數(shù),表示進(jìn)程和被打開文件的關(guān)系,通常有標(biāo)準(zhǔn)輸入(0)、標(biāo)準(zhǔn)輸出(1)和標(biāo)準(zhǔn)錯(cuò)誤(2)三種。重定向可以分為輸出重定向、追加重定向和輸入重定向三種類型。輸出重定向是將本應(yīng)該打印到顯示器的內(nèi)容輸出到了指定的文件中,例如 ls > list.txt;追加重定向是將本應(yīng)該打印到顯示器的內(nèi)容追加式地輸出到了指定的文件中,例如 ls >> list.txt;輸入重定向是將本應(yīng)該從鍵盤中讀取的內(nèi)容改為從指定的文件中讀取,例如 cat < input.txt。在Linux系統(tǒng)中,可以使用dup2系統(tǒng)調(diào)用來實(shí)現(xiàn)重定向,它可以將一個(gè)文件描述符復(fù)制到另一個(gè)文件描述符,并關(guān)閉后者。
dup2函數(shù)的功能和參數(shù)含義
int dup2(int oldfd, int newfd);
其實(shí)這個(gè)函數(shù)挺繞的,要理解起來需要自己研究一下才行:
通過man手冊(cè)我們可以對(duì)dup2函數(shù)進(jìn)行一些了解:int dup2(int oldfd, int newfd);
函數(shù)功能為將newfd描述符重定向到oldfd描述符,相當(dāng)于重定向完畢后都是操作oldfd所操作的文件
但是在過程中如果newfd本身已經(jīng)有對(duì)應(yīng)打開的文件信息,則會(huì)先關(guān)閉文件后再重定向(否則會(huì)資源泄露)
怎么用?怎么傳參數(shù)?— 拷貝的整數(shù)所表示的內(nèi)容 — 注意最后只有oldfd保留就可以了?。?/mark> 就是oldfd把newfd覆蓋了 — 一般傳參例如:fd 1 (重定向輸出)— 只保留了fd
所以dup2函數(shù)是一個(gè)用于復(fù)制文件描述符的系統(tǒng)調(diào)用,它的功能是將參數(shù)oldfd所指的文件描述符復(fù)制到參數(shù)newfd所指定的數(shù)值,如果newfd已經(jīng)被打開,則先關(guān)閉它,如果newfd等于oldfd,則不做任何操作。dup2函數(shù)返回新的文件描述符,或者在出錯(cuò)時(shí)返回-1。
-
輸出重定向:從原來的輸出到屏幕改為輸出到文件中,這就叫做輸出重定向。
而追加重定向的方式也比較簡(jiǎn)單,只要將文件打開方式中的O_TRUNC替換為O_APPEND即可。
8 int main()
9 {
10 umask(0000);
11 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//輸出重定向
E> 12 int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//追加重定向
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 dup2(fd,1);
20
21 printf("open fd:%d\n",fd);// printf --> stdout
22 fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout
23
24 const char* msg = "hello linux";
25 write(1,msg,strlen(msg));//向顯示器上write
26
27 close(fd);
28 return 0;
29 }
-
輸出重定向:從原來的鍵盤中讀取數(shù)據(jù),改為從文件fd中讀取數(shù)據(jù),這就叫做輸入重定向。
文件log.txt中的內(nèi)容,作為輸入重定向重新輸出到顯示器中,即使fgets獲取的方式是stdin也沒有關(guān)系,因?yàn)槲覀兪褂胐up2將stdin中的地址改為了文件log.txt的地址
8 int main()
9 {
10 umask(0000);
13 int fd = open("log.txt",O_RDONLY);//輸入重定向
14 if(fd<0)
15 {
16 perror("open");
17 return 1;
18 }
19
20 dup2(fd,0);//由鍵盤讀取改為從fd文件中讀取
21 char line[64];
22 while(1)
23 {
24 printf("<");
25 if(fgets(line,sizeof(line),stdin)==NULL) break;
26 printf("%s",line);
27 }
28 }
示例:
void func() {
int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);//打開文件
if (fd < 0) {
return -1;
}
//將標(biāo)準(zhǔn)輸出,重定向到文件,這樣則寫往標(biāo)準(zhǔn)輸出的數(shù)據(jù)會(huì)被寫入到文件中,而不是被打印
dup2(fd, 1);
?//printf內(nèi)部操作的是stdout標(biāo)準(zhǔn)輸出文件流指針,而文件流指針本質(zhì)上內(nèi)部包含的是1號(hào)描述符成員
//printf的打印就是向標(biāo)準(zhǔn)輸出寫入數(shù)據(jù),因?yàn)闃?biāo)準(zhǔn)輸出已經(jīng)被重定向,因此數(shù)據(jù)會(huì)被寫入文件中,而不是直接打印
printf("hello bit");
return 0;
}
Linux下面一切皆文件!
不同的硬件的讀寫方法一定是不一樣的,但在OS看來,一切設(shè)備和文件都是struct file內(nèi)核數(shù)據(jù)結(jié)構(gòu),在管理對(duì)應(yīng)的硬件時(shí),雖然硬件的管理方法不在OS層,而是在驅(qū)動(dòng)層,這也沒有關(guān)系,只需要利用struct file結(jié)構(gòu)體中的函數(shù)指針,調(diào)用對(duì)應(yīng)的硬件的讀寫方法即可。
終究還是封裝的思想!
Linux下一切皆文件是指,Linux系統(tǒng)中的所有資源,無(wú)論是硬件設(shè)備、普通文件、目錄、進(jìn)程、網(wǎng)絡(luò)連接等,都可以被抽象為文件,并且可以使用統(tǒng)一的接口來訪問和操作。
這樣做的好處是,簡(jiǎn)化了開發(fā)者和用戶對(duì)不同資源的處理方式,提高了系統(tǒng)的靈活性和可擴(kuò)展性。文章來源:http://www.zghlxwxcb.cn/news/detail-802681.html
這樣做的不利之處是,需要在文件系統(tǒng)中掛載每個(gè)硬件設(shè)備才能使用它們,而且可能會(huì)造成一些性能損失。文章來源地址http://www.zghlxwxcb.cn/news/detail-802681.html
到了這里,關(guān)于【Linux】基礎(chǔ)IO(一) :文件描述符,文件流指針,重定向的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!