1. 定義通用返回結果類 定義ResultVO類,作返回給前端的對象結構,主要有4個欄位 code : 錯誤碼 data : 內容 message : 消息 description : 具體描述 import lombok.Data; import java.io.Serializable; / ...
文件 IO / 系統調用 IO
註: 李慧芹老師的視頻課程請點這裡, 本篇為系統IO一章的筆記, 課上提到過的內容基本都會包含, 上一章為標準IO
文件描述符(fd
)是在文件IO中貫穿始終的類型
本節內容
-
文件IO操作:
open
,close
,read
,write
,lseek
-
文件IO與標準IO的區別
-
IO的效率問題
-
文件共用問題
-
原子操作
-
程式中的重定向: dup, dup2
-
同步: sync, fsync, fdatasync
-
管家:
fcntl()
,ioctl()
FILE 與 fd
stdio中, 可以調用fopen()
(依賴於sysio的open()
)獲得FILE結構體(結構如下表)指針:
欄位 | 說明 |
---|---|
pos | 文件位置 |
fd | 文件描述符 |
... | ... |
磁碟上的每個文件有唯一的標識inode
, 而每次調用open()
時, 都會產生一個結構體, 該結構體包含了要打開的文件的所有信息(包括inode
)
進程維護了一個數組(大小為1024), 存儲所有通過open()
產生的結構體的首地址
文件描述符fd
表示了某一結構體的首地址在上述數組中的下標位置, 因此, fd實際上就是int類型變數!
fd優先使用當前可用範圍內下標值最小的數組位置
設進程維護的數組為A
, close()
函數就相當於:
free(A[fd]);
A[fd] = NULL;
當發生如下圖所示情況(數組中的兩個指針同時指向同一個結構體)時:
close(4)
並不會導致A[6]
變為野指針, 這是由於結構體中包含引用計數器(counter)欄位, 只有當該欄位變為0時, 該結構體占用的空間才會被釋放
打開與關閉操作
- 打開
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
參數flags
是一個點陣圖, 必須包含一個狀態選項:
模式 | 許可權 |
---|---|
O_RDONLY | 只讀 |
O_WRONLY | 只寫 |
O_RDWR | 讀寫 |
可以包含零或多個創建選項:
模式 | 說明 |
---|---|
O_CREAT | 有則情況, 無則創建 |
O_EXCL | 必須打開一個新文件 |
O_APPEND | 追加 |
O_TRUNC | 截斷 |
O_ASYNC | 信號驅動IO |
O_DIRECT | 最小化cache作用 |
O_DIRECTORY | 必須打開目錄 |
O_LARGEFILE | 打開的是大文件(該方法不如設置_FILE_OFFSET_BITS為64) |
O_NOATIME | 不需要更新文件最後讀的時間(節省文件更新時間) |
O_NOFOLLOW | 如果文件是符號鏈接, 那麼不打開它 |
O_NONBLOCK | 非阻塞 |
O_SYNC | 同步 |
cache vs buffer:
cache代表"讀的緩衝區"
buffer代表"寫的緩衝區"
open()
和creat()
執行成功時返迴文件描述符, 失敗則返回-1
下標為fopen()
的參數mode
與open()
的參數flags
的比對:
mode | flags |
---|---|
r | O_RDONLY |
r+ | O_RDWR |
w | O_WRONLY|O_CREAT|O_TRUNC |
w+ | O_RDWR|O_TRUNC|O_CREAT |
當flags & O_CREAT != 0
時, 則open()
必須傳入mode, 創建的文件的許可權服從:
mode & ~umask
- 關閉
#include <unistd.h>
int close(int fd);
成功返回0, 失敗返回-1; 一般認為close()
不會失敗, 因此極少校驗返回值
讀寫與定位操作
- 讀
#include <unistd.h>
// 嘗試從fd中讀取count個位元組到buf中
// 如果成功, 返回讀到的位元組數, 讀到文件尾, 返回0, 失敗返回-1
ssize_t read(int fd, void *buf, size_t count);
- 寫
// 如果成功, 返回寫入的位元組數(返回0表示未寫入任何內容), 失敗返回-1
// 且會設置errno
ssize_t write(int fd, const void *fd, size_t count);
- 定位
#include <sys/types.h>
#include <unistd.h>
// 從whence位置偏移offset個位元組
// whence選項: SEEK_SET(文件首), SEEK_CUR(當前位置), SEEK_END(文件尾)
off_t lseek(int fd, off_t offset, int whence);
重寫 mycpy
mycpy.c:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main(int argc, char **argv)
{
int sfd, dfd;
char buf[BUFSIZE];
ssize_t rs, ws, pos;
int flag = 1;
if (argc < 3)
{
fprintf(stderr, "Usage: %s <src_file> <dst_file>\n", argv[0]);
exit(1);
}
sfd = open(argv[1], O_RDONLY);
if (sfd < 0)
{
perror("open()");
exit(1);
}
dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (dfd < 0)
{
close(sfd);
perror("open()");
exit(1);
}
while (1)
{
rs = read(sfd, buf, BUFSIZE);
if (rs < 0)
{
perror("read()");
break;
}
if (rs == 0)
break;
pos = 0;
while (rs > 0)
{
ws = write(dfd, buf+pos, rs);
if (ws < 0)
{
perror("write()");
flag = 0;
break;
}
rs -= ws;
pos += ws;
}
if (!flag)
break;
}
close(dfd);
close(sfd);
exit(0);
}
Makefile:
CFLAGS+=-D_FILE_OFFSET_BITS=64 -Wall
執行以下命令:
make mycpy
./mycpy /etc/services ./out
diff /etc/services ./out
如果什麼也沒輸出, 則說明mycpy
已正確執行
系統 IO 與標準 IO 比較
區別:
系統調用IO: 每調用一次, 會從user態切換到kernel態執行一次(實時性好)
標準IO: 數據先寫入緩衝區, 在某一事件(如: 強制刷新/緩衝區滿/換行, 詳見上一章對行緩衝/全緩衝/無緩衝的描述)發生時才會將緩衝區內數據寫入文件/設備(吞吐量大)
提醒:
fileno()
可以拿出FILE *
的fd
欄位
fdopen()
可以將fd
封裝到FILE *
中但是, 絕不能將標準IO與系統調用IO混用!
絕大多數情況下,
FILE
結構體中的pos
欄位與存儲文件所有信息的結構體的pos
欄位值不相等! 如:FILE *fp; fputc(fp) // pos ++ fputc(fp) // pos ++
只代表
FILE
中的pos
加二, 文件結構體的pos
沒有增加, 該pos
只會在各種事件後發生改變; 因此, 標準IO與系統調用IO混用基本就會導致錯誤, 如ab.c
:#include <stdlib.h> #include <stdio.h> #include <unistd.h> int main() { putchar('a'); write(1, "b", 1); putchar('a'); write(1, "b", 1); putchar('a'); write(1, "b", 1); exit(0); }
該程式會列印"bbbaaa", 可以用
strace
命令跟蹤系統調用IO的發生:strace ./ab
該命令輸出的最後幾行表示系統調用IO發生的過程:
write(1, "b", 1b) = 1 write(1, "b", 1b) = 1 write(1, "b", 1b) = 1 write(1, "aaa", 3aaa) = 3 exit_group(0) = ? +++ exited with 0 +++
IO 效率問題
在重寫mycpy的案例中, BUFSIZE
為$2^n$, 問n為多少時, 效率最高
程式:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>
long long BUFSIZE;
int main(int argc, char **argv)
{
int sfd, dfd;
char *buf;
ssize_t rs, ws, pos;
int flag = 1, n = 0;
if (argc < 4)
{
fprintf(stderr, "Usage: %s <src_file> <dst_file> <n>\n", argv[0]);
exit(1);
}
n = atoi(argv[3]);
if (n <= 0)
exit(1);
BUFSIZE = 1LL << (n-1);
buf = malloc(BUFSIZE * sizeof(char));
if (buf == NULL)
{
perror("malloc()");
exit(1);
}
sfd = open(argv[1], O_RDONLY);
if (sfd < 0)
{
perror("open()");
exit(1);
}
while (1)
{
rs = read(sfd, buf, BUFSIZE);
if (rs < 0)
{
perror("read()");
break;
}
if (rs == 0)
break;
pos = 0;
while (rs > 0)
{
ws = write(dfd, buf+pos, rs);
if (ws < 0)
{
perror("write()");
flag = 0;
break;
}
rs -= ws;
pos += ws;
}
if (!flag)
break;
}
close(dfd);
close(sfd);
exit(0);
}
測試該程式的腳本:
#!/bin/bash
for((i=1;i<=25;i++))
do
echo $i;
time ./mycpy ~/dance.mp4 ./dance.mp4 $i;
diff ~/dance.mp4 ./dance.mp4;
rm -f ./dance.mp4;
done
運行結果:
經過測試(測試環境: 操作系統: Ubuntu22 CPU: 64位ARM架構 記憶體: 2G), BUFSIZE在64~256k大小時, 效率達到最高, 預設情況下, 16M的BUFFSIZE不會引發段錯誤
文件截斷
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
將一個文件截斷到length
長度
作業
不打開臨時文件的情況下, 刪除文件的某一行:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include "mygetline.h"
int main(int argc, char **argv)
{
char *linebuf = NULL;
size_t bufsize = 0;
FILE *wfp, *rfp;
int curline = 1, l;
if (argc < 3)
{
fprintf(stderr, "Usage: %s <file name> <line number>\n", argv[0]);
exit(1);
}
rfp = fopen(argv[1], "r");
if (rfp == NULL)
{
perror("open file");
exit(1);
}
wfp = fopen(argv[1], "r+");
if (wfp == NULL)
{
perror("open file");
fclose(rfp);
exit(1);
}
l = atoi(argv[2]);
if (l <= 0)
{
fprintf(stderr, "illegal line number %s: %s", argv[2], strerror(errno));
fclose(wfp);
fclose(rfp);
exit(1);
}
while (mygetline(&linebuf, &bufsize, rfp) >= 0)
{
if (curline != l)
{
fputs(linebuf, wfp);
fputc((int)'\n', wfp);
}
curline ++;
}
truncate(argv[1], ftell(wfp));
mygetline_free(&linebuf);
fclose(wfp);
fclose(rfp);
exit(0);
}
要瞭解mygetline()
和mygetline_free()
, 請查看上一節內容
原子操作
原子操作: 不可分割的操作
原子操作的作用: 解決競爭和衝突
dup
舉例說明: 下麵有代碼dup.c
, 要在// 代碼:
一行後, 多行註釋前編寫一些代碼, 使得hello!
不被列印到終端上, 而是列印到/tmp/out
文件中:
#include <stdlib.h>
#include <stdio.h>
#define FNAME "/tmp/out"
int main()
{
// 代碼:
/***********************/
puts("hello!");
exit(0);
}
可以做如下修改:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FNAME "/tmp/out"
int main()
{
// 代碼:
int fd;
close(1); // 關閉 stdout
// 打開/tmp/out, 使其占用進程維護的stream數組的下標1的位置
// 該位置原先由stdout占用
fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (fd < 0)
{
perror("open()");
exit(1);
}
/***********************/
puts("hello!");
exit(0);
}
執行以下命令:
make dup
./dup
cat /tmp/out
引入dup()
:
#include <unistd.h>
// 將oldfd複製到stream數組下標最小的可用位置上
int dup(int oldfd);
有了dup()
後, 可以把上述代碼修改為:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FNAME "/tmp/out"
int main()
{
// 代碼:
int fd;
// 打開/tmp/out, 使其占用進程維護的stream數組的下標1的位置
// 該位置原先由stdout占用
fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (fd < 0)
{
perror("open()");
exit(1);
}
close(1); // 關閉stdout
dup(fd); // 將fd複製到1號
// 當前stream數組下標4,1位置的指針指向同一個文件結構體
/***********************/
puts("hello!");
exit(0);
}
然而, 在多線程場景中, 當前線程可能在執行close(1)
後, CPU時間片結束, 其他線程打開的文件描述符會占據1下標位置(操作不原子)
dup2
為瞭解決dup()
操作不原子的問題, 有了dup2()
:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2()
會將newfd
複製到oldfd
的位置上, 如果oldfd
已被占用, 則首先關閉oldfd
; 如果newfd == oldfd
, 那麼dup2()
什麼也不做, 直接返回newfd
因此, close(1); dup(fd);
可被重寫為:
dup2(fd, 1);
if (fd != 1)
close(fd);
sync
將buffer和cache同步到磁碟上:
#include <unistd.h>
void sync(void);
在解除設備掛載時, 將還沒寫入磁碟的數據儘快寫入磁碟
可以使用fsync
或fdatasync
指定寫入數據的位置:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
fcntl
#include <unistd.h>
#include <fcntl.h>
// 對fd執行cmd命令
int fcntl(int fd, int cmd, .../* arg */);
具體有哪些命令詳見man fcntl
, 文件描述符所變的魔術基本都來源於該函數(比如: dup()
和dup2()
就是封裝好的fcntl
)
ioctl
設備相關的內容
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
/dev/fd
虛目錄, 顯示的是當前進程(如同照鏡子, 誰去查看/dev/fd
, 就會看到誰的文件描述符的信息)的文件描述符信息, 如:
ls -l /dev/fd
該命令會輸出ls
命令實現所用到的文件描述符的信息:
lrwxrwxrwx 1 root root 13 Sep 14 08:27 /dev/fd -> /proc/self/fd