本周学习目标:①理解虚拟存储器的概念和做用;②理解地址翻译的概念;③理解存储器映射;④掌握动态存储器分配的方法;⑤了解C语言中与存储器有关的错误。html
『一 虚拟存储器的概念和做用』linux
虚拟存储器git
地址空间程序员
虚拟存储器做为缓存的工具编程
页表数组
『二 地址翻译』缓存
『三 存储器映射』安全
『四 动态存储器分配的方法』数据结构
『五 C语言中与存储器有关的错误』多线程
『问题一』:教材P565提到,能够利用Linux的getrusage函数监测缺页的数量以及许多其余的信息,具体如何使用这个函数呢?
『问题一解决』:
首先使用“man -k getrusage”获取函数的初步信息:
得知getrusage在手册的第二节,接下来使用“man 2 getrusage”查看函数基本格式和需包含的头文件:
getrusage函数有两个参数。第一个参数能够设置为RUSAGE_SELF或者RUSAGE_CHILDREN。若是设置成 RUSAGE_SELF,那么将会以当前进程的相关信息来填充rusage(数据)结构。反之,若是设置成RUSAGE_CHILDREN,那么 rusage结构中的数据都将是当前进程的子进程的信息。
咱们还注意到,里面介绍了一个很是重要的结构体:struct rusage,其中包含的内容以下所示
各个字段的解释以下:
执行成功返回0,发生错误返回-1,同时设置errno的值。
下面使用一个简单的程序测试函数的功能:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/time.h> #include <sys/resource.h> int main(int argc, char **argv) { struct rusage buf; if(argc == 2) { system(argv[1]); }else { fprintf(stderr,"./getrusage \"ls -l > /dev/null\"\n"); exit(0); } int err = getrusage(RUSAGE_CHILDREN, &buf); //int err = getrusage(RUSAGE_SELF, &buf); printf("ERR=%d\n", err); printf("%20s:%ld/%ld\t%s\n", "ru_utime", buf.ru_utime.tv_sec, buf.ru_utime.tv_usec, "user time used (secs/usecs)"); printf("%20s:%ld/%ld\t%s\n", "ru_stime", buf.ru_stime.tv_sec, buf.ru_stime.tv_usec, "system time used (secs/usecs)"); printf("%20s:%-10ld\t%s\n", "ru_maxrss", buf.ru_maxrss, "maximum resident set size"); printf("%20s:%-10ld\t%s\n", "ru_ixrss", buf.ru_ixrss, "integral shared memory size"); printf("%20s:%-10ld\t%s\n", "ru_idrss", buf.ru_idrss, "integral unshared data size"); printf("%20s:%-10ld\t%s\n", "ru_isrss", buf.ru_isrss, "integral unshared data stack size"); printf("%20s:%-10ld\t%s\n", "ru_minflt", buf.ru_minflt, "page reclaims"); printf("%20s:%-10ld\t%s\n", "ru_majflt", buf.ru_majflt, "page faults"); printf("%20s:%-10ld\t%s\n", "ru_nswap", buf.ru_nswap, "swaps"); printf("%20s:%-10ld\t%s\n", "ru_inblock", buf.ru_inblock, "block input operations"); printf("%20s:%-10ld\t%s\n", "ru_oublock", buf.ru_oublock, "block output operations"); printf("%20s:%-10ld\t%s\n", "ru_msgsnd", buf.ru_msgsnd, "messages sent"); printf("%20s:%-10ld\t%s\n", "ru_msgrcv", buf.ru_msgrcv, "messages received"); printf("%20s:%-10ld\t%s\n", "ru_nsignals", buf.ru_nsignals, "signals received"); printf("%20s:%-10ld\t%s\n", "ru_nvcsw", buf.ru_nvcsw, "voluntary context switches"); printf("%20s:%-10ld\t%s\n", "ru_nivcsw", buf.ru_nivcsw, "involuntary context switches"); exit(0); }
测试结果以下:
『问题二』:教材P567提到,每一个PTE中添加了三个许可位,其中SUP位表示进程是否必须运行在内核(超级用户)模式下才能访问该页。什么是内核模式呢?与之相对应的是什么模式呢?
『问题二解决』:
查阅资料了解到,Linux操做系统使用了双模式(内核模式和用户模式),能够有效地实现时间共享。
在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核自己处于内核模式之外,全部的用户进程都运行在用户模式之中。
内核模式的代码能够无限制地访问全部处理器指令集以及所有内存和I/O空间。若是用户模式的进程要享有此特权,它必须经过系统调用向设备驱动程序或其余内核模式的代码发出请求。另外,用户模式的代码容许发生缺页,而内核模式的代码则不容许。
Linux简化了分段机制,使得虚拟地址与线性地址老是一致,所以,Linux的虚拟地址空间也为0~ 4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间“)。由于每一个进程能够经过系统调用进入内核,所以,Linux内核由系统内的全部进程共享。因而,从具体进程的角度来看,每一个进程能够拥有4G字节的虚拟空间。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,咱们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每一个进程都有本身的内核栈。
当进程在执行用户本身的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而忽然被中断程序中断时,此时用户程序也能够象征性地称为处于进程的内核态。由于中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些相似。
处理器总处于如下状态中的一种:
一、内核态,运行于进程上下文,内核表明进程运行于内核空间;
二、内核态,运行于中断上下文,内核表明硬件运行于内核空间;
三、用户态,运行于用户空间。
用户空间的应用程序,经过系统调用,进入内核空间。这个时候用户空间的进程要传递不少变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,能够看做是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
硬件经过触发信号,致使内核调用中断处理程序,进入内核空间。这个过程当中,硬件的一些变量和参数也要传递给内核,内核经过这些参数进行中断处理。所谓的“中断上下文”,其实也能够看做就是硬件传递过来的这些参数和内核须要保存的一些其余环境(主要是当前被打断执行的进程环境)。
『问题三』:如何理解malloc/brk/mmap函数?
『问题三解决』:
结合以前学习过的fork相关知识,参考如下代码:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<signal.h> #include<sys/mman.h> int ga = 1; int main() { int a = 10; int *pa = malloc(sizeof(int)); *pa = 100; int *ma = mmap(0,4,PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED/*MAP_PRIVATE*/,0,0); *ma = 1000; int *spa = sbrk(4); *spa = 10000; if(fork()) { printf("parent:ga = %d\n",ga); printf("parent:a = %d\n",a); printf("parent:*pa = %d\n",*pa); printf("parent:*ma = %d\n",*ma); printf("parent:*spa = %d\n",*spa); a = 7; ga = 77; *pa = 777; *ma = 7777; *spa = 77777; } else { sleep(5); printf("\nchild:ga = %d\n",ga); printf("child:a = %d\n",a); printf("child:*pa = %d\n",*pa); printf("child:*ma = %d\n",*ma); printf("child:*spa = %d\n",*spa); } return 0; }
运行结果以下:
咱们知道,经过fork建立的子进程克隆父进程的内存区域(全局区、栈区、堆区、代码区),但内存区域经过映射以后指向不一样的物理空间,因此,尽管子进程克隆了父进程的内存区域,但他们的实际内存是独立. 不能相互访问。能够看出,malloc/brk/mmap对fork建立的子进程的操做并不彻底相同。
brk系统调用,可让进程的堆指针增加必定的大小,逻辑上消耗掉一块本进程的虚拟地址区间,malloc向OS获取的内存大小比较小时,将直接经过brk调用获取虚拟地址,结果是将本进程的brk指针推高。
mmap系统调用,可让进程的虚拟地址区间里切分出一块指定大小的虚拟地址区间vma_struct,并返回给用户态进程,被mmap映射返回的虚拟地址,逻辑上被消耗了,直到用户进程调用munmap,才回收回来。malloc向系统获取比较大的内存时,会经过mmap直接映射一块虚拟地址区间。mmap系统调用用处很是多,好比一个进程的全部动态库文件.so的加载,都须要经过mmap系统调用映射指定大小的虚拟地址区间,而后将.so代码动态映射到这些区域,以供进程其余部分代码访问;另外,多进程通信,也可使用mmap,这块另开文章详解。
不管是brk仍是mmap返回的都是虚拟地址,在第一次访问这块地址的时候,会触发缺页异常,而后内核为这块虚拟地址申请并映射物理页框,创建页表映射关系,后续对该区间虚拟地址的访问,经过页表获取物理地址,而后就能够在物理内存上读写了。
malloc是 libc实现的库函数,主要实现了一套内存管理机制,当其管理的内存不够时,经过brk/mmap等系统调用向内核申请进程的虚拟地址区间,若是其维护的内存能知足malloc调用,则直接返回,free时会将地址块返回空闲链表。
malloc(size) 的时候,这个函数会多分配一块空间,用于保存size变量,free的时候,直接经过指针前移必定大小,就能够获取malloc时保存的size变量,从而free只须要一个指针做为参数就能够了calloc 库函数至关于 malloc + memset(0)
『问题四』:mmap() vs read()/write()?
『问题四解决』:
系统调用mmap()能够将某文件映射至内存(进程空间),如此能够把对文件的操做转为对内存的操做,以此避免更多的lseek()与read()、write()操做,这点对于大文件或者频繁访问的文件而言尤为受益。但有一点必须清楚:mmap的addr与offset必须对齐一个内存页面大小的边界,即内存映射每每是页面大小的整数倍,不然maaped_file_size%page_size内存空间将被闲置浪费。
如下代码分别使用mmap与read/write两种方法,将test.txt文件中的字符转换成大写:
/* * @file: t_mmap.c */ #include <stdio.h> #include <ctype.h> #include <sys/mman.h> /*mmap munmap*/ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; char *buf; off_t len; struct stat sb; char *fname = "test.txt"; fd = open(fname, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { perror("open"); return 1; } if (fstat(fd, &sb) == -1) { perror("fstat"); return 1; } buf = mmap(0, sb.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (buf == MAP_FAILED) { perror("mmap"); return 1; } if (close(fd) == -1) { perror("close"); return 1; } for (len = 0; len < sb.st_size; ++len) { buf[len] = toupper(buf[len]); /*putchar(buf[len]);*/ } if (munmap(buf, sb.st_size) == -1) { perror("munmap"); return 1; } return 0; }
使用strace运行程序:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <ctype.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd, len; char *buf; char *fname = "/tmp/file_mmap"; ssize_t ret; struct stat sb; fd = open(fname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR); if (fd == -1) { perror("open"); return 1; } if (fstat(fd, &sb) == -1) { perror("stat"); return 1; } buf = malloc(sb.st_size); if (buf == NULL) { perror("malloc"); return 1; } ret = read(fd, buf, sb.st_size); for (len = 0; len < sb.st_size; ++len) { buf[len] = toupper(buf[len]); /*putchar(buf[len]);*/ } lseek(fd, 0, SEEK_SET); ret = write(fd, buf, sb.st_size); if (ret == -1) { perror("error"); return 1; } if (close(fd) == -1) { perror("close"); return 1; } free(buf); return 0; }
使用strace运行程序:
能够看出:read()/write()在频繁访问大文件时,须要调用多个lseek()来肯定位置。每次编辑read()/write(),在物理内存中的双份数据。而mmap内存映射文件以后,操做内存便是操做文件,能够省去很多系统内核调用(lseek, read, write)。
『问题五』:fork/写时复制是如何使用内存空间的?
『问题五解决』:
咱们知道,一个进程在地址空间上的表现形式主要是:正文段,数据段,堆,栈。内核为其分配相应的数据结构来表示它们,其看作是进程在地址空间的实体,也能够想象为灵魂。随后内核会为这四部分分配相应的载体,即真正的物理存储,那么这些物理存储就是进程的真正实体。就像灵魂要附之于身体同样。
有一个父进程P1,这是一个主体(有灵魂也身体)。如今在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分。相应的,内核要为这四个部分分配各自的物理块,即:正文段块,数据段块,堆块,栈块。
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 20篇 | 400小时 | |
第一周 | 50/50 | 1/1 | 8/8 | 了解计算机系统、静态连接与动态连接 |
第三周 | 451/501 | 2/3 | 27/35 | 深刻学习计算机算术运算的特性 |
第四周 | 503 / 1004 | 1/4 | 20/55 | 掌握程序崩溃处理、Linux系统编程等知识,利用所学知识优化myod,并实现head和tail命令 |
第五周 | 315 / 1319 | 3/7 | 29/84 | 掌握“进程”的概念,并学习应用相关函数;了解程序如何在机器上表示 |
第七周 | 264 / 1583 | 1/8 | 15/99 | 了解处理器的体系结构,学习多线程的概念 |
第九周 | 634 / 2217 | 2/10 | 10/109 | 学习存储器系统的层次结构,以及改善程序性能的方法 |
第十一周 | 486 / 2703 | 2/12 | 23/132 | 理解虚拟内存的工做原理和特性 |