个人主页:仍有未知等待探索-CSDN博客
专题分栏:Linux
目录
[对于现象的解释(操作系统 -> 写时拷贝):](#对于现象的解释(操作系统 -> 写时拷贝):)
一、进程优先级
1、什么是进程优先级?
进程获取某种资源(cpu)的先后顺序。
- task_struct 进程控制块 -> int prio;
- Linux中优先级数字越小,优先级越高。
- 优先级和权限的区别:优先级是能执行,表示执行的顺序;而权限是能不能执行的问题。
2、为什么要有优先级?
进程访问cpu的资源始终是有限的。
操作系统关于调度和有限级的原则:分时操作系统:根据时间片来进行调度 --- 保证基本的公平。
如果进程长时间不被调度,就造成了饥饿问题。
3、Linux的优先级特点、查看方式
- Linux查看优先级的指令:ps -al
- PRI:进程的优先级,NI:进程优先级的修正数据(用于对优先级的动态修改)。最终进程的优先级 = PRI + NI。
- 调整优先级的指令:top ---> r ---> PID to renice -> Value(本人修改的NI值)NI值也就是nice值,不是随便调整的,而是有范围的[-20, 19]。
- 每次优先级的调整,PRI的值都是从80开始的。
4、命令行参数和环境变量
1.命令行参数
int main(int argc, char *argv[]):对于这种写法的main函数里面的参数代表着什么?
是命令行字符串,程序的路径+名称 和 该进程匹配的选项。
命令行参数的意义:命令行参数的本质是交给我们不同的选项,来定制不同的程序功能。
父进程的数据默认能被子进程看到并访问。
命令行中启动的程序,都会变成进程,都是bash的子进程。
2.环境变量
Linux中存在一些全局的设置,告诉命令行解释器,应该去哪个路径下去寻找可执行程序。
- 可以用env指令去查看所有的环境变量:
- 可以用echo指令打印环境变量的内容:
默认我们能查到的环境变量是内存级别的,当xshell重启后,环境变量默认会恢复。
环境变量不是在内存中,而是在系统对应的配置文件中。内存的环境变量,是xshell启动的时候,加载到内存中的。
env:一个查看环境变量的指令
export name=value:导入环境变量的指令
unset name:取消环境变量的指令
本地变量:hello=1234,不能被子进程继承。这个helloc是本地变量,当你用env查看环境变量的时候是不会找到这个变量的。
environ -> 一个char**类型,存放着环境变量(unistd.h、需要声明,extern char** environ)
环境变量可以被子进程拿到的。-> 环境变量默认在bash内部。
bash进程启动的时候,默认会给子进程形成两种表:
argv[]命令行参数表、env[]环境变量表。bash通过各种方式交给子进程。
环境变量具有系统级的全局属性,因为环境变量本身会被子进程继承下去。
echo、export都是内建命令(不是bash子进程创建的,而是bash执行的)。
本地变量旨在本bash内部有效,无法被子进程继承下去,导成环境变量,此时才能够被获取。
获取环境变量的方式:
1、函数:获取环境变量的值:char* getenv(char* name)。
2、extern char** environ。
3、通过main函数的参数。
二、程序地址空间
1、进程的地址空间
现象:在一段代码中,子进程改变了全局变量,但是在父进程中读取到的仍是原来的值,并且子进程和父进程中全局变量的地址一样。
2、基本理解
- 每个进程都有一个独立的地址空间。
- 每个进程都有一个独立的页表。
- 地址空间的本质就是内核中的一个结构体对象。
- 子进程会把父进程的大部分内核数据拷贝一份。
对于现象的解释(操作系统 -> 写时拷贝):
如果子进程要修改的值,这个值不仅仅只有子进程在使用的话,操作系统会重新开辟一块新的物理空间,将数据拷贝给新的物理空间,让子进程指向新的物理空间(修改物理内存、修改页表)。(物理内存中的代码和数据要拷贝一份、task_struct也要拷贝一份,拷贝后的内容大部分和之前是相同的)
子进程和父进程中共享变量的地址相同,其实是虚拟地址相同而已。
3、细节问题
如果父子进程不写的话,全局变量默认是被父子共享的,代码是共享的(只读)。
为什么不在子进程创建的时候就直接全部拷贝一份?
为了节省空间。并且父子进程共享的数据中,有大部分数据都是不会进行修改的 --- 按需申请。
4、如何理解地址空间
1.区域的划分
地址空间本质是内核的一个struct结构体,很多的属性都是表示范围的变量(start,end)
cppstruct area { int start; int end; }
2.地址空间的理解
地址空间是一个结构体,有各种的地址划分。
如下图,
3.为什么要有地址空间
实际的物理空间,代码区、数据区、堆区、栈区、共享区、命令行参数和环境变量都是乱序的。
所以为了好管理,人为的规定了地址空间。
地址空间将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域。
进程管理模块和内存管理模块进行解耦。
拦截非法请求。
4.页表和写时拷贝的理解
5.虚拟地址的理解
程序运行后,使用的是虚拟空间的地址,而想要得到物理地址需要用页表进行映射。
最开始的时候,地址空间和页表中的数据是由操作系统在程序加载到内存中动态创建和管理的。
6.Linux中调度
在Linux中,每个cpu都会由一个运行队列。
大O(1)调度算法
三、进程控制
1、进程创建
进程调度fork之后,内核的工作(步骤)
分配新的内存块和内核数据结构给子进程。
将父进程大部分数据结构内容拷贝至子进程。
添加子进程到系统进程列表中。
fork返回,开始调度器调度。
进程具有独立性的理解
子进程有自己的相关管理数据结构,虽然代码是和父进程共享的,但是数据被写时是以写时拷贝进行独立的。
fork函数的返回值
子进程返回0,父进程返回子进程的pid。
fork函数为什么会有两次返回值
在fork函数返回之前,子进程已经创建完毕,可以开始进行调度了。
fork函数常规用法
1、子进程复制父进程,是父子进程同时执行不同的代码段。
2、子进程执行一个不同的程序。
fork函数调用失败的原因
1、系统中有太多的进程。
2、实际用户的进程数超过了限制。
写时拷贝
2、进程终止
进程终止的意义
1、释放曾经的代码和数据所占用的空间。
2、释放内核数据块。 --- 可能延迟释放。(僵尸状态)
进程终止的三种情况
1、代码跑完,结果正确。
2、代码跑完,结果不正确。
2、代码执行时,出现了异常,提前退出了。
- 结果正不正确可以通过进程的退出码决定!
- 一旦出现异常,退出码就没有任何的意义了。
pl:在用VS编程时,代码运行的时候崩溃了。 --- 本质就是os发现这个进程做了不该做的事情(如:访问了非法地址等),直接杀死了进程。
进程为什么会异常?
进程出现异常 本质上 就是因为进程收到了os发给进程的信号!
我们可以通过查看退出信号来判断进程出现异常的情况。
进程退出的原因?
可以根据退出码来进行判断,退出的原因。
进程退出判断顺序?
1、先看退出信息,判断是否是因为异常而退出的。
2、如果不是异常引起的,在看退出码,判断是否运行正常。
衡量一个进程退出,只需要退出码和退出信号!
如何终止?
1、main函数中return,表示进程结束。(非main函数中return,表示函数结束)
2、调用exit函数。注意:我们代码的任意位置调用exit(int status),都表示进程退出。
3、调用_exit(int status) --- 系统调用。
3、进程等待
进程等待是什么?
任何进程在退出的情况下,一般必须要被父进程进行等待。如果父进程不管不顾,子进程变成Z状态,内存泄漏。
进程等待的原因
- 父进程通过等待解决子进程退出的僵尸问题,回收系统资源。(一定要考虑)
- 获取子进程退出的信息。(不一定需要)
wait/waitpid
status参数是什么?
- 输出型参数
- 输出子进程退出信息。
退出信息
退出信息 = 进程退出码 + 退出信号。
一个int,在X86下是32位bit位,怎么存储信息如下:
如何理解阻塞等待子进程?
阻塞等待子进程的本质是父进程主动放弃CPU使用权,进入等待状态,并将其PCB从运行队列转移到等待队列中。当子进程结束时,父进程会被操作系统唤醒并重新加入到运行队列中。
如何从status中获取到退出码和退出信息?
1、退出码 = (status >> 8) & 0xFF
退出信号 = status & 0x7F
2、WIFEXITED(status) --- 判断是不是正常退出(判断退出信号)。
WEXITSTATUS(status) --- 获取子进程退出码。
非阻塞等待
waitpid的参数optionsu如果为0就是阻塞等待,如果为WNOHANG就是非阻塞等待。
waitpid采用非阻塞等待,本质就是检测子进程的状态变化。
pid_t > 0: 调用成功了,并且子进程退出了,父进程回收成功。
pid_t < 0: 调用失败了。
pid_t == 0:调用是成功的,只不过子进程还没退出,需要要你下一次进行重复等待。
- 非阻塞等待的时候 + 循环 = 非阻塞轮询。
4、进程的程序替换
exec系列函数
就是用exec系列的函数,执行新的程序。
解释原理
进程替换有没有创建新的进程?
没有创建新进程。只是把新程序的代码和数据覆盖老进程。页表可能会修改一下新的映射关系,PCB属性可能会有改变。
站在被替换进程的角度,本质就是这个程序被加载到内存了!
怎么加载?
exec*系列函数类似于是一种Linux上的加载函数。
exec* 系列的函数,执行完毕之后,后续的代码不见了,被替换了。
exec*系列的返回值
exec* 函数的返回值不需要考虑,如果exec*函数运行成功了,后续代码不会被运行;如果运行失败了,才会运行后续代码。
程序替换的意义
创建子进程,让子进程完成任务:
1、让子进程执行父进程代码的一部分
2、让子进程执行一个全新的程序。
系统调用:execve
谢谢大家!!!