0%

文件IO

1. 文件描述符

Linux系统中一切皆文件,所有打开的文件都通过文件描述符进行索引。文件描述符是一个非负整数,当打开或创建文件时,内核向该进程返回一个文件描述符用于指代该文件。

Linux内核对所有打开的文件都维护了一个文件描述符表,用于存储文件描述符。且在文件描述符表中已经默认分配了三个文件描述符:0是标准输入stdin,1是标准输出stdout,2是标准错误stderr。故打开或创建的文件描述符大于或等于3。

进程描述符表

2. open/close

2.1 open函数

通过open函数可以打开或创建一个文件,返回的文件描述符一定是该进程尚未使用的最小描述符,大于等于3。

函数原型:

1
2
3
4
5
#include <fcntl.h>
// 打开一个已经存在的磁盘文件
int open(const char *path, int flags);
// 打开磁盘文件, 如果文件不存在, 就会自动创建
int open(const char *path, int flags, mode_t mode);

参数:

  • path:要打开的文件的路径名。

  • flags:打开文件的方式和选项,它是一个位掩码,可以通过按位或运算组合多个选项。常见选项包括:

    • O_RDONLY —— 以只读方式打开文件
    • O_WRONLY —— 以只写方式打开文件
    • O_RDWR —— 以读写方式打开文件
    • 可选属性:(和以上属性一起使用)
      • O_APPEND —— 新数据追加到文件的尾部
      • O_CREAT —— 如果文件不存在,则创建该文件
      • O_TRUNC—— 如果文件存在且以写入方式打开,则清空文件内容
      • O_EXCL —— 检测文件是否存在, 必须要和 O_CREAT 一起使用, 不能单独使用:O_CREAT | O_EXCL检测到文件不存在, 创建新文件;检测到文件已存在, 创建失败, 函数直接返回-1
    • O_NONBLOCK ——对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O
  • mode:表示文件的权限掩码,指定新建文件的权限,仅在O_CREAT选项被指定时有效

    • 文件权限掩码是一个八进制数,每一位分别代表了不同的权限。
    • 一般情况下,文件权限掩码可以分为三组:用户权限、组权限和其他用户权限。每组权限又包括读(R)、写(W)和执行(X)权限。
    • 读(R):4、写(W):2、执行(X):1
    • 例如:
      • rwxrwxrwx:全部权限,八进制数值为777。
      • rw-r--r--:所有者可读写,其他用户只读,八进制数值为644。

返回值:

  • 成功:返回一个非负整数,表示文件描述符。
  • 失败:返回 -1,并设置 errno 来指示错误类型。

2.2 close函数

通过close函数关闭一个打开文件,释放打开时分配的文件描述符。close()原型:

1
2
#include <unisted>
int close(int fd)

参数:

  • fd:要关闭的文件描述符。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置 errno 来指示错误类型。

2.3 文件操作

打开已有文件:

1
int fd = open("abc.txt", O_RDWR);

创建新文件:

1
int fd = open("./new.txt", O_CREAT|O_RDWR, 0664);

3. read/write

3.1 read函数

用于从打开的文件中读数据

函数原型:

1
2
#include <unisted.h>
ssize_t read(int fd, void *buf, size_t count);

参数:

  • fd: 要读取的文件描述符
  • buf: 指向存放读取数据的缓冲区的指针。
  • count: 要读取的字节数

返回值: 返回值的数据类型是 ssize_t,表示带符号的整型,这样既可以返回正的字节数、0(表
示到达文件末尾)也可以返回负值-1(表示出错) 。

  • 成功:返回读取的字节数,可能小于 count,因为可能在读取count个字节之前已到达文件末尾。
  • 文件结束(EOF):返回 0,表示已读取到文件末尾。
  • 失败:返回 -1,并设置 errno 来指示错误类型。

什么是缓冲区:

  • 缓冲区(Buffer)是指在计算机中用于临时存储数据的一段内存区域。在程序中,缓冲区通常用于临时存放数据,以便稍后进行处理或传输。缓冲区的使用可以提高数据的读写效率,减少频繁的IO操作,特别是在涉及大量数据的情况下。

  • 在文件IO操作中,缓冲区通常用于存储要读取或写入的数据。当调用 read() 系统调用从文件中读取数据时,数据首先被读取到缓冲区中,然后程序可以从缓冲区中获取数据进行进一步处理。类似地,当调用 write() 系统调用向文件中写入数据时,数据首先被写入到缓冲区中,然后由系统决定何时将缓冲区中的数据写入到文件中。

3.2 write函数

用于将数据从用户空间写入到已打开文件中

函数原型:

1
2
#include <unisted.h>
ssize_t write(int fd, const void *buf, size_t count)

参数:

  • fd:要写入的文件描述符。
  • buf:指向要写入数据的缓冲区的指针。
  • count:往磁盘文件中写入的字节数,一般是buf的长度sizeof(buf)。

返回值:

  • 成功:返回写入的字节数,可以小于 count
  • 失败:返回 -1,并设置 errno 来指示错误类型。

3.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}

const char *data = "Hello, World!\n";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
return 1;
}

close(fd);

return 0;
}

4. 阻塞和非阻塞

阻塞和非阻塞是指,程序在执行某种操作时的行为方式。

  • 阻塞(Blocking):

    ​ 在阻塞模式下,当程序执行某个操作时,如果该操作无法立即完成,程序将一直等待,直到操作完成为止。

  • 非阻塞(Non-blocking):

    ​ 在非阻塞模式下,当程序执行某个操作时,如果该操作无法立即完成,程序将立即返回而不会等待。程序可以继续执行其他任务,不会因为某个操作的未完成而被阻塞。

使用非阻塞 I/O 可以使得一个进程能够同时处理多个连接而不会被阻塞。当一个套接字上没有数据可读或者没有数据可写时,程序不会等待,而是立即返回,这样可以更有效地利用 CPU 时间。

非阻塞 I/O 结合事件驱动模型(如 epoll)可以实现异步 I/O。通过 epoll 等机制,程序可以注册对多个文件描述符的监听,一旦有事件发生,程序立即得到通知,而不需要轮询或阻塞等待。

5. fcntl函数

fcntl()可以修改一个已打开文件的属性,可以重新设置读、写、追加、非阻塞等标志,而不必重新open文件 。

函数原型:

1
2
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数fd表示已打开的文件描述符,cmd 是控制命令,指定了要对文件进行的操作,arg 是与控制命令相关的参数,类型和意义取决于具体的命令。

常用命令:

  • F_DUPFD

    • 复制文件描述符,创建一个新的文件描述符,其值大于或等于第三个参数(指定的最小文件描述符值)。

    • fcntl(fd, F_DUPFD, newfd)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETFL`:

      - 获取文件状态标志(file status flags)。

      - ```
      fcntl(fd, F_GETFL)
  • F_SETFL

    • 设置文件状态标志为flags。

    • fcntl(fd, F_SETFL, flags)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETFD`:

      - 获取文件描述符标志(file descriptor flags)。

      - ```
      fcntl(fd, F_GETFD)
  • F_SETFD

    • 设置文件描述符标志为flags。

    • fcntl(fd, F_SETFD, flags)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETLK`:

      - 获取文件锁信息,并将锁信息写入提供的结构体 `lock` 中。

      - ```
      fcntl(fd, F_GETLK, &lock)
  • F_SETLK

    • 设置文件锁,如果无法获取锁,则调用失败。

    • fcntl(fd, F_SETLK, &lock)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_SETLKW`:

      - 设置文件锁,并在必要时阻塞。

      - ```
      fcntl(fd, F_SETLKW, &lock)

将文件设置为非阻塞:

1
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK)

6. Iseek函数

用于移动文件的指针。

每一个打开文件都有一个与其相关联的”当前文件偏移量“,即文件指针指向的位置,作用在于控制文件的读写位置

1
2
#include <unisted.h>
off_t lseek(int fd, off_t offset, int whence);
  • 参数:

    • fd:所操纵文件的描述符
    • offset:偏移量,需要与whence搭配使用
    • whence:通过该参数指定函数功能
      • SEEK_SET:从文件头部开始偏移 offset 个字节
      • SEEK_CUR: 从当前文件指针的位置向后偏移offset个字节
      • SEEK_END: 从文件尾部向后偏移offset个字节
  • 返回值:成功则返回新的文件偏移量(文件头部到当前位置的偏移量),失败则返回-1

  • lseek(fd, 0, SEEK_SET); //将文件指针移动到头部
    lseek(fd, 0, SEEK_CUR); //得到当前文件指针的位置
    lseek(fd, 0, SEEK_END); //得到文件的总大小
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11


    # 7. 复制与重定向

    ## 7.1 dup函数

    dup函数的作用是复制一个现有的文件描述符,使多个文件描述符指向同一个文件。函数原型:

    ```c
    #include <unisted.h>
    int dup(int oldfd);

参数oldfd是被复制的文件描述符

函数调用成功后返回新的文件描述符,且是当前可用文件描述符中的最小数值;调用失败则返回-1

被复制出的新文件描述符是独立于旧的文件描述符的,二者没有连带关系。也就是说当旧的文件描述符被关闭了,复制出的新文件描述符还是可以继续使用的。

7.2 dup2函数

dup2函数可以进行文件描述符的复制,也可以进行文件描述符的重定向。重定向指的是断开文件描述符和当前文件的关联关系,与新的文件建立关联关系。

函数原型:

1
2
#include <unisted.h>
int dup2(int oldfd, int newfd);

作用是复制参数 oldfd 所指向的文件描述符,创建一个新的文件描述符,新的文件描述符的值是参数 newfd。如果 newfd 已经是一个打开的文件描述符,dup2() 会先关闭它。

dup2() 调用成功返回新的文件描述符,该文件描述符与 newfd 相同;调用失败则返回-1