<Linux> 文件理解与操作

目录

前言:

一、关于文件的预备知识

二、C语言文件操作

1. fope

2. fclose

3. 文件写入

3.1 fprintf

3.2 snprintf

三、系统文件操作

1. open

2. close

3. write

4. read

四、C文件接口与系统文件IO的关系

五、文件描述符

1. 理解文件描述符

2. 文件描述符的分配规则

六、再次理解一切皆文件

七、重定向

1. FILE

2. 重定向的本质

3. 利用指令重定向

4. 输出重定向

5. 输入重定向

6. 追加重定向 

7. 函数重定向

八、缓冲区


前言:

        文件操作对于不少编程初学者来说都是一件头疼的事情,不管你学习的是什么编程语言,C/C++/Java/Go/Python ,因为我们学习到的文件相关的操作对你来说都是一个黑盒,我们只是知道我们应该怎么使用相应的文件函数进行操作,而对于内部的实现方式的一无所知,这导致很多初学者对于文件操作都很恐惧。

        其实这不是你的问题,因为文件操作涉及了许多操作系统相关的知识,我们只有站在操作系统的角度才能对文件操作有比较深刻的理解,所以本篇文章我们来学习一下Linux下的基础IO操作,来加深我们对于语言级别文件操作以及系统的理解。

一、关于文件的预备知识

文件操作的本质是什么?

  • 针对文件的操作都是对文件内容和属性的操作,语言层面的文件操作就是直接使用库函数,而事实上,文件操作是系统层面的问题,就像进程管理一样,系统也会通过 先描述,再组织 的方式对文件进行管理、操作。

只有 C/C++ 这种偏底层的语言才有文件操作吗?

  • 并不是,其他语言也支持文件操作,如 Java。在进行文件操作时,不同语言使用方法可能有所不同,但 本质上都是在调用系统级接口进行操作

文件由什么构成?一般文件放在哪里?

  • 文件 = 内容 属性
  • 当我们没有打开文件时,文件位于磁盘中,当文件被打开时,文件就要被操作系统加载到内存中。这也是冯诺依曼体系结构对我们文件操作的限制!
  • 本文讨论的是已被加载至内存文件的相关操作

文件是由谁打开的?

  • 由用户创建进程,调用系统级接口,再交给 OS 完成文件打开任务,文件写入与读取时也是同理 

系统是如何管理文件的?

  • 实际进程在运行的过程中肯定会打开多个文件,对于多个文件我们操作系统要将它们组织起来以便于管理和维护,会为被打开的文件创建相应的内核数据结构。
  • 像使用 task_struct 管理进程一样,通过 struct file 存储文件属性进行管理
  • struct file 结构体包含了文件的各种属性和链接关系

调用库函数进行文件操作时的流程:

二、C语言文件操作

1. fopen

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

fopen 函数返回值类型为 FILE 。参数列表中, path 为文件路径, mode 为打开方式。 

注意: 若参数1直接使用文件名,则此文件需要位于当前程序目录下,如果想指定目录存放,可以使用绝对路径

 打开文件的方式有如下几种:

r打开文本文件进行阅读。若文件不存在,会打开失败
流位于文件的开头。
r+开放读和写。
流位于文件的开头
w打开文件进行写入。如果文件不存在,则创建该文件,写入前,会先清空内容
流位于文件的开头。
w+开放读和写。如果文件不存在,则创建该文件,否则文件被清空。
流位于文件的开头。
a打开以追加(在文件末尾写入),追加前不会清空内容
如果文件不存在,则创建该文件。
流位于文件的末尾。
a+打开以进行读取和追加(在文件末尾写入)。
如果文件不存在,则创建该文件。用于读取的初始文件位置位于文件的开头,
但输出始终附加到文件末尾。
//打开文件进行操作
//在当前目录中打开文件 log.txt
//注意:同一个文件,可以同时多次打开
FILE* fp1 = fopen("log.txt", "w");	//只读
FILE* fp2 = fopen("log.txt", "a");	//追加
FILE* fp3 = fopen("log.txt", "r");	//只写,文件不存在会打开失败
FILE* fp4 = fopen("log.txt", "w+");	//可读可写
FILE* fp5 = fopen("log.txt", "a+");	//可读可追加
FILE* fp6 = fopen("log.txt", "r+");	//可读可写,文件不存在会打开失败

若文件打开失败,会返回空 NULL,可以在打开后判断是否成功

 2. fclose

文件打开并使用后需要关闭,就像动态内存申请后需要释放一样

int fclose(FILE *stream);

关闭已打开文件,只需通过 FILE* 指针进行操作即可。只能对已打开的文件进行关闭,若文件不存在,会报错

//对上面打开的文件进行关闭
//无论以哪种方式打开,关闭方法都一样
fclose(fp1);
fclose(fp2);
fclose(fp3);
fclose(fp4);
fclose(fp5);
fclose(fp6);

3. 文件写入

C语言 对于文件写入有这几种方式:fputcfputsfwritefprintf 和 snprintf

int fputc(int character, FILE *stream);int fputs(const char *str, FILE * stream ;size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);int fprintf(FILE *stream, const char *format, ...);int snprintf(char *s, size_t n, const char *format, ...);

前几种方式比较简单,无非就是 逐字符写入、逐行写入 与 格式化写入,这里来介绍一下 fprintfsnprintf 。

3.1 fprintf

int fprintf(FILE *stream, const char *format, ...);

与 printf 不同, printf 默认向显示器打印消息,而 fprintf则可以指定文件流,向指定文件打印。

#include <stdio.h>
#define LOG "log.txt" // 自定义文件int main()
{FILE* fp = fopen(LOG, "w");// 写方式打开文件if(fp == NULL){perror("fopen");return 1;}const char* msg = "good morning";                                                                                                      int cnt = 3;while(cnt){fprintf(fp, "%s: %d\n", msg, cnt);//fputs(msg, fp);cnt--;}fclose(fp);return 0;
}

 也可以使用 fprintf 函数向显示器文件里打印,以实现和 printf 函数相同的效果: 

3.2 snprintf

int snprintf(char *str, size_t size, const char *format, ...);

snprintf 是 sprintf 的优化版,增加了读取字符长度控制,更加安全

  • 参数1:缓冲区,常写做 buffer 数组,可以把内容打印到缓冲区
  • 参数2:缓冲区的大小,通过设置参数 size 控制打印的长度。
  • 参数3:格式化输入

使用 snprintf 函数写入数据至缓冲区后,可以通过 fputs 函数,将缓冲区中的数据真正写入文件中:

while(cnt)
{char buffer[256];snprintf(buffer, sizeof(buffer), "%s: %d\n", msg, cnt);//写入数据至缓冲区fputs(buffer, fp);//将缓冲区中的内容写入文件中cnt--;
}

三、系统文件操作

1. open

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

我们打开man手册在2号目录里面可以看到 open的函数原型以及介绍: 

  • 参数1:pathname 待操作文件名,和 fopen 一样,文件路径可以是相对路径,也可以是绝对路径
  • 参数2:flags打开选项,open 以位图形式传递选项信号,用一个 int 至多可以表示 32 个选项
  • 参数3:mode 权限设置,文件起始权限为 0666
  • 返回值:如果打开成功会返回一个大于或等于0整数,这个整数叫做文件描述符,用来标定一个文件,如果打开失败会返回-1表示打开失败。下面会详细解释

参数2解释:使用了 位图 的方式进行多参数传递

可以利用这个特性,写一个关于位图的小demo

#include <stdio.h>
#include <stdlib.h>#define ONE 0x1
#define TWO 0x2
#define THREE 0x4void 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(ONE);  //位图使得选项传递更加灵活return 0;
}

 函数 open 中的参数2正是位图,其参数有很多个,这里列举部分

  • O_RDONLY 只读打开                    (英文read only的缩写)
  • O_WRONLY 只写打开                    (英文write only的缩写)
  • O_CREAT 文件不存在就创建该文件             (英文creat)
  • O_TRUNC 文件每次打开都会进行清空         (英文truncate)
  • O_APPEND 文件写入时以追加的方式进行写入    (英文append)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>#define LOG "log.txt"int main()
{// 三种参数组合,就构成了 fopen 中的 wint fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC);// 成功与否打印信息if(fd == -1)       {printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}else{printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}close(fd);return 0;
}

 O_CREAT 为如果文件不存在,则自动创建该文件。 O_WRONLY 为以只读模式打开文件。需要注意的是,他们不会对原始文件内容做清空,下一次写入时,虽然还是从开头开始写,但是原本的内容没被覆盖的部分仍然会残留。

打开文件所返回的文件描述符是 3 。文件描述符下面会做详细解释

参数3解释:

先看上述代码结果:

第一次打开:

这里新创建的文件打开的权限有一点奇怪,我们删除再重新运行程序: 

新创建的文件打开的权限还是有一点奇怪,而且和上一次的创建的文件权限竟然不同,我们再次删除再重新运行程序: 

为什么每次创建文件的权限都是随机的呢?

因为我们使用的第一个open函数是没有指明创建后的文件权限是什么的,所以创建出的文件权限是一个随机值,这时我们就要考虑第二个open函数了!

  • 第三个参数 :mode_t是一种无符号整形,我们的第三个参数可以以8进制的方式传入我们创建的文件的权限。
int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);

最终的结果并不是:-rw-rw-rw- 这是为什么呢?

这是因为我们Linux的普通用户的权限掩码影响了我们文件创建的权限,我们可以使用系统调用umask()来进行设置我们进程的权限掩码。

umask(0);
int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);

刚刚被创建的 log.txt 文件的权限为 666 。 

总结:假若文件不存在,open 中的参数3最好进行设置,否则创建出来的文件权限为随机值。


C语言 中的 fopen 调用 open 函数,其中的选项对应关系如下

  • w -> O_WRONLY | O_CREAT | O_TRUNC
  • a -> O_WRONLY | O_CREAT | O_APPEND
  • r -> O_RDONLY
  • .......

所以只要我们想使用 open 时,也能做到 只读方式 打开 不存在的文件,也不会报错,加个 O_CREAT 参数即可

2. close

close 函数根据文件描述符关闭文件

int close(int fildes);

Linux 下一切皆文件

包括这三个标准流: stdinstdoutstderr
它们的文件描述符依次为:012,也可以通过 close(1) 的方式,关闭标准流

3. write

write函数是写入函数,通过此函数我们能向文件中写入数据。

ssize_t write(int fd, const void *buf, size_t count);

  • 第一个参数是文件标识符,也就是要写入的文件。
  • 第二个参数是一个指针,指向的是要写入的数据
  • 第三个参数是一个变量,表示最多写入多少个字节的数据
  • 返回值:实际写入的字节个数。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>#define LOG "log.txt"int main()
{umask(0);// 三种参数组合,就构成了 fopen 中的 wint fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);// 成功与否打印信息if(fd == -1)       {printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}else{printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}const char *msg = "good morning\n";int cnt = 3;while(cnt--){// 也可以写入缓冲区中//char line[128]//snprintf(line, sizeof(line), "%s, %d\n", msg, cnt);//write(fa, line, strlen(line));write(fd, msg, strlen(msg));// 长度不用+1}close(fd);return 0;
}

注意:通过系统级函数 write 写入字符串时,不要刻意加上 '\0',因为对于系统来说,调用写入时要的只是数据,但不包括'\0'这也只是一个普通的字符(因为'\0' 作为字符串结尾只是 C语言 的规定,但不是系统的规定)

4. read

read函数是读取函数,可以从文件中将数据读进变量中。

ssize_t read(int fd, void *buf, size_t count);

现在我们的文件是存在数据的 

 编写如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>#define LOG "log.txt"int main()
{umask(0);// 只读取文件int fd = open(LOG, O_RDONLY);// 成功与否打印信息if(fd == -1)       {printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}else{printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}char buffer[1024];// 读取整个文件ssize_t n = read(fd, buffer, sizeof(buffer)-1);if(n>0){buffer[n] = '\0';//注意在读取到的字符串后面加'\0'才是C语言风格的字符串  printf("%s\n", buffer);}close(fd);return 0;
}

注意:

  • 使用系统接口来进行IO的时候,一定要注意 '\0' 的问题。 
  • read函数会读取换行符(\n)并将其作为普通字符处理,只有在读取到指定的字节数或者遇到文件的结束才会停止读取,所以读取到的结果就有一行换行,

 这些系统级函数成功使用的前提是文件描述符合法

四、C文件接口与系统文件IO的关系

  • fopen、fclose、fwrite、fputs、fread、fgets 都是C标准库当中的函数,我们称之为库函数(libc)。
  • open、close、write、read都属于系统提供的接口,称之为系统调用接口。

 可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

五、文件描述符

int open(const char *pathname, int flags, mode_t mode);

open 函数的返回值是一个整数,这个整数就是文件描述符。关于 open 函数返回值的介绍如下:

 任何一个进程,在启动的时候,默认会打开当前进程的三个文件: 标准输入 、 标准输出 、 标准错误 。分别对应:

  • C语言中的 stdin 、 stdout 、 stderr 
  • 与C++中的 cin 、 cout 、 cerr 

1. 理解文件描述符

我们知道C语言打开一个文件之后,会返回一个 FILE* 的指针,我们的C的三个标准流也是 FILE*的指针,并且它们分别指向了 键盘文件 、显示器文件、显示器文件

当我们尝试去向这标准输出流或者是标准错误流中去输入数据,其实就是向显示器文件进行数据写入;当我们尝试向标准输入流中读取数据,其实就是对键盘文件进行数据读取。

既然是文件,那么在Linux下进行文件操作必须要有文件描述符,那么我们C程序(以及其他编程语言写的程序)默认打开的三个流,也要有文件描述符,只有有了文件描述符我们系统才能找到对应的文件。实际上C程序(以及其他编程语言写的程序形成的进程默认打开的三个流在Linux中对应的文件描述符都是:

文件描述符
标准输入流0
标准输出流1
标准错误流2
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>#define LOG "log.txt"int main()
{umask(0);// 只读取文件int fd = open(LOG, O_RDONLY);// 成功与否打印信息if(fd == -1)       {printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}else{printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));}
}

为啥是3捏? 实际上文件描述符的本质是数组的下标

根据 先描述、再组织 原则,OS 将所有的文件都统一视为 file 对象,获取它们的 file* 指针,然后将这些指针存入指针数组中,可以进行高效的随机访问和管理,这个数组为 file* fd_array[],而数组的下标就是神秘的 文件描述符 fd。

 一个进程可以打开多个文件,OS为了让进程快速的找到文件,在内核中创建了一种结构体 files_struct 。进程的PCB结构体 task_struct 中有一个指针 *file 指向这个文件结构体files_struct  。 files_struct 之中包含了一个指针数组 struct file* fd_array[ ] ,我们打开文件时,操作系统就会在指针数组 struct file* fd_array[ ] 中从前往后遍历,找到当前没有被使用的最小的一个下标,作为新的文件描述符,并把该下标的指针指向新打开文件的文件对象 file ,最后向上层返回该下标。

注:文件被打开后,并不会加载至内存中(这样内存早爆了),而是静静的躺在磁盘中,等待进程与其进行 IO。 

所以当一个程序启动时,OS 会默认打开 标准输入、标准输出、标准错误 这三个文件流,将它们的 file* 指针依次存入 fd_array 数组中,显然,下标 0、1、2 分别就是它们的文件描述符 fd;后续再打开文件流时,新的 file* 对象会存入当前未被占用的最小下标处,所以用户自己打开的 文件描述符一般都是从 3 开始。

2. 文件描述符的分配规则

fd 的分配规则为:先来后到,优先使用当前最小的、未被占用的 fd 

存在下面两种情况:

  1. 直接打开文件 file.txt,分配 fd 为 3
  2. 先关闭标准输入 stdin 中原文件执行流(键盘),再打开文件 file.txt,分配 fd 为 0,因为当前 0 为最小的,且未被占用的 fd
#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <errno.h>  
#include <string.h>  
#include <assert.h>#define LOG "log.txt"  int main()  
{//先打文件 int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);assert(fd != -1);   //存在打开失败的情况printf("单纯打开文件 fd: %d\n", fd);close(fd);  //记得关闭//先关闭,再打开close(0);   //关闭键盘文件执行流fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("先关闭键盘文件执行流,再打开文件 fd: %d\n", fd);close(fd);return 0;     
}

 注意: 假若将标准输出 stdout 中的原文件执行流(显示器)关闭了,那么后续的打印语句将不再向显示器上打印,而是向此时 fd 为 1 的文件流中打印

    close(1);// 关闭显示器文件执行流fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("先关闭显示器文件执行流,再打开文件 fd: %d\n", fd);close(fd);

 这其实就是 重定向 的基本操作

六、再次理解一切皆文件

学习过Linux的人大多数都听过一句话:“Liunx下一切皆文件”,这一点对于文件操作非常重要,下面我们来谈一谈对于这句话的理解。

我们知道像:键盘,显示器,网卡… 这些都是硬件,根本不是文件,为什么我们能把它们当成文件去看待呢?

首先,我们在操作硬件时都是通过驱动程序进行操作硬件的,而对于不同的硬件它们的驱动程序肯定是不同的,例如键盘只能被读取而不能被写入,显示器只能被写入,而不能被读取,网卡既可以被读取又可以被写入

尽管它们的驱动是不同点,但是我们可以设计一个类,对于它们的进行封装,封装完以后,上面的调用者就看不到底层的内容了,上面的调用者只需要对于这个类进行操作就可以达到同样的效果,这样对于调用者来说,它的操作都是在操作这个类,它接触不到底层的驱动以及硬件,当然对于它来说它看到的是什么那就是什么了。

于是Linux用struct file结构体封装这些驱动的函数指针,对于不同的硬件若不需读或写操作方法,则只需将对应的函数指针置空就行,这样进程在使用键盘,显示器时就像是在使用文件一样,这样一来不管是普通文件还是硬件,对于进程来说都是文件。

而我们用户使用操作系统又是通过进程的方式来使用操作系统的,所以我们用户的视角和进程的视角是一样的,在我们用户来看想要让磁盘帮我们保存一些数据,我们只需要保存一下文件就行了,我们没有必要把磁盘拿出来,然后通过一些物理手段向磁盘中刻入数据,进程也是和我们用户一样,它也认为我只要保存一下文件就能完成任务了,所以对于我们用户和进程来说一切都是文件,我们也只能接触到文件,所以Linux下一切皆文件。

七、重定向

1. FILE

在谈论重定向之前我们先来谈论一下C语言中的FILE

我们使用C语言进行打开文件时,系统都会给我们一个FILE指针那这个FILE指针是什么呢?是谁给我们提供的呢?

答案是:是C语言给我们提供的,这个FILE其实就是一个C库给我们封装的一个结构体,而且这个结构体内部一定要有文件描述符 fd,因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd访问的。所以C库当中的FILE结构体内部,必定封装了 fd

在C库的内部源代码中有这样一些源代码:

通过这段源代码,我们知道FILE内部有一个叫 _fileno的文件描述符,那么我们就可以将stdin stdout stderr的文件描述符打印出来,看看与我们上面的结论是不是一样的。

打印三个标准流的文件描述符

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <errno.h>  
#include <string.h> #define LOG "mylog.txt"int main()
{//打印出文件标识符printf("%d\n", stdin->_fileno);    printf("%d\n", stdout->_fileno);    printf("%d\n", stderr->_fileno);    FILE *fp = fopen(LOG, "w");printf("%d\n", fp->_fileno);fclose(fp);         return 0;
}

2. 重定向的本质

前面说过,OS 在进行 IO 时,只会根据标准输入、输出、错误对应的文件描述符 0、1、2 来进行操作,也就是说 OS 作为上层不必关心底层中具体的文件执行流信息(fd_array[] 中存储的对象) 因此我们可以做到 “偷梁换柱”,将这三个标准流中的原文件执行流进行替换,这样就能达到重定义的目的了

3. 利用指令重定向

下面直接在命令行中实现输出重定向,将数据输出至指定文件中,而非屏幕中

echo you can see me > file.txt

可以看到数据直接输出至文件 file.txt 中,当然也可以 从  file.txt 中读取数据,而非键盘

cat < file.txt

现在可以理解了,> 可以起到将标准输出重定向为指定文件流的效果,而 < 则是从指定文件流中,标准输入式的读取出数据。

除此之外,我们还可以利用程序进行操作,在运行后进行重定向即可

#include <iostream>using namespace std;int main()
{cout << "标准输出 stdout" << endl;cerr << "标准错误 stderr" << endl;return 0;
}

直接运行的结果,此时的标准输出和标准错误都是向显示器上打印 

利用命令行只对 标准输出 进行重定向,file.txt 中只收到了来自 标准输出 的数据,这是因为 标准输出 与 标准错误 是两个不同的 fd,现在只重定向了 标准输出 

对 标准错误 也进行 重定向,打印内容至 file.txt

4. 输出重定向

看下面一段代码,我们就可以尝试如果我们关闭1号文件描述符,然后我们再打开一个文件,之后我们向stdout里面输入一些数据,看一看会发生什么?还是打印到显示器上面吗?

int main()
{  close(1); // fclose(stdout);int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);    printf("you can see me?\n");return 0;
}

答案是并没有打印到显示器中,而是打印到了文件中,相信有了前面的基础你已经明白了,我们将stdout关闭后,新打开的文件占据了1号文件描述符,而我们的printf函数只认识1号文件描述符,所以向1号文件描述符指向的文件输入内容,就导致数据输入到了文件里面:

5. 输入重定向

同理,我们把0文件标识符给关闭,然后打开我们的新文件进行scanf,那么我们应该会从新打开的文件中读取数据,我们看一看结果:

int main()
{close(0); // fclose(stdin);int fd = open(LOG, O_RDONLY);char str[100];scanf("%s", str);printf("%s\n", str);close(fd);return 0;
}

当执行 scanf 函数时,并没有要从键盘输入数据,而是从默认 0 号的键盘文件读取数据也就变成了从现在的  0 号文件 mylog.txt 中读取数据: 

6. 追加重定向 

追加重定向的原理很简单,我们只需要将文件的打开方式加上O_APPEND去掉O_TRUNC 。

例如对于刚才的文件进行重定向:

    close(1);int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd == -1){perror("Failed to open log file");return 1;}printf("this is append!!!!!\n");

追加成功:


根据这些原理我们来实现一个需求:将标准输出流与标准错误流的信息进行分流。

  • 标准输入(stdin)-> 设备文件 -> 键盘文件
  • 标准输出(stdout)-> 设备文件 -> 显示器文件
  • 标准错误(stderr)-> 设备文件 -> 显示器文件

标准输出 与 标准错误 都是向显示器中输出数据,为什么不合并为一个?

  • 因为在进行排错时,可能需要单独查看错误信息,若是合并在一起,查看日志时会非常麻烦,导致我们难以找到错误所在;但如果分开后,只需要将 标准错误 重定向后,即可在一个单独的文件中查看错误信息

我们可以使用重定向进行分流,我们先关闭1号文件描述符,然后新打开一个文件normal.txt,然后关闭2号文件描述符,再打开一个新的文件error.txt这样我们再使用标准输入或标准错误流时,信息会被写入两个不同的文件中,我们关心错误信息就可以打开error.txt进行查看,关心正确信息,就可以打开normal.txt进行查看。

原本不分流时是乱的:

  fprintf(stdout, "stdout->normal\n");                                                  fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");

 进行分流:

  close(1);open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);close(2);open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);fprintf(stdout, "stdout->normal\n");                                                  fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");

这样就把常规消息与异常消息区分开在不同的文件里了,我们C语言中的 printf 与 perror ,C++中的 cout 与 cerr ,所做的就是把不同的消息进行分类,方便查找。

或者不同消息打印的位置可以通过指令的方式来实现,注释掉close(1)和close(2):

以上只是简单演示一下如何通过命令行进行 重定向,在实际开发中进行重定向操作时,使用的是函数 dup2

7. 函数重定向

每次重定向都要先关闭一个文件的话,操作太过于麻烦,所以可以使用系统调用 dup2 来简化操作:

#include <unistd.h>
int dup2(int oldfd, int newfd);

函数解读:将老的 fd 重定向为新的 fd,参数1 oldfd 表示新的 fd,而 newfd 则表示老的 fd,重定向完成后,只剩下 oldfd,因为 newfd 已被覆写为 oldfd 了;如果重定向成功后,返回 newfd,失败返回 -1

参数设计比较奇怪,估计作者认为 newfd 表示重定向后,新的 fd

下面来直接使用,模拟实现报错场景,将正常信息输出至 normal.txt,错误信息输出至 error.txt

  int fdnor = open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fderr = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);// 重定向dup2(fdnor, 1);dup2(fderr, 2);fprintf(stdout, "stdout->normal\n");                                                  fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stderr, "stderr->error\n");    fprintf(stdout, "stdout->normal\n");close(fdnor);close(fderr); 

八、缓冲区

我们以前学习C语言的文件操作时,我们都知道FILE 里面应该是有缓冲区的,现在我们学习操作系统时我们又知道操作系统内核里面也是有缓冲区的,那这两个缓冲区是一样的吗?

对于这个问题我们现在不好回答,我们只能先给出结论:是不一样的,FILE 是C库提供给我们的一个结构体,里面的缓冲区对应的是用户态的缓冲区,Linux内核中的缓冲区,对应的是内核态的缓冲区。

我们先看下面的代码,根据现象我们来分析问题,最后再来理解一下缓冲区。

#include<stdio.h>      
#include<unistd.h>      
#include<string.h>      int main()      
{      fprintf(stdout, "hello fprintf!\n");      const char* str = "hello write!\n";      write(1, str, strlen(str));        //创建子进程    fork();                                                                                                                                                      return 0;      
}

 我们发现当我们直接运行和重定向后的结果是不同的,而且fprintf会比write多一次打印,这时为什么呢?

其实呢这与C库的缓冲区有关系!缓冲区在哪里?在你用C库函数打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中

函数 fprintf 的返回值类型为 FILE* ,在调用 fprintf 函数时,会在内存中 malloc 出一个结构体 FILE 并返回其地址。结构体 FILE 中包含文件描述符与缓冲区。当用户使用 fprintfstdout 打印字符串时,其实只是把字符串放进了C库的缓冲区里,暂时还没有把用户层缓冲区里的数据拷贝到操作系统内核的缓冲区里。C库会结合一定的刷新策略,调用 write 函数将用户层缓冲区中的数据写入给OS。

C库的刷新策略有如下三种:

  1. 无缓冲:数据直接刷新到OS内核
  2. 行缓冲:数据先放到用户层缓冲区中,如果碰到了 '\n' ,就把 '\n' 之前的所有数据刷新到OS 内核。
  3. 全缓冲:数据先放到用户层缓冲区中,如果用户层缓冲区被写满了,就把所有数据刷新到OS 内核。

当直接运行程序时,默认是向显示器刷新,采取的是行缓冲。遇到 '\n' ,直接把用户缓冲区里 '\n' 之前的内容刷新到了OS中,这就导致在程序最后执行 fork();时,用户缓冲区里已经没有数据了,我们观测到的结果就是正常显示。

当运行程序并重定向到普通文件时,是向普通文件刷新,采取的是全缓冲。而我们打印的内容还不足以将缓冲区填满,所以到程序最后执行 fork();时,用户缓冲区里的数据仍然没有被刷新,父子进程各自持有一份数据,程序结束时一起刷新到OS中,就打印了两次。

因为打印 "hello write" 使用的是系统调用 write ,是直接向OS写入数据的,无需用户层刷新,所以不管有没有重定向,都只打印一次。

为什么C库的FILE里面要有缓冲区呢?
答案是:节省调用者的时间! 如果我们想直接把数据写到操作系统内核中就需要调用系统调用,而系统调用的使用代价是要比普通函数大的多的,因此为了尽量少的使用系统调用,尽量一次IO能够读取和写入更多的数据,所以 FILE内部才有了缓冲区。

操作系统有缓冲区吗?

操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)

因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/180453.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一则 MongoDB 副本集迁移实操案例

文中详细阐述了通过全量 增量 Oplog 的迁移方式&#xff0c;完成一套副本集 MongoDB 迁移的全过程。 作者&#xff1a;张然&#xff0c;DBA 数据库技术爱好者~ 爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。 本文约 900…

python炒股自动化(1),量化交易接口区别

要实现股票量化程序化自动化&#xff0c;就需要券商提供的API接口&#xff0c;重点是个人账户小散户可以申请开通&#xff0c;上手要简单&#xff0c;接口要足够全面&#xff0c;功能完善&#xff0c;首先&#xff0c;第一步就是要找对渠道和方法&#xff0c;这里我们不讨论量化…

linux 内核等待队列

等待队列在Linux内核中用来阻塞或唤醒一个进程&#xff0c;也可以用来同步对系统资源的访问&#xff0c;还可以实现延迟功能 在软件开发中任务经常由于某种条件没有得到满足而不得不进入睡眠状态&#xff0c;然后等待条件得到满足的时候再继续运行&#xff0c;进入运行状态。这…

网络安全--基于Kali的网络扫描基础技术

文章目录 1. 标准ICMP扫描1.1使用Ping命令1.1.1格式1.1.2实战 1.2使用Nmap工具1.2.1格式1.2.2实战1.2.2.1主机在线1.2.2.2主机不在线 1.3使用Fping命令1.3.1格式1.3.2实战 2. 时间戳查询扫描2.1格式2.2实战 3. 地址掩码查询扫描3.1格式3.2实战 2. TCP扫描2.1TCP工作机制2.2TCP …

位运算总结

文章目录 &#x1f348;1. 基础位运算&#x1f34c;2. 给一个数n&#xff0c;确定它的二进制表示中的第x位是0还是1&#x1f34f;3. 将一个数n的二进制表示的第x位修改成1&#x1f353;4. 将一个数的n的二进制表示的第x位修改成0&#x1f954;5. 位图的思想&#x1fad2;6. 提前…

医保移动支付程序开发

作为公司最苦命的开发&#xff0c;年初接到任务开发医保移动支付程序&#xff08;微信小程序和支付宝小程序&#xff09;&#xff0c;为医疗机构提供线上医保结算。好家伙&#xff0c;我一看解压后资料大于一个G&#xff0c;内心无比的惊慌。 一、技术流程图 图太大了显示不全需…

0-1背包的初始化问题

题目链接 这道题的状态转移方程比较易于确定。dp[i][j]表示能放前i个物品的情况下&#xff0c;容量为j时能放物品的数量&#xff08;这道题歌曲数量对应物品数量&#xff0c;容量对应时间&#xff09;。 技巧&#xff08;收获&#xff09; 二维dp数组可以视情况优化为一维dp数组…

Linux——vim编辑文件时——.swp文件解决方案

test.cpp样例 当我们vim test.cpp进入编辑文件。 却忘记了保存退出 再次进入就会出现一下画面 当你摁下Enter键位 出现以下几个选项 O——是只读不写 E——是正常打开文件但不会载入磁盘内容 R——覆盖——是加载存储磁盘的文件(当我们忘记保存时&#xff0c;系统会自动帮我…

事件代理?

1.什么是事件代理&#xff1f; 事件代理也叫事件委托&#xff0c;只指定一个事件处理程序&#xff0c;就可以管理某一类型得事件。 可以简单理解为&#xff0c;事件代理就是将本应该绑定子元素事件绑定给父元素代理。它的优点就是&#xff1a;减少事件得执行&#xff0c;减少浏…

CentOS 7 部署 MariaDB 的 2 种方法

有两种安装 MariaDB 服务器的方法。您可以安装 CentOS 7 存储库中可用的默认版本&#xff0c;也可以通过手动添加 MariaDB 存储库来安装最新版本。 如果安装过MariaDB或MySQL&#xff0c;使用以下命令彻底删除它们: yum remove mariadb* yum remove mysql* 方法一: 使用 Yum…

Make Pixels Dance: High-Dynamic Video Generation论文解析

高动态视频生成的新进展 Make Pixels Dance: High-Dynamic Video Generation高动态视频生成的新进展前言视频生成模式摘要论文十问实验数据集定量评估指标消融研究 训练和推理技巧训练技术推理技术 更多的应用 Make Pixels Dance: High-Dynamic Video Generation 高动态视频生…

VBA技术资料MF87:创建固定顺序名称的一组文件夹

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

gRPC Java、Go、PHP使用例子

文章目录 1、Protocol Buffers定义接口1.1、编写接口服务1.2、Protobuf基础数据类型 2、服务器端实现2.1、生成gRPC服务类2.2、Java服务器端实现 3、java、go、php客户端实现3.1、Java客户端实现3.2、Go客户端实现3.3、PHP客户端实现 4、运行效果 本文例子是在Window平台测试&a…

Unity UGUI图片锯齿严重怎么解决

在开发的时候&#xff0c;发现图片锯齿严重&#xff0c;打包到移动端或者在编辑器都这样&#xff0c;如下图 原因&#xff1a; 查了一些资料&#xff0c;找到了原因如下&#xff1a;关于为什么会发生这种情况&#xff1a;看起来你的源资源比你在屏幕上显示的大小大得多。所以当…

深入浅出 Vue 中的插槽 slot

深入浅出 Vue 中的插槽 slot start 最近被问到好几次 Vue 中的插槽相关知识&#xff0c;掌握的还是有些不全面。抱着重新学习的心态&#xff0c;写这篇博客。首先对基础知识做一个回顾&#xff0c;然后再对源码实现做一个学习。作者&#xff1a;番茄编写时间&#xff1a;2023…

STM32_10(I2C)

I2C通信 I2C&#xff08;Inter IC Bus&#xff09;是由Philips公司开发的一种通用数据总线两根通信线&#xff1a;SCL&#xff08;Serial Clock&#xff09;、SDA&#xff08;Serial Data&#xff09;同步&#xff0c;半双工带数据应答支持总线挂载多设备&#xff08;一主多从…

QAC支持的静态度量属性

上面介绍了Coverity支持的12个度量属性&#xff0c;下面我们看看QAC这款工具支持的度量属性。下面分成3类&#xff0c;函数度量、文件度量和类度量。 函数度量指标不多&#xff0c;一共有8个。 文件度量度量指标一共10个&#xff0c;如下表。 类度量指标一个8个&#xff0c;如…

python实现C++简易自动压行

突发奇想&#xff0c;想要将自己的c压行之后交上去。但是苦于手动压行效率太低&#xff0c;在网上搜索压行网站没有找到&#xff0c;突然发现压行不就是检查检查去个换行符吗。于是心血来潮&#xff0c;用python实现了一个简易压行程序。 首先&#xff0c;宏定义等带#的文件不…

正则表达式及文本三剑客grep,awk,sed

目录 正则表达式 前瞻 代表字符 表示次数 位置锚定 分组或其他 grep 选项 范例 awk 前瞻 awk常见的内置变量 范例 sed 前瞻 sed格式 范例 搜索替代 格式 范例 分组后项引用 格式 范例 正则表达式 前瞻 通配符&#xff1a;匹配的是文件名 正则表达式&a…

Windows10免安装PostgreSQL

1. PostgreSQL简介2. 下载3. 安装环境4. 安装 4.1. 初始化数据库4.2. 启动数据库4.3. 注册服务4.3. 卸载服务 1. PostgreSQL简介 PostgreSQL 是一种特性非常齐全的自由软件的对象-关系型数据库管理系统&#xff0c;是以加州大学计算机系开发的 POSTGRES 4.2版本为基础的对象关…