文件和I/O

所有(Unix)文件的通用属性

  • 所有文件:

    • 位于文件系统命名空间中(在根目录下,或 / )。没有驱动器号!

    • 有个名字

    • 实现读、写、打开、关闭和选择系统调用。

  • 所有内容都可以包含在Normal或 special 文件夹

  • 所有人都有一个概念:

    • 拥有用户和组

  • 拥有所有权的用户/组和其他用户/组的读/写/执行位

  • 自定义扩展属性列表

  • 创建日期/时间

  • 上次访问日期/时间

  • 除了这几件事之外,各种文件类型的语义和结构也有很大程度的变化

Unix中的文件类型

  • 常规文件

  • 符号链接

  • 文件夹

  • 阻止设备文件

  • 字符设备文件

  • 命名管道/FIFO

  • Unix域套接字

  • 门(仅限Solaris)

常规文件

  • 持久保存程序中的数据。驻留在文件系统中。

  • 除了所有者/权限之外。常规文件有:

    • 提交的大小和定义的大小(对于支持稀疏文件的文件系统,大小有所不同)

    • 可以按顺序访问

    • 可按随机顺序访问

  • 设备限制也有例外,例如磁带驱动器的退出

文件夹

  • 在早期的Unix实现中,文件夹是列出其他文件的文件,并设置了一个特殊的位以使其成为文件夹。

  • 通过读取和写入文件修改了文件夹。

  • 其中一些语义仍然存在

  • 早期操作系统不支持文件夹:

    • Macintosh文件系统(大约1984年)

    • CP/M文件系统(MS-DOS和FAT的前身)

  • 文件夹没有文件大小

  • 文件夹的执行位确定:

    • 如果可以列出文件夹的内容

    • 如果程序可能发生更改,请将其用作其工作文件夹

数据块设备文件

  • 数据块设备文件是操作系统公开的设备的文件抽象。

  • 常见的设备块文件包括:

    • 硬盘

    • CD/DVD/蓝光驱动器

    • 软盘驱动器

    • USB介质

    • 映射的内存设备(RAM磁盘或诊断设备)

  • 数据块设备支持:

    • 随机访问

    • 缓冲读/写(通过某些特征块大小)

    • 数据块设备文件要么由操作系统通过特殊文件系统自动公开,要么由用户通过特殊系统程序和系统调用创建。方法各不相同。

    • 早期的Linux依赖于特殊的程序

    • 现代的Linux使用特殊的文件系统(devf、sysfs)

字符设备文件

  • 字符设备文件是操作系统公开的设备的文件抽象。

  • 常见的字符设备包括:

    • 航站楼

    • 串口

    • 调制解调器

    • 网卡

    • 视频/声音设备

    • 磁带机

  • 大多数字符设备不支持随机访问。

  • 这样做的人通常查找操作的成本很高

命名管道/FIFO

  • 命名管道是文件系统中存在的管道。

  • 允许在具有不同生存期的程序集(如客户端服务器程序)中进行管道操作。

  • 在讨论进程间通信时,我们将深入讨论管道的更多细节。

Unix域套接字

  • 域套接字是在文件系统中具有名称的套接字。

  • 与命名管道类似,不同之处在于它们可以在流或数据报模式下创建

  • 与常规套接字不同,域套接字没有底层的TCP/IP或UDP/IP协议

文件系统系统调用

  • Unix操作系统中的大多数系统调用都是为了对文件进行操作

  • 首字母缩写MS-DOS扩展为Microsoft磁盘操作系统。这个首字母缩写中的DOS部分似乎非常适用于所有操作系统。

文件系统系统调用

文件系统调用

功能

描述

open()

打开/创建文件并返回文件描述符

creat()

创建新文件

close()

关闭文件描述符(减少对文件的引用)

lseek()

更新文件描述符的当前文件偏移量

read()

将数据从文件描述符读入缓冲区

write()

将数据从缓冲区写入文件描述符

dup()

复制一个文件描述符

dup2()

更新文件描述符以指向另一个文件描述符

fcntl()

更改文件属性(异步I/O、文件锁定)

ioctl()

与设备文件交互、设置非典型属性等的“全部捕获”界面...

stat()

返回rwx位、大小、时间戳和其他详细信息

access()

测试文件的读、写、执行或是否存在

umask()

更新文件创建掩码

chmod()

更新rwx位

更多文件系统系统调用

文件系统调用

chown()

更改文件用户/组所有权

truncate()

更改文件的长度(增大或缩小)

link()

创建硬链接

unlink()

删除文件系统中的名称,并可能删除它所引用的文件(没有进程打开该文件)

rmdir()

删除空目录

remove()

将取消链接/rmdir合并为一个调用

rename()

重命名文件,可能会更改其父文件夹

symlink()

创建符号链接

readlink()

读取符号链接的值

utime()

更新访问和修改时间

mkdir()

创建文件夹

opendir()

打开一个文件夹以供阅读

readdir()

读取文件夹中的下一个条目

rewinddir()

将目录条目重置为开头

closedir()

关闭目录描述符

chdir()

更改当前工作目录

getcwd()

获取当前工作目录

sync()

将文件系统的缓冲区缓存刷新到磁盘

使用OPEN()打开文件

int open(const char *pathname, int flags, mode_t mode)
int open(const char *pathname, int flags)
  • pathname 是文件的路径

  • flags 可以是以下各项的组合:

    • O_APPEND :在附加模式下打开

    • O_ASYNC :使用信号驱动的异步I/O

    • O_CREAT :如果文件不存在,则创建该文件

    • O_DIRECT :最大限度地减少缓冲区缓存的使用

    • O_SYNC :为同步I/O数据块打开,直到将写入调用提交到硬件

    • O_TRUNC :如果文件已存在,则将其截断为长度0

    • 还有其他许多人..。

  • mode 用于 O_CREAT 并且通常作为八进制数传递:

    • 0XYZX 是针对用户的, Y 是为了团队, Z 是给别人的

    • 每个数字是一个八进制数字,由三位组成

    • 最重要的位是读取权限

    • 下一个最高有效位是写入权限

    • 最低有效位是执行权限

    • 0700 表示用户具有rwx,组和其他用户没有访问权限

    • 0660 表示用户/组有读写权限,其他用户没有访问权限

  • 的返回值 open() 是文件描述符,如果发生错误,则为-1

使用Close()关闭文件

int close(int fd)
  • fd 参数是由调用返回的文件描述符:Open、DUP、PIPE等...

  • 成功时返回值为0,失败时返回值为-1(错误的文件描述符,被信号中断)

正在写入文件

ssize_t write(int fd, const void *buf, size_t count);
  • Fd是打开的文件描述符

  • 但它是一个缓冲区

  • Count是该缓冲区中要在当前偏移量处写入文件的字节数

  • 该方法的返回值将为

    • return == -1 如果遇到错误

    • return == count 在大多数成功的案例中

    • return < count 在某些实现中(某些情况下为网络文件系统)

典型写入算法

const char *data = "foobar";
int fd = open("file", O_CREAT | O_TRUNC | O_RDWR, 0666);
size_t length = strlen(data), offset = 0;
while(length > 0) {
   size_t written = write(fd, data + offset, length);
   offset += written;
   length -- written;
}
close(fd)

从文件中读取

size_t read(int fd, void *buf, size_t count);
  • 将文件描述符、目标缓冲区和要读入该缓冲区的字节数作为参数

  • 该方法的返回值为:

    • return == -1 如果发生错误

    • return == 0 如果遇到EOF

    • return == count 在大多数成功案例中

典型的读取算法

int fd = open("file", O_RDONLY, 0666);
char buffer[5];
while((length = read(fd, &buffer[0], 5)) != 0) {
    write(1, &buffer[0], length);
}
close(fd);

在文件中查找

  • 并非所有文件都支持查找。

  • 使用寻道调用是执行随机访问I/O的方式

  • Seek调用的使用会影响性能(稍后将详细介绍...)

  • off_t lseek(int fd, off_t offset, int whence)

    • Fd是一个文件描述符

    • 偏移量是相对于其来源的字节数

    • 从哪里来的是 SEEK_SET (文件开头), SEEK_CUR (文件描述符的当前位置),或 SEEK_END (文件末尾)

    • 这个 off_t 类型通常是64位有符号整数。可以在文件内部和外部进行查找。

  • 在文件外部查找将导致将值0从文件末尾写入到查找位置。

  • 支持稀疏文件的文件系统将对此进行优化,以防止不必要的写入操作。

标准文件描述符

标准

标准输入。默认为来自控制台的输入管道;默认为0

标准输出

标准输出。缺省为控制台的输出管道;缺省值为1

标准

标准误差。默认为控制台的输出管道;默认为2

默认情况下,每个程序都是在这三个文件描述符打开的情况下进行初始化的。它们的特定目标可能已被父程序重定向(稍后将详细介绍...)

复制文件描述符

int dup(int fd) : duplicate a file descriptor
  • 接受文件描述符并返回具有新ID的副本

  • 复制的文件描述符具有独立的文件偏移量和对该文件的引用

  • 复制文件描述符的原因:

    • 用于多线程,以避免调用lSeek()

    • 重定向标准输入/标准输出/标准错误所需的一次调用

重定向文件描述符

int dup2(int oldfd, int newfd) : redirect a file descriptor
  • vbl.使 newfd 是…的复制品 oldfd

  • 如果 newfd 处于打开状态时,它将自动关闭

  • 此呼叫不同于 dup() 因为在这种情况下两个文件描述符共享相同的文件偏移量。

  • 所以,打电话给 lseek() 会导致另一个的偏移量发生变化。

  • dup()dup2() 用于重定向 stdinstdout ,以及 stderr 在命令行上(有时将它们组合在一起)

重定向文件描述符代码示例

int main(int argc, char* argv[]) {
    int pipes[2];
    pipe(pipes);
    int input = pipes[0], output = pipes[1];
    int pid = fork();
    if(pid > 0) {            //parent process
        dup2(input, 0)   //redirect stdin
        close(output);    //close unused half of pipe
        scanf("%d\n", &value);
        printf("child sent value = %d\n", value);
    } else if(pid == 0) {  //child process
        dup2(output, 1); //redirect STDOUT
        close(input);        //close unused half of pipe
        printf("%d\n", 5000);
    }
    return 0;
}

正在读取文件夹

int main(int argc, char* argv[]) {
    const char *dir = "/";
    DIR *d = opendir(dir);

    struct dirent *de;
    while((de = readdir(d)) != NULL) {
        printf("name %s\n", de->d_name);
    }
    closedir(d);
    return 0;
}

展望未来:I/O性能

性能

  • 要获得良好的I/O性能,需要选择正确的缓冲策略。

  • 使用小缓冲区的读/写将导致较低的吞吐量。

  • 使用大缓冲区进行读/写将导致等待读/写返回的时间更长。

  • 这段时间可以用来处理数据。

  • 必须达到平衡。

  • 生产者/消费者模式具有优势:

    • 一个进程/线程读取文件(生产者)

    • 另一个进程/线程运行计算(使用者)

    • 这样,您可以在计算和执行I/O的同时考虑内存映射的I/O-(稍后我们讨论IPC时会有更多内容)

简单的I/O性能实验

dd if=/dev/zero of=tmp.dat bs=1 count=1000000 - 671 kB/s
dd if=/dev/zero of=tmp.dat bs=10 count=100000 - 5.9 MB/s
dd if=/dev/zero of=tmp.dat bs=100 count=10000 - 38.9 MB/s
dd if=/dev/zero of=tmp.dat bs=1000 count=1000 - 244 MB/s
dd if=/dev/zero of=tmp.dat bs=10000 count=100 - 537 MB/s
dd if=/dev/zero of=tmp.dat bs=100000 count=10 - 834 MB/s
dd if=/dev/zero of=tmp.dat bs=1000000 count=1 - 461 MB/s

一般而言:

  • 增加数据块大小可提高性能。

  • 这是一个单一的运行 dd 对于每个块大小。多次运行可能会导致更高的平均吞吐量。

  • 任何给定时间的系统负载都会影响观察到的性能数字。

阅读/写作成绩

  • 另一种需要考虑的方法是向量化I/O。聚集-分散

  • 程序通常会将读/写分离到不同的调用中。

  • 一个例子是这样一个程序,它在两个单独的调用中写入头,然后写出内容。

  • 其他呼叫涉及额外的上下文切换和降低的性能。

  • 矢量化I/O允许组合多个读/写调用。

  • 智能操作系统的实施还将允许对它们进行无序读/写。

  • 这可以带来显著的性能提升。

  • 当我们更深入地研究存储主题时,我们将在学习电梯算法时看到更多关于这方面的信息。

性能示例

char *file_data1 = "1234567890";
char *file_data2 = "abcdefghijk";
char *file_data3 = "lmnopqrstuvwxyz";
const char *file_name = "temp.dat";
int main(int argc, char* argv[]) {

        int fd = open(file_name, O_CREAT|O_TRUNC|O_RDWR, 0666);
        if(fd == (-1)) {
                printf("open returned (-1)\n");
                return (-1);
        }

        struct iovec buffers[3];
        buffers[0].iov_base = file_data1;
        buffers[0].iov_len = strlen(file_data1);
        buffers[1].iov_base = file_data2;
        buffers[1].iov_len = strlen(file_data2);
        buffers[2].iov_base = file_data3;
        buffers[2].iov_len = strlen(file_data3);

        int written = writev(fd, buffers, 3);
        if(written == (-1)) {
                printf("writev returned (-1)\n");
                return (-1);
        }
        printf("wrote %d bytes\n", written);

        close(fd);
        return 0;
}