最全的李慧芹APUE-文件IO笔记 文件 IO / 系统调用 IO
文件 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