老婆出轨 发表于 2024-11-3 21:01:44

【Linux】从open到write:系统文件I/O 的奥秘与实战指南

https://i-blog.csdnimg.cn/direct/ab555cabdc4243bbbf348cc94ef9a7d0.png


1.经典回首C文件接口

在使用C语言时,我们需要访问文件通常会用到fopen、 fwrite、和fread还有fclose等函数。
1.2 fwrite

比如此时我需要往文件中写入一些信息:
#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = fopen("test.txt","w");
    if(fp == NULL)
    {
      perror("fopen failed");
      return 1;
    }
    const char* str = "i am yui~\n";
    int len = strlen(str);
    int num = 5;
    while(num--)
    {
      fwrite(str,len,1,fp);
    }
    fclose(fp);
    return 0;
}

执行结果:
ubuntu@VM-20-9-ubuntu:~/FILETEST$ cat test.txt
i am yui~
i am yui~
i am yui~
i am yui~
i am yui~
1.2 fread

下面再来读一读文件中的内容:
#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = fopen("test.txt","r");
    if(!fp)
    {
      perror("fopen failed");
      return 1;
    }
    const char* str = "i am yui~\n";
    char s;
    int len = strlen(str);
    while(1)
    {

      ssize_t n= fread(s,1,len,fp);
      if(n == len)
      {
            s = 0;
            printf("%s",s);
      }

      if(feof(fp))
            break;
    }
      
    return 0;
}
输出结果:
i am yui~
i am yui~
i am yui~
i am yui~
i am yui~
2. 系统文件I/O

除了使用上述C接口,我们还可以接纳系统接口来访问文件。
系统文件 I/O(输入/输出)是指在操作系统层面进行文件的读写操作。在 Linux 和其他类 Unix 系统中,系统文件 I/O 通常通过系统调用(system call)完成。与 C 尺度库的文件 I/O 函数(如 fopen、fread、fwrite)相比,系统文件 I/O 提供了更底层的控制和更高的效率,但操作也稍显复杂。
为了更好的理解系统文件I/O,我会用系统接口来实现上面的功能,并进行讲解。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
    umask(0);//去除限制,防止后续干扰
    int fd= open("myfile",O_WRONLY|O_CREAT,0644);
    if(fd<0)
    {
      perror("open failed");
      return 1;
    }
    int num = 5;
    const char* str = "i am yui\n";
    int len = strlen(str);
    while(num--)
    {
      write(fd,str,len);
    }
    close(fd);
    return 0;
}

2 open函数

由于如今我们需要用系统接口来打开文件,那么我们会用到open函数而不是fopen函数。
open 函数是 Unix 和类 Unix 操作系统中的一个系统调用,用于打开文件并返回一个文件形貌符。这个文件形貌符用于后续的文件操作,如读、写、关闭等。相比 C 尺度库的 fopen 函数,open 提供了更底层的控制,更得当系统级编程。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);
2.1 参数介绍:



[*]pathname:要打开的文件的路径。
[*]flags:指定文件打开模式和行为的标记,决定文件的打开方式。
[*]mode:新文件的权限掩码,仅在O_CREAT标记创建文件时见效,指定文件的访问权限。
这之中的flags要好好聊聊。
[*]访问模式(必须包罗一个):

[*]O_RDONLY:只读模式打开文件。
[*]O_WRONLY:只写模式打开文件。
[*]O_RDWR:读写模式打开文件。
O_RDONLY、O_WRONLY 和 O_RDWR 中只能选择一个,它们控制文件的基础读写权限。

[*]文件创建和控制:

[*]O_CREAT:若文件不存在,则创建文件。此标记常与 mode 参数一起使用来指定文件的权限。
[*]O_EXCL:必须与 O_CREAT 组合使用。如果文件已存在,则返回错误,制止重复创建。这种组合常用于创建唯一文件。
[*]O_TRUNC:如果文件存在,而且是以写模式(O_WRONLY 或 O_RDWR)打开,文件长度会被截断为 0。
[*]O_APPEND:追加模式,写入操作时,文件指针会主动移动到文件末端,得当日记记录等追加写入的场景。

[*]非阻塞和同步控制:

[*]O_NONBLOCK:以非阻塞模式打开文件。对一些特殊文件(如设备文件)有效,得当需要立即返回结果的场景。
[*]O_SYNC:同步写入模式,确保数据立即写入磁盘。每次 write 操作都不会缓存到内存,而是直接刷新到存储设备,得当数据恒久性要求高的场景。
[*]O_DSYNC:数据同步,类似 O_SYNC,但只同步数据而不包括文件元数据(如最后修改时间)。
[*]O_RSYNC:同步读模式,和 O_SYNC 类似,但影响的是 read 操作。
我们需要选择符合的功能将它们进行|的操作,由于底层是用状态压缩来实现的,通过位运算(位掩码)来实现,使得每一个标记可以独立设置或清除,而不需要为每种组合单独存储。

在下面的写入操作我们只需要选择O_WRONLY|O_CREAT就可以了。
2.2 返回值(文件形貌符)



[*]成功时,open 返回一个文件形貌符(非负整数),用于后续的文件操作。
[*]失败时返回 -1,并设置 errno 来指示错误原因。
这里的返回值也很有说法,
文件形貌符(File Descriptor, FD)是操作系统分配的一个整数,用于表现每一个打开的文件或 I/O 资源。在 Unix 和类 Unix 系统(如 Linux)中,文件形貌符是历程和内核之间进行文件或资源操作的桥梁,几乎所有的 I/O 操作都是通过文件形貌符来完成的。
文件形貌符是一个非负整数,每个历程有一个文件形貌符表来管理文件形貌符。打开文件时,操作系统会分配一个文件形貌符,用于标识这个文件。该文件形貌符可以用于后续的读、写、关闭操作。文件形貌符不但用于文件,也可以表现其他 I/O 资源,如管道、网络套接字、设备文件等。
每个历程在启动时,通常有三个默认的文件形貌符,它们称为尺度文件形貌符:

[*]尺度输入(stdin):文件形貌符为 0,用于从用户或输入源读取数据。
[*]尺度输出(stdout):文件形貌符为 1,用于向终端或输出源输出数据。
[*]尺度错误(stderr):文件形貌符为 2,用于向终端输出错误信息。
0,1,2对应的物理设备一般是:键盘,表现器,表现器。
相识完这些后,我们就可以直接使用文件形貌符来直接向表现屏输出数据了。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main()
{
    char buf;
    ssize_t s = read(0,buf,sizeof(buf));//从键盘读入数据
    if(s>0)
    {
      buf = 0;
      write(1,buf,strlen(buf));
      write(2,buf,strlen(buf));
    }

    return 0;
}

//运行结果
/**
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out
hello world
hello world
hello world

*/
从这段代码我们也可以更加清晰地认识到Linux下的统统皆文件。
一些底层知识:
文件形貌符是从0开始地小整数,当我们打开文件时,操作系统在内存中要创建相对应地数据结构来形貌目标文件,于是就有了file结构体来表现一个已经打开地文件对象。而历程执行open系统调用,必须让历程和文件关联起来。每一个历程都有一个指针*file,指向一张表file_struct该表最重要地部分包罗一个指针数组,每个元素都是一个指向文件地指针。本质上文件形貌符就是该数组地下标,所以只要拿着文件形貌符就可以找到对应地文件。
https://i-blog.csdnimg.cn/direct/f05e64c0a0bd4880b67c774eaff1a57f.png
2.2.1 文件形貌符的分配规则

先看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    int fd = open("myfile",O_RDONLY);//只读模式打开
    if(fd>0)
    {
      printf("%d\n",fd);
    }
    close(fd);
    return 0;
}
//运行结果:
/*
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out
3
*/
结果是3。
那么当我们关闭0这个文件形貌符试试看呢?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(0);//关闭文件描述符0
    int fd = open("myfile",O_RDONLY);//只读模式打开
    if(fd<0)
    {
      perror("open");
      return 1;
    }
    printf("%d\n",fd);
    close(fd);
    return 0;
}
//打印结果:
/*
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out
0
*/
结果是0,你猜到了吗。
由此可见,文件形貌符的分配规则:在file_struct数组当中,找到当前没有直接使用的最小的一个下标,作为新的文件形貌符。
最后在来看看重定向
2.2.2 重定向

如今我们将标记输出1给关闭了,然后再打开一个文件再往内里写点东西,看看会发生什么。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    close(1);//关闭文件描述符1
    int fd = open("myfile",O_WRONLY|O_CREAT,0644);//写模式打开
    if(fd<0)
    {
      perror("open");
      return 1;
    }
    printf("fd:%d\n",fd);
    fflush(stdout);
    close(fd);
    return 0;
}
打开myfile发现,文件中存在fd:1。
也是就说,本该再表现屏中表现的内容被写进了myfile文件。我们把这种现象叫做重定向。常见的重定向>, >>, <
重定向的本质:
https://i-blog.csdnimg.cn/direct/1e0c93c8d5b142c09e1c858d97315a93.png
3. write函数

write 函数是 Unix 和 Linux 系统中进行文件写入操作的系统调用,用于将数据从用户空间的缓冲区写入到文件或设备(例如文件、管道、网络套接字)中。write 是一种底层 I/O 操作,它绕过尺度 I/O 缓冲区,直接写入文件形貌符指向的目标,常用于处理系统资源的原始数据读写。
语法:
ssize_t write(int fd, const void *buf, size_t count);
参数说明:


[*]fd:文件形貌符,表现要写入的目标文件或设备(例如 STDOUT_FILENO 表现尺度输出)。
[*]buf:缓冲区指针,指向要写入的数据。
[*]count:要写入的字节数,指定从 buf 中读取多少字节写入 fd。
返回值:
[*]成功时,返回实际写入的字节数(ssize_t 类型)。
[*]失败时,返回 -1,并设置 errno 变量来指示错误原因。
4. read函数

read 是 Unix 和 Linux 系统中的一个系统调用,用于从文件或其他输入资源(如管道、网络套接字等)中读取数据到用户提供的缓冲区中。与 write 相对应,read 直接从文件形貌符中获取数据,不经过尺度 I/O 缓冲区,得当低级别的 I/O 操作。
语法:
ssize_t read(int fd, void *buf, size_t count);
参数说明:


[*]fd:文件形貌符,表现要读取的文件或输入资源(例如 STDIN_FILENO 表现尺度输入)。
[*]buf:缓冲区指针,指向读取到的数据将要存放的位置。
[*]count:盼望读取的字节数,即 buf 的巨细。
返回值:
[*]成功时,返回实际读取的字节数(ssize_t 类型)。

[*]若返回 0,表现读取到文件末端(EOF)。

[*]失败时,返回 -1,并设置 errno 来指示错误原因。
5. 总结

fopen、fclose、fread、fwrite这些都是C语言尺度库的函数,也就是库函数。
open、close、read、write都是系统提供的接口,也就是系统调用接口。
而这部分库函数会区调用系统接口。
https://i-blog.csdnimg.cn/direct/30c5bab850fb49fcbf5c0d3b0f34573e.png
可以认为,f*系列的函数,都是对系统的封装,方便二次开辟。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】从open到write:系统文件I/O 的奥秘与实战指南