第三章IO

不带缓存I/O

I/O函数

​ open,read,write,lseek,close。这些函数执行的是否都需要操作系统内核的执行和介入,来完成其功能。这些函数都是系统调用。与之对应的带缓存I/O, fopen, fclose, fread, fwrite。这些函数带有缓存。带有缓存的与内核交互依赖对应的不带缓存的I/O。

缓冲I/O:使用缓冲区,减少系统调用频率,提高效率,但可能会导致数据延迟写入。

缓存策略:

- 全缓冲:适用于文件操作,提高I/O效率。
- 行缓冲:适用于交互式输入输出,确保行数据及时写入。
- 无缓冲:适用于需要即时写入的情况,确保每次写操作立即生效。

如何设置缓冲区?

在调用open的时候,其实会给文件流分配一个缓冲取。在close时,自动刷新空间缓冲区,释放缓冲区。

setvbuf函数:

​ 函数原型:int setvbuf(FILE *stream, char *buffer, int mode, size_t size);

​ 函数参数:

	- 文件指针。
	- 缓冲区指针。如果为NULL,则使用系统默认的缓冲区。
	- 缓冲模式,取值可以是 `_IOFBF`(全缓冲)、`_IOLBF`(行缓冲)、`_IONBF`(无缓冲)。
	- 缓冲区大小。
  • 全缓冲:

    #include <stdio.h>
    
    int main() {
        FILE *file = fopen("example.txt", "w");
        if (file == NULL) {
            perror("Failed to open file");
            return 1;
        }
    
        char buffer[1024];
        setvbuf(file, buffer, _IOFBF, sizeof(buffer)); // 设置全缓冲
    
        fputs("Hello, World!", file); // 数据先写入到缓冲区
    
        fflush(file); // 显式刷新缓冲区,数据写入文件
    
        fclose(file); // 关闭文件时,缓冲区中的数据也会写入文件
    
        return 0;
    }
    
  • 行缓冲:

    #include <stdio.h>
    
    int main() {
        FILE *file = fopen("example.txt", "w");
        if (file == NULL) {
            perror("Failed to open file");
            return 1;
        }
    
        char buffer[1024];
    	setvbuf(file, buffer, _IOLBF, sizeof(buffer)); // 设置行缓冲
    	fputs("Hello, World!\n", file); // 输出换行符时,数据会写入文件
        fclose(file); // 关闭文件时,缓冲区中的数据也会写入文件
    
        return 0;
    }
    
  • 无缓冲:

    #include <stdio.h>
    
    int main() {
        FILE *file = fopen("example.txt", "w");
        if (file == NULL) {
            perror("Failed to open file");
            return 1;
        }
    
        FILE *file = fopen("example.txt", "w");
    	setvbuf(file, NULL, _IONBF, 0); // 设置无缓冲
    	fputs("Hello, World!", file); // 数据会立即写入文件
        fclose(file); // 关闭文件时,缓冲区中的数据也会写入文件
    
        return 0;
    }
    

不带缓冲I/O:直接系统调用,数据即时写入或读取,系统调用频繁。

文件描述符

通过open打开一个文件,内核就会给进程返回一个文件描述符。

0,1,2分别表示,标准输入,标准输出,标准出错。

STDIN_FILENO, STDOUT_FILENO,STDERR_FILENO。

举例:将xxxcmd命令运行的 标准出错重定向到标准输出中。将合并后的内容通过管道输入到tee进程所在的标准输入命令中,tee从标准输入读取文件信息,将其打印到终端以及存储到output.txt文件中。

xxxcmd 2>&1 | tee output.txt

open函数

**函数原型:**int open(const char pathname, int flags, … / mode_t mode */ );

**mode:**是可选。执行文件权限。

flags是一下多个参数或运算构成:

三个必选项:

O_RDONLY:以只读模式打开文件。

O_WRONLY:以只写模式打开文件。

O_RDWR:以读写模式打开文件。

可选项:

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

O_EXCL:与 O_CREAT 一起使用,确保文件是唯一创建的,即如果文件已经存在,则返回错误。

O_TRUNC:以写入模式打开文件时,将文件长度截断为0。

O_APPEND:每次写入文件时,数据追加到文件末尾。

O_NONBLOCK:以非阻塞模式打开文件。

O_SYNC:以同步模式打开文件,写操作会立即写入磁盘。

Q:不是说系统调用是直接文件交互,为啥还要O_SYNC选项?

Q:write 写入文件不应该是直接写入到文件吗?为啥为写入缓存?不是带缓存的io才会写入缓存吗?

A:文件写入缓存机制,1 用户空间缓存 -> 2 内核空间缓存 -> 磁盘。使用write函数是将文件内容读取到内核空间缓存(页缓存)。内核空间会在后台处理这些数据,通过异步写入磁盘。这种机制叫做异步写入。带缓存会将文件内容先写入到用户空间缓存,等到满足当前缓存模式/或者调用fllush,flcose,或者程序退出时,才会将用户空间缓存写入到内核空间缓存。后边再通过异步写入的方式。将内核空间缓存写入到磁盘。

当write函数使用O_SYNC选项,写入就不会在内核缓存中存贮而是直接通过内核写入磁盘。

但是fwrite没有对应O_SYNC的选项,可以一步到位到磁盘。可以通过在调用fllush后调用,通过fsync系统调用,将内核空缓存同步到磁盘。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 使用 fopen 打开文件流
    FILE *file = fopen("example.txt", "w");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }

    // 获取底层文件描述符
    int fd = fileno(file);

    // 使用 fprintf 写入数据
    fprintf(file, "Hello, World!\n");
    
    // 使用 fflush 确保用户空间缓存数据写入内核空间缓存
    if (fflush(file) != 0) {
        perror("fflush");
        fclose(file);
        return 1;
    }

    // 使用 fsync 确保数据从内核缓存同步到磁盘
    if (fsync(fd) == -1) {
        perror("fsync");
        fclose(file);
        return 1;
    }

    printf("Data flushed and synced to disk.\n");

    // 关闭文件流
    if (fclose(file) != 0) {
        perror("fclose");
        return 1;
    }

    return 0;
}

close函数

函数原型:int close(int fd);

功能:

关闭文件描述符:释放文件描述符,并关闭与之关联的文件、管道、套接字等资源。

清理资源:关闭文件描述符后,相关的资源(如文件句柄、内存缓冲区)被释放,文件描述符可以重新分配给其他打开的文件。

刷新缓冲区:对于文件描述符,关闭文件描述符会自动刷新与之相关的缓冲区,将用户空间缓冲区内容刷新到内核空间缓冲区。

注意点:一个进程结束是内核会自动关闭所有打开的文件,但是在运行过程中,因为一个进程打开的文件数量有限制,当没有close是,会造成句柄泄露,当所有文件描述符都被占用,就无法打开新的文件。

返回值:

lseek函数

函数原型:off_t lseek(int fd, off_t offset, int whence);

**功能:**移动文件指针

  • fd:已经打开的文件描述符
  • offset:偏移量,无符号。
  • whence:
    • SEEK_SET:基于文件起始移动。
    • SEEK_CUR:基于文件当前移动。
    • SEEK_END:基于文件末尾移动。

返回值:

成功时:返回新的文件指针位置(即文件描述符的当前位置),以字节为单位。

read函数

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:buf缓存区,count希望读到的字节数。

返回值:

​ 失败时:-1.

​ 成功时:0<=实际读到的数据<=count

write函数

ssize_t write(int fd, const void *buf, size_t count);

参数:

buf: 指向要写入数据的内存缓冲区的指针。buf 指向的数据会被写入到文件描述符对应的文件或设备中。

count: 要写入的字节数。它指定了从 buf 中要写入到文件描述符的最大字节数。

返回值:

成功: 返回实际写入的字节数。如果写入的数据少于 count,说明文件描述符的容量受到了限制,写入的字节数会少于请求的字节数。

失败: 返回 -1,并设置 errno 以指示错误原因。

阻塞模式和非阻塞模式

阻塞模式

定义

  • 在阻塞模式下,系统调用会阻塞进程或线程,直到操作能够完成。在等待期间,进程会被挂起,不能执行其他操作。

特点

  • 等待数据:如果你调用 read 函数,而文件描述符中没有数据可读,进程会被挂起,直到有数据可以读取或发生错误。
  • 等待资源:如果调用 write 函数,而文件描述符对应的设备或文件不能立即写入数据,进程会被挂起,直到可以写入数据或发生错误。

优点

  • 简单:阻塞模式通常更易于理解和使用,因为进程会自动等待直到操作完成,无需额外处理等待或轮询逻辑。

缺点

  • 效率低:在等待期间,进程不能做其他事情,这可能导致资源浪费,尤其是在高并发场景下。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));  // 阻塞直到数据可用
    if (bytes_read == -1) {
        perror("read");
        close(fd);
        return 1;
    }

    // 使用读取的数据
    write(STDOUT_FILENO, buffer, bytes_read);

    close(fd);
    return 0;
}
非阻塞模式

定义

  • 在非阻塞模式下,系统调用不会阻塞进程或线程。如果操作不能立即完成,系统调用会返回一个错误,而不是等待操作完成。

特点

  • 立即返回:调用 read 时,如果文件描述符中没有数据可读,函数会立即返回,通常返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  • 轮询:应用程序可以使用循环检查文件描述符的状态(例如,使用 selectpollepoll)来确定是否可以进行读写操作。

优点

  • 高效:允许进程在等待数据时执行其他任务,提高了程序的响应性和效率,特别是在需要处理大量并发 I/O 操作的情况下。

缺点

  • 复杂性:编程模型更复杂,因为需要处理数据不可用的情况,并且可能需要实现额外的轮询逻辑或事件驱动机制
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    // 打开文件并设置为非阻塞模式
    int fd = open("example.txt", O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("No data available right now\n");
        } else {
            perror("read");
        }
        close(fd);
        return 1;
    }

    // 使用读取的数据
    write(STDOUT_FILENO, buffer, bytes_read);

    close(fd);
    return 0;
}

Q:阻塞模式读取一个空文件是不是进程就一直挂起了?

A:不是,空文件read返回0。表示已经读完。阻塞,

​ 空文件:文件本身没有任何内容,文件长度为0。在这种情况下,当你尝试从文件中读取数据时,read 函数会立即返回 0,表示到达了文件末尾(EOF)。文件是空的,所以没有更多数据可以读取,也不需要挂起进程 。

没有数据可读:

  • 管道:如果你从一个管道中读取数据时,管道可能暂时没有数据可用。在这种情况下,read 函数会阻塞进程,直到管道中有数据可读。
  • 套接字:从网络套接字读取数据时,套接字可能暂时没有数据可用。在这种情况下,read 函数会阻塞进程,直到有数据到达。
  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值