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



目录
相关文章
|
2月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
98 0
|
2月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
106 1
Linux C/C++之IO多路复用(aio)
|
4月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
2月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
36 0
Linux C/C++之IO多路复用(poll,epoll)
|
4月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
38 0
|
4月前
|
存储 IDE Linux
Linux源码阅读笔记14-IO体系结构与访问设备
Linux源码阅读笔记14-IO体系结构与访问设备
|
5月前
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
84 0
|
5月前
|
Linux 编译器 C语言
【Linux】基础IO----理解缓冲区
【Linux】基础IO----理解缓冲区
81 0
【Linux】基础IO----理解缓冲区
|
5月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
270 2
|
6月前
|
Linux 编译器 C语言
【Linux】基础IO_4
【Linux】基础IO_4
33 3