七、内存
1.虚拟内存、物理内存
虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。
物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。
虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。
2 物理内存包括半导体内存和换页文件两部分。
当半导体内存(内存条)不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中(硬盘磁盘),这叫页面换出(内存数据存入硬盘),一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入(内存读取硬盘)。因此,系统中的虚拟内存比半导体内存大得多。
3.进程映射(Process Maps)
每个进程都拥有独立的4G字节的虚拟内存(因为是虚拟的各个进程不会冲突),分别被映射到不同的物理内存区域。内存映射和换入换出都是以页为单位,1页=4096字节。4G虚拟内存中高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享(因此严格可以说有3G独立的)。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。用户空间中的代码只能直接访问用户空间3G的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。
用户空间的3G虚拟内存可以进一步被划分为如下区域:
--------------------------------------------------------------------------- 系统内核(1G):内核的代码和数据区,唯有此区各个进程间共享 高地址---------------------------------------------------------------------- 栈区:非静态局部变量 命令行参数和环境变量 ---------------------------------------------------------------------- v 可以活动边界 ^ ---------------------------------------------------------------------- 堆区:动态内存分配(malloc函数族) ---------------------------------------------------------------------- BSS区:无初值的全局和静态局部变量 ----------------------------------------------------------------------上面是运行时候产生 下面是可执行文件中 数据区:非const型有初值的全局变量和局部静态变量 ---------------------------------------------------------------------- 代码区(函数地址):存放可执行指令, 只读常量:字符串,等等字面值常量,const型有初值的全局和静态局部变量 低地址----------------------------------------------------------------------
代码 maps.c
#include <stdio.h> #include <stdlib.h>//标C提供 #include <unistd.h>//不是标C提供是uinx系统提供的,不跨平台 const int const_global = 10; // 常全局变量,必须赋予初值 int init_global = 10; // 初始化全局变量 int uninit_global; // 未初始化全局变量 int main(int argc, char* argv[]) { const static int const_static = 10;// 常静态局部变量 static int init_static = 10; // 初始化静态局部变量 static int uninit_static;// 未初始化静态局部变量 const int const_local = 10;// 常局部变量 int prev_local; // 前局部变量:定义在前 int next_local; // 后局部变量:定义在后 int* prev_heap = malloc(sizeof(int));// 前堆变量:分配在前 int* next_heap = malloc(sizeof(int));// 后堆变量:分配在后 char const* literal = "literal";// 字面值常量地址 extern char** environ; // 环境变量 printf("---- 命令行参数与环境变量 --------\n"); printf(" 环境变量:%p \n", environ); printf(" 命令行参数:%p \n", argv); printf("-------------- 栈 ------------\n"); printf(" 后局部变量:%p \n",&next_local); printf(" 前局部变量:%p \n",&prev_local); printf(" 常局部变量:%p \n",&const_local); printf("-------------- 堆 -------------\n"); printf(" 后堆变量:%p \n", next_heap); printf(" 前堆变量:%p \n", prev_heap); printf("------------- BSS -------------\n"); printf(" 未初始化全局变量:%p \n",&uninit_global); printf(" 未初始化静态变量:%p \n",&uninit_static); printf("------------ 数据 --------------\n"); printf(" 初始化静态变量:%p \n",&init_static); printf(" 初始化全局变量:%p \n",&init_global); printf("------------ 代码区 -------------\n"); printf(" 常静态局部变量:%p \n",&const_static); printf(" 字面值常量: %p \n", literal); printf(" 常全局变量: %p \n", &const_global); printf(" 函数代码区地址:%p \n", main); printf("------------------------------\n"); return 0; }
通过size命令查看一个可执行程序的代码区Text、数据区和BSS区的大小。dec是三个大小加起来。不包括堆栈与命令行参数,因为他是运行时候产生。
每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,物理内存每个进程各用各的,即使两个进程的全局变量g_vm的虚拟内存地址相同。谓之进程间的内存壁垒。彼此之间不能相互修改对方数据。下面代码进行验证。
代码:vm.c
#include <stdio.h> int g_vm = 0; int main(void) { printf("虚拟内存地址:%p\n", &g_vm); printf("输入一个整数:"); scanf("%d%*c", &g_vm);//读取回车到%*c中,读取到就丢掉这个字符 printf("启动另一个进程,输入不同整数,按<回车>键继续..."); getchar();//读取上一个printf后的回车字符 printf("虚拟内存数据:%d\n", g_vm); return 0; }
结论:虚拟内存就是一个代号,实际数据不是存在虚拟内存中,而是存在物理内存中,当一个进程的虚拟内存对应的数据修改后,即使两个进程的虚拟内存地址相同,也不影响另外一个进程的数据。因为他们的物理内存是独立的。
4.内存的分配与释放
malloc/calloc/realloc/free---> brk/sbrk ---> mmap/munmap--->kmalloc/kfree 即上层不断调用底层代码
以增量方式分配或释放虚拟内存 分配即:映射(在地址空间(虚拟内存)和 存储空间(物理内存)之间建立映射关系)+占有(指定内存空间的归属性) 释放即:放弃占有(解除对内存空间的归属约束 )+解除映射(消除地址空间(虚拟内存)和存储空间(物理内存)之间的映射关系)
#include <unistd.h> void* sbrk(intptr_t increment);//intptr_t有符号整数 堆顶->- - - - - - - - 堆区 ------------------- 调用sbrk(10)后堆顶往上移动10个字节,建立起内存映射关系与占优关系,别的进程不能非法占有 堆顶->- - - - - - - - 10字节 - - - - - - - -<-函数返回值 堆区 ----------------- 调用sbrk(-10)释放10个字节内存, - - - - - - - -<-返回值 10字节 堆顶->- - - - - - - - 堆区 ----------------- 成功返回调用该函数之前的堆顶指针,失败返回-1。 increment >0 - 堆顶指针上移,增大堆空间,分配虚拟内存 <0 - 堆顶指针下移,缩小堆空间,释放虚拟内存 =0 - 不分配也不释放虚拟内存,仅仅返回当前堆顶指针 系统内核维护一个指针,指向堆内存的顶端,顶端即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
代码:sbrk.c
#include <stdio.h> #include <unistd.h> int main(void) { setbuf(stdout, NULL);//关掉printf缓冲区,防止它向堆区写数据导致与我们分配堆区空间发生混叠 int* p1 = (int*)sbrk(sizeof(int));//强转成int类型指针赋值给int类型指针 printf("p1首地址:%p\n",p1); if (p1 == (int*)-1) {//牛逼操作 perror("sbrk分配内存失败了哦:"); return -1; } *p1 = 0; printf("%d\n", *p1); printf("内存写上数据后p1地址:%p\n",p1); double* p2 = (double*)sbrk(sizeof(double)); printf("p2首地址:%p\n",p2); if (p2 == (double*)-1) { perror("sbrk分配内存错误:"); return -1; } *p2 = 1.2; printf("%g\n", *p2); printf("内存写上数据后p2地址:%p\n",p2); char* p3 = (char*)sbrk(256 * sizeof(char)); printf("p3首地址:%p\n",p3); if (p3 == (char*)-1) { perror("sbrk"); return -1; } sprintf(p3, "Hello, World!");//向内存里面写数据 printf("%s\n", p3); printf("内存写上数据后p3地址:%p\n",p3); /*释放内存退到最开始堆顶位置:麻烦 if (sbrk(-(256 * sizeof(char) +sizeof(double) +sizeof(int))) == (void*)-1) { perror("sbrk释放内存失败"); return -1; } */ void* p = sbrk(); printf("p:当前堆顶位置%p\n",p); if (brk(p1) == -1) {//释放内存:方便,不过需要用p1保存最开始地址 perror("brk"); return -1; } return 0; }
以绝对地址的方式分配或释放虚拟内存 int brk(void* end_data_segment); 成功返回0,失败返回-1。参数是类型指针类型 end_data_segment >当前堆顶,分配虚拟内存 <当前堆顶,释放虚拟内存 =当前堆顶,空操作 堆顶->- - - - - - - -<-void* p = sbrk(0);获取当前堆顶位置 堆区 ----------------- 调用brk(p+10)后到(P+10)这个地址 - - - - - - - -<-p+10 10字节 堆顶->- - - - - - - - 堆区 ----------------- 调用brk(p)后回到原来p位置 堆顶->- - - - - - - -<-p 堆区 ---------------- 系统内核维护一个指针,指向当前堆顶,brk函数根据指针参数end_data_segment设置堆顶的新位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
代码:brk.c
#include <stdio.h> #include <unistd.h> /* * xxxIIIIDDDDDDDDCC...C * ^ ^ ^ ^ * p1 p2 p3 */ int main(void) { setbuf(stdout, NULL);//关掉printf缓冲区,防止它向堆区写数据导致与我们分配堆区空间发生混叠 int* p1 = (int*)sbrk(0);//获取当前堆顶位置 printf("p1即堆顶位置:%p\n",p1); if (p1 == (int*)-1) { perror("sbrk"); return -1; } double* p2 = (double*)(p1 + 1); if (brk(p2) == -1) {//分配好4字节,把p2指向新位置即+4个字节后就可以把这4个字节写数据了 perror("brk"); return -1; } *p1 = 0;//到这P1分配好4个字节可以写int值了 printf("%d\n", *p1); char* p3 = (char*)(p2 + 1); if (brk(p3) == -1) { perror("brk"); return -1; } *p2 = 1.2; printf("%g\n", *p2); void* p4 = p3 + 256; if (brk(p4) == -1) { perror("brk"); return -1; } sprintf(p3, "Hello, World!"); printf("%s\n", p3); if (brk(p1) == -1) { perror("brk"); return -1; } return 0; }
分配内存sbrk比较简单,不用我们计算分配多少字节空间给要写入的数据。释放内存却麻烦,需要进行统计我们数据占用内存大小再释放。brk分配内存需要我们计算,比较麻烦,但释放十分方便,只需要我们释放堆顶指针即可。因此分配内存我们用sbrk,释放用brk。
建立虚拟内存到物理内存的映射:当我们分配好虚拟内存后,系统会自动根据也换页位单位把虚拟内存映射到物理内存上面去。
#include <sys/mman.h>//映射头文件,sys是更加底层的头文件 添加虚拟内存到物理内存或文件的映射:即分配内存 void* mmap(void* start, size_t length, int prot,int flags, int fd, off_t offset); 成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(即void*类型的-1)。 start - 映射区虚拟内存的起始地址,NULL表示自动选择:自动找空闲区域做映射 length - 映射区的字节数,自动按页取整(按4096字节向上取整) prot - 访问权限,可取以下值:即可以对这个内存干什么操作 PROT_READ - 可读 PROT_WRITE - 可写 PROT_EXEC - 可执行 PROT_NONE - 不可访问 flags - 映射标志,可取以下值: MAP_ANONYMOUS - 匿名映射,即虚拟内存映射到物理内存,用它此时函数的最后两个参数fd和offset会被忽略 MAP_PRIVATE - 私有映射,即虚拟内存映射到文件的内存缓冲区中而非磁盘文件 MAP_SHARED - 共享映射,将虚拟内存映射到磁盘文件中 MAP_DENYWRITE - 拒写映射,文件中被映射区域不能存在其它写入操作 MAP_FIXED - 固定映射,若在start上无法创建映射,则失败(无此标志系统会自动调整) MAP_LOCKED - 锁定映射,禁止被换出到换页文件,即锁定到物理内存中进行映射 fd - 文件描述符 offset - 文件偏移量,自动按页对齐 解除虚拟内存到物理内存或文件的映射 int munmap(void* start, size_t length); 成功返回0,失败返回-1。 start -- 映射区的起始地址 length -- 映射区的字节数
代码:mmap.c
#include <stdio.h> #include <unistd.h> #include <sys/mman.h> int main(void) { char* psz = mmap(NULL, 4096*2,//映射到物理内存多大空间 PROT_READ | PROT_WRITE,//分配区域既可以读又可以写 MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); if (psz == MAP_FAILED) {//判断能不能给我2页进行映射 perror("mmap"); return -1; } sprintf(psz, "写入第一页内容XXXX"); sprintf(psz + 4096, "写入第二页内容XXXX"); printf("%s\n", psz); printf("%s\n", psz + 4096); if (munmap(psz, 4096) == -1) {//解除第一页映射 perror("munmap"); return -1; } //printf("%s\n", psz);解除后就不能访问,段错误:虚拟内存有可是没有对应物理内存 printf("%s\n", psz + 4096);//访问第二页可以,他的物理内存映射还在 if (munmap(psz + 4096, 4096) == -1) {//释放第二页映射 perror("munmap"); return -1; } return 0; }
八、系统调用 应用程序会用到标准库、第三方库。但系统调用如brk/sbrk/mmap/munmap会进入系统内核,请求系统内核服务,直接绕过了标准库与第三方库完成程序功能。程序工作在内核态与用户态之间来回切换。用户态调用对应系统函数,系统函数请求内核态做出程序功能。
1.Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序编写者可以像调用普通C语言函数一样调用这些系统调用函数,以便于访问系统内核提供的各种服务。
2.系统调用函数在形式上与普通C语言函数并无差别。二者的不同之处在于,前者工作在内核态,是操作系统提供的函数接口与平台相关,而后者工作在用户态,是各个不同系统平台函数的上一层进一步封装,可以跨平台。例如用户想要读取键盘,而键盘的内核代码出于安全考虑,不能让你直接操作内核,内核提供安全的外界函数接口即内核层的上一个层面系统调用层,给你一个系统函数去进行键盘读取操作。
3.在Intel的CPU上运行代码分为四个安全级别:Ring0、Ring1、Ring2和Ring3。Linux系统只使用了Ring0和Ring3。用户代码工作在Ring3级,而内核代码工作在Ring0级。一般而言用户代码无法访问Ring0级的资源,除非借助系统调用,即用系统提供的函数使用户代码得以进入Ring0级,使用系统内核提供的功能。
4.系统内核内部维护一张全局表sys_call_table即函数指针数组,表中(数组中)的每个条目记录着每个系统调用在内核代码中的实现入口地址。当用户代码调用某个系统调用函数时,该函数会先将参数压入堆栈,将系统调用标识存入eax寄存器,然后通过int 80h指令触发80h中断。这时程序便从用户态(Ring3)进入内核态(Ring0)。工作系统内核中的中断处理函数被调用,80h中断的处理函数名为system_call,该函数先从堆栈中取出参数,再从eax寄存器中取出系统调用标识,然后再从sys_call_table表中找到与该系统调用标识相对应的实现代码入口地址,挈其参数调用该实现,并将处理结果逐层返回到用户代码中。
5.uinxc系统函数只是冰山一角,内核函数的实现才是牛逼之处。
九、文件
1.文件系统的物理结构 1)硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器 2)磁表面存储器的读写原理 硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。 3)磁道和扇区 磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。 4)柱面、柱面组、分区和磁盘驱动器 硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。硬盘上的每个字节需要通过以下参数定位: 磁头号:确定哪个盘面 \ 柱面号:确定哪个磁道 > 磁盘I/O 扇区号:确定哪个区域 / 偏移量:确定扇区内的位置 若干个连续的柱面构成一个柱面组;若干个连续的柱面组构成一个分区;每个分区都建有独立的文件系统;若干分区构成一个磁盘驱动器
2.文件系统的逻辑结构 磁盘驱动器:| 分区 | 分区 | 分区 | 分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 | 柱面组:| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 | | 副 本 | 信 息 | 射 表 | | | | i节点号:431479 i节点 文件元数据 100 | 200 | 300 根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。 直接块:存储文件实际数据内容 间接块:存储下级文件数据块索引表 100 ----- xxx
200 ----- xxx
300 ---- 400 | 500 | 600
3.文件分类 普通文件(-):可执行程序、文本、图片、音频、视频、网页 目录文件(d):该目录中每个硬链接名和i节点号的对应表 符号链接文件(l):存放目标文件的路径即相当于win的快捷方式 管道文件(p):有名管道,针对进程间通信 套接字文件(s):进程间通信 块设备文件(b):按块寻址,顺序或随机读写 字符设备文件(c):按字节寻址,只能以字节为单位顺序读写
4.文件的打开与关闭
打开即:在系统内核中建立一套数据结构,用于访问文件 进程表项 文件描述符表 |文件描述符标志 | 文件表项指针 | 0 |文件描述符标志 | 文件表项指针 | 1 |文件描述符标志 | 文件表项指针 | 2 ... | ^ +-----------------------------+ | | 文件描述符 v 文件表项里面有:文件状态标志; 文件读写位置; v节点指针(指向v节点)
关闭即:释放打开文件过程中建立的数据结构
标C中FILE* fp = fopen("reame.txt", "r"); 把文件描述符封装在FILE中了,在UinxC中就是不进行封装,把文件描述符直接返回给你,少一层包装而已。uc提供的文件函数如下。 #include <fcntl.h> int open(const char* pathname, int flags,mode_t mode);//打开已有的文件或创建新文件 成功返回文件描述符,可以把文件描述符看出句柄 ,失败返回-1。