最全的Linux教程,Linux从入门到精通
======================
-
linux从入门到精通(第2版)
-
Linux系统移植
-
Linux驱动开发入门与实战
-
LINUX 系统移植 第2版
-
Linux开源网络全栈详解 从DPDK到OpenFlow
第一份《Linux从入门到精通》466页
====================
内容简介
====
本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。
本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。
需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
文章目录
【写在前面】
本文中会介绍很多结构化的知识,我们会通过很多例子里的角色和场景来对一个概念进行定位和阐述,让大家有一个宏观的认识,之后再循序渐进的填充细节,如果你一上来就玩页表怎么映射,那么你可能连页表存在的价值是什么都不知道,最后也只是竹篮打水。
一、回顾与纠正
C/C++ 内存布局这个概念比较重要,之前我们也涉及过 —— 我们在语言上定义的各种变量等在内存中的分布情况,如果没有听说过,那么你的 C/C++ 是不可能学好的。
上图表示的是内存吗 ❓
其实我们曾经在语言中说过的 C/C++ 内存布局,严格来说是错误的,从今天开始我们应该称之为C/C++ 进程地址空间
。为啥要故意说错呢,其实是因为方便理解,如果当时说 C/C++ 进程地址空间,那么不谈进程、地址、空间,就很容易误导大家。也就是说实际上要真正理解 C/C++ 的空间布局,光学习语言是远远不够的,还需要学习系统以及进程和内存之间的关系。
进程地址空间既然不是内存,那么栈、堆等这些空间的数据存储在哪 ???
进程地址空间,会在进程的整个生命周期一直存在,直到进程退出,这也就解释了为什么全局变量具有全局属性。其实这些数据最后一定会存储于内存,只不过进程地址空间是需要经过某种转换才到物理内存的。
上图的共享区/内存映射段会在进程间通信以及动静态库的时候再去细谈,现在可以简单理解计算机中是有很多动静态库的,而共享区主要用来加载它们。
二、 验证进程地址空间的基本排布
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int g_unval;//全局未初始化故意与全局初始化写反
5 int g_val = 100;
6
7 int main(int argc, char\* argv[], char\* env[])
8 {
9 //以下由低地址到高地址分别验证,除了栈
10 printf("code addr: %p\n", main);//对于一个函数的地址,main同&main
11 const char\* p = "hello bit!";
12 printf("read only: %p\n", p);//p就是字符串的首地址
13 static int a = 5;
14 printf("static global val: %p\n", &a);//static后局部变量的存储地方就由栈变为数据段
15 printf("global val: %p\n", &g_val);
16 printf("global uninit val: %p\n", &g_unval);
17 char\* q1 = (char\*)malloc(10);
18 char\* q2 = (char\*)malloc(10);
19 printf("heap addr: %p\n", q1);//栈区的地址&q1会指向堆区的q1
20 printf("heap addr: %p\n", q2);
21 printf("p stack addr: %p\n", &p);
22 printf("q1 stack addr: %p\n", &q1);
23 printf("args addr: %p\n", argv[0]);//数组的第一个元素
24 printf("args addr: %p\n", argv[argc - 1]);//数组的最后一个元素
25 printf("env addr: %p\n", env[0]);
26
27 return 0;
28 }
args 的地址是一样的,根本原因是 ./checkarea 时只有一个命令行参数,如果加上选项那么就不一样了:
三、 进程地址空间
💦 虚拟地址
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int g_val = 0;
int main()
{
printf("begin......%d\n", g_val);
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(1)
{
printf("child pid: %d, ppid: %d, [g\_val: %d][&g\_val: %p]\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
count++;
if(count == 5)
{
g_val = 100;
}
}
}
else if(id > 0)
{
while(1)
{
printf("father pid: %d, ppid: %d, [g\_val: %d][&g\_val: %p]\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
else
{
//TODO
}
return 0;
}
根据我们现有的知识,无可厚非的是前 5 次父子进程的 g_val 的值是一样的,且地址也一样,因为我们没有修改 g_val, 5 次后,子进程把 g_val 的值改了之后,父进程依旧是旧值,这个我们一点都不意外,因为父子共享代码
,数据各自私有
,后面会站在系统角度讲数据各自私有是写时拷贝
来完成的,以前我们是在语言层面上了解;但匪夷所思的是 5 次后,父子进程的 g_val 的地址竟然也是一样的。
推导和猜测???
从上图我们可以知道 &g_val 一定不是物理地址 (真正在内存中的地址),因为同一个物理地址处怎么可能读取到的是不同的值。所以我们断言曾经所看到的任何地址都不是物理地址,而这种地址本质是虚拟地址
,它是由操作系统提供的,那么操作系统一定要有一种方式帮我们把虚拟地址转换为物理地址,因为数据和代码一定在物理内存上存储,这是由冯 • 诺依曼体系结构规定的。上面说到虚拟地址是由操作系统提供的,我们也说过程序运行起来之后,该程序立即变成进程,那么虚拟地址和进程大概率存在某种关系。
💦 什么是进程地址空间
地址空间在 Linux 内核中是一个mm_struct
结构体,这个结构体没有告诉我们空间大小,但是它告诉我们空间排布情况,比如[code_start(0x1000), code_end(0x2000)]
,其中就会有若干虚拟地址,这是因为操作系统为了把物理内存包裹起来,给每个进程画的一把尺子,这把尺子我们叫进程地址空间。进程地址空间是在进程和物理内存之间的一个软件层,它通过mm_struct 这样的结构体来模拟,让操作系统给进程画大饼
,每一个进程可以根据地址空间来划分自己的代码。
所以我们再回顾:进程地址空间当然不是物理内存,它本质只是操作系统让进程看待物理内存的方式,其中 Linux 内核中是用 mm_struct 数据结构来表示的,这样的话每个进程都认为自己独占系统内存资源 (好比每个老婆都认为自己独占10亿);区域划分的本质是将线性地址空间划分成为一个一个的区域[start, end]
;而所谓的虚拟地址本质是在[start, end]
之间的各个地址。
看看源码中怎么写 ❓
💦 页表
进程地址空间如何映射至物理内存这就引出了页表,页表的结构是 b 树,目前不打算深入它。假设存在三个进程 A B C,操作系统就会给每一个进程画一张大饼,叫做当前进程的虚拟地址空间,其中会通过指针将进程和虚拟地址空间关联起来。运行进程 A,就要把进程 A 加载到物理内存中,其中操作系统会给每一个进程创建一张独立的页表结构,我们称之为用户级页表
,当然后面还有内核级页表
,而页表构建的就是从地址空间中出来的虚拟地址到物理地址中的映射,每个进程都通过页表来维护进程地址空间和物理内存之间的关系,这是页表的核心工作,所以进程就可以根据页表的映射访问物理内存。当然单纯一张页表是不可能完成映射的,还要配合某些硬件,以后会谈。
能否把进程 A 中的代码和数据加载到物理内存中的任意位置 ❓
在不考虑特殊情况下,是可以将进程对应的代码和数据在物理内存的任意位置加载的,因为最终只需要将物理内存上的代码和数据与页表建立映射关系,就可以通过虚拟地址找到物理地址。所以进程中的代码和数据是能够加载到物理内存中的任意位置的,其中本质是通过页表去完成的。
多个进程之间会互相干扰吗,不同的进程它们的虚拟地址可以一样吗 ❓
同样进程 B 也可以通过页表把代码和数据加载到物理内存的任意位置,就算不同的进程的虚拟地址完全一样也没问题,因为不同进程通过一样的虚拟地址查的是不同的页表,其中的工作细节是由页表去完成的,这也解释了上面为啥两个进程虚拟地址一样却不会互相影响。
如果物理地址重址呢 ❓
这是操作系统的代码,一般不可能重址。当然也存在这样的特殊情况,如果进程 B 和进程 C 是父子关系,我们在创建子进程 C 的 PCB、地址空间、页表、建立各种映射关系,把代码区、数据区等区域映射时,只需要将子进程 C 映射到物理内存中父进程的代码和数据处,但当子进程 C 修改数据时,操作系统就会重新申请内存,修改当前进程的指向关系,此时子进程就不再指向父进程的数据了,而让子进程指向新的空间,把旧数据拷贝至新数据,最后再修改数据,此时这就是写时拷贝
。所以不同的页表,物理地址可以重址,只不过这种重址是刻意的,因为父子代码共享。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!