【Linux】OS、进程PCB、状态、进程的切换和调度,深入理解虚拟地址空间
目录
- 1、操作系统(OS)
- 2、进程
- 2.1 PCB和进程ID
- 2.2 进程状态和优先级
- 2.3 进程切换和调度
- 2.4 命令行参数
- 2.5 环境变量
- 2.6 进程虚拟地址空间
1、操作系统(OS)
-
CPU在数据层面,不会和外设直接打交道,只会和内存进行交互
-
任何程序运行,都必须先被 (从磁盘)加载到内存
-
当数据在计算机内部流转的时候,本质是在不同的设备间进行拷贝,所以设备间拷贝的效率就是计算机的效率
-
操作系统包括:
- 内核:任务管理,文件管理,内存管理,驱动管理
- 其他程序:函数库,shell程序等
-
OS本质是一种进行软硬件资源管理的软件。
-
为什么要有OS?
- 操作系统对下软硬件资源的管理,稳定的、高效的、安全的、能进行良好工作的(手段)
- 操作系统对上要给用户提供一个稳定的、高效的、安全的运行环境(目的)
-
OS是如何管理的?
- 用struct结构体等描述,再用链表等数据结构管理。
-
如何理解系统调用和函数库?
- 在开发角度,操作系统对外会表现为⼀个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使⽤上,功能比较基础,对用户的要求相对也比较⾼,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
2、进程
2.1 PCB和进程ID
进程是程序的一个执行实例,是担当分配系统资源(CPU时间,内存)的实体。
进程 = 内核数据结构 + 程序的代码和数据。
把程序运行起来,本质就是在系统中启动了一个进程。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。Linux操作系统下的PCB是task_struct。
组织进程:所有运行在系统里的进程都以task_struct
链表的形式存在内核里。
查看进程:
-
进程的信息可以通过
/proc
系统文件夹查看。如:要获取PID为1的进程信息,你需要查看/proc/1
这个文件夹。
-
大多数进程信息同样可以使用
top
和ps
这些用户级工具来获取。
通过系统调用getpid()
和getppid()
获取进程ID:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
-
执行完就退出,比如ls、pwd等
-
一直不退,直到用户主动退出
-
getpid:获得进程的pid
2.2 进程状态和优先级
新建文件时如果不指定地址,会新建到当前目录下,这是因为每个进程都会记录下来自己对应的程序是谁,还会记录下来自己这个程序启动时所处的工作目录,这个工作目录可以更改(chdir)。
- proc不是磁盘级的文件
fork()->两个进程->父子关系->代码共享,但是数据各自一份
进程 = 内核数据结构+代码和数据,子进程只有内核数据结构,代码使用父进程的。进程具有很强的独立性,多个进程之间,运行时互不影响,即便是父子。
fork会有两个返回值,因为共享了代码。
fork之后谁先运行,由OS的调度器自主决定。
1、并发和并行
- CPU执行进程代码,不是把进程代码执行完毕后才执行下一个进程,而是给每一个进程预分配一个时间片,基于时间片进程调度轮转。也就是说多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程都得以推进,称之为并发。而站在我们的角度看感觉不到这种切换是因为CPU切换的速度非常快。(这也就是为什么当我们的程序有死循环时电脑也不会挂,其他的程序还可以运行。)
- 多个进程在多个CPU下同时运行,叫做并行。
2、时间片
Linux、Windows等民用级别的操作系统,通常都是分时操作系统,它的特点是追求调用任务的公平性。
3、进程等待的本质:连入目标外部设备,CPU不再调度。
- 只要进程在运行队列中,该进程就处于运行状态,随时可以被CPU调度。
- 运行和阻塞的本质,是操作系统让不同的进程处在不同队列中。(数据结构的增删查改)
- 进程卡住实际上是CPU不调度它了,也可能是调度周期太长,也就是进程太多了。
4、当内存资源严重不足的时候,操作系统把处于阻塞状态下的进程的代码部分换出到磁盘中,称为阻塞挂起状态,在阻塞状态转为运行状态前再将磁盘中的代码换入到阻塞队列中,换出换入时IO操作,这是用时间换空间的做法。有些场景中不适合操作,当内存资源严重不足时可能会直接杀掉进程。
当有
printf
时是S(休眠)状态,这是因为一个进程的时间片是非常短的,printf
并不是直接往显示器上打印,而是先打印到内存中的输出缓冲区中,速度快时缓存很容易写满,而显示器外设并不是时时刻刻就绪,有printf
就有IO,IO的速度很慢,死循环过程中大部分的时间都在做IO,执行printf
时才是R状态(由操作系统放到运行队列中被CPU调度,这个速度很快)。
等待键盘和等待磁盘是不一样的处理方案,等待键盘(S)可杀掉进程,等待磁盘(D)不可杀掉进程(不常见)。
一般T状态,也是在等待某个条件就绪。
进程被暂停再启动,这个程序就到后台去运行了(S后面没有+),这时候我们无法ctrl c
终止,只能使用kill -9 pid
终止。
可以执行程序后面用空格+& 来让一个进程到后台运行,后面显示进程pid。
打断点的本质是让当前进程暂停。
-
进程退出
代码不会再执行了,可以释放代码和数据,但内核数据结构(task_struct)要被OS维护起来——zombie(僵尸状态):维持退出信息,方便父进程和OS来进程查询,父进程或OS需要知道这个进程的执行情况。
(默认)没人管理,僵尸状态会一直维持,一直占用内存(内存泄漏)。一般需要父进程/OS读取子进程信息,子进程才能退出。 -
孤儿进程:父进程退出,子进程还在,子进程会被系统领养(一般会在后台运行)。
-
进程优先级:确定先后顺序,竞争某种稀缺资源。
-
修改(重置)优先级:指令/代码,不是高频,也建议修改。只能修改nice值。 可以通过
top
命令修改。
top + r +
要修改进程的pid + 预期修改的值。(系统禁止频繁修改)
修改nice
值的范围为:[-20, 20)
。
为什么nice的修改有范围而不能是任意值呢?分时OS注重进程调度的公平。 -
竞争性:系统进程数目众多,而CPU资源只有少量,甚至只有一个,所以进程间是有竞争的,为了高效地完成任务,更合理竞争相关资源,便有了优先级
-
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
2.3 进程切换和调度
- 进程在运行的时候,会有很多临时数据/瞬时数据(进程的上下文数据),在CPU内部的寄存器中保存。
- pc:当前正在执行指令的下一条指令的地址
- ir:指令寄存器,就是正在执行的指令
- 进程切换的核心就是上下文数据的保存和恢复。(相关寄存器中内容的保存和恢复)
- CPU中有一套寄存器,被多个进程共享
调度器要非常均衡地进行进程调度,那优先级的出现不是与其冲突了吗?
1、CPU调度器只会从active队列中选择进程来进行调度
2、调度有三种情况:
- 运行退出
- 时间片到了
- 有新的进程来了
这里有个问题,优先级是绝对的吗?也就是优先级越高一定最先调度吗?
如果一个进程优先级在当前运行队列中是最高的,那它被调度运行,时间片到了后重新插入到运行队列中,那它的优先级可能还是最高的那就还是调度它吗?(如果有新进程来了)显然不是,因为这样调度是不公平的。
当一个进程被调度运行,时间片到了后不会继续插入到active队列中,而是插入到active的同胞兄弟expired队列中,如果有新来的进程也是插入到这个队列中,也就是说active队列中的进程只会越来越少,当active队列中的进程都被调度一遍后,只需要交换active和expired两个指针,然后重复这个操作进程调度。
也就是说,优先级只能保证这个进程在一个调度周期内被优先调度。
从上面的图中我们还可以看到有一个大小为5的数组,这个数组大小为什么是5呢?因为整型有32位,5*32=160刚好覆盖140个位置。
通过类似下面的代码来快速确定队列大致位置,一次就可以检索32位:
for (int i = 0; i < 5; i++)
{
if (bit_map[i] == 0)
continue;
else
{
//在32个比特位中详细确定哪一个队列
}
}
上面的代码可以保证检索队列在常数范围,这种调度算法就是Linux内核O(1)调度算法。
所有的进程都要用链表连接,进程既可以在调度队列中,也可以在阻塞队列中…这是怎么实现的呢?
- 意义是什么?
一个进程,既可以在全局链表中,又可以在不脱离全局链表的情况下在任何一个数据结构中(形成了一张网),只要加节点字段即可。 - 原理是什么?
如果节点结构体中只有前后指针,那如何访问数据呢?这个问题在C语言中我们就讨论过,就是只知道一个结构体中某个成员的地址,如果访问这个结构体中其他成员?当时使用的方法是:计算偏移量。
事实上有一个专门计算偏移量的宏offsetof
:
2.4 命令行参数
我们知道main函数也是有参数的,只是平时不怎么用,也不清楚其参数的意义是什么。
我们经常使用的指令带选项,就是通过命令行参数实现的。
通过上面的示例,看出命令行参数的意义是什么?
同一个程序,可以根据命令行参数,根据选项的不同,表示出不同的功能。
参数是如何传递的?
我们启动起来的程序(子进程)读到了由shell(父进程)解析的数据。
2.5 环境变量
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
shell的环境变量:
环境变量相关的命令:
echo
:显示某个环境变量值export
:设置一个新的环境变量env
:显示所有环境变量unset
:清除环境变量set
:显示本地定义的shell变量和环境变量putenv
:设置特定环境变量的值getenv
:获取特定环境变量的值
前面我们说过,命令行执行我们自己的程序时需要带./
,表示在当前路径下找要执行的这个程序。而执行系统指令比如pwd
时不需要指定路径,系统默认会到/usr/bin/
路径下去找pwd
,为什么系统知道pwd
在usr/bin/
路径下呢?
因为
PATH
环境变量会告诉shell,应该到哪里去找系统指令。
PATH
中保存的是系统可执行文件的搜索路径集合。
所以如果我们不想带./
就可以执行我们自己的程序,可以有两种方法:
- 将我们的可执行文件放到
/usr/bin
路径下 - 将我们的可执行文件所在路径添加到
PATH
集合中。
但是这种修改只是临时的,因为这些环境变量只是加载到bash进程中,它是内存级的。
这些环境变量,开始都是在系统的配置文件中,当启动一个shell进程,它就会读取用户和系统相关的环境变量的配置文件,形成自己的环境变量表,这个环境变量表也可以被子进程读取。
所以如果我们想自定义一个环境变量并让它永久有效,就可以修改源头——系统的环境变量配置文件。
当我们登录的时候->系统创建bash进程->读取当前登录用户下的环境变量配置文件->配置它自己的环境变量->将bash自己的路径改为当前用户的路径。
进程能获得自己所在的路径:
通过USER
环境变量,可以让程序识别用户身份,比如可以让某个程序只能指定用户运行:
环境变量可以被所有bash之后的进程全部看到(继承),所以环境变量具有全局属性。
进程具有独立性,但进程间可以通过环境变量进程数据传递(一般是只读数据)。
2.6 进程虚拟地址空间
所谓的进程虚拟地址空间,本质上是一个内核数据结构对象(类似PCB)。
gval
是一个全局变量,在子进程中我们修改gval
的值,在父进程中不修改,在父子进程运行的过程中我们读取gval
的值,可以看到父子进程拥有各自独立的数据,但是取gval
的地址却是发现一致,为什么同一个地址中的gval
却有不同的值呢?
显然这是不可能的,事实上我们取出的这个地址只是虚拟地址,真实的gval
存在不同的物理地址中,而虚拟地址和真实的物理地址之间通过页表来建立映射关系,实现数据管理。
通过上图可以看到,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。
为什么要有虚拟地址?
- 虚拟地址空间+页表:保护内存
- 进程管理 和 内存管理 在系统层面进行解耦
- 让进程以统一的视角看待内存,代码和数据可以加载到内存的任意位置,通过页表的映射可以让无序变有序
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~