Linux —— 基础IO(2)

简介: Linux —— 基础IO(2)

五、文件描述符的分配规则

我们先来看一下连续打开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被三个流所占用。

1ecd1b2606ed46e9956a89f231c9802c.png

如果我们将0关闭,会是什么样的呢? 在原来代码基础上加上 close(0);

1ecd1b2606ed46e9956a89f231c9802c.png

我们再将2关闭 close(2);

1ecd1b2606ed46e9956a89f231c9802c.png

给新文文件分配fd,是从fd_array数组中找一个最小的,没有被使用过的,作为新的fd。

六、重定向

本来应该写到显示器的数据,去写到了文件中,我们把这种叫做重定向;

1.输出重定向

我们先看一下面的代码,我们本意是想将hello linux!打印到显示器上:

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png

我们只是加了一个close(1);这段代码,为什么就打印到文件中了呢?

1ecd1b2606ed46e9956a89f231c9802c.png

printf函数本质是向stdout输出数据的,而stdout是一个struct FILE*类型的指针,FILE是C语言层面上的结构体,该结构体当中有一个存储文件描述符fd,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。

       C语言的数据并不是立马写到操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

2.追加重定向

追加重定向和输出重定向的本质区别在于,前者不会覆盖原来的数据内容

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png

3.输入重定向

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png

1ecd1b2606ed46e9956a89f231c9802c.png

4.stdout和stderr有什么区别呢?

标准输出流和标准错误流对应的都是显示器,它们有什么区别呢?

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png

       我们同时向标准输出和标准错误中输出数据,都是能够打印到键盘上的;当我们将其重定向到文件中去时,却发现只有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——输出重定向

1ecd1b2606ed46e9956a89f231c9802c.png

运行结果:

1ecd1b2606ed46e9956a89f231c9802c.png

使用dup2——输入重定向

1ecd1b2606ed46e9956a89f231c9802c.png

运行结果:

2020062310470442.png

八、关于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.不关闭文件描述符,进行输出重定向

1ecd1b2606ed46e9956a89f231c9802c.png

1ecd1b2606ed46e9956a89f231c9802c.png

2.关闭文件描述符,进行输出重定向

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png

2.缓冲区的深入理解

       通过上面两次的运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)的。在操作系统内部也是存在一个内核缓冲区的,用户缓冲区到内核缓冲区的刷新策略有如下几种:

  1. 立即刷新:不缓冲
  2. 行刷新(行缓冲 \n),比如,显示器打印
  3. 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入  当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
  4. 1ecd1b2606ed46e9956a89f231c9802c.png 1ecd1b2606ed46e9956a89f231c9802c.png

       当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)

       显示器是行缓冲,即遇到'\n'就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;


       当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;两者都是要通过系统调用接口(open)来完成数据的写入; 在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;

1ecd1b2606ed46e9956a89f231c9802c.png再来看一下下面这段代码:

1ecd1b2606ed46e9956a89f231c9802c.png

运行结果:

1ecd1b2606ed46e9956a89f231c9802c.png

       通过上面的运行结果,再结合之前所说,我们这里不是关闭了1吗?为什么还是能够打印出来呢?我们可以看到这里四条输出语句都是向显示器上打印的,并且都有'\n',表明是行刷新,在关闭1之前就已经刷新到显示器上了。

1ecd1b2606ed46e9956a89f231c9802c.png

标准错误不会重定向我们能够理解,但是其他三条语句应该是重定向到文件中呀,而这里运行结果只有一条hello stdout。这是因为,我们的msg1是直接通过系统调用接口,把数据暂存到内核缓冲区,不会把数据暂存到上层的用户级缓冲区,所以关闭1根本就不会影响这个数据刷新到文件;但是下面两个语句由于重定向的原因,刷新策略发生了变化(行缓冲->全缓冲),数据暂存到用户级缓冲区后,本来是等待进程结束后刷新到文件中去的,但是这个过程中却把1关闭了,才导致这两条数据并没有被刷新到文件中;

对于以上的理解有了新的认识后,我们再来看一下这段代码:

1ecd1b2606ed46e9956a89f231c9802c.png

运行结果:

 通过上面的运行结果发现, 当我们直接运行程序时,它向显示器上打印了3条语句,但是我们程序中有创建子进程的语句,当我们重定向后发现文件中多打印了两条语句,而且只是针对C语言的接口,而非系统调用接口;


       当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。

       而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就由行缓冲变成了全缓冲,此时我们使用printf和fprintf函数打印的数据都暂存到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fprintf函数打印的数据就有两份。

3.如何解决缓冲区不刷新的问题

1ecd1b2606ed46e9956a89f231c9802c.png

2020062310470442.png



目录
相关文章
|
15天前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
22天前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
22 0
|
24天前
|
存储 IDE Linux
Linux源码阅读笔记14-IO体系结构与访问设备
Linux源码阅读笔记14-IO体系结构与访问设备
|
2月前
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
47 0
|
2月前
|
Linux 编译器 C语言
【Linux】基础IO----理解缓冲区
【Linux】基础IO----理解缓冲区
35 0
【Linux】基础IO----理解缓冲区
|
2月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
113 1
|
3月前
|
Linux 编译器 C语言
【Linux】基础IO_4
【Linux】基础IO_4
18 3
|
3月前
|
Linux C++
c++高级篇(三) ——Linux下IO多路复用之poll模型
c++高级篇(三) ——Linux下IO多路复用之poll模型
|
3月前
|
缓存 监控 网络协议
c++高级篇(二) ——Linux下IO多路复用之select模型
c++高级篇(二) ——Linux下IO多路复用之select模型
|
2月前
|
Linux C语言 C++
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
35 0