二、文件描述符
1.0 & 1 & 2
通过对open函数的学习,我们知道了文件描述符就是一个小整数。linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2。0 1 2对应的物理设备一般是:键盘,显示器,显示器,所以输出还可以有以下方式:
下面我们用文件的读写完成简单的输入输出:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int main() { char buf[1024]; ssize_t s = read(0,buf,sizeof(buf)); if (s>0) { buf[s] = 0; write(1,buf,strlen(buf)); write(2,buf,strlen(buf)); } return 0; }
1和2代表标准输出和标准错误,我们先去读一下缓冲区,当返回值是缓冲区的最后一个字符,我们在这个位置放入\0,然后写入刚刚读取到的字符,下面我们运行一下:
通过这个小例子我们就知道了文件描述符就是从0开始的小整数,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了file结构体,表示一个已经打开的文件对象,而进程执行open系统调用,所以必须让进程和文件关联起来,每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
文件描述符的分配规则
我们写一个只读的代码来获取文件的fd:
int main() { int fd = open("my.txt",O_RDONLY); if (fd<0) { perror("open"); return 1; } printf("fd:%d\n",fd); close(fd); return 0; }
我们发现fd是3,那么这个时候我们把0关闭了再看看是什么结果:
int main() { close(0); int fd = open("my.txt",O_RDONLY); if (fd<0) { perror("open"); return 1; } printf("fd:%d\n",fd); close(fd); return 0; }
通过运行结果我们发现,当我们将0关闭后文件描述符立马变成了0,我们再看看把2关闭了如何:
int main() { //close(0); close(2); int fd = open("my.txt",O_RDONLY); if (fd<0) { perror("open"); return 1; } printf("fd:%d\n",fd); close(fd); return 0; }
当我们将2关闭后文件描述符又变成了2,这就说明文件描述符的分配规则是:在file_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
2.重定向
还是刚刚的代码,如果我们将1关闭了会出现什么情况呢?
int main() { //close(0); close(1); int fd = open("my.txt",O_WRONLY | O_CREAT,00644); if (fd<0) { perror("open"); return 1; } printf("fd:%d\n",fd); fflush(stdout); close(fd); exit(0); }
通过运行结果我们可以发现,本来应该输出到显示器上的内容,输出到了文件中,其中fd=1,这种现象叫做输出重定向,常见的重定向有> >> <。那么重定向的本质是什么呢?我们看下图:
当然对于重定向来说还有一个系统调用接口,这个函数叫dup2:
1. #include <unistd.h> 2. int dup2(int oldfd, int newfd);
对于这个函数的参数1来说是被换的那个描述符,参数2是像0,1,2,这样的默认描述符,接下来我们使用这个函数完成以下重定向:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main() { int fd = open("my.txt",O_WRONLY | O_CREAT,00644); if (fd<0) { perror("open"); return 1; } dup2(fd,1); printf("fd:%d\n",fd); close(fd); }
通过上图我们可以看到成功的重定向到文件中。下面我们看看文件缓冲区的概念:
我们先编写一段代码看看现象:
int main() { //c库中的 fprintf(stdout,"hello fprintf\n"); //系统调用 const char* msg = "hello write\n"; write(1,msg,strlen(msg)); //fork(); return 0; }
可以正常打印,那么我们调用一下子进程会变成什么呢?
我们发现运行结果和刚刚是一样的,难道没有什么用吗?我们接下来重定向到文件中:
这个时候我们发现了,居然会多打印一个fprintf,为什么会出现这样的现象呢?
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据
的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲
所以:
printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
行缓冲:碰到\0就刷新缓冲区
全缓冲:缓冲区满了才刷新
显示器采用的刷新策略:行缓冲。普通文件采用的刷新策略:全缓冲
那么缓冲区在哪里呢?在你进行fopen打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中。
总结
对于linux下的文件操作而言,C语言等对于文件操作的函数都是经过linux系统文件接口来封装的,我们在用C语言文件操作的时候看着很简单的一句代码在系统调用中会有很多的操作,对于文件描述符实际上就是file_struct中的指针数组的下标,文件描述符的分配规则就是优先从最小的下标开始分配