【Linux】进程概念
1. 冯诺依曼体系结构我们常见的计算机如笔记本。我们不常见的计算机如服务器大部分都遵守冯诺依曼体系。截至目前我们所认识的计算机都是由⼀个个的硬件组件组成输⼊单元包括键盘,鼠标扫描仪,写板网卡磁盘外部存储等中央处理器(CPU)含有运算器和控制器等存储器内存输出单元显示器打印机喇叭网卡磁盘外部存储等关于冯诺依曼必须强调⼏点这⾥的存储器指的是内存不考虑缓存情况这里的CPU能且只能对内存进行读写不能访问外设(输⼊或输出设备)(数据层⾯)外设(输入或输出设备)要输入或者输出数据也只能写入内存或者从内存中读取。⼀句话所有设备都只能直接和内存打交道。对冯诺依曼的理解不能停留在概念上要深入到对软件数据流理解上请解释从你登录 上qq开始和某位朋友聊天开始数据的流动过程。从你打开窗口开始给他发消息到他的到消息之后的数据流动过程。2. 操作系统(Operator System)2-1概念任何计算机系统都包含⼀个基本的程序集合称为操作系统(OS)。笼统的理解操作系统包括内核进程管理内存管理⽂件管理驱动管理其他程序例如函数库shell程序等等2-2设计OS的目的对下与硬件交互管理所有的软硬件资源对上为用户程序应用程序提供⼀个良好的执行环境2-3核心功能在整个计算机软硬件架构中操作系统的定位是一款纯正的“搞管理”的软件2-4如何理解管理描述被管理对象 组织被管理对象总结计算机管理硬件1. 描述起来⽤struct结构体2. 组织起来用链表或其他⾼效的数据结构2-5 系统调用和库函数概念在开发⻆度操作系统对外会表现为⼀个整体但是会暴露自己的部分接⼝供上层开发使用 这部分由操作系统提供的接⼝叫做系统调用。系统调用是应用程序向操作系统内核请求服务的机制。系统调用由用户态程序发起但实际执行过程是在内核态完成的。当系统调用被触发时CPU会从用户态切换到内核态执行内核代码。就像银行开启的一个个窗口。既保证内部安全有确保顾客的服务。系统调⽤在使⽤上功能⽐较基础对用户的要求相对也⽐较⾼所以有⼼的开发者可以对部 分系统调⽤进⾏适度封装从⽽形成库有了库就很有利于更上层⽤⼾或者开发者进⾏⼆次开 发。就比如printf标准C库函数用于格式化输出通常基于系统调用如write实现但不是系统调用本身。read这是一个系统调用在Unix-like系统中用于从文件描述符读取数据。承上启下那在还没有学习进程之前就问⼤家操作系统是怎么管理进⾏进程管理的呢很简单先把进程描述起来再把进程组织起来3. 进程3-1基本概念与基本操作课本概念程序的⼀个执⾏实例正在执⾏的程序等内核观点担当分配系统资源CPU时间内存的实体。当前进程内核数据结构(task_struct)⾃⼰的程序代码和数据3-1-2描述进程-PCB基本概念进程信息被放在⼀个叫做进程控制块的数据结构中可以理解为进程属性的集合。课本上称之为PCBprocesscontrolblock Linux 操作系统下的 PCB 是: task_structtask_struct-PCB的⼀种在 Linux 中描述进程的结构体叫做 task_struct 。task_struct 是 Linux 内核的⼀种数据结构类型它会被装载到RAM(内存)⾥并且包含着进 程的信息。3-2-3task_struct内容分类标⽰符:描述本进程的唯⼀标⽰符⽤来区别其他进程。状态:任务状态退出代码退出信号等。优先级:相对于其他进程的优先级。程序计数器:程序中即将被执⾏的下⼀条指令的地址。内存指针:包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下⽂数据:进程执⾏时处理器的寄存器中的数据[休学例⼦要加图CPU寄存器]。I∕O状态信息:包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。记账信息:可能包括处理器时间总和使⽤的时钟数总和时间限制记账号等。其他信息组织进程可以在内核源代码⾥找到它。所有运⾏在系统⾥的进程都以 task_struct 双链表的形式存在内核 ⾥。C语言中任何变量的地址数字是开辟众多字节中地址数据是最小的那个。3-1-4查看进程1.进程的信息可以通过/proc系统⽂件夹查看如要获取PID为1的进程信息你需要查看 /proc/1 这个⽂件夹。2. ⼤多数进程信息同样可以使⽤top和ps这些⽤⼾级⼯具来获取#include stdio.h #include sys/types.h #include unistd.h int main() { while(1){ sleep(1); } return 0; }3-1-5通过系统调⽤获取进程标⽰符进程idPID⽗进程idPPID#include stdio.h #include sys/types.h #include unistd.h int main() { printf(pid: %d\n, getpid()); printf(ppid: %d\n, getppid()); return 0; }3-1-6通过系统调用创建进程-fork初识运行 man fork 认识forkfork有两个返回值父子进程代码共享数据各自开辟空间私有⼀份采用写时拷贝#include stdio.h #include sys/types.h #include unistd.h int main() { int ret fork(); printf(hello proc : %d!, ret: %d\n, getpid(), ret); sleep(1); return 0; }• fork之后通常要用 if 进行分流#include stdio.h #include sys/types.h #include unistd.h int main() { int ret fork(); if(ret 0){ perror(fork); return 1; } else if(ret 0){ //child printf(I am child : %d!, ret: %d\n, getpid(), ret); }else{ //father printf(I am father : %d!, ret: %d\n, getpid(), ret); } sleep(1); return 0; }fork为什么会有两个返回值fork之后 代码和数据一般都是父子共享的。父子各自执行return两个返回值各种给父子如何返回给子进程返回的是0给父进程返回的是子进程的pid标识指定的一个子进程未来控制特定的子进程至于一个变量怎么能让 if 和 else if 同时成立这个问题需要在后面才能解释清楚。引用的内存块不一样。3-2进程状态操作系统教材中的状态的说明。运行 阻塞 挂起。一个cpu一个调度队列凡事都在这个队列中的进程状态都是运行状态等待或正在CPU上执行。等待队列并不是一个全局唯一的队列。内核中任何一个需要让进程等待的“资源”或“事件”都会创建并维护自己的等待队列头。例如一个键盘设备有一个等待队列用于存放所有等待键盘输入的进程。一个网络套接字有一个等待队列用于存放等待数据包到达的进程。一个信号量有一个等待队列用于存放所有等待获取该锁的进程。阻塞与运行的本质是看你的task_struct是谁提供的队列中。T:kill -18 -193-2-1Linux内核源代码怎么说为了弄明白正在运行的进程是什么意思我们需要知道进程的不同状态。一个进程可以有几个状 态在Linux内核里进程有时候也叫做任务。下面的状态在kernel源代码里定义/* *The task state array is a strange bitmap of *reasons to sleep. Thus running is zero, and *you can test for combinations of others with *simple bit tests. */ static const char *const task_state_array[] { R (running), /*0 */ //运行 S (sleeping), /*1 */ //浅度睡眠 D (disk sleep), /*2 */ //深度睡眠 T (stopped), /*4 */ //暂停状态 t (tracing stop), /*8 */ //追踪暂停 X (dead), /*16 */ //死亡状态 Z (zombie), /*32 */ //僵尸状态 };R运行状态running:并不意味着进程一定在运行中它表明进程要么是在运行中要么在运行队列里。S睡眠状态sleeping):意味着进程在等待事件完成这里的睡眠有时候也叫做可中断睡眠 interruptible sleep。D磁盘休眠状态Disk sleep有时候也叫不可中断睡眠状态uninterruptible sleep在这个 状态的进程通常会等待IO的结束。T停⽌状态stopped可以通过发送SIGSTOP信号给进程来停止T进程。这个被暂停的 进程可以通过发送SIGCONT信号让进程继续运行。X死亡状态dead这个状态只是⼀个返回状态你不会在任务列表里看到这个状态。3-2-2进程状态查看ps aux / ps axj 命令a显示⼀个终端所有的进程包括其他用户的进程。x显示没有控制终端的进程例如后台运行的守护进程。j显示进程归属的进程组ID、会话ID、父进程ID以及与作业控制相关的信息u以用户为中心的格式显示进程信息提供进程的详细信息如用户、CPU和内存使用情况等3-2-3Z(zombie)-僵尸进程僵死状态Zombies是⼀个比较特殊的状态。当进程退出并且父进程使用wait()系统调用,后面讲没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中并且会一直在等待父进程读取退出状态代码。所以只要子进程退出父进程还在运行但父进程没有读取子进程状态子进程进入Z状态来⼀个创建维持30秒的僵死进程例子#include stdio.h #include stdlib.h int main() { pid_t id fork(); if(id 0){ perror(fork); return 1; } else if(id 0){ //parent printf(parent[%d] is sleeping...\n, getpid()); sleep(30); }else{ printf(child[%d] is begin Z...\n, getpid()); sleep(5); exit(EXIT_SUCCESS); } return 0; }编译并在另⼀个终端下启动监控开始测试看到结果进程退出了退出信息是main函数的返回值或收到的信号值退出的信息保存在进程自己的task_struct结构体中检测Z状态进程回收z状态进程本质是在做什么检查获取task_struct内部的数据来判断集体怎么回收谁来回收父进程-系统调用os僵尸状态指得是只保留进程的task_struct未来让父进程 os帮我们获得到子称的退出数据。3-2-4僵尸进程危害进程的退出状态必须被维持下去因为他要告诉关心它的进程父进程你交给我的任务我 办的怎么样了。可父进程如果⼀直不读取那子进程就一直处于Z状态维护退出状态本⾝就是要⽤数据维护也属于进程基本信息所以保存在task_struct(PCB)中 换句话说Z状态⼀直不退出PCB⼀直都要维护那⼀个父进程创建了很多子进程就是不回收就会造成内存资源的浪费因为数据结构对象本身就要占用内存想想C中定义⼀个结构体变量对象是要在内存的某个位置进行开辟空间最后就会导致内存泄漏?3-2-5孤儿进程父进程如果提前退出那么子进程后退出进入Z之后那该如何处理呢父进程先退出子进程就称之为“孤儿进程”孤儿进程被1号init/systemd进程领养当然要有init/systemd进程回收喽。孤儿进程必须被领养#include stdio.h #include unistd.h #include stdlib.h int main() { pid_t id fork(); if(id 0){ perror(fork); return 1; } else if(id 0){//child printf(I am child, pid : %d\n, getpid()); sleep(10); } else{//parent printf(I am parent, pid: %d\n, getpid()); sleep(3); exit(0); } return 0; }3-3进程优先级3-3-1基本概念因为资源不足需要分配资源设置优先级决定进程获得某种资源的先后顺序。cpu资源分配的先后顺序就是指进程的优先权priority。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用可以改善系统性能。还可以把进程运行到指定的CPU上这样⼀来把不重要的进程安排到某个CPU可以大改善系统整体性能。3-3-2查看系统进程在linux或者unix系统中用ps ‒l命令则会类似输出以下几个内容我们很容易注意到其中的几个重要信息有下UID代表执行者的身份PID代表这个进程的代号PPID代表这个进程是由哪个进程发展衍生而来的亦即父进程的代号PRI代表这个进程可被执行的优先级其值越小越早被执行NI代表这个进程的nice值3-3-3PRI and NIPRI也还是比较好理解的即进程的优先级或者通俗点说就是程序被CPU执行的先后顺序此值越小进程的优先级别越高那NI呢?就是我们所要说的nice值了其表示进程可被执行的优先级的修正数值PRI值越小越快被执行那么加入nice值后将会使得PRI变为PRI(new)PRI(old)nice这样当nice值为负值的时候那么该程序将会优先级值将变小即其优先级会变高则其越快被执行所以调整进程优先级在Linux下就是调整进程nice值nice其取值范围是-20至19一共40个级别。3-3-4PRI vs NI需要强调一点的是进程的nice值不是进程的优先级他们不是一个概念但是进程nice值会影 响到进程的优先级变化可以理解nice值是进程优先级的修正数据分时系统-给进程分配时间片相对公平公正的调度策略较为均衡的让不同的进程都能在一段时间内都能得到CPU的资源。实时系统-3-3-5查看进程优先级的命令⽤top命令更改已存在进程的nicetop进入top后按“r”‒输入进程PID‒输入nice值注意其他调整优先级的命令nicerenice系统函数#include sys/time.h #include sys/resource.h int getpriority(int which, int who); int setpriority(int which, int who, int prio);3-3-6补充概念-竞争、独立、并行、并发竞争性:系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为 了高效完成任务更合理竞争相关资源便具有了优先级独立性:多进程运行需要独享各种资源多进程运行期间互不干扰并行:多个进程在多个CPU下分别同时进行运行这称之为并行并发:多个进程在⼀个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进称 之为并发3.4进程切换CPU上下文切换其实际含义是任务切换或者CPU寄存器切换。当多任务内核决定运行另外的任务时它保存正在运行任务的当前状态也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运行这一过程就是context switch。寄存器是共享的但是寄存器里的数据本质是进程私有的叫做进程上下文。参考⼀下Linux内核0.11代码注意时间片当代计算机都是分时操作系统没有进程都有它合适的时间片(其实就是一个计数器)。时间片到达进程就被操作系统从CPU中剥离下来。3-4 Linux2.6内核进程O(1)调度队列上图是Linux2.6内核中进程队列的数据结构之间关系也已经给大家画出来方便大家理解优先级数字本质就是数组下标。根据优先级进程的时候本质就是一个hash的过程一旦确定是那个队列剩下的就是FIFO.3-4-1 ⼀个CPU拥有⼀个runqueue如果有多个CPU就要考虑进程个数的负载均衡问题3-4-2优先级普通优先级100〜139我们都是普通的优先级想想nice值的取值范围可与之对应实时优先级0〜99不关心3-4-3活动队列时间片还没有结束的所有进程都按照优先级放在该队列nr_active:总共有多少个运行状态的进程queue[140]:⼀个元素就是⼀个进程队列相同优先级的进程按照FIFO规则进行排队调度,所以 数组下标就是优先级从该结构中选择⼀个最合适的进程过程是怎么的呢从0下表开始遍历queue[140]找到第⼀个非空队列该队列必定为优先级最高的队列拿到选中队列的第⼀个进程开始运行调度完成遍历queue[140]时间复杂度是常数但还是太低效了bitmap[5]:一共140个优先级一共140个进程队列为了提高查找非空队列的效率就可以用 5*32个比特位表示队列是否为空这样便可以大提高查找效率3-4-4过期队列过期队列和活动队列结构⼀模一样过期队列上放置的进程都是时间片耗尽的进程当活动队列上的进程都被处理完毕之后对过期队列的进程进行时间片重新计算3-4-5 active指针和expired指针active指针永远指向活动队列expired指针永远指向过期队列可是活动队列上的进程会越来越少过期队列上的进程会越来越多因为进程时间片到期时⼀直 都存在的。没关系在合适的时候只要能够交换active指针和expired指针的内容就相当于有具有了⼀批 新的活动进程3-4-6总结在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数不随着进程增多⽽导致时间成 本增加我们称之为进程调度O(1)算法struct rq { spinlock_t lock; /* * nr_running and cpu_load should be in the same cacheline because * remote CPUs use both these fields when doing load calculation. */ unsigned long nr_running; unsigned long raw_weighted_load; #ifdef CONFIG_SMP unsigned long cpu_load[3]; #endif unsigned long long nr_switches; /* * This is part of a global counter where only the total sum * over all CPUs matters. A task can increase this counter on * one CPU and if it got migrated afterwards it may decrease * it on another CPU. Always updated under the runqueue lock: */ unsigned long nr_uninterruptible; unsigned long expired_timestamp; unsigned long long timestamp_last_tick; struct task_struct *curr, *idle; struct mm_struct *prev_mm; struct prio_array *active, *expired, arrays[2]; int best_expired_prio; atomic_t nr_iowait; #ifdef CONFIG_SMP struct sched_domain *sd; /* For active balancing */ int active_balance; int push_cpu; struct task_struct *migration_thread; struct list_head migration_queue; #endif #ifdef CONFIG_SCHEDSTATS /* latency stats */ struct sched_info rq_sched_info; /* sys_sched_yield() stats */ unsigned long yld_exp_empty; unsigned long yld_act_empty; unsigned long yld_both_empty; unsigned long yld_cnt; /* schedule() stats */ unsigned long sched_switch; unsigned long sched_cnt; unsigned long sched_goidle; /* try_to_wake_up() stats */ unsigned long ttwu_cnt; unsigned long ttwu_local; #endif struct lock_class_key rq_lock_key; }; /* * These are the runqueue data structures: */ struct prio_array { unsigned int nr_active; DECLARE_BITMAP(bitmap, MAX_PRIO1); /* include 1 bit for delimiter */ struct list_head queue[MAX_PRIO]; };4. 命令行参数和环境变量4-1基本概念环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运行环境的⼀些参数如我们在编写C/C代码的时候在链接的时候从来不知道我们的所链接的动态静态库在哪 里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性命令行参数的本质应用是为了实现一个命令可以根据不同的选项实现不同的子功能也是Llnux中所有命令选项功能的实现方式。4-2常见环境变量PATH:指定命令的搜索路径HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)SHELL:当前Shell,它的值通常是/bin/bash。4-3查看环境变量⽅法echo $NAME //NAME:你的环境变量名称测试PATH1. 创建hello.c文件#include stdio.h int main() { printf(hello world!\n); return 0; }2. 对比 ./hello 执行和之间 hello 执行3. 为什么有些指令可以直接执行不需要带路径而我们的二进制程序需要带路径才能执行4. 将我们的程序所在路径加入环境变量PATH当中, export PATH$PATH:hello程序所在路径5. 对比测试6. 还有什么方法可以不用带路径直接就可以运行呢测试HOME1. 用root和普通用户分别执行 echo $HOME ,对比差异2. 执行 cd ~; pwd ,对应 ~ 和HOME 的关系4-4和环境变量相关的命令echo:显⽰某个环境变量值export:设置⼀个新的环境变量env:显⽰所有环境变量unset:清除环境变量set:显示本地定义的shell变量和环境变量4-5 环境变量的组织⽅式每个程序都会收到⼀张环境表环境表是⼀个字符指针数组每个指针指向⼀个以’\0’结尾的环境 字符串4-6通过代码如何获取环境变量命令⾏第三个参数#include stdio.h int main(int argc, char *argv[], char *env[]) { int i 0; for(; env[i]; i){ printf(%s\n, env[i]); } return 0; }通过第三方变量environ获取#include stdio.h int main(int argc, char *argv[]) { extern char **environ; int i 0; for(; environ[i]; i){ printf(%s\n, environ[i]); } return 0; }libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头⽂件中,所以在使⽤时要⽤ extern声明。4-7通过系统调用获取或设置环境变量putenv ,后⾯讲解getenv ,本次讲解#include stdio.h #include stdlib.h int main() { printf(%s\n, getenv(PATH)); return 0; }常⽤getenv和putenv函数来访问特定的环境变量。4-8环境变量通常是具有全局属性的环境变量通常具有全局属性可以被子进程继承下去#include stdio.h #include stdlib.h int main() { char *env getenv(MYENV); if(env){ printf(%s\n, env); } return 0; }直接查看发现没有结果说明该环境变量根本不存在导出环境变量export MYENVhello world再次运行程序发现结果有了说明环境变量是可以被子进程继承下去的想想为什么4-9实验如果只进行 MYENV“helloworld” ,不调用export导出在用我们的程序查看会有什么结果为什么普通变量如果时间允许做⼀下~/.bash_profile~/.bashrc修改文件级环境变量5. 程序地址空间5-1研究平台kernel2.6.3232位平台5-2程序地址空间回顾我们在讲C语⾔的时候⽼师给⼤家画过这样的空间布局图可是我们对他并不理解可以先对其进⾏各区域分布验证#include stdio.h #include unistd.h #include stdlib.h int g_unval; int g_val 100; int main(int argc, char *argv[], char *env[]) { const char *str helloworld; printf(code addr: %p\n, main); printf(init global addr: %p\n, g_val); printf(uninit global addr: %p\n, g_unval); static int test 10; char *heap_mem (char*)malloc(10); char *heap_mem1 (char*)malloc(10); char *heap_mem2 (char*)malloc(10); char *heap_mem3 (char*)malloc(10); printf(heap addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) printf(test static addr: %p\n, test); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) printf(read only string addr: %p\n, str); for(int i 0 ;i argc; i) { printf(argv[%d]: %p\n, i, argv[i]); } for(int i 0; env[i]; i) { printf(env[%d]: %p\n, i, env[i]); } return 0; }$ ./a.out code addr: 0x40055d init global addr: 0x601034 uninit global addr: 0x601040 heap addr: 0x1791010 heap addr: 0x1791030 heap addr: 0x1791050 heap addr: 0x1791070 test static addr: 0x601038 stack addr: 0x7ffd0f9a4368 stack addr: 0x7ffd0f9a4360 stack addr: 0x7ffd0f9a4358 stack addr: 0x7ffd0f9a4350 read only string addr: 0x400800 argv[0]: 0x7ffd0f9a4811 env[0]: 0x7ffd0f9a4819 env[1]: 0x7ffd0f9a482e env[2]: 0x7ffd0f9a4845 env[3]: 0x7ffd0f9a4850 env[4]: 0x7ffd0f9a4860 env[5]: 0x7ffd0f9a486e env[6]: 0x7ffd0f9a4892 env[7]: 0x7ffd0f9a48a5 env[8]: 0x7ffd0f9a48ae env[9]: 0x7ffd0f9a48f1 env[10]: 0x7ffd0f9a4e8d env[11]: 0x7ffd0f9a4ea6 env[12]: 0x7ffd0f9a4f00 env[13]: 0x7ffd0f9a4f13 env[14]: 0x7ffd0f9a4f24 env[15]: 0x7ffd0f9a4f3b env[16]: 0x7ffd0f9a4f43 env[17]: 0x7ffd0f9a4f52 env[18]: 0x7ffd0f9a4f5e env[19]: 0x7ffd0f9a4f93 env[20]: 0x7ffd0f9a4fb6 env[21]: 0x7ffd0f9a4fd5 env[22]: 0x7ffd0f9a4fdf5-3虚拟地址来段代码感受⼀下#include stdio.h #include unistd.h #include stdlib.h int g_val 0; int main() { pid_t id fork(); if(id 0){ perror(fork); return 0; } else if(id 0){ //child printf(child[%d]: %d : %p\n, getpid(), g_val, g_val); }else{ //parent printf(parent[%d]: %d : %p\n, getpid(), g_val, g_val); } sleep(1); return 0; }输出//与环境相关观察现象即可 parent[2995]: 0 : 0x80497d8 child[2996]: 0 : 0x80497d8我们发现输出出来的变量值和地址是⼀模⼀样的很好理解呀因为⼦进程按照⽗进程为模版⽗ ⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:#include stdio.h #include unistd.h #include stdlib.h int g_val 0; int main() { pid_t id fork(); if(id 0){ perror(fork); return 0; } else if(id 0){ //child,⼦进程肯定先跑完也就是⼦进程先修改完成之后⽗进程 再读取 g_val100; printf(child[%d]: %d : %p\n, getpid(), g_val, g_val); }else{ //parent sleep(3); printf(parent[%d]: %d : %p\n, getpid(), g_val, g_val); } sleep(1); return 0; }输出结果//与环境相关观察现象即可 child[3046]: 100 : 0x80497e8 parent[3045]: 0 : 0x80497e8我们发现父子进程输出地址是一致的但是变量内容不⼀样能得出如下结论:变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量但地址值是⼀样的说明该地址绝对不是物理地址在Linux地址下这种地址叫做 虚拟地址我们在⽤C/C语⾔所看到的地址全部都是虚拟地址物理地址⽤⼾⼀概看不到由OS统⼀ 管理OS必须负责将 虚拟地址 转化成 物理地址 。5-4进程地址空间所以之前说‘程序的地址空间’是不准确的准确的应该说成 进程地址空间 那该如何理解呢看 图分页虚拟地址空间虚拟地址空间和页表每一个进程各自有一套fork之后父子共享代码和数据。为什么因为子进程会拷贝父进程的页表类似发生了浅拷贝os规定父子中任何一个进程尝试对共享的变量进行修改时不能之间修改而要发生写时拷贝。-----深拷贝在CPU和物理内存之间进行地址转换时MMU将地址从虚拟逻辑地址空间映射到物理地址空间TCB 线程控制块MMU内存管理单元,一种负责处理中央处理器CPU的内存访问请求,功能包括虚拟地址到物理地址的转换即虚拟内存管理、内存保护、中央处理器高速缓存的控制CACHE 高速缓存DMA 直接内存存取5-5虚拟内存管理 - 第一讲描述linux下进程的地址空间的所有的信息的结构体是 mm_struct 内存描述符。每个进程只有⼀ 个mm_struct结构在每个进程的 task_struct 结构中有⼀个指向该进程的mm_struct结构体指 针。struct task_struct { /*...*/ struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他 的虚拟地址空间的⽤⼾空间部分对于内核线程来说这部分为NULL。 struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当 该进程是内核线程时它的mm字段为NULL表⽰没有内存地址空间可也并不是真正的没有这是因 为所有进程关于内核的映射都是⼀样的内核线程可以使⽤任意进程的地址空间。 /*...*/ }可以说 mm_struct 结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的 mm_struct 这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由 task_struct 到 mm_struct 进程的地址空间的分布情况定位 mm_struct ⽂件所在位置和 task_struct 所在路径是⼀样的不过他们所在⽂件是不⼀样 的 mm_struct 所在的⽂件是 mm_types.hstruct mm_struct { /*...*/ struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/ /*...*/ // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; /*...*/ }那既然每⼀个进程都会有⾃⼰独⽴的 mm_struct 操作系统肯定是要将这么多进程的 mm_struct 组织起来的虚拟空间的组织⽅式有两种1. 当虚拟区较少时采取单链表由mmap指针指向这个链表2. 当虚拟区间多时采取红⿊树进⾏管理由mm_rb指向这棵树。linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA)由于每个不同质的虚 拟内存区域功能和内部机制都不同因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA⽅便进程快速访问。struct vm_area_struct { unsigned long vm_start; //虚存区起始 unsigned long vm_end; //虚存区结束 struct vm_area_struct *vm_next, *vm_prev; //前后指针 struct rb_node vm_rb; //红⿊树中的位置 unsigned long rb_subtree_gap; struct mm_struct *vm_mm; //所属的 mm_struct pgprot_t vm_page_prot; unsigned long vm_flags; //标志位 struct { struct rb_node rb; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain; struct anon_vma *anon_vma; const struct vm_operations_struct *vm_ops; //vma对应的实际操作 unsigned long vm_pgoff; //⽂件映射偏移量 struct file * vm_file; //映射的⽂件 void * vm_private_data; //私有数据 atomic_long_t swap_readahead_info; #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif struct vm_userfaultfd_ctx vm_userfaultfd_ctx; } __randomize_layout;5-6为什么要有虚拟地址空间这个问题其实可以转化为如果程序直接可以操作物理内存会造成什么问题在早期的计算机中要运⾏⼀个程序会把这些程序全都装⼊内存程序都是直接运⾏在内存上的 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。 那当程序同时运⾏多个程序时操作系统是如何为这些程序分配内存的呢例如某台计算机总的内存 ⼤⼩是128M现在同时运⾏两个程序A和BA需占⽤内存10MB需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法先将内存中的前10M分配给程序A接着再从内存中剩余的118M中划分 出110M分配给程序B。这种分配⽅法可以保证程序A和程序B都能运⾏但是这种简单的内存分配策略问题很多。安全⻛险每个进程都可以访问任意的内存空间这也就意味着任意⼀个进程都能够去读写系统相关内 存区域如果是⼀个⽊⻢病毒那么他就能随意的修改内存空间让设备直接瘫痪。• 地址不确定◦ 众所周知编译完成后的程序是存放在硬盘上的当运⾏的时候需要将程序搬到内存当中 去运⾏如果直接使⽤物理地址的话我们⽆法确定内存现在使⽤到哪⾥了也就是说拷⻉ 的实际内存地址每⼀次运⾏都是不确定的⽐如第⼀次执⾏a.out时候内存当中⼀个进程 都没有运⾏所以搬移到内存地址是0x00000000但是第⼆次的时候内存已经有10个进程 在运⾏了那执⾏a.out的时候内存地址就不⼀定了• 效率低下◦ 如果直接使⽤物理内存的话⼀个进程就是作为⼀个整体内存块操作的如果出现物理 内存不够⽤的时候我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中好腾出内 存但是如果是物理地址的话就需要将整个进程⼀起拷⾛这样在内存和磁盘之间拷⻉ 时间太⻓效率较低。存在这么多问题有了虚拟地址空间和分⻚机制就能解决了吗当然地址空间和⻚表是OS创建并维护的是不是也就意味着凡是想使⽤地址空间和⻚表进⾏映射 也⼀定要在OS的监管之下来进⾏访问也顺便 包括各个 进程以及内核的相关有效数据! 保护了物理内存中的所有的合法数据因为有地址空间的存在和页表的映射的存在我们的物理内存中可以对未来的数据进⾏任意位置 的加载物理内存的分配和进程的管理就可以做到没有关系 。 进程管理模块和内存管理模块就完成了解耦合因为有地址空间的存在所以我们在C、C语⾔上new, malloc空间的时候其实是在地址 空间上申请的物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问 的时候才执⾏内存的相关管理算法帮你申请内存构建⻚表映射关系延迟分配这 是由操作系统⾃动完成⽤⼾包括进程完全0感知因为⻚表的映射的存在程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的 虚拟地址和物理地址进⾏映射在进程视⻆所有的内存分布都可以是有序的。