五、文件描述符的分配规则
我们先来看一下连续打开4个文件,所对应的fd都是什么?
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd1 = open("./log1.txt", O_CREAT | O_WRONLY, 0644); int fd2 = open("./log2.txt", O_CREAT | O_WRONLY, 0644); int fd3 = open("./log3.txt", O_CREAT | O_WRONLY, 0644); int fd4 = open("./log4.txt", O_CREAT | O_WRONLY, 0644); printf("fd1:%d\n",fd1); printf("fd2:%d\n",fd2); printf("fd3:%d\n",fd3); printf("fd4:%d\n",fd4); return 0; }
从运行结果看,它是从3开始向上增长的,因为0/1/2被三个流所占用。
如果我们将0关闭,会是什么样的呢? 在原来代码基础上加上 close(0);
我们再将2关闭 close(2);
给新文文件分配fd,是从fd_array数组中找一个最小的,没有被使用过的,作为新的fd。
六、重定向
本来应该写到显示器的数据,去写到了文件中,我们把这种叫做重定向;
1.输出重定向
我们先看一下面的代码,我们本意是想将hello linux!打印到显示器上:
我们只是加了一个close(1);这段代码,为什么就打印到文件中了呢?
printf函数本质是向stdout输出数据的,而stdout是一个struct FILE*类型的指针,FILE是C语言层面上的结构体,该结构体当中有一个存储文件描述符fd,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
C语言的数据并不是立马写到操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
2.追加重定向
追加重定向和输出重定向的本质区别在于,前者不会覆盖原来的数据内容
3.输入重定向
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据
4.stdout和stderr有什么区别呢?
标准输出流和标准错误流对应的都是显示器,它们有什么区别呢?
我们同时向标准输出和标准错误中输出数据,都是能够打印到键盘上的;当我们将其重定向到文件中去时,却发现只有stdout的内容重定向到了文件中。实际上我们在使用重定向的时候,是把文件描述符1的标准输出流重定向了,而不会对标准错误流重定向。
七、系统调用dup2
以上的操作我们都是在关闭标注输入和标准输出后完成重定向的,显得很麻烦,如果标准输入和标准输出都被占用(已经打开了),我们如何去完成重定向呢?要完成重定向我们就可以将fd_array数组中的元素进行拷贝即可;例如:我们将fd_array[3]中的内容拷贝到fd_array[1]中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt 。在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。本质上dup2就是将进程中文件描述表中的需要重定向的内容进行相关的拷贝,dup2的函数原型如下:
#include<unistd.h> int dup2(int oldfd, int newfd);
函数功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]中;
函数返回值:调用成功返回0,失败返回-1;
使用dup2时,我们需要注意以下两点:
如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd
使用dup2——输出重定向
运行结果:
使用dup2——输入重定向
运行结果:
八、关于FILE
1.C库当中的FILE结构体
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
我们可以使用vim 打开usr/include/stdio.h的文件查看FILE。
typedef struct _IO_FILE FILE; //在/usr/include/stdio.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 };
2.FILE结构体中的缓冲区
从FILE的源码当中我们发现了FILE结构体里面封装了fd,也就是里面的_fileno。不难发现我们在里面还看到了缓冲区。这里的缓冲区指的是C语言当中的缓冲区。 1.初步了解缓冲区 我们先来看一下下面的代码,代码的含义是输出重定向,观察是否关闭文件描述符,对结果有何影响?
1.不关闭文件描述符,进行输出重定向
2.关闭文件描述符,进行输出重定向
2.缓冲区的深入理解
通过上面两次的运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)的。在操作系统内部也是存在一个内核缓冲区的,用户缓冲区到内核缓冲区的刷新策略有如下几种:
- 立即刷新:不缓冲
- 行刷新(行缓冲 \n),比如,显示器打印
- 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入 当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
显示器是行缓冲,即遇到'\n'就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;
当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;两者都是要通过系统调用接口(open)来完成数据的写入; 在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;
再来看一下下面这段代码:
运行结果:
通过上面的运行结果,再结合之前所说,我们这里不是关闭了1吗?为什么还是能够打印出来呢?我们可以看到这里四条输出语句都是向显示器上打印的,并且都有'\n',表明是行刷新,在关闭1之前就已经刷新到显示器上了。
标准错误不会重定向我们能够理解,但是其他三条语句应该是重定向到文件中呀,而这里运行结果只有一条hello stdout。这是因为,我们的msg1是直接通过系统调用接口,把数据暂存到内核缓冲区,不会把数据暂存到上层的用户级缓冲区,所以关闭1根本就不会影响这个数据刷新到文件;但是下面两个语句由于重定向的原因,刷新策略发生了变化(行缓冲->全缓冲),数据暂存到用户级缓冲区后,本来是等待进程结束后刷新到文件中去的,但是这个过程中却把1关闭了,才导致这两条数据并没有被刷新到文件中;
对于以上的理解有了新的认识后,我们再来看一下这段代码:
运行结果:
通过上面的运行结果发现, 当我们直接运行程序时,它向显示器上打印了3条语句,但是我们程序中有创建子进程的语句,当我们重定向后发现文件中多打印了两条语句,而且只是针对C语言的接口,而非系统调用接口;
当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就由行缓冲变成了全缓冲,此时我们使用printf和fprintf函数打印的数据都暂存到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fprintf函数打印的数据就有两份。
3.如何解决缓冲区不刷新的问题