揭秘Linux IO的魅力所在!

在这里插入图片描述

Linux基础I/O

Linux 的 I/O 操作是指在 Linux 系统上进行的输入和输出操作,包括文件的读写、设备的读写、标准输入输出等。

系统调用

Linux 提供了一些基础的系统调用,用于直接处理文件描述符进行 I/O 操作:

  • open(): 打开一个文件,返回文件描述符。

    int open(const char *pathname, int flags, mode_t mode);
    
  • read(): 从文件描述符中读取数据。

    ssize_t read(int fd, void *buf, size_t count);
    
  • write(): 向文件描述符中写入数据。

    ssize_t write(int fd, const void *buf, size_t count);
    
  • close(): 关闭文件描述符。

    int close(int fd);
    
  • lseek(): 在文件中移动文件指针。

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

打开和关闭文件

open() 函数用于打开一个文件,成功时返回文件描述符,失败时返回 -1。文件的打开模式由 flags 参数指定,可以是以下几种:

  • O_RDONLY:只读模式
  • O_WRONLY:只写模式
  • O_RDWR:读写模式
  • O_CREAT:如果文件不存在则创建文件
  • O_TRUNC:将文件长度截断为0(如果文件存在)
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
  int fd = open("file.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
  if(fd == -1)
  {
    perror("open");
    return -1;
  }
  char buffer[] = "hello world\n";
  write(fd, buffer, sizeof(buffer));

  close(fd);
  return 0;
}

image-20240813101444323

文件指针移动

lseek() 函数用于在文件中移动文件指针。它返回移动后的文件偏移量(字节数),失败时返回 -1

参数 whence 指定了偏移的起始位置,可以是以下值:

  • SEEK_SET:从文件开头开始偏移
  • SEEK_CUR:从当前文件指针位置开始偏移
  • SEEK_END:从文件末尾开始偏移
#include <fcntl.h>
#include <unistd.h>

int fd = open("file.txt", O_RDWR);
if (fd == -1) 
{
    // 处理错误
}

// 将文件指针移动到文件开头
off_t offset = lseek(fd, 0, SEEK_SET);
if (offset == -1) 
{
    // 处理错误
}

close(fd);

系统调用 vs 标准库函数

  • 系统调用: 是操作系统内核提供的接口,用于与硬件资源交互,包括文件系统、进程管理、内存管理等。在 Linux 中,系统调用直接与操作系统内核交互,例如 open(), read(), write(), close() 这些系统调用函数。

  • 标准库函数: 是编程语言(如 C 语言)提供的更高层次的接口,通常由系统调用函数实现。这些函数提供了更友好的接口、更丰富的功能以及缓冲机制,简化了开发者的使用。例如,C 语言的 fopen 函数就是通过调用 open 系统调用来打开文件,并在此基础上增加了缓冲和错误处理等功能。

在 C 语言中,标准 I/O 库函数如 fopenfclosefreadfwrite 是对系统调用的封装。这些函数使用了 FILE 结构体来表示文件流,提供了更高级的功能,例如缓冲区管理、格式化输入输出等。

例如,fopen 函数的实现大致如下:

  1. 调用 open 系统调用: fopen 函数首先会调用 open 系统调用来打开一个文件,获取文件描述符(file descriptor)。
  2. 分配和初始化 FILE 结构体: fopen 随后会分配一个 FILE 结构体,并将文件描述符和其他必要信息存储在该结构体中。
  3. 返回 FILE * 指针: 最终,fopen 返回一个指向该 FILE 结构体的指针,供后续操作使用。

这种方式的好处是,标准库函数对用户隐藏了底层的复杂细节,提供了更易于使用的接口。

不仅仅是 C 语言,几乎所有高级编程语言(如 Python、Java、C++、Go 等)都提供了对文件操作的封装。这些语言的文件操作函数通常也都是在底层系统调用的基础上实现的。

  • Python: open 函数封装了底层的 open 系统调用,返回一个文件对象。

    with open('file.txt', 'r') as file:
        data = file.read()
    
  • Java: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter 等类封装了底层的文件系统调用。

    BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
    String line = reader.readLine();
    
  • Go: os.Open, os.Create 等函数封装了底层系统调用,返回 *os.File 类型的对象。

    file, err := os.Open("file.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    

一个重要的差异是,标准库函数通常会在系统调用的基础上增加缓冲机制。比如 C 语言中的 freadfwrite 函数会先将数据存储在用户空间的缓冲区中,等到缓冲区满了或文件关闭时,才真正通过系统调用将数据写入磁盘。这种缓冲机制可以大大减少系统调用的次数,提升 I/O 操作的效率。

因此可以做出一个结论:所有语言层级提供的文件操作函数都是对系统调用接口的封装。

缓冲区

缓冲模式一般分为无缓冲行缓冲全缓冲,不同的缓冲模式决定了数据何时从程序的缓冲区刷新到实际的输出设备(如屏幕、文件)。

缓冲区是内存中的一块区域,用于暂时存储数据。程序在执行 I/O 操作时,会先将数据写入缓冲区,待满足一定条件后再将数据一次性输出到目标设备。这种机制能够提高性能,因为减少了频繁的 I/O 操作。

行缓冲

  • 概念:在行缓冲模式下,缓冲区会在遇到换行符 \n 时自动刷新,将数据输出到目标设备。除此之外,如果缓冲区满了,数据也会被刷新。

  • 触发条件

    • 缓冲区遇到换行符时。
    • 缓冲区满时。
    • 明确调用刷新函数(如 fflush())时。
    printf("Hello, World!");   // 不会立即输出
    printf("\n");              // 触发行缓冲,之前的内容和换行符一起输出
    

全缓冲

  • 概念:在全缓冲模式下,只有当缓冲区被填满或显式刷新时,数据才会被输出。这种模式适合于非交互式设备,如文件或网络传输,因为它减少了I/O操作的次数,从而提高性能。

  • 触发条件

    • 缓冲区满时。
    • 明确调用刷新函数(如 fflush())时。
    • 程序正常终止时(如 exit()return)。
    FILE *fp = fopen("example.txt", "w");
    fprintf(fp, "This is a test.");  // 数据写入缓冲区,但不会立即写入文件
    fclose(fp);                      // 关闭文件时,缓冲区数据被刷新到文件
    

无缓冲

  • 概念:在无缓冲模式下,数据不会被缓存在内存中,而是立即被发送到目标设备。这种模式通常用于标准错误输出(stderr),以确保错误信息可以立即被用户看到。

    fprintf(stderr, "An error occurred!");  // 立即输出,不经过缓冲
    

缓冲模式的设置

缓冲模式可以使用 setvbuf() 函数进行设置。

int setvbuf(FILE *stream, char *buffer, int mode, size_t size);
  • 参数

    • stream:要设置的文件流(如 stdout, stdin 等)。
    • buffer:指向用户分配的缓冲区(通常为 NULL,由系统自动分配)。
    • mode:缓冲模式,取值为 _IONBF(无缓冲)、_IOLBF(行缓冲)和 _IOFBF(全缓冲)。
    • size:缓冲区大小(字节数)。
    setvbuf(stdout, NULL, _IOLBF, 0);  // 设置标准输出为行缓冲
    setvbuf(stdout, NULL, _IOFBF, 1024);  // 设置标准输出为全缓冲,缓冲区大小为 1024 字节
    

文件描述符

文件描述符(File Descriptor, FD)是操作系统内核为每个打开的文件或I/O资源(如套接字、管道等)分配的一个非负整数,作为对这些资源的引用或句柄。

文件描述符的主要作用是充当应用程序与操作系统之间的桥梁。当应用程序需要读写文件或其他I/O资源时,会通过文件描述符向操作系统发出系统调用。操作系统内核通过文件描述符找到对应的文件或资源,并执行相应的操作。

文件描述符的工作原理

每个进程打开后都有一个进程控制块(PCB)来描述该进程,而PCB(task_struct)中又有一个结构体指针files,指向files_struct结构体。

每个进程在内核中都有一个 files_struct 结构,管理该进程打开的文件。这个结构包含了一个指向文件指针的数组,每个文件指针对应一个打开的文件。

  • files_struct 结构:包含了进程打开的所有文件的相关信息。其中最重要的部分是一个数组,数组的每个元素都是一个指向文件对象(file 结构)的指针。
  • 文件描述符:本质上就是这个数组的索引(下标)。通过这个索引,进程可以直接访问数组中的文件指针,从而操作对应的文件。

这个数组中的每个元素(文件指针)都指向一个 file 结构,该结构描述了文件的具体状态,比如文件的读写位置、打开模式等。

因此当你在应用程序中打开一个文件时,操作系统会执行以下步骤:

  1. 调用 open() 系统调用:应用程序请求打开一个文件。
  2. 分配文件描述符:内核检查该进程的 files_struct 结构,在文件指针数组中找到一个空闲位置,分配一个文件描述符(数组的索引)。
  3. 创建文件对象:内核为文件创建一个 file 结构,并将指向这个结构的指针存放在文件指针数组的相应位置。
  4. 返回文件描述符:内核将文件描述符返回给应用程序,应用程序随后可以通过该描述符执行读写等操作。

当你使用 read()write() 等系统调用时,内核会根据文件描述符找到对应的文件指针,并操作该文件指针指向的 file 结构,从而实现对文件的实际操作。

当你调用 close() 系统调用时,内核会释放文件描述符对应的文件指针数组中的元素,并关闭文件。如果没有其他文件描述符指向该文件,则内核会销毁与该文件关联的 file 结构,释放其占用的资源。

image-20240813110539239

常见文件描述符

在大多数操作系统(如Unix/Linux)中,文件描述符具有一些默认值,用于表示标准输入、标准输出和标准错误:

  • 标准输入(stdin): 文件描述符 0,用于接收来自键盘或其他输入设备的数据。
  • 标准输出(stdout): 文件描述符 1,用于将数据输出到终端或其他输出设备。
  • 标准错误(stderr): 文件描述符 2,用于输出错误信息到终端或其他输出设备。

文件描述符的分配规则符合最小可用原则

文件描述符的限制

每个进程能打开的文件描述符数量是有限的,这个限制由操作系统设定,通常可以通过命令 ulimit -n 查看或修改该限制。默认值因系统而异,通常在1024左右。超出限制时,open() 等调用会失败,并返回错误。

重定向

在上层无感知的情况下更改进程对应的文件描述符表中的指针指向。

image-20240813115029256

重定向函数

dupdup2 是 Unix/Linux 系统中用于复制文件描述符的两个系统调用。它们允许你创建新的文件描述符,并将其指向已经打开的文件。这在进程重定向、子进程创建等场景中非常有用。

dup 函数

int dup(int oldfd);

dup 创建一个新的文件描述符,该文件描述符是 oldfd 的副本,并指向相同的文件。

  • 返回值:

    • 成功时,返回新的文件描述符。
    • 失败时,返回 -1,并设置 errno 来表示错误类型。
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1) 
    {
        perror("open");
        return 1;
    }
    
    int new_fd = dup(fd);
    if (new_fd == -1) 
    {
        perror("dup");
        close(fd);
        return 1;
    }
    
    // 现在 fd 和 new_fd 都指向同一个文件
    

dup2 函数

int dup2(int oldfd, int newfd);

dup2 类似于 dup,但它允许指定新文件描述符的值。如果 newfd 已经打开,则 dup2 会先关闭它,然后将 oldfd 复制到 newfd。如果 oldfdnewfd 相同,则 dup2 直接返回 newfd,而不执行任何操作。

  • 返回值:

    • 成功时,返回 newfd
    • 失败时,返回 -1,并设置 errno 来表示错误类型。
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1) 
    {
        perror("open");
        return 1;
    }
    
    // 将 fd 复制到文件描述符 5
    int new_fd = dup2(fd, 5);
    if (new_fd == -1) 
    {
        perror("dup2");
        close(fd);
        return 1;
    }
    
    // 现在 new_fd (5) 和 fd 都指向同一个文件
    

-1,并设置 errno 来表示错误类型。

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

// 将 fd 复制到文件描述符 5
int new_fd = dup2(fd, 5);
if (new_fd == -1) 
{
    perror("dup2");
    close(fd);
    return 1;
}

// 现在 new_fd (5) 和 fd 都指向同一个文件
  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拖拉机厂第一代码手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值