当前位置: 首页 > news >正文

Linux修炼:基础IO(一)

         Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》

感谢您打开这篇博客!希望这篇博客能为您带来帮助,也欢迎一起交流探讨,共同成长。

目录

1、回顾

2、回顾C语言中的文件操作

      2.1、C语言文件打开方式

      2.2、基本语法

      2.3、常用打开模式

      2.4、示例代码

      2.5、错误处理

      2.6、文件关闭

3、系统文件IO

      3.1、IO接口

        低级IO接口

        标准IO库(C语言阶段学习过)

      3.2、Linux系统下通过命令访问文件

      3.3、格式化输入与输出

      3.4、打印到显示器的多种方式

      3.5、C语言的文件操作接口与系统调用

      3.6、文件的管理方式

      3.7、文件描述符的分配规则

      3.8、输入重定向和输出重定向


1、回顾

        我们在学习C和C++时就接触过IO了,现在我们把过去学习的一些知识再回顾一下。

        文件等于内容加属性,我们想访问一个文件,就得打开它,打开文件就是把文件从磁盘加载到内存中。也就是说,如果一个文件没被打开,他就在磁盘上,如果文件被打开了,他就加载到了内存中。

        这个打开是由系统执行的。用户通过bash,启动进程,进程通过操作系统,执行系统调用,打开了文件。

        我们狭义理解的文件是存储在磁盘中的。磁盘作为外设,每次从磁盘中读取文件都是对外设的输入和输出,简称IO。

        尽管我们之前学过C/C++的文件操作,但是C/C++提供的文件操作接口,并不能直接进行文件读写操作,这些接口本质上还是调用了系统调用接口。

2、回顾C语言中的文件操作

      2.1、C语言文件打开方式

        在C语言中,文件操作主要通过标准库函数 fopen 实现。该函数用于打开文件并返回文件指针,供后续读写操作使用。

      2.2、基本语法

        文件打开的基本语法如下:

FILE *fopen(const char *filename, const char *mode);
  • filename:要打开的文件名(包含路径)
  • mode:文件打开模式

      2.3、常用打开模式

        以下是最常用的文件打开模式:

        读取模式

  • "r":以只读方式打开文本文件,文件必须存在
  • "rb":以只读方式打开二进制文件,文件必须存在

        写入模式        

  • "w":以只写方式创建文本文件,若文件存在则清空内容
  • "wb":以只写方式创建二进制文件,若文件存在则清空内容
  • "a":以追加方式打开文本文件,若文件不存在则创建
  • "ab":以追加方式打开二进制文件,若文件不存在则创建

        读写模式

  • "r+":以读写方式打开文本文件,文件必须存在
  • "rb+":以读写方式打开二进制文件,文件必须存在
  • "w+":以读写方式创建文本文件,若文件存在则清空内容
  • "wb+":以读写方式创建二进制文件,若文件存在则清空内容
  • "a+":以读写方式打开文本文件,写入时追加,若文件不存在则创建
  • "ab+":以读写方式打开二进制文件,写入时追加,若文件不存在则创建

      2.4、示例代码

#include <stdio.h>int main() {FILE *fp;// 以只读方式打开文本文件fp = fopen("example.txt", "r");if(fp == NULL) {perror("Error opening file");return 1;}// 文件操作代码...fclose(fp); // 关闭文件return 0;
}

      2.5、错误处理

fopen 函数在失败时会返回 NULL,因此应该检查返回值:

FILE *fp = fopen("nonexistent.txt", "r");
if(fp == NULL) {perror("Error");// 处理错误
}

      2.6、文件关闭

使用 fclose 函数关闭已打开的文件:

fclose(fp);

不关闭文件可能导致数据丢失或资源泄漏。

#include<stdio.h>
#include<unistd.h>int main()
{FILE* fp=fopen("log.txt","w");if(!fp){perror("fopen");return 1;}while(1){sleep(1);}fclose(fp);return 0;
}

        每个进程都有自己的当前工作路径,当我们调用fopen时,尽管我们只给了一个文件名,但是fopen内部会先获取当前进程的cwd,然后再当前进程的工作路径中创建文件。如果我们使用chdir更改当前工作路径,那么新建的这个文件就会直接新建到指定路径下。

        所以说,想到打开文件,必须要先找到文件,那就需要知道文件的路径和文件名,因此进程必须要有cwd。就比如在C语言阶段,我们就直到#include<>是编译器(进程)现在系统路径找,找不到了再在当前路径找,而#include""直接在当前路径找。

3、系统文件IO

      3.1、IO接口

        Linux系统提供了多种IO接口,用于处理文件、设备、网络等数据的输入输出操作。这些接口涵盖了从低级到高级的不同层次,满足不同场景的需求。我们这期先介绍低级(底层)系统接口。

        低级IO接口

        低级IO接口通常直接调用系统调用,提供最基本的文件操作功能:

  • open():打开或创建文件
  • read():从文件描述符读取数据
  • write():向文件描述符写入数据
  • close():关闭文件描述符
  • lseek():移动文件指针位置

示例代码:

#include <fcntl.h>
#include <unistd.h>int fd = open("file.txt", O_RDWR | O_CREAT, 0644);
char buffer[1024];
read(fd, buffer, sizeof(buffer));
write(fd, "Hello", 5);
close(fd);

        我们C语言的文件接口,本质上也是调用了这些系统调用接口。

       标准IO库(C语言阶段学习过)

        标准IO库(stdio)在低级IO基础上提供了缓冲功能,提高了IO效率(下期会介绍为什么提高了IO效率):

  • fopen():打开文件流
  • fread():从文件流读取数据
  • fwrite():向文件流写入数据
  • fclose():关闭文件流
  • fseek():移动文件流指针

示例代码:

#include <stdio.h>FILE *fp = fopen("file.txt", "r+");
char buffer[1024];
fread(buffer, 1, sizeof(buffer), fp);
fwrite("World", 1, 5, fp);
fclose(fp);

        文件分为文本文件和二进制文件。文件可以看成一维数组,因为文本文件可以被当作一个长字符串,换行无非就是'\n'。所以说,文件的读写位置就可以看作是一维数组的下标。

      3.2、Linux系统下通过命令访问文件

        在Linux系统中,执行echo "string" > "文件名"命令时,系统会以截断模式(对应C语言"w"模式)打开文件。该操作会清空文件原有内容,再将新字符串写入文件。

        使用echo "string" >> "文件名"命令时,系统以追加模式(对应C语言"a"模式)打开文件。该操作会在文件末尾追加新内容,保留原有数据。

        当然以上只是用C语言打个比方,echo的底层行为由Shell实现决定,内置版本通常直接调用系统调用,而外部版本可能间接使用stdio库。两者最终均通过内核接口完成输出。
        cat命令通过父进程fork出的子进程来实现文件内容输出。子进程调用fget,fread等函数读取文件内容,将数据写入标准输出(显示器)。这种设计遵循Unix的进程模型,实现了高效的文件操作和输出重定向功能。

      3.3、格式化输入与输出

        键盘和显示器都是字符设备。当我们想输入一个int变量123,实际上我们输入的是'1','2','3'这三个字符,在scanf函数内部,他会把这三个字符合起来存储到一个int变量中。当我们想往显示器打印一个int变量123时,其实是由printf函数,把123这个int类型变量,拆散成'1','2','3'三个字符,然后写入到显示器文件中。这就叫格式化输入与输出。

       读写二进制文件时,直接往文件中读写,把每个字节都照搬过来。而读写文本文件时,需要做格式化输入输出。读的是字符,要转换成整数,写的是整数,要转换成字符。这些工作都是由printf和scanf底层去实现的。当然如果我们要是打印字符串的话那就不用转换了。

      3.4、打印到显示器的多种方式

       向显示器中打印字符串有很多种方法,可以调用printf,fprintf,fputs,fwrite。那么这几种方式有什么区别,又有什么共同点呢?

        进程启动的时候,默认会打开三个输入输出流:stdin,stdout,stderr。其实就是打开三个文件。而以上四种打印到显示器的方式无不都是输出到stdout这个文件。其中,fprintf,fputs,fwrite在传参的时候都得显示的传入stdout,而printf默认绑定了stdout。

#include<stdio.h>
#include<string.h>int main()
{const char* s1="hello printf\n";printf(s1);                                                                                                                                                                               const char* s2="hello fprint\n";fprintf(stdout,s2);const char* s3="hello fputs\n";fputs(s3,stdout);const char* s4="hello fwrite\n";fwrite(s4,strlen(s4),1,stdout);return 0;
}

        其实这四个接口有一个共同点,就是都调用了系统调用接口open:

        如果文件存在,那就调用两个参数的open,如果这个文件不存在,那就调用三个参数的open。这三个参数分别是文件路径+文件名、打开文件的方式,设定权限(八进制数)。这里的文件打开方式flags其实就是一个整数(是系统定义好的宏)。这个int类型整数有32个比特位,所以一共有32个标志位。
        open成功时返回一个非负整数,表示文件描述符(File Descriptor)。该描述符是当前进程未使用的最小描述符,后续对文件的读写操作均通过此描述符进行访问。
        open失败时返回-1,同时设置全局变量errno以指示具体错误类型。常见的错误码包括:

  • ENOENT:文件或路径不存在。
  • EACCES:权限不足。
  • EEXIST:文件已存在(与O_CREATO_EXCL标志冲突时触发)。
  • EISDIR:尝试以写方式打开目录文件。

        系统提供了几种可传入到flags的宏,每个宏都对应一个数字。这些宏对应的数字有一个特点,就是只有一个比特位为1,换句话说,只有一个标志位。在传参的时候,我们可以把不同的宏通过|(或)的方式组合起来使用。这种传参的方式叫位图传参。

        以下是系统提供的几种宏:

  • O_RDONLY:只读模式。
  • O_WRONLY:只写模式。
  • O_RDWR:读写模式。
  • O_CREAT:如果文件不存在,则创建文件。
  • O_EXCL:与 O_CREAT 一起使用,确保文件不存在时才创建。
  • O_TRUNC:如果文件已存在且为普通文件,将其长度截断为 0(清空文件)。
  • O_APPEND:以追加模式打开文件。
  • O_NONBLOCK:以非阻塞模式打开文件。
  • O_SYNC:每次写操作都会同步到磁盘。

        需要注意的是,我们执行以下代码,他并不会给我们创建文件,因为在只写模式下没有文件创造文件的那是C语言,而这是系统调用接口,如果我们想让他创造文件需要或上O_CREAT。只要是新建文件,就必须指明文件的权限。但是由于umask的影响,如果我们想创建出权限为666的文件,可以暂时把该文件的umask改为0。

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd=open("log.txt",O_WRONLY);//umask(0);//int fd=open("log.txt",O_WRONLY|O_CREAT,0666);if(fd<0){perror("open");return 1;}close(fd);return 0;                                                                                                                                                                                 
}    

      3.5、C语言的文件操作接口与系统调用

        为什么C语言要封装文件操作接口呢?

        第一,系统调用麻烦,并且需要学习系统的相关知识才可以使用。在C语言的学习阶段我们是不会系统的,如果只有系统调用才能进行文件操作那么文件操作的学习成本太大。

        第二,Linux和其他操作系统的系统调用接口不一样!但是由于C语言跨平台的特性,在Linux上写的代码直接复制到其他操作系统上就能直接使用。所以说C语言对系统接口的封装本质上方便了我们程序员。

        第三,C语言的文件操作接口通常带有用户态缓冲机制,减少了频繁调用系统调用的开销。换一句通俗的话说,就是提高了IO效率。

        那么语言的跨平台性是如何实现的呢?

        对于C/C++这种编译型语言来说,他实际上实现了所有平台的系统调用接口的封装,通过#ifdef和#elif来判断当前的平台,什么平台他就编译对应的那一部分代码。

      3.6、文件的管理方式

        在系统中,每个被打开的文件都被对应的struct file管理。struct file中直接或间接包含了文件的属性和内容。由于系统中存在很多被打开的文件,这些struct file之间通过双链表连接,管理文件就成了对struct file进行增删查改。

        这些文件是被多个进程打开的,系统要怎么表示哪些文件是被哪些进程打开的呢?
        在进程pcb中,包含一个叫struct files_struct* files的指针,这个指针指向的结构体之中包含了一个指针数组struct file* fd_array[]。这个指针数组之中包含的指针指向了各个files_struct。而文件描述符实际上就是这个指针数组的下标。

        在打开文件时,系统还会把这个文件的部分属性和内容加载到内存中,然后在struct file内部用分别用两个指针指向加载到内存中的文件的属性和内容。内容是保存在文件内核缓冲区的。

        files_struct内部包含了文件的inode信息,inode包含的关键信息有以下几种:

  • 文件类型:普通文件、目录、符号链接等。
  • 权限:读、写、执行权限(如 rwxr-xr-x)。
  • 所有者与组:文件所属用户和组。
  • 大小与时间戳:文件大小、最后访问时间(atime)、修改时间(mtime)、元数据变更时间(ctime)。
  • 数据块指针:指向存储文件内容的磁盘块。

        当我们使用write写入文件时,并不是直接把字符串写入到磁盘中,而是把数据从用户空间拷贝到对应文件的内核缓冲区中。首先,write根据调用他的进程控制块pcb找到files_struct,然后根据文件描述符,找到要写入文件的struct fils_struct,接着把内容拷贝到fils_struct内部指针指向的文件内核缓冲区。内核缓冲区的内容什么时候写入到磁盘中就是由操作系统决定的了。我们进行任何文件增、删、查、改操作,都必须把文件的内容提前加载到文件内核缓冲区。

      3.7、文件描述符的分配规则

        我们打印几个文件描述符,发现文件描述符是从3开始的,那么为什么不是从0开始呢?

        系统内部是通过文件描述符来区分文件的,在C语言中,File*是定义文件的一个结构体,这个结构体中封装了文件描述符。而0,1,2这三个文件描述符(三个下标)被stdin,stdout,stderr这三个文件封装了起来。我们可以通过以下方式访问File*中封装的文件描述符:

int main()
{printf("stdin->%d\n",stdin->_fileno);printf("stdin->%d\n",stdout->_fileno);printf("stdin->%d\n",stderr->_fileno);return 0;                                                                                                                              
}               

        在C++中,是cin,cout,cerr这三个类封装了文件描述符0,1,2。

        分配描述符时,系统会从文件描述符数组中寻找最小的,没有被使用的下标,作为该文件的文件描述符。

      3.8、输入重定向和输出重定向

        输入重定向和输出重定向是怎么实现的呢?

        经过上面的介绍我们知道分配文件描述符时,会从小到大分配。如果我们现在执行close(0)把0号下标对应的文件关闭了,那么0号下标就空出来的。这时候我们打开文件,这个文件的描述符就被分配成了0号。这时候我们读取内容就是从这个文件中读的。基于此我们就实现了输入重定向。

        输出重定向同理,我们把1号下标分配给新打开的文件,这时候printf就把内容打印到了这个文件中,即完成了输出重定向。     

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{close(0);int fd=open("log.txt",O_RDONLY);//需要手动创建log文件,并写入内容if(fd<0){perror("fd");return 1;}int a,b,c;scanf("%d %d %d",&a,&b,&c);printf("%d %d %d\n",a,b,c);close(fd);
}

        当然,这只是底层实现的原理,如果我们想实现输入输出重定向可以通过系统调用接口实现。接下来我们介绍以下系统调用接口dup2:

        我们需要向dup2中传入两个文件描述符,dup2会把oldid中存储的内容(file*)拷贝到newid中,最后,下标newid和下标oldid都存储着原来下标oldid中的内容,也就是我们新打开的文件的file_struct。换句话说,我们想要完成输出重定向,需要执行的是dup2(fd,1);执行之后,我们即可以直接printf向文件中写入内容,也可以通过write向文件中写入内容

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd=open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);if(fd<0){perror("open");return 0;}dup2(fd,1);printf("hello Linux\n");const char* string="hello linux";write(fd,string,strlen(string));return 0;
}

        在创建子进程的时候,子进程同样会把父进程的文件描述表继承下来。子进程会先开辟一个一样的表,再把内容都拷贝过来。但是子进程不需要把文件拷贝过来,子进程和父进程指向的file是一样的。正因此,父子进程printf的时候,会同时打印到同一个显示器文件中。

        Bash默认打开了标准输入,标准输出,标准错误,所以Bash的子进程默认也都打开了标准输入,标准输出,标准错误!子进程和父进程共享的文件会通过引用计数的方式来共同使用。只有该文件的引用计数变为0,该文件才会关闭。所以说尽管子进程把1关闭,父进程照样能打印。

        好了,今天的内容就分享到这,我们下期再见!

http://www.dtcms.com/a/521835.html

相关文章:

  • 中山市网站制作app 推广
  • 免费制图网站做国外网站的公证要多少钱
  • leetcode 3461 判断操作后字符串中的数字是否相等I
  • 机器学习(8)梯度下降的实现与过拟合问题
  • h5游戏免费下载:维京战争
  • 门户网站建设投标书小程序软件定制开发
  • 外贸网站建设价格外贸网站推广公司最大
  • 【北京迅为】iTOP-4412精英版使用手册-第六十五章 Linux-定时器
  • 网页设计网站怎么做网站制作文案
  • rocky 9.5系统安装zabbix监控实现邮件告警
  • 梅河口网站建设张艺兴粉丝做的网站
  • 做杂志的网站有哪些哪个页面设计培训好
  • 国际贸易网站有哪些电影网站的建设目标
  • cuda13.0 torch2.9 python3.12 安装flash-attn window版的哪里有
  • 外贸公司网站制作公司网站的字体颜色
  • 免费源代码网站瑞安做网站
  • 专门做礼物的网站广州红盾信息门户网站
  • 单片机的开发——无人机篇(未完待续,有时间写)
  • 做网店哪个网站好中小互联网企业有哪些
  • 广州学建设网站啥都能看的浏览器
  • 丰浩网站建设中心软件开发合同范本免费
  • wordpress咋建站附近卖建筑模板市场
  • 性男女做视频观看网站响应式网站手机端尺寸
  • SpringBoot-Web开发之数据响应
  • 珠海专业网站建设费用360建筑官网
  • Linux 中的 DNS 工作原理(一):​​从 getaddrinfo 到 resolv.conf
  • 自己编程做网站骆诗网站建设
  • 在哪查找网站的建设者中文网站建设
  • python asyncio的各种用法与代码示例
  • 深圳网站营销型建设免费网络电话呼叫系统