✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:Linux学习
贝蒂的主页:Betty’s blog
1. C语言文件操作
C语言文件操作接口如下,详情可参照——C语言文件
文件操作函数 | 功能 |
---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
读写方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 出错 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
下面是一个使用C语言文件的示例:
#include<stdio.h>
int main()
{
FILE*fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen fail:");
return 1;
}
//open success
const char*msg="hello betty!\n";
int count=5;
while(count--)
{
fputs(msg,fp);
}
fclose(fp);
return 0;
}
一般而言如果没有定义对应的log.txt
文件,系统会在当前路径自动创建该文件。并且当前路径并不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。比如我们可以在上级目录执行testfile
文件:
可以看见log.txt
是在该对应路径创建的,而不是对应可执行文件所在目录创建的。
其中我们也可以通过监视进程的方式,观察一下:
然后我们可以看见两个软连接cwd
和exe
,分别对应的就是进程运行时我们所处的路径,以及可执行文件所处路径。
2. 三个默认打开流
我们常说Linux
下一切皆文件,那么我们的键盘与显示器自然也是文件。我们向键盘输入数据,本质就是操作系统向键盘文件中读取数据;我们能从显示器看见数据,本质就是操作系统向显示器文件写入数据。但是我们在使用键盘与显示器时并没有手动进行任何文件相关的读写操作,那我们又是如何对键盘文件与显示器文件进行读写的呢?
答案自然是操作系统自动帮我们打开的,任何进程在运行时,操作系统都会默认打开三个输入输出流,分别为:标准输入流,标准输出流以及标准错误流。对于C语言分别就是:stdin
、stdout
以及stderr
。对于C++分别就是:cin
、cout
和cerr
,自然其他语言也会有相似的概念,因为这是操作系统所支持的,而不是某个语言所独有的。
我们可以在Linux
中的man
查看对应的声明:
其中标准输入流对应的就是我们的键盘,而标准输出流与标准错误流对应的就是我们显示器。
其中我们也可以通过fputs
函数验证一下:
#include<stdio.h>
int main()
{
//向显示器打印
fputs("hello betty!\n",stdout);
fputs("hello betty!\n",stdout);
fputs("hello betty!\n",stdout);
fputs("hello betty!\n",stdout);
return 0;
}
3. 系统文件I/O
在前面我们学习操作系统时知道,为了方便用户使用,一般我们会对系统接口进行封装。我们的文件操作也不例外,像fopen
,fclose
等接口本质其实对操作系统提供的文件接口的封装。接下来我们就来学习一下系统提供的文件接口。
3.1 open函数
首先我们来介绍文件打开操作的系统接口。
pathname
:表示打开或者创建的目标文件,若pathname
以路径的方式给出,则当需要创建该文件时,就在pathname
路径下进行创建。若pathname
以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。- ·
flags
:表示打开文件的方式。mode
:表示创建文件的默认权限(八进制数)。
其中常用文件打开方式有如下几个:
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
如果想同时兼具多个打开方式,可以使用逻辑与|
链接两个选项。比如说我们想打开文件并且文件不存在时创建文件,可以写成:
O_WRNOLY|O_CREAT
这些选项本质也就是一个宏定义,其中flags
是一个整型,若将一个比特位作为一个标志位,则理论上flags
可以传递32种不同的标志位。
所以我们也可以使用按位与&
操作来检测是否设置某个选项:
if (flags&O_RDONLY){
//设置了O_RDONLY选项
}
if (flags&O_WRONLY){
//设置了O_WRONLY选项
}
if (flags&O_RDWR){
//设置了O_RDWR选项
}
if (flags&O_CREAT){
//设置了O_CREAT选项
}
//...
并且如果我们打开的文件已存在就使用第一个接口(两个参数),如果打开的文件不存在就需要使用第二个接口(三个参数),即需要为创建的文件设置默认权限。
如果我们要为文件设置默认权限,就需要考虑文件默认掩码umask
的影响。我们之前讲过文件的默认权限为:mode&(~mask)
,我们除了可以在命令行通过指令umask 八进制数
来修改默认的掩码umask
(默认为002)外,还能在程序中调用umask
函数进行修改。比如我们将umask
设置为0:
umask(0); //将文件默认掩码设置为0
最后再来探究一下open
的返回值,也就是文件描述符fd
。
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);//设置文件掩码为0
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
运行之后我观察到文件描述符是从3开始的,并且依次递增,这起始并不是偶然。至于为什么,我们等会儿在揭晓。
当然这只是文件成功返回的情况,如果文件打开失败,那将返回-1。
3.2 close函数
我们可以调用系统接口close
来关闭指定文件,其原型为:
int close(int fd);
使用close
函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
3.3 write函数
同样我们也能通过系统接口write
对文件进行写入,其原型为:
ssize_t write(int fd, const void *buf, size_t count);
其中fd
指的是文件描述符,buf
为用户缓冲区,而count
为期望写的字节数。如果写入成功返回实际写入的字节数,若写入失败则返回-1。
注意:ssize_t
其实就是一个有符号整型,具体来说就是被typedef
重新定义过:typedef int ssize_t
以下我们可以利用write
函数对一个log.txt
文件进行写入:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT);
if(fd<0)
{
//open error
perror("open fail:");
return 1;
}
const char*msg="hello betty!\n";
for(int i=0;i<8;i++)
{
write(fd,msg,strlen(msg));
}
close(fd);
return 0;
}
3.4 read函数
同样我们也能通过系统接口read
对文件进行读写,其原型为:
ssize_t read(int fd, void *buf, size_t count);
其中fd
指的是文件描述符,buf
为用户缓冲区,而count
为期望读的字节数。如果读出成功返回实际读出的字节数,若读出失败则返回-1。
以下我们可以利用read
函数对一个log.txt
文件进行读出:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd=open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open fail:");
return 1;
}
char buf[1024]={'\0'};
ssize_t ret=read(fd,buf,1023);
if(ret>0)
printf("%s",buf);
close(fd);
return 0;
}
4. 文件描述符——fd
在我们的操作系统中,文件是由我们进程所打开的,存在大量进程就意味着存在大量被打开的文件。为了方便我们对文件进行管理,我们就将每个文件struct file
链入我们的双向链表之中。
struct File
{
//包含了打开文件的相关属性
//链接属性
};
而一个文件也可能被多个进程所读写,为了让操作系统能够准确识别每个进程对应的文件,我们就一定要让进程与我们的文件建立联系。事实也是如此,我们的进程控制块task_struct
中就存在一个指针指向一个名为struct file_struct
的结构体,这个结构体中存在一个结构体指针数组struct file*fd_array[]
分别存放着着每个文件struct file
的地址。这样我们的进程就与文件建立起了联系。
一般我们的指针数组struct file*fd_array[]
的0,1,2下标分别对应我们的标准输入流,标准输出流,标准错误流这三个文件,而这些下标就是我们所说的文件描述符——fd。这也解释了我们打开文件的描述符为什么从3开始,并且依次递增。并且,通过对应的文件描述符,进程只需要找到对应的指针数组fd_array
就能访问对应的文件,这也是为什么我们文件的系统调用接口的参数一定会有fd
的原因。
当然如果我们在中途关掉某个文件,操作系统就会为该下标重新分配对应的文件。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(0);
close(2);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
我们也知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct
、mm_struct
、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。如果与我们的文件管理联系起来,就是一个磁盘文件log.txt
加载进内存形成内存文件,最后加入对应双向链表中管理起来。
当文件存储在磁盘上时,我们称之为磁盘文件。而当磁盘文件被加载到内存中后,就变成了内存文件。磁盘文件与内存文件的关系,恰似程序和进程的关系。程序在运行起来后成为进程,同样,磁盘文件在加载到内存后成为内存文件。磁盘文件主要由两部分构成,即文件内容和文件属性。文件内容指的是文件中存储的数据,而文件属性则是文件的一些基本信息,包括文件名、文件大小以及文件创建时间等。这些文件属性也被称为元信息。在文件加载到内存的过程中,一般会先加载文件的属性信息。这是因为在很多情况下,我们可能只需要了解文件的基本属性,而不一定立即需要对文件内容进行操作。当确实需要对文件内容进行读取、输入或输出等操作时,才会延后式地加载文件数据。这样的设计可以提高系统的效率,避免在不必要的时候浪费资源加载大量的文件数据。