相册网站建设方案,wordpress 文档插件,沈阳人流哪个医院好安全,重庆黔江做防溺水的网站#x1f30e;进程控制【上】
文章目录#xff1a;
进程控制 为什么要有地址空间和页表 程序的内存 程序申请内存使用问题 写时拷贝与缺页中断 父子进程代码共享 为什么需要写时拷贝 页表的权限位 缺页中断 退出码和错误码…进程控制【上】
文章目录
进程控制 为什么要有地址空间和页表 程序的内存 程序申请内存使用问题 写时拷贝与缺页中断 父子进程代码共享 为什么需要写时拷贝 页表的权限位 缺页中断 退出码和错误码 进程的退出码 错误码 总结 前言 进程控制涉及到操作系统如何管理和控制运行在计算机系统中的各个进程。那么话不多说开启我们今天的话题 为什么要有地址空间和页表 上次我们说进程中存在着一张张由操作系统画的 “大饼”也就是虚拟地址空间但是很多人并不明白为什么需要虚拟地址空间今天我们继续听故事 阿熊在小的时候每逢过年都会收到压岁钱总的加起来对阿熊来说是一笔巨大的财富但是这个时候阿熊的老妈总是以帮阿熊保管为理由把压岁钱给收走了于是阿熊一哭二闹三上吊终于老妈做出了让步说“钱还是在我这里放着但是你可以去买东西看到什么想要的就跟我说我给你买。” 这个时候阿熊也妥协了那就这么办吧。 接下来的几天阿熊跟小伙伴出去玩路过小卖铺看到一款新上的零食小伙伴拿着自己的压岁钱纷纷买了这款新零食给阿熊分了一点点“太好吃了我也要买”于是阿熊回到家里跟老妈说“妈我想买个零食要5毛钱。”老妈一想小孩子吃一点零食很正常也不贵“好钱给你买去吧” 于是阿熊拿着钱蹦蹦跳跳的去了小卖铺这些天阿熊买各种小零食、小玩具老妈都默许了。 可是没过多久阿熊在货架上看到了变形金刚玩具瞬间阿熊就定在原地不走了阿熊想了很久于是回家跟老妈说“老妈我想买个玩具要100块钱。” 老妈瞬间火气就上来了“你买了那么多玩具都还没坏又想要新的了我看你想找抽” 于是阿熊不仅没能买到玩具还吃了一顿皮带炒肉丝… 其实由此例子我们就能明白为什么要存在虚拟地址空间把阿熊比作用户把老妈比作页表把商店比作内核数据。 处于安全考虑内核是不能随便被用户访问的为了防止用户直接对内核数据操作于是在用户和内核之间添加了页表和虚拟地址当用户想用虚拟地址通过页表访问物理地址时操作系统会做检测是否为非法访问如果是则中断访问。 如果你的错误情节很严重操作系统甚至会杀死这个进程。这也就像当我们写C语言代码不小心犯了空指针问题时进程会直接挂掉但是并不影操作系统的原因。 程序的内存
✈️程序申请内存使用问题 我们再使用C/C开辟内存时使用malloc或new那么这些开辟的空间是立马就使用吗 实际上我们可以先开辟空间但是我们并不着急使用这种情况绝对是存在的。而操作系统给程序申请物理内存的原理如下图 这个过程就像支票你有支票你不一定会立马去银行换程序空间开辟也是这个道理。 所以由此我们可以知道程序申请空间实际上是从虚拟地址空间申请。这样做有以下几点好处 1、充分保证内存的使用率不会空转。 2、提升new或者malloc的速度。 所以我们 程序申请的空间都是虚拟地址空间程序未访问时是被页表拦截的而当程序真正访问这个空间时操作系统才会在物理内存中开辟空间重新建立虚拟地址与物理地址之间的映射关系。 这个过程我们称之为——缺页中断Page Fault 写时拷贝与缺页中断
✈️父子进程代码共享 我们来说一说写时拷贝的问题通常父子进程代码共享父子不再写入时数据也是共享的当任意一方试图写入便以写时拷贝的方式各自一份副本。 通过上面的图我们就能清晰的了解父子进程代码共享本质上是父子进程的页表映射到同一片内存区域 ✈️为什么需要写时拷贝 在刚开始说进程这个话题的时候我们说过操作系统向上是为了给用户提供良好的运行环境既然如此父进程创建子进程的时候父进程的数据并没有直接给子进程的原因也就得到了解释 子进程被创建时操作系统并不知道你用不用父进程的数据就算确定使用也不确定是不是现在就用为了尽可能不浪费资源所以 操作系统设置只有当进程需要写入时就会发生写时拷贝。 可能有些人还有些疑惑为什么写时拷贝开辟空间就算了为什么还要将原始数据再拷贝一份呢 我们对数据的操作无外乎就是增、删、查、改比如原来进程中代码区有一个整形变量i 100这个时候子进程要对i做自增运算。 简单来说就是对变量i进行写入操作既然有写入操作就会触发写时拷贝子进程单独开辟内存将父进程原始数据拷贝下来如果没有拷贝那么CPU根本就找不到需要修改的值 而不需要进行写入操作那么就不会触发写时拷贝这种方式有时会大大提高程序运行速度。 ✈️页表的权限位 页表不仅仅只有虚拟地址与物理地址之间的映射页表的每一个映射之间还存在权限位也就是 可读可写可执行(r ,w, x) 页表中存在着权限位这一栏为了能更好理解权限位在这里的作用我们来看下面这段代码
#includestdio.hint main()
{char *str this is a good day!\n;*str T;//这里我想把str字符串第一个字母大写return 0;
}这是一段很简单的C语言代码也是很经典的错误我们学过C语言都知道字符串信息是放在常量区的而常量区具有常兴不可修改代码放在shell下运行 不出意外发生了段错误那么 程序是如何知道这个字符串是常量呢或者说 为什么常量区具有常性 我们前面学了程序都所占用的空间都是虚拟地址空间而对str进行修改本质上就是对字符串进行写入而 写入操作会触发页表通过虚拟地址到物理地址之间的转换但是在映射时页表发现映射的权限位为 只读r于是就终止了映射。
注意 有些小伙伴在这里可能会晕把const和这里搞混其实const仅仅是在语法层面上提前设定好的在编译阶段起作用因为前面你在某个程序段上加了const其实是为了后文不会对该程序进行修改操作如果执行了修改操作在编译阶段就编不过去所以const是为了提前告诉你你写的代码有问题而页表是在程序编译通过了运行期间操作系统返回的错误。 其实加上const 属于防御性编程在运行之前就将错误爆出来减少程序运行时崩溃的风险。 ✈️缺页中断 上面我们提到了缺页中断可是并没提到什么应用场景其实缺页中断与写时拷贝也脱不了干系。 写时拷贝我们已经很清楚了当父子进程有一方发生写入操作时就会触发写时拷贝可是关键是操作系统是用什么机制触发写时拷贝的 当子进程已经复制父进程的页表此时操作系统不管你原来数据的权限是什么绝大部分数据权限都设置为 只读r。 当触发了写时拷贝子进程读取到 “写入”的指令页表就会在虚拟地址和物理地址之间做映射而页表在检查时发现需要修改数据的权限位为只读则发生 缺页中断此时进程就会强制 暂停操作系统就会在物理内存开辟空间并且将新开辟的物理内存映射到虚拟地址进程再继续运行并且修改当前执行数据的权限位为可读可写rw。 因为操作系统是软硬件资源的管理者而开辟物理空间是需要操作系统参与的程序为了让操作系统发现自己使用了缺页中断进程暂停 操作系统一定会来查看原因发现正在执行写时拷贝于是操作系统开辟物理地址重新建立映射关系并且将修改数据权限更改。 所以说写时拷贝过程中缺页中断是故意触发的让系统出错目的就是为了 让操作系统发现并执行接下来的步骤 退出码和错误码
✈️进程的退出码 我们不论是在写C/C在main函数里都会有 return 0; 这个语句这个语句的作用是什么你是否是一知半解 在程序中一般大家写函数时如何看函数的执行结果可能有人说把结果打印出来不就完事了吗那么我限制不让你打印呢你可能会说那就 通过返回值 没错程序就是通过返回值来确定程序是否运行成功bash根据程序的返回值来判断程序是否是正常运行。而一个程序的返回值我们称之为——退出码 一般进程的退出码为0表示成功非0表示失败。 阿熊小时候的某一天数学考了100分回家父亲问阿熊“数学考了多少分啊”阿熊如实回答但是父亲总不能说为什么你考了一百分吧第二天阿熊英语成绩也出来了回到家父亲又问“英语考多少分啊”阿熊说“59分。”这是父亲问“为什么考这么少什么原因是因为没发挥好还是因为考试状态不佳或者其他原因” 计算机世界只有0和其他数0是特殊的所以以0作为进程执行成功的标志就像阿熊考了100分。而其他的数就是失败也就是进程出问题就像阿熊英语考试出现问题而出问题却可能是不同的原因。所以用不同的数字来表示不同的错误原因。 我们看下面这段代码
#includestdio.hint main()
{printf(Hello Axiong!\n);return 8;//注意返回值为8
}我们执行代码从打印结果来看是正常运行的程序可是我们前面说了一个程序是否能运行我们要看退出码而查看退出码的命令是
echo $?//查看最近一次进程的退出码我们开始的结果显示是8后面却显示未0了其实这是因为这个命令是显示最近一次进程的退出码除了第一次最近一次执行的进程就是这个命令的本身所以退出码为0。 我们在C语言中曾经学过这个函数 为了清晰看到函数不同退出码表示什么意思我们来看下面的程序 %d
#includestdio.h
#includestring.hint main()
{for(int i 0; i 200; i){printf(%d :%s\n, i, strerror(i));}return 0;
}打印出来之后我们发现错误码一共有133个不同的错误信息。当然这是基于Linux平台下显示的退出码在其他平台可能会是不同的结果。 我们来证实一下上面的错误码 这个是系统的退出码其实我们完全可以自己写一套适用于自己的退出码比如下面代码
#includestdio.h
#includestring.henum{success0,open_err,alloc_err
};const char* errorToDesc(int code)
{switch(code){case success:return success;case open_err:return file open error!;case alloc_err:return alloc error!;default:return unknow error;}
}int main()
{int ind alloc_err;printf(%s\n, errorToDesc(ind));return ind;
}可以得出结论main函数return返回表示进程退出而return跟的数字为退出码可以设置相应退出码的信息。而其他函数退出仅表示函数调用完毕 ✈️错误码 除了进程退出函数退出我们如何知晓函数的退出情况和main函数一样通过返回值调用函数我们称为——错误码我们通常想看到两种结果 1、函数的执行结果。 2、函数的执行情况。 我们在学C语言的时候可能会见过这个函数接口errno用来返回错误信息借此可以看到函数执行情况。 想要看到函数执行结果很经典的就是返回类型为 FILE* 结构体指针的函数失败时返回NULL成功我们并不需要太过关注那么我们以fopen函数为例
#includestdio.h
#includeunistd.h
#includestdlib.h
#includestring.h
#includeerrno.henum{success0,open_err,alloc_err
};const char* errorToDesc(int code)
{switch(code){case success:return success;case open_err:return file open error;case alloc_err:return malloc error;default:return unknow error;}
}int Print()
{printf(hello Linux\n);printf(hello Linux\n);printf(hello Linux\n);return 0;
}int main()
{FILE *fp fopen(./log.txt, r);//尝试打开当前目录的文件如果没有则返回NULLprintf(%d:%s\n, errno, strerror(errno));//获取错误码以及错误信return 0;
} 根据错误码我们通过调用函数也可获取相应错误信息。 我们说了这么多其实就是想要说进程退出时可能会发生错误简单总结为一下三条 1、进程代码执行完结果正确。 2、进程代码执行完结果错误。 3、进程代码未执行完进程异常。 进程退出的情况就这三种情况 ✏️总结 我们曾使用的 C/C 有关内存操作全部是在 虚拟地址空间 操作的目的是 为了内存隔离和内存管理避免内存冲突。 虚拟地址与物理地址之间由 页表 建立映射关系程序开辟空间时会发生写时拷贝而 写时拷贝是通过缺页中断技术得以执行。 进程存在 退出码根据退出码 判断进程是否执行成功否则可以判断出了哪些问题。 错误码 和退出码类似作用对象是函数判断函数执行失败情况退出码和错误码可自行设置。 创作不易还望给作者一个小小的赞呀~~