前言:大佬写博客给别人看,菜鸟写博客给自己看。我是菜鸟
认知1:
stdin → 标准输入 → 键盘文件
stdout → 标准输出 → 显示器文件
stderr → 标准错误 → 显示器文件
注:把显示器以及键盘看作是文件,广义的说:Linux下一切都是文件
1.系统接口的使用(open/close/write/read)
写一段简单的代码来实现一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
exit(1);
}
const char* msg = "hello kivotos\n";
write(fd,msg,strlen(msg));
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// const char* msg = "hello kivotos\n";
// write(fd,msg,strlen(msg));
char buffer[64];
int n = read(fd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n] = 0;
printf("%s",buffer);
}
close(fd);
return 0;
}
int open(const char *pathname, int flags, mode_t mode);
pathname:任意路径
flag:常用的有以下几个
O_RDONLY → 只读
O_WRONLY → 只写
O_RDWR → 读写
O_CREAT → 文件不存在则创造(若路径下文件不存在,则一定要加)
O_TRUNC → 将文件中的内容删除并重写
O_APPEND → 在文件新的一行追加
mode:当创建文件时,必须要给文件赋予权限
返回值:
成功时,会返回给一个大于2的数给fd(稍后说)
失败时,返回-1
注1:O_TRUNC和O_APPEND二者只能存在其中之一
注2:flag是通过位段操作来提供对应权限的(例如a的权限是001,b的权限是010,c的权限是100,那么a|b|c 就获得了abc三个权限,a|b就只获得了ab的权限)
ssize_t write(int fd, const void *buf, size_t count);
fd:需要写入文件对应的fd
buf:buf指针,指向需要写入的数据,可以是任意类型,但通常是字符数组或字节数组
count:写入数据的字节数,不需要 +1 处理(这是写入到文件中,不需要预留\0)
返回值:
成功时,返回写入的字节数大小
失败时:返回-1
ssize_t read(int fd, void *buf, size_t count);
fd:需要写读取文件对应的fd
buf:用于存储读取到的文件中的数据
count:所读取到的最大的字节数,需要做-1处理,给\0做预留
注:当我们将数据写入到文件时,不要预留\0,因为文件没有这项规定,预留\0只会造成乱码后果,而当我们读取文件中的数据时,需要预留\0,因为这是C语言的语法规定,要给字符串预留\0的位置
close(int fd);
关闭指定文件
2.文件描述符(fd)
认知2:每一个文件都是通过进程打开的
用户通过系统接口创建进程(PCB)时,PCB中包含了一个指针,该指针指向struct files_struct(文件描述符表),该结构体中含有一个指针数组(struct file* fd_array[]),数组中不同的下标指向了不同的文件,这样就建立了映射关系。我们能够通过文件描述符找到对应的文件,再将文件缓冲区的内容拷贝出来。因此有:文件描述符 = 数组下标。系统也是通过上述方法来确定文件是由哪个进程打开的
注1:对于冯偌伊曼体系而言,文件内容是保存在磁盘上的,因此对文件内容做任何操作都必须先把文件加载(从磁盘到内存的拷贝)到内核对应的文件缓冲区当中(从磁盘拷贝到内存中,再交给CPU处理)
注2:文件(file)的属性和数据不是直接存储在file中,而是通过一个指针指向一个叫inode的数据结构中,该数据结构用于保存文件的属性,而对于数据而言,存储在内核文件缓冲区
注3:file是一个结构体,内部包含fd,以及缓冲区,当打开一个文件时,内部会自动开辟一块空间,同时提供相应fd和缓冲区
接下来我们看这样一段代码:
int main() { int fd1 = open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); int fd2 = open("log2.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); int fd3 = open("log3.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); printf("%d\n",fd1); printf("%d\n",fd2); printf("%d\n",fd3); return 0; }
他的运行结果为:
问:0、1、2去哪了?
答:对于操作系统而言,下标0 → 标准输入;下标1 → 标准输出;下标2 → 标准错误
正如我们在认知1中所说的,对应计算器而言,标准输入对应键盘,标准输出和标准错误对应显示器。
注:需要注意的是,下标是不变的,也就是说,0、1、2永远对应标准输入、输出、错误,
但是下标对应的文件是可以改变的,也就是说,我们可以将原先1指向显示器文件,改变为指向我们自己写的文件,这就是重定向。
3.重定向
重定向的分配原则:系统会把下标最小的且没被使用的下标作为新的fd分配给新的文件
int dup2(int oldfd, int newfd);用于重定向的函数
newfd 将被 oldfd 覆盖,如果oldfd无用,也可以通过 close 删除 oldfd
我们来看看以下代码:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/fcntl.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main() { int fd = open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); dup2(fd,1); printf("hello kivotos\n"); return 0; }
上面提过,1对应的标准输出,而对于计算器而言,默认情况下,他的标准输出(下标1)对应的文件为显示器文件,当我们执行dup2(fd,1);时,此时标准输出指向了我们自己写的文件,而printf()函数只认下标,不认你是不是显示器,所以当我们执行 printf()函数时,显示器没有任何输出,而当我们 cat log.txt 我们所创建的文件时,能够发现以下结果:
这就是所谓的输出重定向。
我们还可以实现输入重定向,代码如下:
int main()
{
int fd = open("log.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
char buffer[64];
dup2(fd,0);
while(1)
{
if(!fgets(buffer,sizeof(buffer),stdin)) break;
printf("%s",buffer);
}
return 0;
}
可以看到代码中,我们从指定输入流读取数据,转变为从自己写的文件中读取数据并打印。
复习:
char *fgets(char *str, int n, FILE *stream);
str:用于存储文件流中读取的字符串
n:读取的最大字符数,包含\0,在末尾会自动添加0
stream:指定输入流
注:
问:原先1(标准输出)指向显示器文件,当我们通过输出重定向,使得1指向别的文件时,此时显示器文件会发生什么?
答:一个文件可以被多个进程打开,当我们重定向了一个,还有其他进程也会打开当前文件,文件中一个计数器,专门用于记录当前有多少个进程打开了该文件,当计数器为0时,此时文件才会被释放掉空间。
4.操作系统对外设的访问
问:不同的外设都可以抽象成文件,例如显示器文件、键盘文件、磁盘文件等,那么操作系统是如何对不同文件进行访问的?
答:上面我们提到,用户创建进程会生成PCB,PCB内部包含一个指针指向文件描述符表,文件描述符表中又包含了一个指针数组,不同的下标对应不同的文件,其中这每个文件又是一个结构体,每个结构体中包含了指向文件属性的指针,以及指向内核文件缓冲区的指针,同时还包含了两个函数指针void(*read)(int fd,char* , int ); void(*write)(int fd,char* , int );
通过这两个指针可以找到对应外设的读写方法,从而实现操作系统对外设的访问,大致结构入下图
认知3:struct file 以及 struct file_operation被称为虚拟文件系统(VFS),这与虚拟地址空间有着一定的类似之处,上层与下层建立映射关系,操作系统通过这种映射关系,再通过相应的读写方式进行上层对下层的访问。
认知4:对于计算机世界,任何问题都可以通过增加一层软件层来解决,越靠近上次越抽象。
想想刚才所讲的虚拟文件层,如果没有这一层,那么需要多写多少个if else 语句?大佬加了VFS一层,从而达到了屏蔽底层差异的作用。
问:怎么个屏蔽法?
答:每一个设备都对应有一个 struct device的结构体,内部包含了设备的所有属性以及数据,但是无论你底层硬件的数据是怎样的,对于操作系统而言,我只需要通过文件描述符以及驱动程序跟你建立起了映射关系后,通过调用void(*read)(int fd,char* , int ); void(*write)(int fd,char* , int ); 这两个函数指针,就能够访问对应的外设。
5.缓冲区
缓冲区分为语言层缓冲区和文件内核缓冲区,我们口中常说的缓冲区其实是语言层缓冲区。对于文件而言,文件有着自己的文件内核缓冲区,而对于用户级语言(printf/fprintf/fputs/fwirte)有着语言层缓冲区。对于语言层缓冲区而言需要在文件关闭前,通过fd+系统调用(例如write),或者通过强制刷新(fflush(stdout))、进程退出或是刷新条件满足,才能正常显示结果。
针对刷新条件满足做三点补充:
1.立即刷新 ——无缓冲
2.缓冲区写满——全缓冲,一般用于普通文件
3.行刷新——行缓冲,一般用于显示器
语言层缓冲区的目的:减少系统调用,系统调用是有成本的,如果没有语言缓冲区,十行语句要通过十次系统调用,而有了语言缓冲区,可以先将这十行语句拷贝到语言缓冲区中,当满足一定刷新条件时,再通过系统调用,将其拷贝到文件内核缓冲区中,以达到减小成本的目的。
以显示器为例(此时的fd就是1):用户想把"aaaa"字符串打印到显示器上,可以直接通过系统调用(write)来实现,但是一旦系统调用次数过多,会增加成本,所以得通过语言层缓冲区。具体过程是,通过(printf/fprintf/fputs/fwirte)函数,将内容拷贝到语言层缓冲区,再通过fd+系统调用,例如write(),系统通过fd找到对应文件内核缓冲区,将语言层缓冲区中的内容拷贝到文件内核缓冲区实现结果。
接下来我们看这样一串代码,如下图:
#include <iostream> #include <cstdio> #include <cstring> #include <unistd.h> #include <stdio.h> int main() { printf("hello\n"); fprintf(stdout,"kivotos\n"); const char* msg = "world\n"; fwrite(msg,strlen(msg),1,stdout); const char* msg2 ="Sensei\n"; write(1,msg2,strlen(msg2)); fork(); return 0; }
当我们执行分别执行下述指令时,会得到两个完全不同的结果,如下图:
问:为什么右图比左图多了三行输出?
答:在分析左图和右图之前,需要先知道一个概念,那就是对于用户级语言(printf/fprintf/fputs/fwirte),当他们没有被刷新时(或者说被系统调用时),他们会暂存在语言层缓冲区中,而子进程同样会继承语言层缓冲区。但对于系统级函数write而言,因为他是直接拷贝到文件内核缓冲区中,所以会立即刷新,子进程无法继承,因此右图我们能够看到七条语句。左图是将结果打印在显示器上,而显示器属于行刷新,只要有\n就会刷新,因此子进程创建之前,所有语言层缓冲区中的内容都已刷新完毕,因此只有四条语句。
不妨我们验证一下,如下代码:
int main() { printf("hello\n"); fprintf(stdout,"kivotos\n"); const char* msg = "world"; fwrite(msg,strlen(msg),1,stdout); //printf("\n"); return 0; }
结果如下图:
可以看到,当我们不打印\0时,显示器只显示两条,而当我们打印\0时,显示器上显示了三条。
补充:
重定向会改变刷新方式