目录
2.3.2、进程状态:就绪/运行/阻塞(从cpu的角度理解)
1、操作系统的描述
1.1、操作系统是什么?
操作系统 = 操作系统内核 + 一堆应用
1.2、操作系统在做什么?
操作系统在管理计算机的软硬件资源。
1.3、操作系统使怎样完成这些事的?
通过管理完成的。
管理 = 描述(结构体) + 组织(串联结构体)
1.4、系统调用 & 库函数
系统调用函数:错做系统内核提供的函数
库函数:c标准库提供的函数。库函数的代码实现中调用了系统调用函数。
2、进程相关概念
2.1 程序 & 进程
程序:源代码经过编译产生的可执行文件,是静态的
进程:程序运行起来的实例,是动态的
2.2 管理进程
管理 = 描述(PCB) + 组织(链表)
2.3 描述(PCB)
2.3.1、进程号(PID)
ps aux (一般搭配管道过滤)
ps -ef
gitpid() (是系统调用函数,#include<unistd.h>; 返回值为pid_t型,其实就是int型;返回当前调用此函数的进程的进程号)
1 #include<stdio.h>
2 #include<unistd.h>
3 int main(){
4 while(1){
5 printf("pid = %d\n",getpid());
6 sleep(3);
7 }
8 return 0;
9 }
10
2.3.2、进程状态:就绪/运行/阻塞(从cpu的角度理解)
运行:进程占用cpu,并在cpu上运行(进程正在使用cpu来执行自己的代码)。
就绪:进程已具备了运行条件,但是cpu还没有分配过来;理解为进程已经将运行前的准备工作全部做好了,就等着系统调用,占用cpu。
阻塞:进程因等待某件事而暂时不能运行(例:iO输入、调用某些阻塞接口)。
进程是抢占式执行的:原则上,谁准备好了谁就可以执行
2.3.3、并发&并行
并发:多个进程在一个cpu下,采用进程切换的方式,各自独占cpu运行各自的代码,交替运行,让多个进程都得以推进。
并行:多个进程在多个cpu下,同时运行各自的代码。
2.3.4:细分的进程状态(重点)
R 运行状态:可能是在执行代码,也有可能是在运行队列(就绪队列)。也就是在就绪队列 + 运行状态的进程。
S 可中断睡眠状态:进程正在睡眠(被阻塞),等待资源到来时唤醒,也可以通过其他进程信号或时钟中断唤醒,进入运行队列。
D 不可中断睡眠状态(磁盘休眠状态):通常是等待某一个IO的结束。
T 暂停状态(ctrl + z):不要用ctrl + z来结束进程,不是结束,而是暂停。终止一个进程为ctrl + c.
t 跟踪状态:调试程序的时候可以看见。
X 死亡状态:这个状态是用户看不到的,在PCB被内核释放的时候,进程会被置为X,紧接着进程就退出了。
Z 僵尸状态:子进程先于父进程退出,子进程就变成了僵尸进程。
此外,有+和无+的区别:
有+:表示进程是一个前台进程。特性为:阻塞bash,让bash无法处理其他问题。
没有+:表示进程是一个后台进程。特性:不会阻塞启动的bash,默默的在后台运行。
2.3.5、程序计数器
进程切换:进程从cpu当中剥离出来,将cpu资源让其他程序使用。在进程切换的时候要保存现场(记录进程目前执行到哪里&即将执行什么指令)。恢复现场:通过程序计数器的内容来确定下一步要执行的指令。指令指的是汇编指令,而不是原生的高级语言的代码。
程序计数器的作用:记录当前进程下一步要执行的指令。本质就是为下一次执行做准备。
2.3.6、上下文信息
作用:保存寄存器当中的信息。
寄存器是OS的资源,在进程切换的过程中如果不保存寄存器的信息,等到该进程再次被cpu调用的时候,可能会由于寄存器的内容被其他进程改变而导致程序运行错误。
2.3.6、内存指针
指向”程序地址空间“,这里的程序地址空间本质上是OS虚拟出来的虚拟地址空间。
2.3.7、记账信息
记录使用cpu的时长、占用内存大小。
2.3.7、IO信息
保存进程打开的文件信息
每一个进程被创建的时候都会默认打开三个文件:
stdin:标准输入 :scanf
stdout:标准输出 :printf
stderr:标准错误 :perror
验证:
①创建一个进程
②在/proc目录下找到并进入以进程号命名的目录
③进入fd目录,就是那三个文件。
2.4、创建子进程
1、fork函数
pid_t fork() 系统调用函数,在使用的时候需要包含头文件#include<unistd.h>
2、函数返回值
①创建成功:
fork会返回两次,在父进程中返回一次,在子进程中返回一次。
返回值 > 0 :返回给父进程
返回值 == 0:返回给子进程
②创建失败:返回 -1
正常在命令行当中启动的进程,它的父进程是bash。
子进程与父进程代码相同,那么子进程从哪里开始执行代码?
答案:从fork指令之后的汇编指令开始执行。
原因:父进程在执行完fork函数时,子进程才会被创建出来。此时,父子进程竞争使用cpu来运行自己的代码,而在一个进程被剥离cpu的时候,程序计数器就会记录下一条要执行的指令。因此,子进程程序计数器其实记录的一定是fork函数执行完毕后的第一条汇编指令。
3、子进程的作用
父进程想把”脏活累活“(可能导致程序崩溃)都交给子进程去处理。
2.5僵尸进程 &僵尸状态
概念:子进程先于父进程退出,子进程就会变为僵尸进程。
形成原因:
子进程在退出的时候会告知父进程(信号),父进程忽略处理,父进程并没有回收子进程的状态信息。
此时,子进程就会变为僵尸状态(Z状态)。
危害:
子进程的PCB没有被释放,会造成内存泄露。
解决方案:
①杀死父进程,子进程由僵尸进程转变为孤儿进程(不推荐)
②重启操作系统(不推荐)
③父进程进行进程等待
扩展内容
kill命令可以终止一个进程
kill pid 普通终止
kill -9 pid pid强杀
注意:程序员针对僵尸进程,使用kill命令是不能杀死的。
2.6、孤儿进程
概念:
父进程先于子进程退出,子进程就会变成孤儿进程。孤儿进程没有危害。
注意:没有孤儿状态
形成原因:
父进程先于子进程退出后,既定回收子进程的父进程就不在了,子进程就变成了孤儿进程。此时它会被1号进程所领养。
1号进程:/user/lib/systemd/:操作系统启动的第一个进程,后续有很多进程都是由该进程创建出来的或者由该进程的子孙进程创建出来的操作系统初始化的一些工作。
2.7、环境变量
1、概念:
环境变量指在操作系统中用来指定操作系统运行的一些参数。也就是说,操作系统通过环境变量来找到运行时的一些资源。执行命令的时候,帮助用户找到该命令在哪一个位置。
例如:链接的时候,帮助链接器找到动态库(标准库的)。(用户自己编写的动态库,需要自己指定环境变量)
2、常见的环境变量
①PATH:指定可执行程序的搜索路径。程序员执行的命令之所以能够被找到,就是环境变量的作用。
②HOME:登录到Linux操作系统的家目录
HOME = /home/DL
③SHELL:当前的命令行解释器,默认是”/bin/bash“
3、查看当前环境变量
①env命令
②echo $[环境变量名称]
系统当中的环境变量是有多个的,每一个环境变量的组织方式都是 :
环境变量名称 = vaule(环境变量的值,多个值之间用:隔开)
4、环境变量对应的文件
① 系统级文件:/etc/bashrc
针对与各个用户都起作用(root用户才有权限修改),强烈不推荐修改系统级的文件,因为会影响其他用户。
②用户级文件:~/.bashrc && ~/.bash_profile
~/.bash_profile包含~/.bashrc包含/etc/bashr,推荐修改,因为这两个文件只会对自己用户的环境变量做出修改,指挥影响自己。
5、修改环境变量
命名范式:
export 环境变量名称 = $环境变量名称:新添加的环境变量的内容
①命令行当中直接修改:
特点:临时生效,在当前终端内生效,如果重新打开终端,是找不到该环境变量的。
新增环境变量:命名范式可以不要$环境变量名称:
修改老的环境变量:必须加上 $环境变量名称,如果不加之前的环境变量的值就丢失了。
②文件中修改:
特点:修改后不会立即生效。需要配合source[环境变量的名称]激活。永久生效的。
新增环境变量:在文件末尾直接添加:export 环境变量 = 【新添加的环境变量内容】
修改老的环境变量:在老的后面添加”:【新添加的环境变量内容】“
问题:如何让自己的程序,不加./直接使用程序名称执行?
①将我们的程序放在/user/bin下面(这里存放的是系统生成的可执行程序,不推荐使用)
②设置环境变量:在PATH环境当中增加可执行程序的路径
6、环境变量的组织方式
通过字符指针数组的方式组织,数组最后的元素以NULL结尾(当程序拿到环境变量的时候,读取到NULL,说明已经读取完毕)
7、代码获取环境变量
①通过main函数的参数获取,main函数的三个参数的含义:
int argc :命令行参数的个数。注意:命令行参数的个数是包括程序包括程序本身的。
char* argv:命令行参数的具体内容,放到了字符指针数组中。
char* env:环境变量的具体内容,放到了字符指针数组中,数组的最后一个元素为NULL.
②environ
extern char** environ:这是一个全局的外部变量,在lib.so中定义,使用的时候需要extern关键字。
③getenv
是一个库函数,包含头文件<stdlib.h>
char* getenv(const char* name)
参数是环境变量名称,返回值为环境变量的值
2.8、进程虚拟地址空间
1、 虚拟地址
我们在c/c++语言所看到的地址,全部都是虚拟地址。物理地址一般·用户看不到,由操作系统统一管理,操作系统负责将程序当中的虚拟地址转换为物理地址。
2、进程虚拟地址空间
操作系统为每一个进程虚拟出来一个4G的虚拟地址空间(32位操作系统),程序在访问内存的时候,使用的是虚拟地址进行访问。并不是每一个虚拟地址都对应一个物理地址,而是在使用的时候,操作系统才会对应。存储数据还是在真实的物理内存中,因此操作系统要将虚拟地址转化为物理地址(通过页表)。
为什么操作系统要给每一个进程虚拟出来一个地址空间,直接访问物理内存,不是更快?
各个进程访问同一个物理地址空间,就会造成不可控。在有限的内存空间中,进程是不清楚哪一个内存被其他进程所使用、那些内存是空闲的。所以在这种情况下,冒昧地使用,一定会导致多个进程在访问内存的时候出现混乱。因此内存由操作系统统一管理。
为什么不能采用预先直接分配内存的方式给进程?
操作系统不清楚进程能使用多少内存,使用多久。所以就虚拟给每一个进程分配4G的地址,进程真正要保存数据或者申请内存空间的时候,操作虚拟地址,让操作系统给进程分配。 这样会节省很多空间。
3、页表
映射关系:
答: 虚拟地址空间分为一页一页,物理内存分为一块一块,使用页表将页和块映射起来。
为什么内存分配的时候是一页一页离散的分配的方式?
答:防止内存碎片。
fork在创建子进程的时候,会不会拷贝页表?
答:会,因为每个进程都有自己的页表,子进程最初的页表映射的内容就是来自父进程。后面子进程在运行的时候可能会有不同的映射关系。
2.9、进程优先级
cpu资源分配的先后顺序就是指进程的优先权。
ps -l 命令显示的内容:
PRI :表示进程可被执行的优先级,值越小,优先级越高。
NI :进程的nice值(表示进程可被执行的优先级的修正值)
进程多cpu少,进程之间具有竞争性。为了高效的完成任务,更合理竞争相关资源,于是便有了优先级。