LinuxC——1.文件讀寫
1.❤️文件IO
從CPU到文件是Output的一個過程,從文件到CPU是一個Input的過程,這個過程是以CPU為點的
2.🧡系統函數
- open:打開文件
- close:關閉文件
- read:讀數據
- write:寫數據
- lseek:移動文件中讀寫位置
- dup:文件書寫位置重定位函數,重定位可以寫入另一個文件
- fcntl:文件描述符設置
- ioctl:一個特殊函數
3.💛文件讀寫的簡單例子
- open函數:通過fd,找到塊設備文件
- 文件系統是一個程序代碼,組織塊設備所有文件
- 文件系統屬於OS一部分
- 找到文件后,調用塊設備驅動,打開文件
- 打開成功,返回非負操作符
- 打開失敗,返回-1
- write函數:利用打開成功返回的,向文件里面寫數據
- lseek函數:利用文件描述符,將文件讀寫位置調整到文件相應位置
- why設置文件頭
- write的時候,文件讀寫位置已經到了末尾
- read函數:從文件頭開始,讀取指定長度的數據到buf中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = 0;
fd = open("./file.txt", O_RDWR);
if(-1 == fd) {
printf("open fail\n");
return 1;
}else {
printf("open ok\n");
}
char buf[] = "Steve Yu, today is nice";
write(fd, (void *)buf, strlen(buf));
lseek(fd, SEEK_SET, 0);
char buf2[30] = {0};
read(fd, buf2, sizeof(buf));
puts(buf2);
close(fd);
return 0;
}
4.💚文件打開后,OS做了什么?
-
記錄打開文件信息
- 程序運行后是一個進程,OS會創建一個task_struct的結構體,記錄運行的各種信息
- open將文件打開后,在task_struct會創建一些數據結構,用來創建當前進程的打開文件的信息,后面所有的都依賴這些信息
-
open函數會申請一段內存空間(內核緩存)
- 內核緩存是在OS的一個空間,比如char buf[100],就開辟了緩存
- 由os開辟的叫內核緩存
-
open為啥要開辟內存緩存空間?
內存讀寫 > 磁盤讀寫,我們會省時間
-
open僅僅開辟了內存緩存空間,並沒有數據,只有讀寫數據的時候
先將數據讀到內核緩存中,之后下層會讓內存緩存和磁盤數據進行交換
-
讀寫的時候,數據的流動?
硬件 -> 驅動緩存 -> 內核緩存 -> 應用程序中的數組
(僅掌握open函數,其余函數已經過時)
5.💙指定宏
- O_RDONLY:只讀
- O_WRONLY:只寫
- O_RDWR:可讀可寫
以上三個不可以用 | 進行宏運算,而可以與以下進行宏運算
- O_TRUNC:打開進行清空
- O_APEND:打開后寫數據追加
- O_NONBLOCK和O_AYNC:非阻塞,同步
flag參數之O_CREAT、O_EXCL
- O_CREAT:新建一個文件
- O_EXCL:O_EXCL和O_CREAT同時被指定,打開文件,如果文件存在,就報錯
可以使用O_RDWR|O_CREAT進行組合,那么可以不存在進行創建,存在不打開的功能
6.💜文件描述符
- 文件描述符是什么?
文件描述符指向打開的文件,后面read/write/close等操作,都是基於文件描述符進行操作
- 文件描述符池
每個程序運行起來后,就是一個進程,系統給每個進程分配01023的描述符的范圍,返回的文件描述符,是在01023的某個數字
- 為啥第一個打開的文件,open返回的是3?
open返回的文件描述符是規則的:
-
open返回文件描述符池中,返回當前沒用的最小的一個
-
進程運行起來,0/1/2會默認使用,最小沒用的是3
- fopen文件指針
fopen是C庫標准io函數
fopen成功后,FILE*的文件指針,打開了文件
fopen成功后,返回的是FILE*文件指針,指向打開的文件
- 對於Linux C庫,fopen對open進行二次封裝
7.❤️errno和perror
Linux中,如果像上述中,打開失敗,那么就直接失敗,遇到比較難排查的錯誤原因,難以查處具體錯誤
- 什么是errno?
errno我們查看man 3 errno中,可以看到,一系列錯誤宏定義,我們include "errno.h"即可
printf("%d: open error", errno);
這樣即可發現17:open error
,打開錯誤
2.具體錯誤
我們僅僅知道錯誤號,不知道具體錯誤,怎么辦呢?
-
perror函數:我們通過man 3 perror,可以查看到錯誤號的具體使用
perror可以打印的錯誤號以及具體錯誤
perror("open fail");
我們可以知道,控制台打印:open fail: File exists
3.使用man 2 open
進行查看ERRORS下錯誤號
比如EACCES,不允許訪問
我們因為記不住錯誤號,也記不住宏定義,所以,我們有了perror
8.🧡close、write、read
- close函數
close(fd):不主動關閉,應用也會自動關閉fd,但是我們得進行手動關閉
- close函數做了啥
-
open打開的時候會在task struct中創建結構體空間,如果文件關閉,那么該結構體空間被釋放
類似free(空間地址)
-
malloc和free是給C調用的庫看書,Linux在釋放的時候,有自己的釋放函數
好習慣必須關閉,否則我們在生產環境,容易內存溢出
9.💛task struct結構體
-
結構體在存在在運行的進程中,在task struct是程序在運行的時候進行開辟
-
進程運行結束,linux系統會調用自己的系統函數,進行釋放task struct
10.💚文件描述符
有關0/1/2文件描述符
- stdin,文件描述符為0,0代表鍵盤,實現鍵盤輸入
鍵盤->鍵盤驅動緩存->內核緩存->應用緩存
#include <unistd.h>
#include <stdio.h>
int main() {
int ret = 0;
char buf[30] = {0};
ret = read(0, (void *)buf, 30);
if(-1 == ret) {
perror("read fail");
return -1;
}
puts(buf);
return 0;
}
scanf底層調用read(0, buf, size)這個,這樣就能兼容不同操作系統
- close(0),scanf還能工作不?
答案是不可以,用perror打印,會得到bad description的
- stdout,文件描述符為1,使用1,則可以進行顯示器顯示
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd = 1, ret = 0;
char buf[] = "steve\n";
write(fd, buf, sizeof(buf));
close(fd);
return 0;
}
假設我們write(1, &a, sizeof(a))
, a是一個int值,那么會打印一個(char)a進行
- close(1)
printf不能工作
- stderr, 文件描述符是2,是標准出錯輸出
使用2文件描述符也能進行打印
1打印普通信息,2打印報錯信息
- close(2)
perror是通過2進行打印,如果close(2),則不能進行perror
- 宏定義
0:STDIN_FILENO
1:STDOUT_FILENO
2:STDERR_FILENO
11.💙lseek
lseek用來調整讀寫的位置,調用成功返回偏移量,調用失敗返回-1
我們可以通過lseek獲取文件大小
1.fd:打開文件
2.whence:粗定位
SEEK_SET:起始位置
SEEK_CUR:當前讀寫位置
SEEK_END:文件末尾位置
3.offset:微調
進行偏移,正數向前移動,負數向后
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd = 0, ret = 0;
fd = open("./file.txt", O_RDONLY);
if(-1 == fd) {
perror("file open failed");
exit(-1);
}
ret = lseek(fd, 0, SEEK_END);
printf("文件大小%d\n", ret);
return 0;
}
4.使用od -c filename,可以查看以字符進行顯示字符
12.💜進程表和文件描述表
進程表:task_struct
- 這個結構體成員多達300個
- 每個進程運行起來后,linux系統會開辟task_struct 結構體
- task_struct專門用於進程運行中,涉及進程相關信息
13.❤️共享操作文件
- 同一個進程共享相同文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
void print_error(char* str) {
perror(str);
exit(-1);
}
int main() {
int fd1, fd2;
fd1 = open("./file.txt", O_RDWR);
if(-1 == fd1) print_error("open fail");
fd2 = open("./file.txt", O_RDWR);
if(-1 == fd2) print_error("open fail");
// 寫入操作
return 0;
}
存在相互覆蓋的關系
解決方案:使用O_APPEND進行追加
- 多個進程共享相同文件
gcc share_op_file.c -o a
gcc share_op_file.c -o b
./a
./b
那么會出現覆蓋情況
解決方案:也使用O_APPEND進行追加
14.🧡dup和dup2
- int dup(int fd)
-
在unistd.h中
-
dup用來復制文件描述符,得到一個新的文件描述符,指向原來的文件,指向最小沒用的那一個
fd2 = dup(fd1)
- int dup2(int oldfd, int newfd)
- 在unistd.h中
- dup2指定一個文件描述符,用來復制文件描述符,得到一個新的文件描述符,如果已經打開,則關閉后再次打開
fd2 = (fd1, 4)
- dup、dup2的意義
實現文件共享操作,不使用APPEND也不會出現文件覆蓋。使用dup或dup2的時候,永遠只有一個文件表。
15.💛實現文件重定位
步驟:
-
打開文件fd
-
close(1)
-
進行復制fd到1
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int fd1;
fd1 = open("data.txt", O_RDWR|O_CREAT);
close(1);
dup(fd1);
printf("Hello World");
return 0;
}
當我們文件描述符寫死了,那么可以進行文件重定位來進行,linux底層的重定位>是使用dup/dup2作為底層的支持
16.fcntl函數
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl是文件控制函數(file control)
-
fd:指向打開文件
-
cmd:控制命令
- F_DUPFD
- 模擬DUP
- F_GETFL、F_SETFL
- 獲取設置文件狀態標注,比如open沒有指定O_APPEND,可以使用fcntl進行補設
- F_GETFD、F_SETFD(文件描述符)
- F_GETOWN、F_SETOWN(OWN)
- F_GETLK、F_SETLK、F_SETLKW(加鎖)
先掌握前兩個
F_DUPFD
和F_GETFL、F_SETFL
- F_DUPFD
模擬DUP:
fcntl(fd, F_DUPFD, 0);// 第三個參數沒有,用0表示
模擬DUP2:
fcntl(fd, F_DUPFD, 1);// 模擬DUP2,進行用1
設置
fcntl(fd, F_SETFL, O_RDWR|O_APPEND); // 修改打開時的Flag,返回新設置的標志
保留原標志,加新標志
flag = fcntl(fd, F_GETFL); // 獲取
fcntl(fd, F_SETFL, flag|O_APPEND); // 疊加