基础IO(一)—— C语言文件操作接口/文件操作相关系统调用/文件标识符fd/file_struct/重定向

基础IO

文件相关知识补充

当我们在磁盘上创建了一个空文件的时候,空文件是否会占据空间呢?

答案是会占据空间,当我们创建一个文件,包括文件的名字,创建时间,大小等。这些称作文件的属性。所以 文件 = 内容 + 属性。

  • 所有对文件的操作分为两部分 A:对文件内容的操作 B:对文件属性的操作

  • 文件的内容是数据,文件的属性也是一种特殊的属性,所以存储文件时既要存储内容也要存储属性数据。

  • 进程要访问一个文件的时候,要先把这个文件打开,文件打开前是普通的磁盘文件,将文件加载到内存当中后文件才算被打开。文件按照是否被打开,分为被打开的文件,没有被打开的文件。

一个进程通过操作系统可以打开多个文件吗

打开到文件就是加载到内存中,一个进程可以打开多个文件。

结论

因为文件默认是在磁盘上的,所以将磁盘上的文件加载到内存当中,一定会涉及到访问磁盘设备需要操作系统来做,操作系统在运行的过程中可能会打开多个文件,那么操作系统也需要对打开的文件进行管理(先描述,在组织)

一个文件要被打开,一定要在内核中形成被打开的文件对象。

C语言文件接口

fopen

作用

打开文件

头文件

#include<stdio.h>

函数参数及返回值

FILE* fopen(const char *path, const char *mode);

参数

path要打开文件的路径

mode打开文件的模式

  • w方式:打开文件会先清空文件内容 (echo >)
  • r方式:只以读方式打开文件
  • a方式:从文件结尾处开始写,追加不清空文件(echo >>)
  • r+方式:以读和写的方式打开文件

返回值

成功时返回文件指针,失败时返回NULL

fclose

作用

关闭一个文件流

函数参数以及返回值

int fclose(FILE* stream)

返回值:

成功时返回0,失败时返回EOF

fread

作用

从文件中读取内容

函数参数以及返回值

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数

指定的文件流stream中读取数据到ptr指向的数组中。

size是每个数据项的大小(以字节为单位),

nmemb是要读取的数据项的数量。

返回值

成功时返回实际读取的数据项数(不是字节数)

fwrite

作用

向文件中写入内容

函数参数以及返回值

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

参数

  • 将数据写入到指定的文件流stream中。

  • ptr指向要写入的数据

  • size是每个数据项的大小(以字节为单位)

  • nmemb是要写入的数据项的数量。

返回值

  • 成功时返回实际写入的数据项数(不是字节数)。

代码示例

int main()
{
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        return 1;
    }

    char* message = "hello world\n";
    size_t n = fwrite(message,1,strlen(message),fp);
    printf("写入文件的数据项的数量:%zu\n",n);
    fclose(fp);
    return 0;
}

运行结果

在这里插入图片描述

系统文件I/O

系统调用相关接口

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。我们都知道一个进程想要打开一个文件需要操作系统的介入,操作系统为打开文件提供系统调用接口。

我们学习的C语言打开文件的接口,底层一定是封装了系统调用接口。

open

作用

打开文件

头文件

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

函数参数及返回值

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

参数

  • flags 参数是通过命令文件访问模式与其他可选模式相结合的方式来指定的,open 调用必须指定以下文件访问模式之一:

    1)O_RDONLY:以只读方式打开;

    2)O_WRONLY:以只写方式打开;

    3)O_RDWR :以读写方式打开。

    4)O_APPEND:把写入数据追加在文件的末尾;

    5)O_TRUNC:把文件长度设置为零,丢弃已有的内容

    这些模式用 | (按位或运算符)

  • pathname:要打开文件的路径

  • mode:如果文件不存在创建文件时赋给文件的权限

返回值

如果调用成功,它将返回一个可以被 read、write 和其他系统调用使用的文件描述符。这个文件描述符是唯一的,不会与任何其他运行中的进程共享。在调用失败时,将返回 -1 并设置全局变量 errno 来指明失败的原因。

下面我们举个例子来讲解flags参数的原理

#include<stdio.h>

#define Print1 1 // 0001 
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000

void Print(int flags)
{
    if(flags & Print1)
    {
        printf("hello 1\n");
    }

    if(flags & Print2)
    {
        printf("hello 2\n");
    }

    if(flags & Print3)
    {
        printf("hello 3\n");
    }

    if(flags & Print4)
    {
        printf("hello 4\n");
    }

}

int main()
{
    Print(Print2);
    Print(Print1 | Print2 | Print3);
    return 0;
}

按位或运算符是有一个为1就为1,那么通过按位或运算,得到一串二进制,最后判断哪个位上为1就证明用了此方式。

close

作用

关闭一个打开的文件

头文件

#include<unistd.h>

函数参数以及返回值

int close(int fildes);

参数

  • fildes:要关闭文件的文件描述符

返回值

当 close 系统调用成功时,返回 0,文件描述符被释放并能够重新使用;调用出错,则返回 -1

write

作用

向一个指定的文件标识符中写入数据

头文件

#include<unistd.h>

函数参数及其返回值

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

系统调用 write 的作用是把缓冲区 buf 的前 nbytes 个字节写入与文件描述符 fildes 关联的文件中。它返回实际写入的字节数。如果文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能会小于 nbytes。如果函数返回值为 0,就表示没有写入任何数据;如果返回值为 -1,则表明 write 系统调用出现了错误,错误代码保存在全局变量 errno 里。

注:

count要不要 + 1就是\0加入进去

  • 当我们想像文件中写入字符串的时候,不需要strlen + 1,\0是C语言的规定,不是文件的规定。

向有内容的文件中写入的时候是覆盖式的写入,如果要清空源文件中内容需要使用O_TRUNC选项

read

作用

从某个文件描述符中读取内容

函数头文件

#include<unistd.h>

函数参数及返回值

size_t read(int fildes,void *buf,size_t nbytes);

参数

  • fildes:文件描述符
  • buf:把文件读到的缓冲区
  • nbytes:从文件中读出的字节数

返回值

实际读到的字节数这可能会小于请求的字节数

C语言函数和 系统调用接口之间的关系

在C语言中,常用的文件操作函数包括fopen、fclose、fread、fwrite、fseek等。这些函数分别用于打开文件、关闭文件、从文件中读取数据、向文件中写入数据以及定位文件中的读写位置等操作。

fopen函数为例,它用于打开一个文件并返回一个指向该文件的FILE指针。在内部实现中,fopen函数会调用操作系统的open系统调用来打开文件,并处理相关的错误和异常情况。开发者在使用fopen函数时,无需关心底层的系统调用细节,只需按照函数接口说明传递参数即可。

文件描述符fd

从规律上来看我们发现fd是一个连续的小整数

文件在操作系统中的表现

每次操作系统运行代码时会为这个进程创建一个task_struct,进程打开文件的时候,相当于操作系统打开了很多文件,操作系统对打开的文件要创建对应的结构体来方便管理,这个结构体就叫做 struct file,这个就是一个被打开文件的描述结构体对象。

但是在操作系统中存在很多进程和很多的被打开的文件的描述结构体对象,那么一个进程怎么找到这个进程对应的打开的文件呢?

在操作系统内核中为进程设计了一个结构体叫做,struct files_struct,这个结构体中包含了一个数组,类型为struct file *fd array[]

当打开一个文件的时候 会帮我们创建一个struct file对象,让后把文件对象的地址填入到数组中,然后把数组的下标返回给上层,我们就把这个数组的下标叫做文件描述符。把这个数组叫做进程文件描述表。

文件描述符的本质就是数组的下标!!!

在这里插入图片描述

那么对应的C语言的接口中的FILE其实就是一个C语言提供的结构体类型,里面必定封装了文件描述符!!!

验证代码

int main()
{
    //打开文件
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        perror("open");
        return -1;
    }

    printf("log.txt的文件描述符 %d\n",fp->_fileno);
    printf("stdin的文件描述符 %d\n",stdin->_fileno);
    printf("stdout的文件描述符 %d\n",stdout->_fileno);
    printf("stderr的文件描述符 %d\n",stderr->_fileno);

    //关闭文件
    fclose(fp);
}

在这里插入图片描述

文件描述符的0,1,2

进程在运行的时候默认是把,标准输入(键盘),标准输出(显示器),标准错误(显示器)。在C语言中标准输入对应的是stdin,在系统中对应的为0,标准输出对应的是stdout,在系统中对应的是1,标准错误对应的是stderr,在系统中对应的是2。

标准错误

在Linux中,标准错误(stderr)是计算机程序与其环境之间预连接的一个重要的输出通信通道。它是程序用于输出错误消息、警告或其他诊断信息的输出流。以下是关于Linux中标准错误的详细解释:

定义与特点

  • 定义:标准错误是程序在运行时遇到错误、警告或其他非正常情况时,用于向用户报告这些信息的一个独立的输出流。
  • 特点:
    • 它是独立于标准输出(stdout)的,可以单独进行重定向。
    • 默认情况下,标准错误和标准输出都被连接到屏幕(通常是终端或控制台),但用户可以通过重定向操作来改变它们的目的地。

2>&1

把文件标识符1里面的内容覆盖到文件表示符2里

操作系统为什么默认要把stdinstdoutstderr打开呢

为了让程序员默认进行输入输出代码编写!!!

我们都听说过Linux下一切皆文件,如何理解呢

键盘,显示器,磁盘,网卡都有自己的读,写方法。读写方法都是不同的,但是当我们对文件操作的系统调用是只有一套的,这是因为在上层存在struct file结构体里面会存放着两个函数指针一个是int(*read)一个是int ( *write )这两个函数指针指向的就是底层的自己的读写方法。这一层就是虚拟文件系统简称VFS

在这里插入图片描述

fd的分配规则

进程默认已经打开了0,1,2我们可以直接使用0,1,2进行数据的访问

代码验证

nt main()
{
    //打开文件
    int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    printf("log.txt 的文件描述符为:%d\n",fd);
    //关闭文件
    close(fd);

}

在这里插入图片描述

文件描述符的分配规则是,寻找最小的,没有被使用的数据的位置,分配给指定的打开文件。

代码验证

int main()
{
    //关闭标准输入
    close(0);
    //打开文件
    int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    printf("log.txt 的文件描述符为:%d\n",fd);
    //关闭文件
    close(fd);
}

在这里插入图片描述

struct file内核对象

在Linux文件系统中,struct file 是内核中表示打开文件的一个核心数据结构。这个结构体包含了关于一个打开文件的所有信息,包括文件的状态(如当前读写位置、打开模式、文件状态标志等)、文件操作的方法(通过指向 struct file_operations 的指针实现)、以及与文件关联的底层文件系统或设备的信息。

当用户在用户空间打开一个文件时,内核会创建一个 struct file 实例来表示这个打开的文件。这个实例包含了从用户空间程序到内核中文件系统的所有必要链接和状态信息。

struct file 的一些关键成员包括:

  • f_pathf_dentryf_vfsmnt:这些成员包含了关于文件在文件系统中的位置的信息,包括文件的 dentry(目录项)和文件所在的 vfsmount(挂载点信息)。
  • f_op:指向 struct file_operations 的指针,后者包含了对该文件进行操作的函数指针集合,如读、写、打开、关闭等。
  • f_lock:用于保护文件结构的自旋锁,确保在多线程或多进程环境中对文件的访问是安全的。
  • f_count:表示文件被打开的次数。只有当 f_count 变为0时,文件才会被真正关闭,并且相关的 struct file 实例和底层资源才会被释放。
  • f_flagsf_mode:表示文件的打开标志和模式(如只读、只写、读写)。
  • f_pos:文件的当前读写位置。
  • f_ownerf_uidf_gid:与文件打开相关的用户和组信息。
  • private_data:一个指向任意数据的指针,通常由文件系统或设备驱动使用,以存储与特定文件实例相关的私有数据。
  • struct file结构体对象是在内核中创建,专门用来管理被打开文件

  • struct file结构体中存在着一个文件缓冲区,上层想读文件,前提必须要把磁盘中的文件的数据加载到文件缓冲区中。所以读数据要先把数据加载到内存

  • 当写数据的时候要讲数据先加载到文件缓冲区当中,然后最后换入到磁盘当中

所以我们在应用层进行数据的读写的本质就是将内核缓冲区中的数据,进行来回的拷贝。

重定向

dup2

我们可以不需要像上面一样先关闭一个再打开一个进行重定向,我们可以使用文件描述符表级别的数组的内容的拷贝。

我们可以使用dup2接口

函数参数及返回值

int dup2(int oldfd, int newfd)
  • 最后oldfd会覆盖newfdoldfd保留下来了
  • 此时newfd,oldfd指向相同的struct file,为了保证多个fd指向同一个struct file,引入了引用计数,在stuct file中存在f_count
输出重定向

代码

int main()
{
    //打开文件
    int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    dup2(fd,1);//输出重定向
    
    printf("hello file!\n");
    //关闭文件
    close(fd);
}

在这里插入图片描述

追加重定向

代码

int main()
{
    //打开文件
    int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND ,0666);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    dup2(fd,1);//追加重定向
    
    printf("hello file!\n");
    //关闭文件
    close(fd);
}

在这里插入图片描述

输入重定向

代码

int main()
{
    //打开文件
    int fd = open("log.txt", O_RDONLY );
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    dup2(fd,0);//输入重定向
    
    char buffer[1024];
    read(stdin->_fileno,buffer,1024);
    printf("%s\n",buffer);
   
    //关闭文件
    close(fd);
}

在这里插入图片描述

总结

重定向的本质,其实就是修改特定文件fd的下标内容

重定向原理

上层fd不变,底层fd指向的内容在改变

程序替换与重定向

程序替换不会影响曾经的重定向

为什么

程序替换只会替换代码和数据,对地址空间和PCB,文件描述符表是不受影响的。

本专栏为“小菜”linux学习之路
该文章仅供学习参考,如有问题,欢迎在评论区指出。

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值