14.[文件]Linux的文件

14.[文件]Linux的文件

一,文件的理解

1.文件操作的本质:在编程语言层面,是调用库函数。具体来说用户创建进程,调用系统接口,交给操作系统,完成文件打开任务。

2.文件=内容+属性,未使用的文件位于Mass Storage中,使用的文件会被加载进内存中。

二,C语言文件操作的回顾

2.1文件打开

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

参数1:

  1. 当文件位于当前路径的时候,可以直接输入文件名

  2. 可以使用绝对路径

参数2:

image

文件打开失败,会返回空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个选项

image

关于位图的小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;
}

image

参数列表:

 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;
}

演示

image

image

会发现实际权限为644

image

这是因为系统减去了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的文件流中打印东西~效果:

image

而这个就是重定向的基本操作

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;
}

image

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;
    

    }

image

由此可见是否启动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

image

3.缓冲区的刷新策略

无缓冲 -> 没有缓冲区

行缓冲 -> 遇到 **\n** 才进行刷新,一次冲刷一行

全缓冲 -> 缓冲区满了才进行刷新

显示器的刷新策略为****行缓冲

普通文件的刷新策略为****全缓冲

对于c语言,scanf遇到空白字符或者换行就会刷新,因此输入的时候需要按下回车,缓冲区中的数据才能刷新到内核缓冲区中。printf刷新策略为行缓冲。

4.关于缓冲区的位置

其实缓冲区是被FILE*内部来进行维护的

(查看:vim /usr/include/libio.h)

image

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:重定向结果

结果是有差别的

image

解释:

现象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);  // 立即刷新缓冲区

image

相关推荐

  1. Linux 文件权限管理

    2024-06-11 04:30:03       40 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-11 04:30:03       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-11 04:30:03       5 阅读
  3. 在Django里面运行非项目文件

    2024-06-11 04:30:03       4 阅读
  4. Python语言-面向对象

    2024-06-11 04:30:03       7 阅读

热门阅读

  1. C++线程

    2024-06-11 04:30:03       16 阅读
  2. HOT100与剑指Offer

    2024-06-11 04:30:03       21 阅读
  3. 游戏心理学Day10

    2024-06-11 04:30:03       14 阅读
  4. 使用EFCore和Linq查询语句封装复杂的查询结果

    2024-06-11 04:30:03       20 阅读
  5. Python爬虫实现“自动重试”机制的方法(1)

    2024-06-11 04:30:03       13 阅读
  6. OpenAI 发布的 GPT-4o是什么,有什么功能?

    2024-06-11 04:30:03       19 阅读