[OS-Linux]详解Linux的基础IO ------- 文件描述符fd

Posted TT在长大

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OS-Linux]详解Linux的基础IO ------- 文件描述符fd相关的知识,希望对你有一定的参考价值。

 本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。

目录

一、C语言中的文件I/O操作

二、系统文件I/O

1. 接口介绍

2. open函数返回值

三、文件描述符fd

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

六、dup2 系统调用的使用

七、FILE


一、C语言中的文件I/O操作

首先来回顾一下C语言中的文件I/O操作[C/C++]C语言中对文件的操作方法_RMA515T的博客-CSDN博客

之前的博客中就有详细的介绍。这里就只做简单演示。

#include <stdio.h>

int main()
{

  FILE *fp = fopen("zht2", "w+");
  if(!fp)
  {
    printf("erroe");

    return 1;
  }

  int count = 5;

  while(count--)
  {
    fwrite("hello\\n", 6, 1, fp);
  }

  fseek(fp, 0, SEEK_SET);
  char buf[1024];
  while(1)
  {
    ssize_t s = fread(buf, 1, 7, fp);
    if(s > 0)
    {
      buf[s] = 0;
      printf("%s", buf);
    }
    if(feof(fp))
    {
      break;
    }
  }

  fclose(fp);
  return 0;

}

C默认会打开三个输入输出流,分别是stdin, stdout, stderr,这三个流的类型都是FILE*, fopen返回值类型,文件指针。

文件的打开方式:

   “r”(只读):为了输入数据,打开一个已经存在的文本。如果文件不存在则文件出错。
    “w”(只写): 为了输出数据,打开一个文本文件。如果文件不存在则建立一个新的文件。
    “a”(追加):向文本文件尾添加数据。如果文件不存在则出错。
    “rb”(只读): 为了输入数据,打开一个二进制文件。如果文件不存在则出错。
    “wb”(只写) : 为了输出数据,打开一个二进制文件。如果文件不存在则建立一个新的文件。
    “ab”(追加): 向一个二进制文件尾添加数据。如果文件不存在则出错。
    “r+”(读写): 为了读和写,打开一个文本文件。如果文件不存在则出错。
    “w+”(读写): 为了读和写,建议一个新的文件。如果文件不存在则建立一个新的文件。
    “a+”(读写): 打开一个文件,在文件尾进行读写。如果文件不存在则建立一个新的文件。
    “rb+”(读写):为了读和写打开一个二进制文件。如果文件不存在则出错。
    “wb+”(读写): 读和写,新建一个新的二进制文件。如果文件不存在则建立一个新的文件。
    “ab+”(读写): 打开一个二进制文件,在文件尾读写。文件不存在则写建立一个新的文件。

二、系统文件I/O

除了上述C接口操作文件,,我们还可以采用系统接口来进行文件访问,

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
  int fd = open("zht", O_RDWR | O_CREAT, 0666);

  if(fd < 0)
  {
    printf("erroe");

    return 1;
  }

  int count = 5;

  while(count--)
  {
    write(fd, "hello\\n", 6);
  }

  lseek(fd, 0, SEEK_SET);
  char buf[1024];
  while(1)
  {
    ssize_t s = read(fd, buf, 6);
    if(s > 0)
    {
      write(1, buf, 6);
    }
    else 
    {
      break;
    }
  }

  close(fd);
  return 0;

}

当前路径是指进程运行时所处的路径。 

1. 接口介绍

open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
        O_RDONLY: 只读打开
        O_WRONLY: 只写打开
        O_RDWR : 读,写打开
                这三个常量,能且只能用一个。
        O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
        O_APPEND: 追加写

返回值:
        成功:新打开的文件描述符
        失败:-1

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

write、read、close、lseek 可以类比C文件相关接口。

2. open函数返回值

open函数返回值是文件描述符fd,是一个整数。

在认识文件描述符fd之前,先来认识一下两个概念: 系统调用和库函数:
        fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)

        open close read write lseek 都属于系统提供的接口,称之为系统调用接口

可以认为,f系列的函数,都是对系统调用的封装。

三、文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个整数。

文件是进程运行时打开的,在Linux中一切皆文件,时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;
}

文件是

文件是由进程打开的,一个进程可以打开多个文件,文件被操作系统通过文件管理统一进行管理。 

这里涉及到一个文件的相关概念,磁盘文件和内存文件,磁盘文件就是我们保存在磁盘上的文件

,由内容和元属性构成的。内存文件,更多的是文件的属性,在缓冲区延后式的慢慢加载数据,是操作系统直接读取的。

 所谓文件描述符本质就是fd_arry数组的下标。

        当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。

        进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,向一张表files_struct,该表最重要的部分就是包涵一个指针数组每个元素都是一个指向打开文件的指针

        本质上,文件描述符就是该数组的下标。只要有文件描述符,就可以找到对应的文件。

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

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("file", O_RDONLY);

    if(fd < 0){
        perror("open");
        return 1;
    }

    printf("fd: %d\\n", fd);

    close(fd);
    return 0;
}

输出发现是 3。

关闭0:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    close(0);

    int fd = open("file", O_RDONLY);

    if(fd < 0){
        perror("open");

        return 1;
    }

    printf("fd: %d\\n", fd);

    close(fd);
    return 0;
}

结果是: fd: 0

文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

五、重定向

那如果关闭1:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    close(1);

    int fd = open("file", O_WRONLY|O_CREAT, 00644);

    if(fd < 0){

        perror("open");
        return 1;
    }

    printf("fd: %d\\n", fd);

    fflush(stdout);

    close(fd);
    exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢,如图所示:

 重定向的本质是修改文件描述符fd下标对应的struct file* 的内容。

六、dup2 系统调用的使用

函数:

#include <unistd.h>

int dup2(int oldfd, int newfd);

使用示例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() 
{

    int fd = open("./log", O_CREAT | O_RDWR);

    if (fd < 0) {

        perror("open");

        return 1;
    }

    close(1);

    dup2(fd, 1);

    for (;;) {
        char buf[1024] = {0};

        ssize_t read_size = read(0, buf, sizeof(buf) - 1);

        if (read_size < 0) {

            perror("read");

            break;
        }

    printf("%s", buf);

    fflush(stdout);
    }

    return 0;
}

 七、FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的,所以C库当中的FILE结构体内部,必定封装了fd。

首先,我们明确一下fopen在做什么:

        1.给调用的用户申请struct FILE结构体变量,并返回地址;

        2.在底层通过open打开文件并返回fd,把fd填充进FILE变量中的FILENO

再来明确缓冲,缓冲分三种

        1.无缓冲;

        2.半缓冲(对显示器刷新数据);

        3.全缓冲(对文件写入时);

来一段代码:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{

    const char *msg0="hello printf\\n";
    const char *msg1="hello fwrite\\n";
    const char *msg2="hello write\\n";

    printf("%s", msg0);

    fwrite(msg1, strlen(msg0), 1, stdout);

    write(1, msg2, strlen(msg2));

    fork();

    return 0;

}

运行出结果:

如果对进程实现输出重定向./a.out > file , 结果变成了:

 我们发现printf 和fwrite (库函数)都输出了2次,而write 只输出了一次(系统调用)

这是因为:

        C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
        printf、fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后,但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
        write 没有变化,说明没有所谓的缓冲。

在FILE结构体中

以上是关于[OS-Linux]详解Linux的基础IO ------- 文件描述符fd的主要内容,如果未能解决你的问题,请参考以下文章

[OS-Linux]详解Linux基础开发工具

[OS-Linux]详解Linux基础开发工具

[OS-Linux]详解Linux的权限

[OS-Linux]详解Linux的权限

[OS-Linux]详解Linux的进程间通信1------管道

[OS-Linux]详解Linux的进程间通信1------管道