不带缓存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
,并设置errno
为EAGAIN
或EWOULDBLOCK
。 - 轮询:应用程序可以使用循环检查文件描述符的状态(例如,使用
select
、poll
或epoll
)来确定是否可以进行读写操作。
优点:
- 高效:允许进程在等待数据时执行其他任务,提高了程序的响应性和效率,特别是在需要处理大量并发 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
函数会阻塞进程,直到有数据到达。