最全的李慧芹APUE-文件IO笔记 文件 IO / 系统调用 IO

文件 IO / 系统调用 IO

: 李慧芹老师的视频课程请点这里, 本篇为系统IO一章的笔记, 课上提到过的内容基本都会包含, 上一章为标准IO

文件描述符(fd)是在文件IO中贯穿始终的类型

本节内容

  1. 文件IO操作: open, close, read, write, lseek

  2. 文件IO与标准IO的区别

  3. IO的效率问题

  4. 文件共享问题

  5. 原子操作

  6. 程序中的重定向: dup, dup2

  7. 同步: sync, fsync, fdatasync

  8. 管家: 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()的参数modeopen()的参数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);

在解除设备挂载时, 将还没写入磁盘的数据尽快写入磁盘

可以使用fsyncfdatasync指定写入数据的位置:

#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

热门相关:如果能少爱你一点   楚氏赘婿   我成了暴戾帝君的小娇包   重生之女将星   异能特工:军火皇后