Linux:文件(二)
1. 重定向的本质
重定向的本质就是将fd改变了
(1)输出重定向
输出重定向将我们要输出到a文件的数据重定向输出到另一个文件中,本质就是关闭标准输出流的文件描述符1,然后让该文件描述符指向新的文件,最后如果我们再对该文件描述符写入,数据就不会打印在屏幕上而是重定向到这个新的文件里。
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
close(1);//将这个标准输出流关闭
int fd=open("xxx.txt",O_WRONLY|O_CREAT,0666);//将新文件打开
if(fd<0)//如果失败返回-1
{
perror("open error!!\n");
return 1;
}
//将信息打印
printf("hello file\n");
printf("hello file\n");
printf("hello file\n");
printf("hello file\n");
printf("hello file\n");
fflush(stdout);//刷新缓冲区
close(fd);//将新文件关闭
return 0;
}
(2)输入重定向
将本来输入到a文件的数据重定向输入到另一个文件b中。本质就是关闭标准输入流的文件描述符0,让此文件描述符指向新的文件,此时本来是对键盘进行读取就变为从新文件读取
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
close(0);//将这个标准输出流关闭
int fd=open("xxx.txt",O_RDONLY,0666);//将新文件只读打开
if(fd<0)//如果失败返回-1
{
perror("open error!!\n");
return 1;
}
//将信息打印
char buf[128]={'\0'};
while(scanf("%s",buf)!=EOF)
{
printf("%s\n",buf);
}
close(fd);//将新文件关闭
return 0;
}
由于scanf读入遇到空格就换下一个所以输出结果如下
没有从屏幕读取数据而是从xxx.txt文件读取的数据
(3) 追加重定向
就写入文件时带上O_APPEND选项即可
int fd=open("log.txt",O_WRONLY|O_APPEND|O_CREAT,0666);
另外说一下,标准输出与标准错误流对应的设备都是显示器,两者之间的区别:
将1号文件描述符给关闭了不会影响2号文件描述符,所以错误信息不会和正常的输出信息一起被我们重定向。这样就可以将错误信息单独重定向来打印到单独文件。当然也可以输出到同一文件
所以存在标准错误的主要作用就是可以通过重定向能力,把常规消息和错误消息进行分离。
2.dup2函数
Linux操作系统也为我们提供了专门的重定向接口--dup2函数
所需头文件:#include <unistd.h>
原型:int dup2(int oldfd, int newfd);
功能:将一个文件描述符复制到另一个指定的文件描述符上,若目标文件描述符已经打开,dup2会先关闭它再进行复制
返回值:调用成功返回新的fd(newfd),否则返回-1
注意事项
1. 当oldfd并不是一个有效文件描述符时,dup2就会调用失败,此时文件描述符为newfd的文件没有关闭。
2. 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,返回newfd
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd=open("xxx.txt",O_WRONLY|O_CREAT,0666);//将新文件只读打开
if(fd<0)//如果失败返回-1
{
perror("open error!!\n");
return 1;
}
//将信息打印
close(1);//将文件关闭
dup2(fd,1);
printf("hello dup2\n");
printf("hello dup2\n");
fprintf(stdout,"hello dup2!!\n");
fprintf(stdout,"hello dup2!!\n");
//fflush(stdout);//刷新缓冲区
close(fd);
return 0;
}

3.缓冲区
缓冲区的概念
缓冲区是内存空间的一部分。在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备分为输入缓冲区还是输出缓冲区。
为什么有缓冲区?
读写文件时,如果不开辟对文件操作的缓冲区,直接就通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问使程序执行效率大大降低。而加入了缓冲区。
例如在磁盘文件进行操作时,我们可以一次性从文件中读入大量数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区数据取完再去磁盘中读取,减少磁盘读写次数,计算机对缓冲区的操作远远快于对磁盘的操作,所以应用缓冲区可以很大程度上提高计算机运行速度。
又比如打印机打印文档,可以将文档输出到打印机相应的缓冲区,打印机自行打印,这样CPU就可以去处理别的事情了。
缓冲区就是一块内存区,在输入输出设备和CPU之间用来缓存数据。它使得低速的输入输出设备和高速的CPU能协调工作,避免低速的输入输出设备占用CPU,使其能高效率工作。
小总结
引入缓冲区是为了:提高效率,提高使用者效率。
缓冲区是:内存的一段空间。
语言层缓冲区
用户级,语言层缓冲区在C标准库中
当用户:
1. 强制刷新 fflush
2.刷新条件满足
(1) 立即刷新---无缓冲--写透模式
(2) 满了---全缓冲 (写满缓冲区,效率最高,普通文件一般用这种方式)
(3) 行缓冲---显示器用
3.进程退出
使用以下代码实验
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
close(1);
//close(2);
int fd=open("long.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open error\n");
return 6;
}
printf("hello io\n");
printf("hello io\n");
close(fd);
return 0;
}
输出结果如下:
printf没有\n的话就是全缓冲,否则就是行缓冲。我们将文件进行了重定向,将本来打印到屏幕的信息输入进文件中,此时缓冲区的刷新方式成为了全缓冲。我们写入的内容并没有填满整个缓冲区,导致并不会立即将缓冲区的内容刷新到磁盘文件中,而是等到程序结束后才会向磁盘刷新文件内容。由于我们在程序结束前使用close关闭了文件,程序结束就无法找到对应的文件,自然也不会对文件进行任何写入。我们一般用fflush来提前刷新缓冲区来解决这个问题。
由于printf是C语言提供的接口,所以这个缓冲区也肯定是C语言提供的被包含在FILE结构体中。因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上访问文件都是通过fd来访问的。所以C库中的FILE结构体内部必定封装了fd。
//在/usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的⽂件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
系统缓冲区
操作系统内部也存在一个缓冲区,我们一般称为内核缓冲区。同样语言缓冲区刷新到系统缓冲区也遵循三种刷新策略:
(1) 无缓冲
(2) 全缓冲 (写满缓冲区,效率最高,普通文件一般用这种方式)
(3) 行缓冲---显示器用
我们使用语言提供接口例如printf对文件进行写入数据,首先将数据存放在语言缓冲区,然后根据不同的刷新规则将其交给文件内核缓冲区然后OS自动决定刷新方案,将数据刷新到磁盘或对应的外设中。(数据交给系统,交给硬件本质都为拷贝,计算机数据流动的本质就是一切皆是拷贝 )
看一下下面这段代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
const char* msg0="hello printf\n";
const char* msg1="hello fwrite\n";
const char* msg2="hello write\n";
printf("%s",msg0);
fwrite(msg1,sizeof(char),strlen(msg1),stdout);
write(1,msg2,strlen(msg2));
fork();
return 0;
}
输出结果为
我们将进程来输出重定向来看一下
两个库函数printf和fwrite都输出2次,而系统调用write只输出一次。这样肯定与fork函数有关系:
- 一般C库函数写入文件时是全缓冲,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区,当发生重定向时到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区的数据不会立即刷新,在fork之后也不会。
- 进程退出后会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,当父进程准备刷新缓冲区的时候本质是对数据进行修改,为了维护进程独立性子进程也就有了同样的一份数据,随即产生两份数据。
- write没有变化说明没有进入C标准库提供的缓冲区
即两个库函数会先写入语言提供的缓冲区,而write系统调用是直接写入系统(内核)缓冲区而内核通常会尽快将数据刷新到目标设备(如屏幕或文件)。子进程不会发生写实拷贝(因为没写入用户空间的内存中而是写入到内核缓冲区了)。
4. 一切皆文件
在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,例如进程、磁盘、显示器、键盘等硬件设备也被抽象成了文件,可以通过访问文件的方法来访问它们例如read、write等,甚至管道也是文件。
linux中几乎所有度(读文件、读系统状态、读PIPE)的操作都可以用read来进行。
几乎所有更改(更改文件、更改系统参数、写PIPE)的操作都可以用write函数来进行。
Linux引入了然健的虚拟层VFS(虚拟文件系统)。VFS统一维护每一个文件的结构体struct file,这个结构体包含了一批函数指针。这些函数指针指向底层,我们在上层,可以统一的struct_file的方式去看待文件,所以我们在上层就可以通过struct_file的方式看待文件,这就是一切皆文件,并非在硬件层面上一切皆文件。
这与C++中的多态类似,父类指针指向谁,调用的就是谁的方法。在C语言中可以通过函数指针指向不同的对象时执行不同的方法来实现多态的性质。每个struct file中有很多函数指针,在上层看来访问设备都是通过函数指针指向的方法来访问的,函数指针类型命名参数都一个样。所有文件都是调用一样的接口,但是在底层,通过函数指针具体指向的硬件来用对应的方法来实现具体硬件对应的操作。
这篇就到这里啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤