文件描述符fd
fd:打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定读写的文件。
文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表得到形式链接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。
进程和文件之间的对应关系是如何建立的?
我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct,mm_struct,页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
而task_struct当中有一个指针,该指针指向一个名为file_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
因此,我们只要有一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
注意:向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
什么叫做进程创建的时候会默认打开0,1,2?
0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。
而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘,显示器,显示器形成各自的struct_file,将3个struct_file连入文件双链表中,并将这3个struct_file的地址分别填入fd_array数组下标为0,1,2的位置,至此就默认打开了标准输入流,标准输出流和标准错误流。
磁盘文件VS内存文件?
当文件存储在磁盘当中时,我们将其称为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称为内存文件。
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取,输入或输出等操作时,再延后式的加载文件数据。
文件描述符的分配规则
在前面已经演示了fd的分配规则,就是默认从3开始分配,因为Linux进程默认情况下会打开3个缺省的文件描述符,上面介绍过。所以我们的进程在打开文件时,就是从3开始分配。下面做一个实验:关闭fd为0的文件,也就是标准输入,此时我们打开两个文件,看看这两个fd分别是多少
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { close(0); int fd1 = open("log.txt1", O_WRONLY|O_CREAT, 0664); int fd2 = open("log.txt2", O_WRONLY|O_CREAT, 0664); printf("fd1:%d\n", fd1); printf("fd2:%d\n", fd2); close(fd1); close(fd2) return 0; }
运行结果:
观察结果,可以发现,关闭fd为0的文件后,后序打开的文件,文件描述符就是从0开始分配,然后分配没有被使用的fd,也就是3。从最小被使用的文件描述符开始分配。
文件描述符分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,做为新的文件描述符。
重定向
概念
概念:重定向是指修改原来默认的一些东西,对原来系统命令的默认执行方式进行改变。
重定向一般有以下几种方式:
- 输出重定向:>
- 输入重定向:<
- 追加重定向:>>
演示:
正常使用echo命令,字符串是输出在显示器上,加了输出重定向后,字符串被输出到文件上,也就是把本应该打印到显示器上的内容打印到了文件上。输出重定向改变了默认的输出方式。
原理
输出重定向
输出重定向的本质就是修改文件描述符下标对应的struct file*的内容。将本应该输出到一个文件的数据重定向输出到另一个文件中。
例如,如果我们想让本应该输出到"显示器文件"的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将"显示器文件"关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(1); int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); if (fd < 0){ perror("open"); return 1; } printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); fflush(stdout); close(fd); return 0; }
运行结果如下,可以看到显示器上没有输出数据,对应数据输出到了log.txt文件当中
注意:
- printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
- C语言中的数据并不是立刻可以写到内存操作系统里面,而是写到C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
追加重定向
实例:关闭标准输出流,也就是fd为1的文件,此时我们再以追加(O_APPEND)的方式打开一个文件,然后进行输出,观察现象。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main() { close(1); int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 00644); if(fd < 0){ perror("open"); return 1; } printf("你好啊"); fflush(stdout); close(fd); return 0; }
运行结果
根据输出重定向原理,我们也不难介绍这个现象,关闭了标准输出流,以追加的方式打开一个文件,这个文件被分配一个为1的文件描述符。因为printf是库函数,是往fd为1的文件进行输出,所以这里也是直接在log.txt文末进行追加
输入重定向
实例:如果我们想让本应该从"键盘文件"读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将"键盘文件"关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0
代码;
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(0); int fd = open("log.txt", O_RDONLY | O_CREAT, 0666); if (fd < 0){ perror("open"); return 1; } char str[40]; while (scanf("%s", str) != EOF){ printf("%s\n", str); } close(fd); return 0; }
运行结果:
dup2系统调用
作用:复制文件描述符给一个新的文件描述符,让fd_array数组中下标为oldfd的内容拷贝给下标为newfd的内容,也就是让newfd的指向发生改变,指向oldfd所指向的文件。
函数原型:
int dup2(int oldfd,int newfd);
参数介绍:
- oldfd:要复制的文件的文件描述符
- newfd:让文件描述符文newfd的文件称为文件描述符oldfd文件的一份拷贝
函数返回值:调用成功,返回newfd,否则返回-1
实例演示:例如,我们将打开log.txt时获取到对的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件中。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("log.txt",O_WRONLY|O_TRUNC); if (fd < 0){ perror("open"); return 1; } dup2(fd, 1); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); fputs("hello fputs\n",stdout); close(fd); return 0; }
运行结果:printf是C库中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入。
FILE
概念:FILE是C语言的一个对文件进行描述的一个结构体。因为IO相关的函数与系统调用接口是对应的,且库函数封装了系统调用,所以本质上访问文件都是通过fd进行访问的,所以C语言中的FILE结构体内部,必定封装了fd.
首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct_IO_FILE结构体的一个别名。
typedef struct _IO_FILE FILE;
而我们在/usr/include/libio.h头文件中可以找到struct_IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。
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 };
从FILE结构体的源码上看,FILE内部其实是封装了fd的,这里面就是_fileno,里面还有对缓冲区的划分,这里的缓冲区就是C语言级别的缓冲区,之前进度条小程序试验过。
C语言中的fopen函数究竟是做什么?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言中的其他文件操作函数,比如fread,fwrite,fputs,fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行一系列操作。
FILE当中的缓冲区
我们来看看下面这段代码,代码中分别用了两个C库函数和一个系统接口向显示器输出内容,在代码最后还调用了fork函数。
#include <stdio.h> #include <unistd.h> int main() { //c printf("hello printf\n"); fputs("hello fputs\n", stdout); //system write(1, "hello write\n", 12); fork(); return 0; }
运行结果:printf是C库中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入。
将程序的结果重定向到log.txt文件当中后, 我们发现文件当中的内容与我们直接打印输出到显示器的内容不一样
那为什么C库函数打印的内容重定向到文件后变成了两份,而系统接口打印的内容还是原来的一份?
首先应该知道,缓冲的方式有以下三种:
- 无缓冲
- 行缓冲(常见的对显示器进行刷新数据)
- 全缓冲(常见的对磁盘文件写入数据)
当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程,一份子进程,所以重定向到log.txt文件当中printf和puts函数打印的数据就有两份。但由于write函数时系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
这个缓冲区是谁提供的?
实际上这个缓冲区是C语言自带的,如果说这个缓冲区是操作系统提供的,那么printt,fputs和write函数打印的数据重定向到文件后都应该打印两次
这个缓冲区在哪?
我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关信息的。
//缓冲区相关 /* 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. */
也就是说,这里的缓冲区是由C语言提供的,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。
操作系统有缓冲区吗?
操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或显示器上。
硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。
总结:printf和fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,数据也并不是直接刷新在硬件设备上,而是先刷新到操作系统的缓冲区,最后由操作系统来刷新到对应的硬件设备上。