Linux 基础IO

发布于:2024-03-29 ⋅ 阅读:(21) ⋅ 点赞:(0)

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:Linux知识分享

🚚代码仓库:Linux代码练习🚚

🌹关注我🫵带你学习更多Linux知识
  🔝

目录

前言

预备知识 

一.C语言文件操作

        1.C语言文件写入

         2.C语言文件读取

3.标准输入、输出、错误 

二. 系统文件I/O

 1.open 

返回值

2.wirte

3.read 

4.close

5.文件描述符fd 

三.重定向 

 1.输出重定向

 2.追加重定向

 3.输出重定向


前言

本篇非常重要,承上启下的作用,对linux一切皆文件和重定向从底层原理剖析,为什么要有缓冲区?以及什么是文件系统,为什么C语言有了文件操作的函数,操作系统还要有自己的一套操作文件的方式?

预备知识 

文件等于什么?等于文件内容+属性 ,我们访问一个文件本质是什么?本质是先写代码 -> 编译 -> 形成exe -> 运行 -> 访问文件  本质还是进程再访问文件。

如果我们要向文件里写入内容,只有谁有权力?答案:OS

普通用户怎么办? 必须OS提供系统接口  所以我们用C/C++/Java这些高级语言的文件操作的函数,其实都封装了文件类的系统调用接口,那既然系统自己有文件操作的函数,那为什么这些高级语言还要自己搞一套出来?

一是为了跨平台,二是OS文件操作比较难,所以各种高级语言对这些接口进行了封装,为了让程序员更好使用这些接口。

为了对比困难程度,下面我们先用C演示文件操作,再用系统调用接口演示

一.C语言文件操作

        1.C语言文件写入

指令: man 3 fopen

上面的6种模式,下面文档非常详细,请认真看,看完就会了,下面也有代码演示。非常简单 

在我这里路径是没有my.txt这个文件的,我们运行下面代码试试 

 

#include <stdio.h>


int main()
{   FILE* fd = fopen("my.txt", "r");
    if(fd == NULL)
    {   printf("Error opening file\n");
        return 1;
    }
    
    fclose(fd);
    return 0;
}

我们再试试其他fopen模式。

int main()
{   FILE* fd = fopen("my.txt", "r+");
    if(fd == NULL)
    {   printf("Error opening file\n");
        return 1;
    }
    
    fclose(fd);
    return 0;
}

代码运行结果:不管是r还是r+文件必须存在。 

#include <stdio.h>
#include <string.h>

int main()
{   FILE* fd = fopen("my.txt", "w");
    if(fd == NULL)
    {   printf("Error opening file\n");
        return 1;
    }
    const char* str = "Hello world!";
    fwrite(str, 1, strlen(str), fd);
    
    fclose(fd);
    return 0;
}

 w模式 文件不存在就创建文件。我们再次向my.tst这个文件写入hello world。看看会怎么样?

还是只有一个hello world ,w和w+模式每次写入都会清空之前的文件内容。 a模式就不演示了英文append 追加的意思。

         2.C语言文件读取

        

这个函数尝试从给定的文件流stream中读取count个元素,每个元素的大小为size字节,并将这些数据存储在ptr指向的内存位置。函数返回实际读取的元素个数,如果发生错误或到达文件末尾,这个数可能小于count。 参数说明:

  • ptr:指向内存块的指针,用于存储读取的数据。

  • size:每个元素的大小,以字节为单位。

  • nmemb:要读取的元素个数。

  • stream:指向FILE对象的指针,该对象标识了要读取的文件流

#include <stdio.h>
#include <string.h>
#define BUFFER_SIZE 1024  

int main()
{   FILE* fd = fopen("my.txt", "r");
    if(fd == NULL)
    {   printf("Error opening file\n");
        return 1;
    }
    const char* str = "Hello world!";
    char buffer[BUFFER_SIZE];
    memset(buffer, '\0', BUFFER_SIZE);
    //fwrite(str, 1, strlen(str), fd);
    while(1)
    {
        size_t f = fread(buffer, 1, strlen(str), fd);
        if(f>0)
        {   buffer[f] = 0;
            printf("%s", buffer);
        }
        if(feof(fd))
            break;
       
    }
    fclose(fd);
    return 0;
}

feof 是一个标准C库函数,其原型定义在 <stdio.h> 头文件中。feof 函数用于检查给定的文件流是否到达了文件末尾(EOF,即 "End Of File")。 函数原型如下: 

int feof(FILE *stream);

 其中,stream 是一个指向 FILE 类型的指针,该指针指向你想要检查的文件流。 feof 返回一个非零值(通常为1)如果流已经到达了文件末尾,或者返回0如果尚未到达文件末尾。 需要注意的是,feof 只有在尝试从流中读取数据之后,且已到达文件末尾时,才会返回非零值。也就是说,你不能仅仅通过调用 feof 来确定是否已到达文件末尾,而应该在读取操作之后检查它。

3.标准输入、输出、错误 

C语言中有那些方法可以向显示器打印?

int main()
{
    printf("Hello world!\n");
    const char* str = "Hello world!";
    fputs(str, stdout);
    fprintf(stdout, "Hello world!\n");
    return 0;
}

为什么printf可以直接向显示器打印?

可以直接向显示器打印是因为它默认将输出发送到标准输出(stdout),而stdout通常连接到终端或显示器的命令行界面。 而且C语言都是默认打开(stdin),(stdout),(stderr)。

一句简单的printf函数调用,其实OS背后是做了大量的工作。 

底层机制printf 函数内部使用了操作系统提供的系统调用(如Linux中的 write)来向stdout写入数据。这些系统调用与终端或文件系统进行交互,将数据发送到相应的位置。

看看fputs和fprintf函数原型。 

int fputs(const char *str, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);

stream 是一个指向目标文件流的 FILE 指针。 fputs 函数将把 str 指向的字符串(不包括末尾的空字符)写入到 stream 指向的文件流中。如果写入成功,函数返回非负值;如果发生错误,则返回 EOF(通常被定义为 -1)。 

fputs 和 fprintf都是向文件写入,可是显示器是硬件啊? 

在linux下,一切皆文件。显示器是硬件没错,在Linux中,所有的硬件都被看成文件,只是这种"文件"和我们平时在电脑中看到的文件所操作方式不同。

二. 系统文件I/O

通过上面的printf函数,我们知道其实是这个函数调用了系统接口write向显示器写入。我们就来看看C语言打开、读写、关闭分别对应系统接口是什么?

 1.open 

        函数原型

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

  1. pathname:要打开或创建的文件的路径名。

  2. flags:指定文件如何打开或创建的文件状态标志。以下是一些常用的标志:

  • O_RDONLY:以只读方式打开文件。

  • O_WRONLY:以只写方式打开文件。

  • O_RDWR:以读写方式打开文件。

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

  • O_TRUNC:如果文件已存在并且是一个常规文件,且以只写或读写方式打开,则将其长度截断为 0。

  • O_APPEND:每次写入都在文件的末尾添加数据。

  • O_EXCL:与 O_CREAT 一起使用,如果文件已存在,则调用会失败。

  1. mode:如果 flags 中指定了 O_CREAT,则 mode 参数指定新文件的权限。它是使用八进制数字指定的权限掩码,比如 S_IRUSR | S_IWUSR | S_IRGRP 等。

返回值

如果成功,open 返回一个新打开的文件描述符(一个非负整数)。如果失败,返回 -1,并设置全局变量 errno 以指示错误。

2.wirte

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

参数说明:

  • fd:这是你想要写入的文件的文件描述符。这个文件描述符通常是通过opencreat等系统调用获取的。

  • buf:这是一个指向你想要写入数据的缓冲区的指针。

  • count:这是你想要写入的字节数。 write系统调用会尝试将count字节的数据从buf指向的缓冲区写入到文件描述符fd指向的文件中。成功时,write返回写入的字节数;如果出现错误,则返回-1,并设置全局变量errno以指示错误原因。 

3.read 

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

  • fd 是通过open函数打开文件时返回的文件描述符。

  • buf 是一个指向缓冲区的指针,用于存储从文件中读取的数据。

  • count 是希望从文件中读取的最大字节数。 read函数返回实际读取的字节数,如果返回值为0,则表示已到达文件末尾(EOF)。如果发生错误,则返回-1,

4.close

int close(int fd);
  • 如果成功,返回0。

  • 如果失败,返回-1,并设置全局变量errno以指示错误。 调用close函数后,任何进一步的读或写操作尝试都可能导致错误

下面是4个函数如何操作的代码 

#include <stdio.h>
#include <string.h>
#include <unistd.h> // 引入unistd.h以使用read, write, close等系统调用
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFFER_SIZE 1024

int main()
{
    int fd = open("test.txt", O_RDWR | O_CREAT, 0666); // 使用O_RDWR以读写模式打开文件
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    // 写入数据到文件
const char* write_str = "Hello, world!";
ssize_t bytes_written = write(fd, write_str, strlen(write_str));
if (bytes_written == -1) {
    perror("Error writing to file");
    close(fd);
    return 1;
}

// 将文件指针重置到文件开头
lseek(fd, SEEK_SET, 0);

// 读取数据从文件
char buffer[BUFFER_SIZE];
memset(buffer, '\0', BUFFER_SIZE);// 初始化buffer
ssize_t bytes_read = read(fd, buffer,BUFFER_SIZE); 
if (bytes_read == -1) {
    perror("Error reading from file");
    close(fd);
    return 1;
}


// 打印从文件读取的内容
printf("Read from file: %s\n", buffer);

// 关闭文件
close(fd);

    return 0;
}

代码在执行write函数后,fd其实已经指向了文件末尾,我们在使用read时候,是读不到的。所以使用lseek函数重置fd到文件开头。

代码有注释,这样对比以后是不是感觉系统调用的是不是比C提供的难,上面的6个参数 感兴趣的可以下去试试。 我们在看看下一段代码运行结果

5.文件描述符fd 

        int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
        printf("open success, fd: %d\n", fd1);
        int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
        printf("open success, fd: %d\n", fd2);
        int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
        printf("open success, fd: %d\n", fd3);
        int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
        printf("open success, fd: %d\n", fd4);

 咦?这fd怎么4567  前面的0123 去哪里了? 

在Unix和Linux系统中,文件描述符(file descriptor,简称fd)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件。每个进程都有自己独立的文件描述符表,该表由内核维护,记录了该进程当前所有打开的文件。 以下是关于文件描述符分配的一些基本规则:

  1. 非负整数:文件描述符是非负整数,通常从0开始分配。

  2. 最小可用值:当打开一个新文件时,内核会分配当前文件描述符表中未被使用的最小整数作为新的文件描述符。因此,如果你只打开了一个文件,这个文件描述符通常是0;如果打开了两个文件,那么第二个文件描述符可能是1,以此类推。

  3. 标准输入、输出和错误:文件描述符0、1、2分别被预留给进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。

  4. 顺序性:每次打开新文件时,系统都会分配下一个可用的文件描述符,除非这个描述符已被使用。

  5. 关闭与释放:当一个文件被close系统调用关闭时,其对应的文件描述符就会被释放,可以被后续打开的文件所使用。

  6. 文件描述符的继承:当一个进程创建子进程时,子进程会继承父进程的文件描述符表的一个拷贝。

OS把0 1 2 位置给了 标准输入、输出和错误 我们刚才先用了fd 所以就是3 后面的就只能从4开始了。

光是这样说fd是文件描述符,确实很抽象,下面我就给大家说说fd的本质是什么?在代码层面是什么?

试问一个进程要访问一个文件,是不是要先打开,然后再进行读写操作?一个进程可以打开多个文件吗?当然可以打开多个文件啊 ,那既然这样,在OS中就有大量被打开的文件,这些文件需不需要被管理?需要被管理,那既然要管理,就要先描述,再组织。请开下图。 

所以fd在内核中,本质就是数组的一个下标。

三.重定向 

        通过上面我们讲解什么是fd,fd的作用,以及分配规则,既然这样我们是不是可以在打开一个文件之前先关闭标准输出。然后根据分配规则我们打开文件的fd是不是就是1了?

 先了解一个函数:

int dup2(int oldfd, int newfd);

dup2是Unix和Linux系统中的一个系统调用,用于复制一个文件描述符到另一个文件描述符。其基本语法是 

  • oldfd:这是源文件描述符,即你想要复制的文件描述符。

  • newfd:这是目标文件描述符,即你想要将oldfd复制到哪个文件描述符上。如果newfd之前已经打开,dup2会先关闭它。 dup2的主要用途是重定向文件描述符。

 1.输出重定向

#include <stdio.h>
#include <string.h>
#include <unistd.h> // 引入unistd.h以使用read, write, close等系统调用
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFFER_SIZE 1024


int main(int argc, char* argv[])
{   
    if(argc < 2) {
        printf("Usage: ./myproc arg1 [arg2 ...]\n");
        return -1;
    }
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC); //输出重定向
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }
    dup2(fd, 1); //将标准输出重定向到文件
    fprintf(stdout,"%s\n", argv[1]);
}

对于追加重定向,无非就是把open参数修改以下,后面的代码都不用变,把O_TRUNC改成O_APPEND 。

 2.追加重定向

#include <stdio.h>
#include <string.h>
#include <unistd.h> // 引入unistd.h以使用read, write, close等系统调用
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFFER_SIZE 1024


int main(int argc, char* argv[])
{   
    if(argc < 2) {
        printf("Usage: ./myproc arg1 [arg2 ...]\n");
        return -1;
    }
    //int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC); //输出重定向
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND); //追加输出
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }
    dup2(fd, 1); //将标准输出重定向到文件
    fprintf(stdout,"%s\n", argv[1]);
}

 3.输出重定向

#include <stdio.h>
#include <string.h>
#include <unistd.h> // 引入unistd.h以使用read, write, close等系统调用
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFFER_SIZE 1024


int main(int argc, char* argv[])
{   
    //if(argc < 2) {
        //printf("Usage: ./myproc arg1 [arg2 ...]\n");
        //return -1;
    //}
    //int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC,0666); //输出重定向
    //int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND,0666); //追加输出
    int fd = open("test.txt", O_RDONLY); //只读
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }
    dup2(fd, 0); //将标准输入重定向到文件
    //fprintf(stdout,"%s\n", argv[1]);
    char buffer[BUFFER_SIZE];
    memset(buffer, '\0', BUFFER_SIZE);
    while(fgets(buffer, BUFFER_SIZE, stdin)!=NULL)
    {
        printf("buffer: %s\n", buffer);
    }
    close(fd);
}

输入重定向本来是从键盘读取,通过上面代码重定向为从文件读取。

从这里我们也得出一个结论,对于OS的而言,管你是什么文件,我只认fd。 

下节预告,文件系统、动静态库,关注我带你学习更多Linux知识,家人们感觉对你们有帮助的话,一键三连给博主一个小小的鼓励。感谢您的观看!!! 

本文含有隐藏内容,请 开通VIP 后查看