一.关于页表组成
1.权限(rwx)
作用:如1.让代码区变成只读的 2.写时拷贝的实现:子进程创建时其页表指向的父进程代码和数据权限都是只读的,子进程试图修改,触发错误,系统开始写时拷贝。
来源:1.可执行程序本身已经包含了区域权限信息,代码区不可写等
2.真实地址是否存在的位(isexits)
作用:节省内存空间:
1.分批加载::系统创建进程先加载进程对应磁盘内一部分数据,将页表记录位记录不存在,等进程使用这部分虚拟地址对应的数据时,再从磁盘加载到指定物理地址位置。2.挂起操作。
二.关于mm_struct初始化时怎么分配各区域的大小
1.可执行程序在编译时,各个区域已经确定,直接给mm_struct初始化,(程序内申请空间实质是扩大程序记录的堆的大小(也就是虚拟空间的大小),给mm_struct,操作系统在需要用的时候再开辟空间),执行时mm_struct给操作系统实时开辟
三.进程地址空间(上)
再谈创建进程:
写时拷贝
是什么:创建子进程,子进程实际指向的代码和数据和父进程相同,修改时再拷贝,互不干扰。
实现:创建子进程时,页表所有位置权限位设为只读,一旦子进程和父进程任意一方对共同数据进行写入,触发缺页中断,系统监测是不是需要写时拷贝,是,将数据拷贝一份,修改页表指向,修改权限。互不干扰。
再谈进程终止
1.进程正常退出:看进程退出码判断运行结果是否正常(return 0,return -1...)
2.进程意外终止:OS提前用信号终止了进程(访问非法等)。看退出信号判断情况:退出码为0则没收到信号,退出信号判断进程是否提前退出了,提前退出原因是什么(kill就是给进程信号,退出进程),正常退出了看退出结果是否正确看退出码。
再谈进程等待:
回收等待的进程:子进程结束了,就要将退出信号和退出码存入PCB并等待,直到父进程回收
回收系统调用:
pid_t wait(int *status);进程一直等待,直到有它的子进程退出,
接收退出状况status(若不接受可传入空指针),返回退出进程的pid,失败时返回 -1
,并设置 errno
以指示错误原因。
pid_t waitpid(pid_t pid,int *status, int options);进程等待一次,若没退出
pid为要等待的子进程(传入-1为任意),options为0为阻塞等待,WNOHANG非阻塞等待
status:
两个判断status情况的宏:bool WIFEXITED(int status)如果进程正常退出则返回真
int WEXITSTATUS(int status).如果正常退出,提取退出码
进程替换
不是创建新进程,是在原进程的基础上替换新的可执行程序的代码和数据(堆栈重新初始化),PCB不换!
创建一个已有可执行程序为基础的进程,可以先创建一个子进程,再进程替换(bash也是这么做的,只需要全部写时拷贝再替换)
补充:
1.readelf -s 可执行程序名
看可执行程序的各个区域大小
2.野指针实质是页表内没有对应映射,操作系统不给访问,运行错误
3.为什么要用虚拟地址空间
-->1.虚拟地址空间+页表保护内存
--->2.让进程以统一视角看物理内存:加载各个区里的物理地址不必连续,虚拟地址连续,进程不必分别管理一块一块。作用:让代码数据可以加载到内存的任意地址处,页表映射寻找效率高O(1)。
---->3.进程管理和内存管理在系统层面解耦合:进程创建只需创建数据结构,其余的代码加载,进程需要的内存操作(申请空间),操作系统可以先骗过进程,等到使用的时候再真进行内存操作。两者具有滞后性,可以更好的省内存,省资源。
4.环境变量为什么一直能被进程看到
进程虚拟地址空间内就有环境变量表
5.1. 打印错误的代码
#include <errno.h>
变量int errno最近函数的错误码,(依赖于最近的函数内是否设置了错误码,fopen,fork都设置了)
#include <string.h>
char *strerror(int errnum);根据错误码返回错误原因字符串
(标准C库的功能)
5.2. 退出代码
#include<cstdlib>
void exit(int i);让进程直接退出,通过正常退出机制终止进程,设置退出码i。且刷新缓冲区(缓冲区是语言级别的概念,语言编译了维护,不是操作系统的)
_exit(int i);系统调用,退出不刷新缓冲区
exit语言封装了_exit。
6.数据段(Data Segment) 中的数据在 进程创建之前 就已经存在了。具体来说,这些数据是在 程序编译和链接时 确定的,并存储在可执行文件中。当程序启动时,操作系统会加载可执行文件,并将数据段的内容映射到进程的地址空间中。