目录
初识计算机操作系统
描述进程
创建子进程——fork
进程状态
进程优先级
环境变量
进程地址空间
初识计算机操作系统
计算机五大组件
输入设备:键盘、磁盘、网卡、显卡、话筒、摄像头等
输出设备:显示器、磁盘、网卡、显卡、音响等,与输入设备共同被称为外设
存储器(内存)
运算器&&控制器(CPU)
大部分计算机都遵守冯诺依曼体系,如笔记本电脑或服务器等等
数据经过输入设备input到存储器,然后存储器中的数据经过CPU处理后,再output到输出设备,
因此有了内存,CPU就不需要直接和外设打交道了,内存也是体系结构的核心设备,
input+output = IO,IO也分为本地IO和网络IO,区分是这两种中的哪一个,取决于输入输出设备
是什么
例:比如小明和小红通过电脑qq聊天,一个在成都,另一个在长沙,小明首先得需要将自己想要
输入的信息在键盘中敲出来,input到内存,此时的qq软件已经加载到了内存,然后交给CPU进行
处理(如计算等等),再output到输出设备网卡,再经过网络发出等等,最后小红在显示器上看到小
明所发的信息
任何外设,在数据层面,基本优先和内存打交道!CPU,在数据层面上,也直接和内存打交道
提示:距离CPU越近,速度越快!如下图,自上而下,速度就越来越慢
操作系统(operating system)
启动的操作系统(软件数据与代码,加载到内存中)才有意义
概念:一款专门针对软硬件资源进行管理工作的软件,简称os
作用:
对下:管理好软硬件资源
对上:给普通用户提供稳定的,高效的,安全的运行环境,为程序员提供各种基本功能!
怎么样管理(以校长管理学校为例)
校长是管理者,负责决策,辅导员是执行者,负责执行校长做出的决策,学生则是被管理者,比如
校长想开除某个学生,那就得有依据(属性数据),因为决策的执行是需要依据的,但是学生太多,
又怎么去找到某个学生的信息呢?
先是聚合一个学生的数据
定义一个结构体,将学生的各种信息包含进去 ————先描述被管理对象
然后聚合多个学生的数据,之间产生关联————再组织,将被管理者使用特定的数据结构组织
起来,就可以用一个双链表,将描述每个学生个人信息的结构体连接起来,需要某个人的信息
时,只需遍历一遍即可,对学生的管理工作,变成了对数据结构的增删查改!
管理者与被管理者并不直接打交道,你的数据被校长知道是因为辅导员,他是决策的执行者
如下图,几乎每一款硬件都有自己的驱动程序
os相当于上述例子中的校长,驱动程序就相当于辅导员,硬件相当于学生!
os管理的理念:先描述,再组织
os不信任任何用户,os如何为程序员提供服务?
因为如果你是个恶意用户,对软件进行非法操作,就无法管理了,以银行为例,银行因为不相信任
何用户,所以设置了柜台(窗口),你要办理业务,就需要经过柜台,所以os就提供了系统调用接
口,即函数!比语言中的库函数就要复杂一些,这时就有一些厉害的人,对系统调用接口进行软件
封装!以第三方库的形式呈现,语言中的库函数与系统调用接口就构成了上下层关系!
注意:只有涉及硬件时,语言中的函数才会与系统调用接口有关,同时,也不是所有的系统调用接
口都被封装了
进程
描述进程
概念:加载到内存的程序,就叫做进程(书本上)
系统中可能存在大量的进程,所以进程就需要被管理!
管理方式:先描述,再组织,这也是有PCB的原因!
任何进程在形成时,操作系统要为该进程创建PCB——进程控制块
描述进程的结构体struct task_struct{}——PCB-进程控制块
如下图,726就是此进程的编号!
我们所有的启动程序的过程,本质上都是在系统上面创建进程!
进程与程序的区别
进程 = 程序文件内容+与进程相关的数据结构(task_struct),该数据结构由操作系统帮我们自动
创建,该数据结构中包含了进程内部的所有属性信息,即数据
如下图,有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!!与进程对应的
内核创建的该进程的PCB强相关!
PCB的内部构成
标示符: 描述进程的唯一标示符,用来区别其他进程,也就是上面所说的编号
如下图,getpid就是获取进程编号的函数,而getppid则就是获取父进程编号的函数
在命令行上运行的命令,基本上父进程都是bash
如下图,kill -9 进程编号,也能关掉进程,和Ctrl+c是一样的
状态: 任务状态,退出代码,退出信号等
main函数返回的退出码会通过系统给其父进程,echo $?:输出最近执行的程序的退出码
优先级: 相对于其他进程的优先级
程序计数器: 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针,即可以找
到进程中的代码和数据
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
OS中有一个调度模块,较为均衡的调度每个进程来获得CPU资源,即进程被执行,所以需要有记
账信息
其他信息
理解上下文(单个CPU为例)
如下图,进程的代码可能不是在很短的时间内就能运行完毕的!所以规定每个进程单次运行的时间
片:比如:10ms,运行完就继续在后面排队等待,用户感受到的多个进程同时在运行,本质是通
过CPU的快速切换完成的!!
由于进程在运行期间是有切换的,进程可能存在大量的临时数据!暂时在CPU的寄存器中保存!
寄存器存储的是当前正在运行的进程的临时数据
虽然寄存器硬件只有一份,但是寄存器里面的数据是这个进程的!比如大学你去入伍,需要休学,
学校会保留你的学籍之类的,你回校时接着你之前的学习内容,继续学习,这也就是保护上下文和
恢复上下文
通过上下文,我们能感受到进程是被切换的
查看进程
通过/proc可以查看进程的信息,4029是该进程的编号
exe那一行是正在执行的程序,cwd是当前的工作目录
系统调用创建子进程——fork
pid_t可以把它看成int
hello world直接打印了两次
if和else都被执行了
在死循环中,连续打印了两个不同的进程编号
如何理解fork
./cmd 、run command、fork:在操作系统角度,上面的创建进程的方式,是没有差别的!!!
fork本质是创建进程,系统里多了一个进程,即与进程相关的内核数据结构(task_struct)+进程的
代码和数据在系统里面多了一份!fork之后,默认情况下,会"继承"父进程的代码和数据,所以子
进程和父进程代码是共享的,数据也是共享的,不过需要考虑修改的情况!
注意:父子代码只有一份,代码是不可以被修改的!!!
进程是具有独立性的,比如windows中,QQ、微信、ppt就是独立的,所以在数据需要修改时,
会通过"写时拷贝"来完成进程数据的独立性
内核数据结构task_struct也是以父进程为模板,初始化子进程的task_struct
创建子进程的目的
一般还是要让子进程和父进程做不一样的事情,比如if else分流,让父子进程做不一样的事,通过
fork的返回值来完成!返回值小于0就失败,否则就成功,成功之后给父进程返回子进程的pid,给
子进程返回0
如何理解两个返回值,及两个返回值的设置
一个函数已经开始执行return,函数的核心功能也就执行完了,而父子进程都会有一个返回值,而
且是数据,就会写入,发生写时拷贝
因为父:子=1:n,所以父进程需要通过子进程返回给父进程的编号来找到并控制子进程,而
子进程可以通过getppid()找到父进程
fork之后,父子进程谁先运行,这是不确定的,由调度器来确定
进程状态
进程运行状态转换图
进程状态的意义:方便os快速判断进程,完成特定的功能,比如调度,本质是一种分类!
所谓的进程,在运行的时候,有可能因为运行需要,可以在不同的队列里,在不同的队列里,所处
的状态是不一样的!
R:运行状态
如果一个进程正处于运行状态,它不一定正在占用CPU,只要它在运行队列中,随时可能会被调
度,它就是处于运行状态!
S:浅度睡眠(可中断睡眠),D:深度睡眠(不可中断睡眠)
当我们完成某种任务的时候,任务条件不具备,需要进程进行某种等待,S,D
注意:千万不要认为,进程只会等待CPU资源!!!
我们把从运行状态的task_struct(run_queue),放到等待队列中,就叫做挂起等待(阻塞),从等待
队列,放到就绪队列,然后被CPU调度就叫做唤醒进程!
S状态
如下图,因为进行了打印,所以外设中有显示器参与了,速度在CPU看来很慢,IO等待外设就绪
是需要花费时间的,所以是S状态
D状态
如下图,把进程中的数据写入磁盘,磁盘就去工作了,而进程在等待,os发现该进程在等待,可
用资源不够,就把进程关了,这时磁盘把数据写入成功与否的情况汇报给进程,却找不到进程,数
据就可能会丢失,还找不到根源!
进程如果处于D状态,不可被杀掉!!!
T:暂停状态
如下图,哪个表,表明系统也是可以向特定的进程发信号的,9是杀掉进程,19是暂停进程,18是
启动进程,S与S+的区别在于,S时的进程是后台进程,不能用Ctrl+c来杀掉进程,只能通过
kill-9才行,不能输命令。S+是前台进程,可以输命令
X:死亡状态
回收进程资源=进程相关的内核数据结构+你的代码和数据
Z:僵尸状态
有僵尸状态的原因,是为了辨别退出死亡原因!即进程退出的信息,写入task_struct
如果没有人检测或回收进程,即父进程没有做这件事,该进程退出就进入僵尸进程
孤儿进程
如下图,当其父进程退出后,它的父进程就变为了1号进程,也就是操作系统
进程优先级
原因:因为资源太少,所以才有优先级,其本质是分配资源的一种方式
查看优先级
可以用ps-al来查看优先级,PRI是该进程可被执行的优先级,NI是该进程的nice值,优先级的调整幅度
注意:Linux中的优先级,值越小优先级越高!!!
如下图的1001代表执行者的身份,而LOJ是执行者的名称,就和人一样,有身份证号码和姓名
修改优先级
PRI(new)=PRI(old)+nice
nice其取值范围是-20至19,一共40个级别
用top来修改优先级,top后按r键找到想要的进程,再回车输入修改的幅度NI,q键再退出即可
注意:再次修改时,PRI(old)一般还是80,不是第一次修改后的值
nice值是一个相对比较小的范围的原因
优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进
程"饥饿问题",部分进程得不到CPU资源
调度器:较为均衡的让每个进程享受到CPU资源!
其它概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称
之为并发
环境变量
命令、程序、工具...本质上都是一个可以执行的文件,那么我们运行的时候,为什么要./?原因是
为了帮系统确认对应的程序在哪里,而系统的命令不用带路径,则是因为环境变量PATH,如下
图,在执行系统命令时,它会依次从后找,找到后就去执行
echo: 显示某个环境变量值
设置环境变量
export: 设置值给环境变量
用户登陆到Linux系统中时,默认的目录不同,就是因为环境变量HOME不同
语言上面定义变量本质是在内存中开辟空间(有名字)
不要去质疑os开辟空间的能力!
概念:
环境变量的本质是os在内存/磁盘文件中开辟的空间,用来保存系统相关的数据!
本地变量
系统上还存在一种变量,是与本次登录(session)有关的变量,只在本次登录有效
env: 显示所有环境变量
set: 显示本地定义的shell变量和环境变量
export:将本地变量变为环境变量
unset: 清除环境变量
命令行参数
argc决定有几个有效命令行字符串
指令有很多选项,用来完成同一个命令的不同子功能,选项底层使用的就是我们的命令行参数
函数没有参数是可以传参的,获取参数可以通过可变参数列表去获取,比如printf函数!
获取环境变量
法一:通过main函数的第三个命令行参数
法二:第三方变量environ
我们一般不用上面两种方法,用的是getenv
环境变量具有全局属性
命令行上启动的进程,父进程都是bash
如下图,当前的理解:环境变量具有"全局属性",本质是环境变量可以被子进程继承下去!影响了
整个"用户"系统
进程地址空间
虚拟地址
如下图,在子进程更改g_val的值后,父子进程打印出来的值就不一样了,但是它们的地址却一
样,因此可以判定我们使用的地址,绝对不是物理地址,而是虚拟地址!否则不会出现下面这种现
象,所以上面C/C++程序地址空间一定不是内存,而是进程虚拟地址空间
概念
以大富翁给私生子留遗产为例,如下图,大富翁有8个私生子,资产100亿,但是都不知道对方的
存在,都认为自己是独占大富翁的财富,大富翁就给每个私生子画了一张大饼,告诉他们怎么做就
能获得他的资产,比如给A说你好好学习,将来就能继承我的资产,给B说你要好好打理公司,将
来才能继承我的资产等等,就这样每个私生子都规划好了获得资产如何花。大富翁如此做,是为了
简化私生子处理钱的方式,否则不能确定谁把钱花的好,而私生子需要钱时,大富翁都会去尽量满
足(除了需要太多时)
每个进程都有一个地址空间,都认为自己在独占物理内存!
管理方式还是先描述,再组织,用一个struct mm_struct的结构体来描述它,在内核中是一个数
据结构类型,具体进程的地址空间变量
别人给我们画大饼,是可以通过数据的方式进行画大饼的!比如:你在银行里存了1万块钱,你就
可以在你的手机的银行app上,看到余额,而这就是银行画给你的大饼
如下图,虽然这里只有start和end,但是每个进程都认为mm_struct代表整个内存且所有的地址为
0x00000...000~0xFFFFF...FFF
每个进程都认为地址空间的划分是按照4GB空间划分的,即每个进程都认为自己拥有4GB!
地址空间上进行区域划分时,对应的线性位置是虚拟地址!比如code_start是50,code_end是
100,那就认为51,52,53...都是这段区域的,其中51,52...就是线性地址
如下图,虚拟地址最终还是要转换为物理地址的,通过页表+MMU来映射实现,毕竟进程地址空间
是不能存储数据的,还得存储在物理内存中
注意:MMU是一个集成在CPU上的硬件,管理内存的,把它看成一个查页表即可
比如char* str = "hello",*str = 'l',做不到,因为"hello"是存在字符常量区的,而操作系统给的
权限只有r权限,所以os不让其映射
为什么要有进程地址空间?
1、如下图,如果没有进程地址空间,进程直接访问物理内存,那就可能会修改掉其它进程的数
据,造成数据不安全,所以通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限
管理),本质目的是为了保护物理内存以及各个进程的数据安全
2、将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的
过程,达到进程读写内存和os进行内存管理操作,进行软件上面的分离!
意思就是假如你要申请1000字节,但是暂时不会全部使用或暂时不使用,在os角度,如果空间立
马给你,就意味着会有一部分空间本来可以给别人使用,却被你闲置(有了空间,却从来没有过读
写),所以就给你虚拟内存空间,让你认为开辟好了空间,在你需要使用空间时,os就会让你等
等,即基于缺页中断进行物理内存申请,就算剩下的空间不够了,os也会通过内存管理算法从其
它进程哪里获取,也就是说os做的内存申请动作是透明的,进程根本不知道是自己所使用的空间
是剩下的,还是别的进程哪儿获取的
3、站在CPU和应用层的角度,进程可以看作统一使用4GB空间,而且每个空间区域的相对位
置,是比较确定的
如果没有地址空间,而每个进程的main函数的地址是不确定的,而CPU处理每个进程时,CPU都
要去查找该进程的main函数的地址,就会很麻烦,而有了地址空间,CPU就不再需要去找main函
数的地址,每个进程的地址空间的main函数地址都是一样的,只是映射后的物理地址不同而已,
比如进程空间的main函数的地址是0x1234,而进程A的main函数的物理地址是0x5555,进程B
的main函数的物理地址是0x4321,无论是处理A进程还是B进程,CPU只要知道地址0x1234即可
另外,程序的代码和数据可以被加载到物理内存的任意位置!大大的减少内存管理的负担!
os最终这样设计的目的:让每个进程都认为自己是独占系统的资源的!
对前面父子进程变量的值不一样,但是地址却一样的解释:
因为子进程的创建是以父进程为模板的!所以父子进程代码一般是共享的!而地址相同是因为是该
虚拟空间上的地址,所以也会相同!但是子进程变量的值更改后,os会在物理内存上开辟和原来
变量大小一样的空间,存放更改后的值,即写时拷贝,而虚拟空间上的地址通过页表映射后的物理
内存的地址会改变,变为新开辟空间的地址!
所有的只读的数据,一般可以只有一份,比如char* p = "hello world",char* str = "hello
world",p和str的值都是h的地址,所以操作系统维护一份是成本最低的!