Linux虚拟地址空间与文件描述符

内容来源:
LinuxC应用开发学习笔记(八)—系统调用I/O
LinuxC应用开发学习笔记(九)——文件系统
爱编程的大丙

1. 虚拟地址空间

虚拟地址空间是一个非常抽象的概念,先根据字面意思进行解释:
    ——它可以用来加载程序数据(数据可能被加载到物理内存上,空间不够就加载到虚拟内存中)
    ——它对应着一段连续的内存地址,起始位置为 0。
    ——之所以说虚拟是因为这个起始的 0 地址是被虚拟出来的, 不是物理内存的 0 地址。

虚拟地址空间的大小也由操作系统决定,32位的操作系统虚拟地址空间的大小为 2的32次方 字节,也就是 4G,64 位的操作系统虚拟地址空间大小为 2的64次方字节,这是一个非常大的数。当我们运行磁盘上一个可执行程序, 就会得到一个进程,内核会给每一个运行的进程创建一块属于自己的虚拟地址空间,并将应用程序数据装载到虚拟地址空间对应的地址上。

进程在运行过程中,程序内部所有的指令都是通过 CPU 处理完成的,CPU 只进行数据运算并不具备数据存储的能力,其处理的数据都加载自物理内存,那么进程中的数据是如何进出入到物理内存中的呢?其实是通过 CPU 中的内存管理单元 MMU(Memory Management Unit)从进程的虚拟地址空间中映射过去的。

在这里插入图片描述

1.1 存在的意义

为什么操作系统不直接将数据加载到物理内存中而是将数据加载到虚拟地址空间中,在通过 CPU 的 MMU 映射到物理内存中呢?

先来看一下如果直接将数据加载到物理内存会发生什么事情:

在这里插入图片描述
假设计算机的物理内存大小为 1G, 进程 A 需要 100M 内存因此直接在物理内存上从 0 地址开始分配 100M, 进程 B 启动需要 250M 内存,因此继续在物理内存上为其分配 250M 内存,并且进程 A 和进程 B 占用的内存是连续的。之后再启动其他进程继续按照这种方法进行物理内存的分配。

使用这种方式分配内存会有如下几个问题:

1、每个进程的地址不隔离,有安全风险。

由于程序都是直接访问物理内存,所以恶意程序可以通过内存寻址随意修改别的进程对应的内存数据,以达到破坏的目的。虽然有些时候是非恶意的,但是有些存在 bug 的程序可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。

2、内存效率低。

如果直接使用物理内存的话,一个进程对应的内存块就是作为一个整体操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区(虚拟内存)中,以便腾出内存,因此就需要将整个进程一起拷走,如果数据量大,在内存和磁盘之间拷贝时间就会很长,效率低下。

3、进程中数据的地址不确定,每次都会发生变化。

由于物理内存的使用情况一直在动态的变化,我们无法确定内存现在使用到哪里了,如果直接将程序数据加载到物理内存,内存中每次存储数据的起始地址都是不一样的,这样数据的加载都需要使用相对地址,加载效率低(静态库是使用绝对地址加载的)。

有了虚拟地址空间之后就可以完美的解决上边提到的所有问题了,虚拟地址空间就是一个中间层,相当于在程序和物理内存之间设置了一个屏障,将二者隔离开来。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。

1.2 分区

从操作系统层级上看,虚拟地址空间主要分为两个部分内核区和用户区。
内核区: 内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
内核总是驻留在内存中,是操作系统的一部分。
系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上(系统内核只有一个)。
用户区:
存储用户程序运行中用到的各种数据。

先来看一下进程对应的虚拟地址空间的各个分区,再来详细介绍用户区的组成(以 32 位系统的虚拟地址空间为例)。
在这里插入图片描述
每个进程的虚拟地址空间都是从 0 地址开始的,在程序中打印的变量地址也其在虚拟地址空间中的地址,程序是无法直接访问物理内存的。虚拟地址空间中用户区地址范围是 0~3G,里边分为多个区块:

保留区:位于虚拟地址空间的最底部,未赋予物理地址。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址。

.text段:代码段也称正文段或文本段,通常用于存放程序的执行代码 (即 CPU 执行的机器指令),代码段一般情况下是只读的,这是对执行代码的一种保护机制。

.data段:数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态变量。数据段属于静态内存分配 (静态存储区),可读可写。

.bss段:未初始化以及初始为 0 的全局变量和静态变量,操作系统会将这些未初始化变量初始化为 0
堆(heap):用于存放进程运行时动态分配的内存。
堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
堆向高地址扩展 (即 “向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。

内存映射区(mmap): 作为内存映射区加载磁盘文件,或者加载程序运作过程中需要调用的动态库。

栈(stack):存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址 “向下生长”,分配的内存是连续的。

命令行参数:存储进程执行的时候传递给 main() 函数的参数,argc,argv []

环境变量: 存储和进程相关的环境变量,比如:工作路径,进程所有者等信息

2. 文件描述符

2.1 文件描述符

在 Linux 操作系统中的一切都被抽象成了文件,那么一个打开的文件是如何与应用程序进行对应呢?解决方案是使用文件描述符(file descriptor,简称fd),当在进程中打开一个现有文件或者创建一个新文件时,内核向该进程返回一个文件描述符,用于对应这个打开/新建的文件。这些文件描述符都存储在内核为每个进程维护的一个文件描述符表中。

在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。

在 Linux 系统中一切皆文件,系统中一切都被抽象成了文件。对这些文件的读写都需要通过文件描述符来完成。标准 C 库的文件 IO 函数使用的文件指针 FILE* 在 Linux 中也需要通过文件描述符的辅助才能完成读写操作。FILE 其实是一个结构体,其内部有一个成员就是文件描述符(int _fileno; // 文件描述符)。

FILE 结构体在 Linux 头文件中的定义

// linux c FILE结构体定义: /usr/include/libio.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
};

// 在文件: /usr/include/stdio.h
typedef struct _IO_FILE FILE;

2.2 文件描述符表

前面讲到启动一个进程就会得到一个对应的虚拟地址空间,这个虚拟地址空间分为两大部分,在内核区有专门用于进程管理的模块。Linux 的进程控制块 PCB(process control block)本质是一个叫做 task_struct 的结构体,里边包括管理进程所需的各种信息,其中有一个结构体叫做 file ,我们将它叫做文件描述符表,里边有一个整形索引表,用于存储文件描述符。

内核为每一个进程维护了一个文件描述符表,索引表中的值都是从 0 开始的,所以在不同的进程中你会看到相同的文件描述符,但是它们指向的不一定是同一个磁盘文件。
在这里插入图片描述
知识小科普: Linux 中用户操作的每个终端都被视作一个设备文件,当前操作的终端文件可以使用 /dev/tty 表示。

打开的最大文件数

每一个进程对应的文件描述符表能够存储的打开的文件数是有限制的,默认1024 个,这个默认值是可以修改的,支持打开的最大文件数据取决于操作系统的硬件配置。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main()
{
    int count;
    FILE *fp = NULL;
    while(1)
    {
    	/*以读的形式打开文件*/
        fp = fopen("tmp","w");
        if (fp == NULL)
        { 
            perror("fopen()");
            break;
        }
        count++;
    }
    /*打印出最大能够打印出的文件个数*/
    printf("count = %d\n",count);
    exit(0);
}

默认分配的文件描述符

当一个进程被启动之后,内核 PCB 的文件描述符表中就已经分配了三个文件描述符,这三个文件描述符对应的都是当前启动这个进程的终端文件(Linux 中一切皆文件,终端就是一个设备文件,在 /dev 目录中)
STDIN_FILENO:标准输入,可以通过这个文件描述符将数据输入到终端文件中,宏值为 0。
STDOUT_FILENO:标准输出,可以通过这个文件描述符将数据通过终端输出出来,宏值为 1。
STDERR_FILENO:标准错误,可以通过这个文件描述符将错误信息通过终端输出出来,宏值为 2。

这三个默认分配的文件描述符是可以通过 close() 函数关闭掉,但是关闭之后当前进程也就不能和当前终端进行输入或者输出的信息交互了。

给新打开的文件分配文件描述符

因为进程启动之后,文件描述符表中的 0,1,2 就被分配出去了,因此从 3 开始分配。

在进程中每打开一个文件,就会给这个文件分配一个新的文件描述符,比如:
——通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,保持这个打开状态,再次通过 open() 函数打开 /hello.txt,文件描述符 4 被分配给了这个文件,也就是说一个进程中不同的文件描述符打开的磁盘文件可能是同一个。
——通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,将打开的文件关闭,此时文件描述符 3 就被释放了。再次通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,也就是说打开的新文件会关联文件描述符表中最小的没有被占用的文件描述符。

总结

1、每个进程对应的文件描述符表默认支持打开的最大文件数为 1024,可以修改。

2、每个进程的文件描述符表中都已经默认分配了三个文件描述符,对应的都是当前终端文件(/dev/tty)。

3、每打开新的文件,内核会从进程的文件描述符表中找到一个空闲的没有别占用的文件描述符与其进行关联。

4、文件描述符表中不同的文件描述符可以对应同一个磁盘文件。

5、每个进程文件描述符表中的文件描述符值是唯一的,不会重复。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值