上图是进程的虚拟地址空间示意图。
堆栈段:
1. 为函数内部的局部变量提供存储空间。
2. 进行函数调用时,存储“过程活动记录”。
3. 用作暂时存储区。如计算一个很长的算术表达式时,可以将部分计算结果压入堆栈。
数据段(静态存储区):
包括BSS段的数据段,BSS段存储未初始化的全局变量、静态变量。数据段存储经过初始化的全局和静态变量。
代码段:
又称为文本段。存储可执行文件的指令。
堆:
就像堆栈段能够根据需要自动增长一样,数据段也有一个对象,用于完成这项工作,这就是堆(heap)。堆区域用来动态分配的存储,也就是用 malloc 函数活的的内存。calloc和realloc和malloc类似。前者返回指针的之前把分配好的内存内容都清空为零。后者改变一个指针所指向的内存块的大小,可以扩大和缩小,他经常把内存拷贝到别的地方然后将新地址返回。
1、栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap):由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
4、文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区:存放函数体的二进制代码。
子进程中str=bello
子进程中str指向的首地址:bfdbfc06
父进程中str=hello
父进程中str指向的首地址:bfdbfc06
这里就涉及到物理地址和逻辑地址(或称虚拟地址)的概念。
为了执行,程序应被调入内存并放在进程内。在磁盘上等待调入内存以便执行的进程形成了输入队列
用户程序在运行之前需要经历若干步骤。在这些步骤中,地址可能有不同的表示形式
符号(源程序中)
可重定位的地址(目标模块)
绝对地址(内存映像)
将指令与数据捆绑到内存地址可以在以下步骤的任何一步中执行:
(1)编译时:MS-DOS的COM格式程序
(2)加载时:编译器生成可重定位代码
(3)执行时:进程在执行时可以从一个内存段移到另一内存段,那么捆绑必须延迟到执行时才进行。
内存空间: 是由存储单元(字节或字)组成的一维连续的地址空间; 内存空间用来存放当前正在运行程序的代码及数据, 是程序中指令本身地址所指的存储器、亦即程序计数器所指的存储器。
被绑定到物理地址空间的逻辑地址空间概念是内存管理的中心。
逻辑地址:用户程序经过编译之后的每个目标模块都是以0为基址开始顺序编址的,由CPU生成,这种地址称为相对地址或逻辑地址
物理地址:内存中存储单元的地址, 可直接寻址。
逻辑地址空间:由程序中逻辑地址组成的地址范围叫做逻辑地址空间,或简称为地址空间
内存空间/物理空间/绝对空间:由内存中一系列存储单元所限定的地址范围
内存管理单元(MMU):运行时从虚拟地址映射到物理地址的硬件设备称为内存管理单元
用户进程所生成的地址在送交内存之前,都将加上重定位寄存器的值。
用户程序处理的是逻辑地址,它永远不会看到真实的物理地址。
fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。
每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
具体过程是这样的:
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,知道其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。
这就是所谓的“写时复制”。正因为fork采用了这种写时复制的机制,所以fork出来子进程之后,父子进程哪个先调度呢?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。。。,这就避免了“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度滴。
假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。
(注1:在理解时,你可以认为fork后,这两个相同的虚拟地址指向的是不同的物理地址,这样方便理解父子进程之间的独立性)
(注2:但实际上,Linux为了提高 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))
exec家族一共有六个函数,分别是:
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
- 子进程继承父进程
- 用户号UIDs和用户组号GIDs
- 环境Environment
- 堆栈
- 共享内存
- 打开文件的描述符
- 执行时关闭(Close-on-exec)标志
- 信号(Signal)控制设定
- 进程组号
- 当前工作目录
- 根目录
- 文件方式创建屏蔽字
- 资源限制
- 控制终端
-
子进程独有
- 进程号PID
- 不同的父进程号
- 自己的文件描述符和目录流的拷贝
- 子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
- 不继承异步输入和输出
-
父进程和子进程拥有独立的地址空间和PID参数。
- 子进程从父进程继承了用户号和用户组号,用户信息,目录信息,环境(表),打开的文件描述符,堆栈,(共享)内存等。
- 经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈,就像父进程把自己克隆了一遍。事实上,父进程只复制了自己的PCB块。而代码段,数据段和用户堆栈内存空间并没有复制一份,而是与子进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。由于父进程的PCB和子进程的一样,所以在PCB中断中所记录的父进程占有的资源,也是与子进程共享使用的。这里的“共享”一词意味着“竞争”
2、进程、线程的区别:http://blog.csdn.net/pmt123456/article/details/57424068
进程是具有一定功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源调度和分配的一个独立单位。
线程是指进程内的一个执行单元,也是进程内的可调度实体,是CPU调度和分派的基本单位,与进程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以发并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
(4)系统开销:在创建和撤销进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建撤销线程的开销。
注:子进程产生时会拷贝父进程的变量的值,然后生成自己的一份。
虽然两个指针的值相同,但他们是不同进程空间的,所以会映射到不同的物理内存。
子进程复制了父进程的数据之后,两者就完全没有关系了。
多进程地址空间是独立的 要共享数据需通过进程间通信
也可考虑用多线程来解决 Linux多线程间地址空间是共享的
相关题目
有一个变量int a=0;两个线程同时进行+1操作,每个线程加100次,不加锁,最后a的值是
单核 100~200,100每次同时+1
多核2~200
i++不是原子操作,也就是说,它不是单独一条指令,而是3条指令:
1、从内存中把i的值取出来放到CPU的寄存器中
2、CPU寄存器的值+1
3、把CPU寄存器的值写回内存
如果是单线程操作,i++毫无问题;但是在多核处理器上,用多线程来做i++会有什么问题呢?
我再仔细地重复一遍问题:进程有一个全局变量i,还有有两个线程。每个线程的功能,就是循环100次,执行i++。问线程代码全部执行完毕后,i的值是否一定是200?如果不是,它的最大最小值是多少?
=========分析=======
i++是由3条指令构成的运算操作,两个线程在i变量上共计需要执行100(次循环)*3(条指令)*2(个线程)=600条指令,这600条指令在某种排列下会导致最终i的值仅为2。
(下面是我复制过来的)
假设两个线程的执行步骤如下:
1. 线程A执行第一次i++,取出内存中的i,值为0,存放到寄存器后执行加1,此时CPU1的寄存器中值为1,内存中为0;
2. 线程B执行第一次i++,取出内存中的i,值为0,存放到寄存器后执行加1,此时CPU2的寄存器中值为1,内存中为0;
3. 线程A继续执行完成第99次i++,并把值放回内存,此时CPU1中寄存器的值为99,内存中为99;
4. 线程B继续执行第一次i++,将其值放回内存,此时CPU2中的寄存器值为1,内存中为1;
5. 线程A执行第100次i++,将内存中的值取回CPU1的寄存器,并执行加1,此时CPU1的寄存器中的值为2,内存中为1;
6. 线程B执行完所有操作,并将其放回内存,此时CPU2的寄存器值为100,内存中为100;
7. 线程A执行100次操作的最后一部分,将CPU1中的寄存器值放回内存,内存中值为2;
3、进程、线程间通信:http://blog.csdn.net/pmt123456/article/details/56479737
(1)线程同步的方式有哪些?互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
4、进程间传递文件描述符
http://blog.csdn.net/y396397735/article/details/50684558危害有以下两点:
程序崩溃,导致拒绝额服务
跳转并且执行一段恶意代码
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
9、死锁
1) 因为系统资源不足。
2) 进程运行推进的顺序不合适。
3) 资源分配不当等
(1)资源独占(mutual exclusion),即一个资源在同一时刻只能分配给一个进程。
如果进程申请某一资源, 而该资源正被另一进程占用, 则申请者需等待, 直到占有者释放该资源。
(2)不可剥夺(non-preemption),即资源申请者不能从资源占有者手中抢夺资源。
(4)循环等待(circular wait),即存在一个进程等待序列{p1,p2,…,pn}, 其中p1等待p2占用的某一资源, p2等待p3占用的某一资源,…,pn等待p1占用的某一资源。
注意:
1.只要破坏一个条件, 死锁就不会发生.
2.每类资源只有一个时, 为充要条件.
可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
阻塞状态: 进程等待某种条件,在条件满足之前无法执行
只要部分程序需要放在内存中就能使程序执行
逻辑地址空间可以比物理地址空间大。
允许地址空间被多个进程共享
允许更多进程被创建
段的大小不固定,决定于用户所编写的程序;页大大小固定,由系统决定
段向用户提供二维地址空间,程序员在标识一个地址时,既需给出段名(比如数据段、代码段和堆栈段等),又需给出段内地址;页向用户提供的是一维地址空间,即单一的线性空间
段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制:段有读、写和执行三种权限;而页只有读和写两种权限
分页对程序员而言是不可见的,而分段通常对程序员而言是可见的,因而分段为组织程序和数据提供了方便。与页式虚拟存储器相比,段式虚拟存储器有许多优点:
(1) 段的逻辑独立性使其易于编译、管理、修改和保护,也便于多道程序共享。
(2) 段长可以根据需要动态改变,允许自由调度,以便有效利用主存空间。
(3) 方便编程,分段共享,分段保护,动态链接,动态增长
因为段的长度不固定,段式虚拟存储器也有一些缺点:
(1) 主存空间分配比较麻烦。
(2) 容易在段间留下许多碎片,造成存储空间利用率降低。
(3) 由于段长不一定是2的整数次幂,因而不能简单地像分页方式那样用虚拟地址和实存地址的最低若干二进制位作为段内地址,并与段号进行直接拼接,必须用加法操作通过段起址与段内地址的求和运算得到物理地址。因此,段式存储管理比页式存储管理方式需要更多的硬件支持。
用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
为什么要有用户态和内核态
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 -- 用户态 和 内核态当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。
当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
特权指令:只能由操作系统使用、用户程序不能使用的指令。 举例:启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机
非特权指令:用户程序可以使用的指令。 举例:控制转移 算数运算 取数指令 访管指令(使用户程序从用户态陷入内核态)
3.特权级别:
特权环:R0、R1、R2和R3
R0相当于内核态,R3相当于用户态;
不同级别能够运行不同的指令集合;
CPU状态之间的转换:
用户态--->内核态:唯一途径是通过中断、异常、陷入机制(访管指令)
内核态--->用户态:设置程序状态字PSW
内核态与用户态的区别:
1)内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
2)当程序运行在0级特权级上时,就可以称之为运行在内核态。
3)运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
4)这两种状态的主要差别是:
处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ;
而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
a. 系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
- 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
- 用户态程序执行陷阱指令
- CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
- 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
- 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果
首先介绍一个概念“池化技术 ”。池化技术就是:提前保存大量的资源,以备不时之需以及重复使用。池化技术应用广泛,如内存池,线程池,连接池等等。内存池相关的内容,建议看看Apache、Nginx等开源web服务器的内存池实现。
由于在实际应用当做,分配内存、创建进程、线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作。因此,当程序中需要频繁的进行内存申请释放,进程、线程创建销毁等操作时,通常会使用内存池、进程池、线程池技术来提升程序的性能。
线程池:线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作,当工作完成后,线程又处于睡眠状态,而不是将线程销毁。
进程池与线程池同理。
内存池:内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。
库函数:把一些常用的函数编写完放到一个文件里,编写应用程序时调用(include),这是由第三方提供的,发生在用户地址空间。
在移植性方面,不同操作系统的系统调用一般是不同的,移植性差;而库函数一般具有较好平台移植性,通过库文件(静态库或动态库)向程序员提供功能性调用。程序员无需关心平台差异,由库来屏蔽平台差异性。
在调用开销方面,系统调用需要在用户空间和内核环境间切换,开销较大;而库函数调用属于“过程调用”,开销较小。