【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用

文章目录

  • 前言
  • 1. open函数的参数的理解和和使用
  • 2. 文件描述符 fd 0 1 2 的理解
  • 3. 深入理解文件描述符
  • 4. 部分源码说明查看文件描述符
  • 5. 理解一切皆文件
  • 6. 文件描述符的分配规则
  • 7. 重定向的原理
  • 8. dup2--重定向函数

前言

我们知道我们平时使用的文件操作,如C语言C++或者JAVA等语言,都会封装自己的对文件操作的函数接口,供我们对文件进行操作;而实际上,当我们使用这些文件操作的函数接口时候,本质都是访问硬件设备(磁盘);
一个硬件设备是如何被函数接口的调用访问到的呢?
当然是通过操作系统,操作系统是管理硬件设备的;所有语言所封装的文件操作接口,都必须通过操作系统的允许,才可以访问到磁盘这个硬件设备,而操作系统是不相信任何用户的,所以为了能够得到操作系统的允许,我们又必须提供一些系统调用接口,供操作系统和用户打交道;
也就是说,当我们在语言层面所使用的文件操作函数接口,本质要访问物理硬件设备磁盘,而访问该磁盘时候,必须要操作系统进行管理,同时操作系统会提供一系列的系统调用供用户去访问操作系统,而这些系统调用接口有很多,我们这里所说的系统调用接口是于文件操作相关的系统调用接口;


1. open函数的参数的理解和和使用

open函数原型:
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第1张图片


pathname: 要打开或创建的目标文件;
flags: 表示以何种方式打开文件,其实就是C语言的w,r,a,rb,w+等操作的封装;打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags;
flag的参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRONC:文件以只读或者只写打开是,清空文件内容;
mode_t:打开文件的权限,以八进制形式写;


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


测试:以O_CREAT方式打开文件,但是没设置权限,这是错误的;

#include
#include
#include
#include
int main(){
	//O_WRONLY | O_CREAT组合等价C语言fopen中传入"w"
	//以O_CREAT方式打开文件,但是没设置权限,这是错误的
  int fd = open("./log.txt",O_WRONLY | O_CREAT); 
  if(fd < 0){
    perror("open failed:\n");
  }
  close(fd);
  return 0;
}

测试结果:虽然成功打开了文件,创建成功,但是生成的文件log.txt是错误的权限,都是乱的,所以这种方式是不可以取的;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第2张图片


正确使用方式:使用了O_CREAT的参数,必须给文件名,赋予权限;

#include
#include
#include
#include
int main(){
  //正确使用方式,当我们使用参数O_CREAT时候,必须给文件赋予权限
  //O_WRONLY | O_CREAT组合等价C语言fopen中传入"w"
  //下面等价FILE* fp = fopen("./log.txt","w");
  int fd = open("./log.txt",O_WRONLY | O_CREAT,0644);
  
  if(fd < 0){ //打开失败,返回fd = -1
    perror("open failed:\n");
  }
  
  close(fd);
  return 0;
}

理解标志位:open第二个参数传入的形式:
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第3张图片
为什么我们可以用 |方式可以传入那么多个参数呢?就是因为 |会取出设置为1的bit上的位数,这样在open内部进行判断就可以,知道你传入了什么参数;


如何查找系统内部定义的flag参数的标志位:

gerp -ER 'O_WRONLY' /usr/include/ #这样获取到O_WRONLY所在的文件

在这里插入图片描述


在复制该路径:用vim打开即可:

vim /usr/include/asm-generic/fcntl.h # 这就可以看到标志位的信息了

理解文件描述符fd:也就是open成功返回的函数的返回值

  1. 这个返回值的文件描述符:是从3开始的,也即在一个进程中每成功打开一个文件,就会依次递增,3-》4-》5-》6…;
  2. 那为什么从 3开始,0,1,2这三个数字呢?这三个数字被,标准输入(键盘),标准输出(显示器),标准错误(显示器)三个硬件所占用了;
  3. 这里打开的文件,都是加载到内存,被操作系统所管理,强调是打开的文件,是因为没打开的文件,是由文件系统所管理,也就是该文件还在磁盘中呢。
  4. 【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第4张图片

2. 文件描述符 fd 0 1 2 的理解

【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第5张图片


系统层面的的 0 1 2 和 C语言上的 stdin和stdout stderr有什么联系嘛?
其实本质是 stdin 和 stdout stderr 就是一个变量名,类型为 FILE* 而这个FILE 结构体里面有个成员就是 fd,文件描述符;
总的来说:就是C语言的 stdin和stdout stderr 包含 系统的 0 1 2;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第6张图片


其实不单单是C语言有封装fd,其他语言也是一样封装了fd,只不过使用的是不同的结构体而已;


为什么当一个进程运行起来,系统会默认给我们打开 0 1 2 标准输入 便准输出 便准错误这三个文件呢?
本质是因为我们所有进程都是的父进程都是bash,而一旦我们又新的进程运行起来,就会复制父进程bash的PCB,而bash本身就需要打开这三个文件;所以说,所有进程运行起来都会默认打开这三个文件;其中需要注意的是,这三个文件,是通过task_struct的里面的成员 struct files_sturct继承下去的,而不是直接继承这三个文件啊,这三个文件是操作系统层面上的东西,而不是进程里面的东西;
FILE结构体:的源代码:
里面有个成员:int _fileno; //封装的文件描述符

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;
#if

3. 深入理解文件描述符


我们知道这个文件描述符,有点类似数组的下标,也就是open函数返回的值,open是一个系统调用,也就意味着,这个返回值的来源就是操作系统,也就是说明,这个文件描述符是在操作系统内部的;

而一个进程,可以打开多个文件,也就是说,会有多个文件描述符从操作系统返回给用户层拿到;

一个文件 = 文件内容+文件属性(文件创建时间,权限等);我们所用的C接口,对文件的操作,fread fwrite等都是对文件的内容操作,而使用chmod chgrep等命令都是对文件的属性操作;

同时也说说明,一个空文件被创建出来(内容为空),也是有大小的;

文件被进程打开,操作系统就要管理该文件,那么操作系统如何管理该文件呢?那自然而然是通过一个数据结构:
strcut file{ //打开的文件相关属性;}来进行描述,而这个结构体的属性数据来源,自然而然是文件被创建时候,还在磁盘时候的数据属性;

所以说:一个文件进程打开,操作系统一定要给文件一个数据结构去描述它,表示它被打开了;而操作系统是如何组织好这么多个被打开的文件呢?,那自然而然就是通过双链表的数据结构形式去管理这些结构体,也就是struct file;


问题的关键是:操作系统创建了那么多文件,有那么多的结构体file_struct,操作系统是双链表的方式管理好了这些结构体,但这些file是如何和进程建立关联的呢????

为了建立两者这两者之间的联系,在操作系统内部,还有一个结构体:struct file_struct{ },这个结构体里面包含了一个很重要的成员就是,struct file* fd_array[ ],这个fd_arrary数组是指针数组,数组每个元素就是 描述文件属性的结构体 struct file{ },对应得下标就是0,1,2。。。。。过去下标;

而这个 struct file_struct*的结构体,就是在 进程控制块PCB里的一个成员,也就是 strcut struct_task { }里的一个成员


【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第7张图片


当你在创建一个新的文件时候,那么操作系统就会给你搞一个 strcut file, 然后把它存放到 fd_array[ ] 数组里,然后把对应的下标返回给上一层用户;那么用户就可以拿到下标,也就是描述符干自己的事了


所以说:本质 0 1 2 对应于标准输入标准输出和标准错误,只不过是操作系统创建好了struct file ,然后这三个位置的数组占用罢了,当新的文件被打开只能从第三个位置开始;


而我们的 read write 函数的调用,也需要传入 open函数返回的fd进去,我们知道指向 read write的函数,是进程,那么进程就有pcb,自然而然可以找到 struct file_stuct这个结构题体,也就可以找到 struct * fd_ array[]这个数组,找到对应的文件描述符下标,找到对应的struct file,也就是找到对应的文件了;


4. 部分源码说明查看文件描述符

截取部分Linux源代码,看看里面是否和我上面所说的一样,很明显,内核源码就是这样组织管理文件的;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第8张图片


5. 理解一切皆文件

我们知道,我们的键盘,显示器,网卡,磁盘等其他设备都是硬件设备,在系统的角度来看,这些都是外设IO接口;
也就是说,这些硬件设备,都是有自己对应的read write 函数的实现,完成硬件设备的访问;只不过对应键盘来说,它的write方法为空,对于显示器来说它的read方法为空;而实现这些外设的read 和 write方法都是在驱动层完成的,每个设备虽然它们的方法名称相同,但是功能不一样,因为设备都不一样了,读写的方式肯定也不一样;


而在操作系统的角度,在Linux操作系统中,我们做了一层虚拟软件层的封装,也就是所谓的vfs,但是我们打算vfs是什么,因为我们知道,在操作系统的角度上来讲,一旦我们要访问某一个文件时候,我们就会给该文件创建一个
struct file的结构体,这样,我们外设设备就可以和每个结构体 struct file 对应起来。这个 struct file里面包含了两个重要的指针,就是 read write 函数指针,这个指针指向对于的外设设备,这样我们就可以把硬件设备和软件联系了起来;


我们继续站在上层角度,操作系统的上一层就是用户层,用户层就可以通过read write的方法,直接去访问你对应的硬件设备,用户层,使用统一的接口,去操作访问不同硬件设备,用户层根本不会关心,你的底层干了什么;
因为对于用户来说,一旦使用read write的接口,你就会在操作系统的底层创建了 struct file这个结构体,它会帮你完成去找到对应的硬件设备;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第9张图片



总的来说:一切皆文件:是在操作系统的角度上去看,我们把所有设备都看出一个一个的struct file;


我们也从源码层面简单的看看,struct file的内容,是否可以找到硬件设备的read write;发现验证是有的;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第10张图片


6. 文件描述符的分配规则

直接看代码:

#include 
#include 
#include 
#include 
int main()
{
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

输出发现是 fd: 3


关闭0或者2,在看

#include 
#include 
#include 
#include 
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
	perror("open");
	return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
return 0;
}

发现是结果是: fd: 0 或者 fd 2 ;
可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。


7. 重定向的原理

那如果关闭1呢?
看代码:

#include 
#include 
#include 
#include 
#include 
int main()
{
	close(1);
    int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);

    printf("fd: %d\n", fd);
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");

    close(fd);
	exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 log.txt 当中,其中,fd=1。这种现象叫做输出
重定向。


其实,当我们运行起这个进程时候,操作系统会默认打开三个文件,就是标准输入,便准输出,标准错误,它们对应就是键盘,显示器,显示器,在操作系统层面上,这三个东西就是一个 struct file 的结构体而已;而进程和这三个结构体联系的关键就是进程的PCBtask_strtuct里面有个结构体指针struct files_struct*,而struct files_struct 里面又有一个 file* fd_arrry[ ] 数组;这个数组里面前三个元素,也就是下标为0 1 2 的元素,里面存放的就是 标准输入便准输出的结构体指针,也就是strcut file*

当我们关闭了 1号文件描述符,也就是说close(1),断开了 fd_arrary 数组元素1号位置的 便准输入 struct file的联系,而当我们再次用open函数打开一个文件为 log.txt时候,文件描述符分配原则告诉我们,就会分配一个数组 1号位置给该文件log.txt;一旦我们使用printf输出时候,就不会显示到屏幕了,而显示到文件;这是因为printf默认是往便准输入输出内容的,而printf的便准输入就是stdout这个变量,而stdout这个变量就是一个FILE类型的结构体指针,而这个结构体指针里面有一个成员就是文件描述符fd,而fd就是1号,而这个1号就是指向struct file 这个结构体,这个结构体就是便准输入啊;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第11张图片


总的来说,重定向原理就是关掉 0 1 这两个文件描述符,也就是断开, 0 1 对应的键盘和显示器的结构体 struct file之间的联系,让新的文件strcut file 再放入到 0 1 这个对应数组下标去,这样就会完成了重定向;


8. dup2–重定向函数

【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第12张图片


代码实例:
使用dup2完成输出重定向:

#include
#include
#include
#include
#include //perror
#include
#include //exit
int main(){
  int fd = open("./log.txt",O_CREAT | O_WRONLY |O_TRUNC,0644 );
  if(fd < 0){
    perror("open error:");
    exit(1);
  }
  dup2(fd,1); //本应该输出到1的,输出到了fd中

  printf("hello 我是printf,我不再输出到屏幕了,我输出到了log.txt\n");
  fprintf(stdout,"hello 我是fprintf,我不在输出到屏幕了,我输出到了log.txt\n");
  fputs("hello 我是fputs,不再输出到屏幕了,我输出到了log.txt\n",stdout);
  close(fd);
  return 0;

【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第13张图片


原理就是,把oldfd位置的值,复制给了newfd位置的值,这会导致,newfd位置的值和oldfd位置值一样,也就是说,newffd位置的值,不再指向原来的struct file,而是指向了 oldfd的 struct file;
【Linux】从系统层面上深入理解文件描述符fd 和 理解一切皆文件 和 重定向原理 和 dup2的基本原理和使用_第14张图片


你可能感兴趣的:(Linux,文件描述符,重定向原理,一切皆文件)