目录
(4)既然父子进程的代码是相同的,那么子进程是从那行代码开始执行的?(重点)
(3)扩展:1.如何通过虚拟地址+页表的方式找到具体的物理地址
扩展:2为什么内存分配空间的时候,不采用连续分配的方式,而是一页一页离散分配的方式。
1.操作系统概念与定位
1.1操作系统是什么?
操作系统 = 操作系统内核(win/linux) + 一堆应用
操作系统内核:也就是代码程序,代码的作用就是(进程管理,内存管理,文件管理,驱动管 理等等)
一堆应用:依附在操作系统内核上完成的某些功能,例如:QQ,微信,飞书等等。
1.2操作系统在做什么事情
操作系统在管理计算机的软硬件资源
硬件资源:CPU,内存,硬盘,网卡,显示器等等
软件资源:进程资源,驱动程序
1.3操作系统是怎么完成这些事情的
通过管理
管理 = 描述(task_struct结构体)+ 组织(双向链表串联结构体)
1.4系统调用 & 库函数
系统调用:操作系统提供的函数,被称为系统调用函数
库函数:C标准库提供的函数,被称之为库函数。库函数的代码实现当中调用了系统调用函数。(为啥这样做?)
2.1什么是程序?什么是进程?
程序:源代码经过编译产生的可执行文件,这个文件是静态的。
进程:程序运行起来是实例,是动态在运行的。
2.2操作系统是如何来管理进程的?
进程的管理 = 描述(PCB) + 组织方式(链表)
2.3描述(PCB)
(1)进程号(PID)(进程标识符):
作用:标识当前在操作系统当中的一个进程,在一个操作系统中,一个进程拥有的进程号是唯一的,进程号可以反复使用。
实践:linux操作系统当中查看进程号
ps :查看当前操作系统当中进程信息的命令
ps aux
ps -ef
getpid():(函数)-----2号手册中的函数
测试:
创建了一个PID_test进程,并查看他的进程号(ps aux | grep PID_test)
注意:要产看一个进程的进程号,则一定要让进程处于持续运行的状态。
(2)进程状态:就绪/运行/阻塞:从CPU角度来理解
运行:进程占用CPU,并在CPU上运行;理解为进程正在使用CPU来执行自己的代码
就绪:进程已经具备运行条件,但是CPU还没有分配过来;理解为进程已经将运行前的工作全部做好了,就等着操作系统调用,占用CPU了。
阻塞:进程因等待某件事情发生而暂时不能运行;例如:等待IO输入,调用某些阻塞接口
(3)进程是抢占式执行的(插入内容)
- 在机器的CPU数量少,进程多的情况下(常态),操作系统在调度的时候要做到雨露均沾,让每个程序都运行上,但是操作系统在调度的时候,是从就绪队列当中获取进程,进行运行,换句话说,进程谁准备好了,谁就绪了,原则上就可以调度谁,所以,进程为了能够执行自己的代码,都是抢占式执行,不会互相谦让。所以进程就会有不同的状态。
- 操作系统调度的时候会有各种的算法:先来先服务,短作业优先,高优先级优先,时间片(毫秒级别:5-800)轮转等等。
- 并发:多个进程在一个CPU下,采取进程间切换的方式,各自独占CPU运行各自的代码,交替运行,让多个进程都得以推进,称之为并发
- 并行:多个进程在多个CPU下,同时运行各自的代码,称之为并行。
(4)细分的进程状态(重点)
R:运行状态 | 处于运行状态的代码,有可能在执行代码,有可能在运行队列(就绪队列)。 |
S:可中断睡眠状态 | 进程正在睡眠(被阻塞),等待资源到来时唤醒,也可以是其它进程信号或时钟中断唤醒,进入运行队列。 |
D:不可中断睡眠状态 | 通常等待一个IO结束(也就是输入输出的结束) |
T:暂停状态(ctrl + z) | 在linux下不要使用ctrl + z结束进程,因为他只是暂停了,并没有结束程序。(注意:ctrl + c是终止一个进程) |
t:跟踪状态 | 调试程序的时候可以看到(gdb) |
X:死亡状态 | 这个状态是用户看不到的,在PCB内核被释放的时候,进程会被置为X,进阶着进程就退出了 |
Z:僵尸状态(重点) |
(5)程序计数器:保存程序下一条执行的指令(重点)
(6)上下文信息:保存寄存器当中的内容(重点)
进程切换:进程从CPU当中剥离出来的,将CPU资源让给其他进程使用。
在进程切换出去的时候要保存现场(进程目前执行到哪里,即将执行什么指令),以便于下次执行这个程序的时候用于恢复现场。
恢复现场:程序计数器当中的指令来获取当前进程下一步要执行的代码为下一次进程执行做准备工作。
指令:指的是汇编指令,汇编代码,而不是原生的发高级语言代码。
(7)内存指针:指向一块自己的“程序地址空间”(图文表述)
(8)IO信息:保存进程打开文件的信息
每个进程被创建的时候,都会默认打开三个文件
stdin 标准输入 :scanf getchar
stdout 标准输出 :printf
strerr 标准错误 :perror
验证:一个进程被创建出来后,操作系统会以进程的进程号命名一个文件夹,该文件夹下的内容都是该进程的相关内容。
/proc:都被放到这个目录下了
3.创建子程序
(1)fork() 创建出来一个子程序(2号手册)
头文件:#include<unistd.h>:后续系统调用函数都需要包含该头文件
(2)fork()的返回值
创建成功
fork会返回两次,在父进程中返回一次,在创建的子程序当中返回一次
>0 :返回给父程序
==0 :返回给子程序
创建失败
-1
代码测试:
(3)原理:
子进程拷贝父进程的PCB;
父子进程代码共享(父子进程拥有的代码是一样的),数据独有(各自有各自的进程地址空间)
进程独立性:多个进程运行,需要独享各种资源,各自拥有自己的进程地址空间(进程虚拟地址),互相在执行的时候,数据不会窜,会不干扰。
(4)既然父子进程的代码是相同的,那么子进程是从那行代码开始执行的?(重点)
答:从fork之后开始执行的
测试:
父子进程的代码,都是由if分支语句的。但是在执行的时候:
父进程执行的是 >0 的代码块;子进程执行的是 == 0的代码块
作用:可以通过分支语句让父子进程执行不同的代码块;通过创建子进程,父进程可以把脏活累活(程序崩溃)都交给子程序去执行。
4.僵尸进程 & 僵尸状态
(1)子进程先于父进程退出,子进程就会变成僵尸进程
(2)模拟代码:
僵尸状态:Z
(3)原因:子进程在退出的时候,会告知父进程(信号),父进程忽略处理,父进程没有回收子进程的退出状态信息
(4)僵尸进程的危害:子进程的PCB没有被释放,导致内存泄漏。
(5)僵尸进程的解决方案:
杀死父进程(不推荐)
重启操作系统(不推荐)
父进程进行进程等待
扩展:
kill命令:可以终止一个进程
kill [pid] :普通终止
kill -9 [pid] :强杀
5.孤儿进程
(1)父进程先于子进程退出,子进程就会变成孤儿进程
(2)模拟代码:没有孤儿状态!!(切记)
(3)原因:父进程先于子进程推出后,既定回收子程序父进程不在了,所以子进程就变成孤儿了,所以称之为孤儿进程
(4)孤儿进程的退出信息由谁回收呢?
答:孤儿进程的退出信息由1号进程回收
(5)孤儿进程有危害吗?
答:孤儿进程没有危害。
注意:只有孤儿进程,没有孤儿状态!!!!!
补充:
(1)一旦父进程将子进程创建出来,父子进程也是抢占式执行的,并没有规定一定要谁先运行。
(2)什么是1号进程
/user/lib/systemd/systemd:操作系统启动的第一个进程,后续有很多进程都是由该进程 的子孙创建出来的操作系统初始化一些工作,操作系 统就是一个软件(一堆代码)。
(3)进程状态显示那一栏
S+:表示当前进程是一个前台进程,特性:阻塞bash,让bash不能处理其他问题
S:没有+表示进程是后天进程,特性:不会阻塞重启的bash,默默的在后台运行。
6.环境变量
(1)什么是环境变量
环境变量是指在操作系统运行的一些参数:换句话说,操作系统通过环境变量来找到运行时 的一些资源。
例如:链接的时候,帮助链接器找打动态库(标准库的);(用户自己编译的动态库,需要自己指定环境变量)
执行命令的时候帮助用户找到该命令在哪一个位置,例如ls
(2)常见的一些环境变量:
- PATH:指定可执行程序的搜索路径,程序员执行的命令之所以能够找到,这个环境变量起到的作用。
- HOME:登录到linux操作系统的用户家目录(HOME=/home/Ld)
- SHELL:当前的命令行解释器,默认是“/bin/bash”(SHELL=/bin/bash)
(3)查看当前环境变量
env
环境变量名称:环境变量的值(使用:进行间隔)
1.系统当中的环境变量是有多个的
2.每个环境变量的组织方式都是key(环境变量名称)=value(环境变量的值-可以拥有多个值,每个值都是“ :”进行间隔)。
echo $NAME //NAME:你的环境变量名称[重点]
(4)环境变量对应的文件
系统级文件:针对各个用户都起作用(root用户修改),强烈不推荐修改系统级别的环境变量文件,因为会影响其他用户(/etc/bashrc)
用户级别环境变量文件:推荐大家修改这两个级别的文件,只对自己用户的环境变量做出修改,影响自己(~/.bashrc; ~/.bash_profile)
~/.bash_profile包含 ~/.bashrc包含 /etc/bashrc
(5)修改环境变量
命令范式:export 环境变量名称 = [$环境变量名称]:[新添加的环境变量内容]
1>名令行中直接修改:(特点:临时生效)
如果是新增的,可以不要[$环境变量名称],即export 环境变量名称=[新添加的环境变量的内容]
如果是修改老的:必须加上[$环境变量名称],否则之前的环境变量就找不到了。
2>文件当中修改:特点:修改完毕之后,不会立即生效,需要配合source[环境变量文件名称],永久生效;
新增:在文件末尾添加:export 环境变量名称=[新添加的环境变量内容]
修改老的:在老的后面添加“:[新添加的环境变量内容]”
扩展:如何让自己的程序,不需要加./,直接使用名称就可以执行呢?
1>把我们的程序放在user/bin(不推荐使用)下面设置
因为user/bin下面放的是系统生成的可执行程序,用户不要在这里使用。
2>设置环境变量,在PATH环境变量中增加可执行程序的路径。
(6)环境变量的组织方式
环境变量是以字符指针数组的方式进行组织的,最后的元素是以NULL结尾(当程序拿到环境变量的时候,读取到NULL,就知道读取完毕了)
char* env[ ] :本质上是一个数组,数组的元素是char*,每一个char*,都指向一个环境变量(key=value)
(7)代码获取环境变量:
1>main的参数
main(int argc, char* argv[ ], char* env[ ])
2>environ
extern char **environ:这个是全局的外部变量,在libc.so当中定义,使用的时候,需要extern关键字
3> getenv
#include<stdlib.h>
char*getenv(const char *name);
参数:环境变量名称
返回值:环境变量的值,没找到返回NULL
7.程序地址空间
首先,先看一段代码
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
OS必须负责将 虚拟地址 转化成 物理地址 。
扩展:1.为什me操作系统要给每个程序都虚拟出来一个进程地址空间呢?为啥不直接让进程访问物理内存,这样不是更快一点吗?
原因:因为各个进程访问同一个物理地址空间,就会造成不可控。在有限的内存空间中,进程是不清楚哪一个内存被其他进程使用,哪些内存是空闲的,没有办法知道。所以,这种场景下,冒昧的使用,一定会导致多个进程在访问物理内存的时候,出现混乱;所以,内存由操作系统统一管理起来,但是又不能采用预先分配内存的方式,给进程。
原因:因为OS也不清楚进程能够使用多少内存,使用多久。所以,就虚拟给每一个进程分配了4G的地址(虚拟地址)当进程真正要保存数据,或者申请内存的时候,操作虚拟地址,让操作系统在给进程进行分配。这样就比较合理(同时也会节省很多空间,毕竟谁用才分配真正的物理内存)。(每个进程都无感的使用拿到的虚拟地址,背后就是OS进行了转换(就是后面所说的页表映射))。
2.为什么32为操作系统,给进程分配的是4G的虚拟地址空间?
因为32位的操作系统总共有32根地址线,每个地址线能模拟的数字为0/1,所以,最小就是0x00000000,最大就是0xFFFFFFFF。
64位同理。
8页表
(1)图解
(2)映射关系
虚拟地址空间分成一页一页,物理内存分成一块一块,使用页表将页和块映射起来。
问:fork创建子进程的时候,会不会拷贝页表?
答:会,每个进程都有自己的页表,子进程最初的页表映射的内容就是来自父进程,后面子进程在运行的时候,可能就会有不同的映射关系写到 自己的页表中。
(3)扩展:1.如何通过虚拟地址+页表的方式找到具体的物理地址
虚拟地址 = 页号 + 业内偏移
页号 = 虚拟地址/页的大小
业内偏移 = 虚拟地址%页的大小
扩展:2为什么内存分配空间的时候,不采用连续分配的方式,而是一页一页离散分配的方式。
防止内存碎片。
扩展:3
分段式:虚拟地址 = 段号 + 业内偏移
段表 段号:段的起始地址
段页式:虚拟地址 = 段号 + 页号 + 业内偏移
段表 段号:页表的起始地址
页表 页号:块号