14.[文件]Linux的文件
一,文件的理解
1.文件操作的本质:在编程语言层面,是调用库函数。具体来说用户创建进程,调用系统接口,交给操作系统,完成文件打开任务。
2.文件=内容+属性,未使用的文件位于Mass Storage中,使用的文件会被加载进内存中。
二,C语言文件操作的回顾
2.1文件打开
FILE * fopen ( const char * filename, const char * mode );
参数1:
当文件位于当前路径的时候,可以直接输入文件名
可以使用绝对路径
参数2:
文件打开失败,会返回空NULL。
2.2文件关闭
int fclose ( FILE * stream );
//对上面打开的文件进行关闭
//无论以哪种方式打开,关闭方法都一样
fclose(fp1);
fclose(fp2);
fclose(fp3);
fclose(fp4);
fclose(fp5);
fclose(fp6);
2.3文件写入
//逐字符写入
int fputc ( int character, FILE * stream );
//逐行写入
int fputs ( const char * str, FILE * stream );
fwrite格式化写入
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//1:指向要写入的数据的指针
//2:每个数据项的大小(以字节为单位)
//3:要写入的数据项的数量
//4:要写入的文件的指针
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
FILE* file;
char data[] = "Hello, World!";
size_t size = sizeof(data) / sizeof(data[0]);
file = fopen("example.txt", "wb");
if (file == NULL) {
printf("无法打开文件");
return 1;
}
//格式化输入
//size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//1:指向要写入的数据的指针
//2:每个数据项的大小(以字节为单位)
//3:要写入的数据项的数量
//4:要写入的文件的指针
size_t result = fwrite(data, sizeof(char), size, file);
if (result != size) {
printf("写入文件失败 ");
return 1;
}
fclose(file);
printf("写入成功");
return 0;
}
fprintf格式化写入
int fprintf( FILE *stream, const char *format, [ argument ]...)
#include <stdio.h>
int main() {
FILE* file;
int a = 10;
float b = 3.14;
file = fopen("example.txt", "w");
if (file == NULL) {
printf("无法打开文件");
return 1;
}
fprintf(file, "整数:%d", a);
fprintf(file, "浮点数:%.2f", b);
fclose(file);
printf("写入成功");
return 0;
}
snprintf()增加了sprintf的字符长度的控制
先看这么一个程序的例子:
#include <stdio.h>
#include <stdlib.h>
#define LOG "log.txt" //日志文件
#define SIZE 32
int main()
{
FILE* fp = fopen(LOG, "w");
if(!fp)
{
perror("fopen file fail!"); //报错
exit(-1); //终止进程
}
char buffer[SIZE]; //缓冲区
int cnt = 5;
while(cnt--)
{
snprintf(buffer, SIZE, "%s\n", "Hello File!"); //写入数据至缓冲区
fputs(buffer, fp); //将缓冲区中的内容写入文件中
}
fclose(fp);
fp = NULL;
return 0;
}
snprintf(缓冲区,缓冲区的大小,格式化输入(例如:**"%d\n", 10**
))
2.4文件读取
int fgetc ( FILE * stream );
char * fgets ( char * str, int num, FILE * stream );
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
int fscanf ( FILE * stream, const char * format, ... );
看sscanf的例子
#include <stdio.h>
int main()
{
char s[] = "2024:6:4";
int arr[3];
char* buffer[4];
sscanf(s, "%d:%d:%d", arr, arr + 1, arr + 2);
printf("%d\n%d\n%d\n", arr[0], arr[1], arr[2]);
return 0;
}
结果输出
2024
6
4
三,系统级文件操作
3.1打开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); //可以修改权限
参数1:待操作文件符
参数2:flags使用标记位的方式传递选项信号,可传递32个选项
关于位图的小demo
#include <stdio.h>
#include <stdlib.h>
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
void Test(int flags)
{
//模拟实现三种选项传递
if(flags & ONE)
printf("This is one\n");
if(flags & TWO)
printf("This is two\n");
if(flags & THREE)
printf("This is three\n");
}
int main()
{
Test(ONE | TWO | THREE);
printf("-----------------------------------\n");
Test(THREE); //位图使得选项传递更加灵活
return 0;
}
参数列表:
O_RDONLY //只读
O_WRONLY //只写
O_APPEND //追加
O_CREAT //新建
O_TRUNC //清空
参数3:mode权限设置
代码展示
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> //write 的头文件
#define LOG "log.txt" //日志文件
#define SIZE 32
int main()
{
//三种参数组合,就构成了 fopen 中的 w
int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //权限最好设置
if(fd == -1)
{
perror("open file fail1");
exit(-1);
}
const char* ps = "Hello System Call!\n";
int cnt = 5;
while(cnt--)
write(fd, ps, strlen(ps)); //不能将 '\0' 写入文件中
close(fd);
return 0;
}
演示
会发现实际权限为644
这是因为系统减去了umask掩码0022
0666-0022=0644为0110 0100 0100 rw-r-r
3.2关闭close
#include <unistd.h>
int close(int fildes);//根据文件描述符关闭文件
//stdin=0 stdout=1 stderr=2
3.3写入write
#include <unistd.h>
ssize_t write(int fildes, const void *buf, size_t nbyte);
3.4读取read
#include <unistd.h>
ssize_t read(int fildes, void *buf, size_t nbyte);
example1:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd=open("log.txt",O_RDONLY);
char buffer[100];
if(fd==-1)
{
printf("无法打开文件\n");
return -1;
}
printf("%d\n",sizeof(buffer)-1);
ssize_t bytes_read =read(fd,buffer,sizeof(buffer)-1);
printf("%d\n",bytes_read);
buffer[bytes_read]='\0';
printf("读取到的数据:\n%s",buffer);
close(fd);
return 0;
}
example2:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> //write 的头文件
#define LOG "log.txt" //日志文件
#define SIZE 1024
int main()
{
int fd = open("test.c", O_RDONLY);
if(fd == -1)
{
perror("open file fail1");
exit(-1);
}
int n = 50; //读取50个字符
char buffer[SIZE];
int pos = 0;
while(n--)
{
read(fd, (char*)buffer + pos, 1);
pos++;
}
printf("%s\n", buffer);
close(fd);
return 0;
}
四,文件描述符fd
文件描述符
文件描述符fd表示一个file对象,进行实际操作的时候os只需要使用相应的fd就可以。
其实对于C语言的FILE,其中包含了文件描述符这个成员。
Q:现在有个问题就是文件描述符是怎么设计的呢?
需求:如果不设计文件描述符,那么OS就会把所有文件都扫描一边然后才能找到目标文件。
设计:所以根据先描述再组织的设计原则,OS将所有文件设计为file对象,获取他们的指针,将这些指针存入指针数组中以便进行高效的随机访问和管理,这个数组为**file* fd_array[]**
,而数组的下标就是神秘的 文件描述符 **fd**
。
当一个程序启动的时候,OS会默认打开标准输入、标准输出、标准错误这三个文件流,将他们的指针存入fd_array,分别为0 1 2!
**N_Q:**各种文件属性汇集在一起,构成了**struct files_struct**
这个结构体,而它正是 **task_struct**
中的成员之一。
2.fd的分配原则:先来后到
先来后到,优先使用当前最小的、未被占用的 **fd**
#include<iostream>
#include <cstdio>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
//先打开文件 file.txt
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1); //存在打开失败的情况
cout << "单纯打开文件 fd: " << fd << endl;
close(fd); //记得关闭
//先关闭,再打开
close(1); //关闭1号文件执行流
fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout << "先关闭1号文件执行流,再打开文件 fd: " << fd << endl;
close(fd);
return 0;
}
当关闭1号文件流的时候,stdout关闭了,那么此时cout就会向fd为1的文件流中打印东西~效果:
而这个就是重定向的基本操作
3.一切皆文件
其实从stdin和stdout的fd中就可以窥见一斑~
五,重定向
顺着四,2来继续谈论重定向
标准输入(
**stdin**
)-> 设备文件 -> 键盘文件标准输出(
**stdout**
)-> 设备文件 -> 显示器文件标准错误(
**stderr**
)-> 设备文件 -> 显示器文件
5.1重定向的本质:将三个标准流的原文件执行流进行替换
5.2指令重定向
echo you can see me > file.txt
读数据
cat < file.txt
>:标准输出重定向为文件流
>>:追加写入
<:从文件流中,标准输入式的读取数据
practice:实现运行程序的重定向
#include <iostream>
using namespace std;
int main()
{
cout << "标准输出 stdout" << endl;
cerr << "标准错误 stderr" << endl;
return 0;
}
need1:将标准输出和错误定向到文件中
./test_redirect >file.txt 2>&1
need2:将标准错误定向到文件中
./test_redirect 2> file.txt
need3:将标准输出和错误定向到文件中
./test_redirect >file.txt 2>&1
need4:将标准输入和错误分别定向到文件中
./test_redirect 1>file.txt 2>file2.txt
5.3利用函数重新定向
int dup2(int oldfd, int newfd)
将老的 **fd**
重定向为新的 **fd**
,参数1 **oldfd**
表示新的 **fd**
,而 **newfd**
则表示老的 **fd**
,重定向完成后,只剩下 **oldfd**
,因为 **newfd**
已被覆写为 **oldfd**
了;如果重定向成功后,返回 **newfd**
,失败返回 **-1**
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
//打开两个目标文件
int fdNormal = open("log.normal", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdError = open("log.error", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fdNormal != -1 && fdError != -1);
//进行重定向
int ret = dup2(fdNormal, 1);
assert(ret != -1);
cout<<ret<<endl;
ret = dup2(fdError, 2);
assert(ret != -1);
cout<<ret<<endl;
//重定向打印
for(int i = 10; i >= 0; i--)
cout << i << " "; //先打印部分信息
cout << endl;
int fd = open("cxk.txt", O_RDONLY); //打开不存在的文件
if(fd == -1)
{
//对于可能存在的错误信息,最好使用 perror / cerr 打印,方便进行重定向
cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
//errno:C标准库中的一个全局变量,用于存储最近一次系统调用或库函数调用失败时的错误码。
//每次系统调用或库函数调用失败时,errno会被设置为对应的错误码
//strerror是C标准库中的一个函数,用于返回一个指向描述错误码errno的字符串的指针。这样可以将错误码转化为对应的可读性较高的错误信息。
exit(-1); //退出程序
}
close(fd);
return 0;
}
我们将在下一个篇章中再对简单shell进行升级**[重定向]**
六,缓冲区
生活中的缓冲区:垃圾桶,猫粮碗,电池
1.为什么需要缓冲区
实验:测试IO和没有IO的时候cpu的算力差别:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int count = 0;
int main()
{
//定一个 1 秒的闹钟,查看算力
alarm(1); //一秒后闹钟响起
while(true)
{
cout << count++ << endl;
}
return 0;
}
void (*signal(int signum, void (*handler)(int)))(int);
//signum:信号编号,表示你想处理的信号。
//handler:信号处理程序,是一个函数指针,指向当信号发生时需要执行的处理函数。
//void (*handler)(int):一个指向以 int 类型参数为输入且无返回值的函数的指针
//void (函数名)(int);func 返回一个函数指针,指向的函数接受一个 int 类型的参数且无返回值。
程序开始运行后,
alarm(1)
设置了一个1秒的闹钟。当1秒过去后,系统会发送
SIGALRM
信号。#include
#include <unistd.h>
#include <signal.h>
#include<stdlib.h>
using namespace std;int count = 0;
void handler(int signo)
{
cout << "count: " << count << endl;
exit(1);
}int main()
{
//定一个 1 秒的闹钟,查看算力
signal(14, handler);
alarm(1); //一秒后闹钟响起
while(true) count++;return 0;
}
由此可见是否启动IO对cpu的算力影响巨大
因此,需要借助缓冲区buffer进行辅助读取和写入
2.使用缓冲区
#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1);
char buffer[256] = { 0 }; //缓冲区
int n = read(0, buffer, sizeof(buffer)); //读取信息至缓冲区中
buffer[n] = '\0';
//写入成功后,在写入文件中
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
//这里fd为stdin 0
3.缓冲区的刷新策略
无缓冲 -> 没有缓冲区
行缓冲 -> 遇到 **\n**
才进行刷新,一次冲刷一行
全缓冲 -> 缓冲区满了才进行刷新
显示器的刷新策略为****行缓冲
普通文件的刷新策略为****全缓冲
对于c语言,scanf遇到空白字符或者换行就会刷新,因此输入的时候需要按下回车,缓冲区中的数据才能刷新到内核缓冲区中。printf刷新策略为行缓冲。
4.关于缓冲区的位置
其实缓冲区是被FILE*内部来进行维护的
(查看:vim /usr/include/libio.h)
4.普通缓冲区与内核级缓冲区
参考阅读:
1.https://zhuanlan.zhihu.com/p/625185749
practice:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
int main()
{
fprintf(stdout, "hello fprintf\n");
const char* str = "hello write\n";
write(1, str, strlen(str));
fork(); //创建子进程
return 0;
}
两种不同的现象:
现象1:直接运行
现象2:重定向结果
结果是有差别的
解释:
现象1:
fprintf(stdout, "hello fprintf\n");
:因为是行缓冲,所以立即输出到终端。write(1, str, strlen(str));
:直接写到终端,没有缓冲,因此立即输出。fork();
:创建子进程。父进程和子进程都会继续执行,且由于缓冲区的内容已经在父进程中输出,所以不会重复。
现象2:
fprintf(stdout, "hello fprintf\n");
:由于是重定向,stdout
变为全缓冲,缓冲区不会立即输出。write(1, str, strlen(str));
:直接写到文件,立即输出。fork();
:子进程复制了父进程的缓冲区状态,因此父子进程在后续执行时都会输出缓冲区的内容,导致重复输出。
如果想保持结果一样,就要立即刷新缓冲区
fprintf(stdout, "hello fprintf\n");
fflush(stdout); // 立即刷新缓冲区