扩展内容:
我们都知道程序在系统中是以进程为单位运行的,进程是可执行程序运行的单元。进程执行过程中就需要OS为其分配可供执行的资源。其中最难理解的就是 内存资源的分配,OS到底是怎么为新进程合理的分配资源呢,这就产生了虚拟地址空间的概念。
1.虚拟地址空间和物理内存
虚拟内存存在的意义:
计算机内存资源是相对匮乏的资源,每个可执行程序都需要哦OS为其分配一定的内存资源,现代计算机已经实现了多并发、多处理的处理要求,一台计算机每时每刻都有很多的程序在运行,而每个程序又会有很多的进程在执行,每一个进程又需要独立的堆栈为其服务,这些都需要内部才能作为载体才能正确完整的执行。问题是,计算机内存容量是有限的 ,不可能为可执行程序都合理的分配内存资源供其使用,于是就提出了虚拟内存的概念。
虚拟内存是逻辑上的内存空间(逻辑空间),是在物理磁盘上划分出来的。比如OS会为每一个正在运行的程序分配大概4G的虚拟地址空间(每一个正在执行的程序都会有4G),这4G虚拟地址空间和物理内存空间之间存在某种映射关系的。
一般虚拟地址4G的空间对应的物理内存应该也是4G的,但是,虚拟地址空间是在磁盘上的,当这段代码需要执行的时候(就必须加载到内存),OS 就需要把需要执行的一部分利用某种算法加载到内存(只加载一部分,不可能把该程序虚拟地址空间的所有内容都加载到内存),然后运行。
2.进程的执行过程
虚拟地址空间分为两部分:用户地址空间和内核地址空间,i386系统体系结构的计算机的虚拟地址空间一般是4G , 0~3G的空间是用户空地址空间;3G到4G的地址空间为内核地址空间。相应地,程序的运行状态分为 用户态和内核态,程序在用户态只能占用用户地址空间,程序在内核态只能占用内核地址空间,显而易见的,内核态执行级别是高于用户态的执行级别。
每一个进程的地址空间都分为这两种地址空间,对于内核进程,由于始终运行在内核态,所以内核进程的tast_sruct结构体的mm域也就被赋值为NULL。在内核地址空间中,不存在堆数据结构,所以堆的概念仅仅是用户地址空间中的数据结构,所以对于内核而言,也不会需要堆这种数据结构来存储变量或代码。Kmalloc或vmalloc 用户内核进程在运行时申请内存。申请到的虚拟内存在整个内存中都可以被其他程序使用。举个例子来说,假如内核线程1 申请到了一块内存A,只要把该内存的首地址传给另一个内存线程2,那么在线程2中就可以使用这个内存A。
对于用户进程,其既有用户地址空间中的栈,也有它自己的内核栈; 对于内核进程,就只有自己的内核栈。所有的进程都有一个内核栈,在x86的32机器上内核栈大小可以为4KB 或者 8KB,这个可以在编译内核的时候配置。
创建内核堆栈的情况为:
- 当进程进入内核态,系统调用的参数就放在内核栈上,内核栈记录着进程在内存上的调用链。
2.在内核栈被配置为8KB大小的情况下,当中断服务程序中断当前进程时,它将使用当前被中断进程的内核栈。
3. fork创建进程过程
Fork()是内核程序创建进程的一种方式(其他还有vfork()和clone()方法)
Fork函数原型: pid_t fork(void);
函数返回类型 pid_t 实质上是 int类型
fork函数每次回新生成一个进程,调用fork函数的进程是父进程,fork函数生成的函数是子进程。Fork函数调用一次,返回两次。两次分别返回的是父进程和子进程。
有三种不同的返回值:
- 在父进程中返回子进程的pid;
- 在子进程中返回0;
- 如果返回失败,则返回-1.
举例说明:
将子进程ID返回给父进程的理由:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。
对子进程来说,之所以fork返回0给它:是因为它随时可以调用 getpid()函数来获取它自己的PID;也可以调用 getpid()来获取父进程的PID(进程ID 0总是由内核交换进程使用,所以一歌子进程的进程ID不可能为0)。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程的数据空间、堆栈的副本。父子进程并不共享这些存储空间部分。什么意思呢,就是父进程和子进程共享代码段,但是分别拥有自己的数据段和堆栈段(子进程复制父进程的数据段,BSS段,代码段,堆空间,栈空间,文件描述符,但是对于文件描述符关联的内核文件表项(即struct file结构体)则是采用共享的方式)。
Fork函数在生成子进程的时候,用到了写时拷贝技术(原因:在fork之后经跟随着exec,所以很多的实现并不执行一个父进程数据段、栈、堆的完全复制。): 不执行一个父进程数据段、栈和堆的完全复制,这些区域由父、子进程共享,而内核将他们的访问权限改为只读的。如果父、子进程任何一个试图修改这些区域,则内核只为修改区域的那块内存做一个副本,并且是以虚拟存储器系统中的“一页”为单位复制。
(父子进程共享页帧而不是赋值页帧,然而,只要页帧被共享,他们就不能被修改,即页帧保护。无论父进程还是子进程何时视图写一个共享的页帧,都会产生一个异常,这时内核就会把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程视图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。)
过程:
(1)分配PID;
(2)分配进程描述符也就是PCB,,同时分配好内核堆栈;
(3)复制进程实体,即打开文件、工作目录、信号信息、进程地址空间等等。
(4)用父进程内核栈上存放的现场信息、初始化子进程的现场信息,并将eax置为0;
(5)将父进程时间片 分 子进程一半,设置状态为就绪。
4.fork函数底层实现原理:
Fork()系统调用通过复制一个现有进程来创建一个新的进程。进程被存放在一个叫做 任务队列的双向循环链表当中,链表当中的每一项都是 类型为 task_struct 称为进程描述符发结构,也就是我们所说的 进程PCB。
Tips: 内核通过一个位置的进程标识值或者PID 来标识一个进程。 //最大值默认为 32768,short int 短整型的最大值,他就是系统中允许同时存在的进程最大的数目。
可以到目录 /proc/sys/kernel 中查看pid_max :
当进程调用fork之后,当控制转移到 内核中的fork代码后,内核会做4件事情:
- 分配新的内存块和内核数据结构给子进程;
- 将父进程部分数据结构内容拷贝至子进程;
- 添加子进程到系统进程列表中;
- Fork返回,开始调度器返回。
Fork函数在底层到底做了什么呢?
Linux平台通过 clone()函数调用 fork(), vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用 do_fork(),再然后do_fork完成了创建中的大部分工作,该函数调用 copy_process() 做最后的那部分工作。
(图片转自博客https://blog.csdn.net/Dawn_sf/article/details/78709839)
如图所示:
- vfork函数
Vfork 函数是另一个创建线程的函数vfork;
Vfork 最初是因为fork没有实现 cow机制,而很多情况系fork之后会跟exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了 vfork。
#include<sys/types.h>
#include<unistd.h>
Pid_t vfork(void)
函数解释:
- Vfork 函数用于创建一个子进程,而子进程和父进程共享地址空间,fork的子进程具有独立的地址空间。
- Vfork 保证子进程先运行,在它调用exec(或者exit)之后,父进程才可能被调度运行
注意:fvfork创建的子进程在退出的时候需要调用exit()函数退出,如果没有调用函数是会出错误的;因为在函数栈上,子进程运行结束了,main的函数栈被子进程释放了,然后父进程在使用的时候,就访问不到了,所以一旦vfork出子进程,退出的时候需要使用exit来退出。
5.fork和vfork的区别
1.fork: 子进程拷贝父进程的代码段和数据段
Vfork: 子进程和父进程共享代码段和数据段
2.fork:父子进程的先后运行次序不定
Vfork:保证子进程先运行,子进程exit之后,父进程才开始被调度运行。
3.vfork()保证子进程先运行,在它调用exec或者而成exit之后,父进程才开始调度运行,如果在调用这俩个函数之前,子进程依赖父进程的进一步动作,则会导致死锁。
4.就算fork实现了写时拷贝,效率也没有 vfork高,但是vfork一般平台会出现问题,所以很少用。