目录
1.2 如何来描述进程 -- PCB(process control block)
1. 进程
1.1 什么是进程?
- 字面意思:程序执行的一个实例,正在执行的程序等
- 内核观点:担当分配资源的实体
1.2 如何来描述进程 -- PCB(process control block)
- 每一个进程的信息都是存放在一个PCB中,也可以理解为一个进程属性的集合
- PCB是操作系统控制块的统称,而在Linux中被称为task_struct
1.3 task_struct
task_struct是Linx内核中的数据结构,一般储存在内存当中并且包含了进程的信息
task_struct信息分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
这也是我们接下来要了解的属性。
1.4 如何查看进程
两种方式:
1. ps -la
这里我们可以发现process这个程序被显示出来
2. ls /proc/进程的pid
每个进程都有一个独一无二的编号来标识,这个编号就是pid(process identification)
可以通过proc来查看进程的信息,我们也可以发现进程其实也维护了当前所在路径
1.5 获取标识符
1.pid
在1.4中我们认识了pid,那么我们如何获取呢?这时就有一个函数-getpid(),可以帮助我们获取当前进程的pid
2.ppid
那么什么是ppid呢?
ppid其实是父进程的进程编号,同样我们可以用函数来获取:getppid()
1.6 如何创建一个进程呢?
这里我们要引入一个新的函数:fork(),它会帮助我们创建父子进程
fork函数会有两个返回值,如果成功则给父进程返回子进程的pid,给子进程返回0,如果失败,则给子进程返回-1
一般情况而言,一段程序是不允许有两个死循环同时存在
我们可以发现通过fork函数实现了两份while死循环,但我们已知一份程序是无法进行两次死循环的,因此我们可以得知fork函数调用了两个进程,我们也可以通过ps-la来查看
我们发现果然有两个进程在同时运行,那么这是如何实现的呢?
父子进程是公用一份代码的,在内存上开始是公用一份内存,但由于fork函数的返回值不同,导致要修改内存中的变量,因为此时父子进程是公用一份内存,且进程是具有独立性的,因此子进程会进行实时拷贝(深拷贝)将父进程的数据绝大部分都拷贝,此时再修改变量就不会影响父进程。
1.7 进程的状态
进程是在CPU上进行的,而总会有一个CPU面临多个进程的问题,因此CPU中存在着runqueue的东西,来帮助进行进程进行。
操作系统的状态一般分为4类:
1.运行态
处于runqueue上,说明已经准备好随时被调度,这种状态就被称为运行态
2.终止态
已经完成了任务,但由于操作系统很忙,没有来得及释放,但永远不会再运行,这种就被称为终止态
3.阻塞态
由于进程的运行,不仅仅只使用CPU这一个资源,可能还会引用别的设备,而进程相比较设备总是比例更大,因此当需要使用别的资源,仍有排队的可能,这种情况下就是阻塞态
4.挂起态
我们已知进程是在内存上的,且一个进程可能短时间内不会运行,那么就有进程是占着内存空间不做事的,那么如果新的进程想进内存,而没有空间了怎么办?这时OS就会将没有进行的进程的代码和数据先交换在磁盘上,这样的进程就处于一种挂起的状态
而在Linux中的进程状态分为:
- R:也就是我们所讲的运行态
- S:浅度睡眠,指的是阻塞态,因为一直在等待别的设备使用
- D:深度睡眠,如果等待的是磁盘资源,那么就是DS,一般情况我们都是浅度睡眠
- T:停止态,也十分好理解
- X:死亡态
如何查看进程状态?
ps aux / ajx 命令
我们可以发现在STAT这个下面的状态是S+,也就是表明目前a.out这个程序处于睡眠状态,可是我们发现在左侧在一直打印,那么为什么不是R状态呢?因为一个进程不可能只调用CPU这一个资源,而我们大多数在等待别的资源时,都需要进行等待,因此进程大多数都是在等待也就是S状态。
那如果只调用CPU这一个资源呢?
我们取消掉打印这个代码,再来执行这个进程。
我们可以发现其中一个状态变成了R状态
1.7.1 僵尸状态
什么是僵尸状态?
当子进程退出后,而父进程没有收到子进程退出的任何信息,那么就会产生僵尸进程。
僵尸进程会以终止状态保持在进程表中,直到父进程获取到退出信息。
僵尸状态的条件:
子进程退出,父进程运行且父进程没有收到子进程的退出信息,因此子进程的状态就是Z。
僵尸进程的危害:
如果一个子进程退出后,且父进程一直不读取它的退出信息,那么子进程一直就处于僵尸状态,但是进程的所有属性都保存在pcb当中,维护进程退出信息就需要耗费资源,那么如果一直不退出说明该资源一直被占用,自然也就造成了内存泄漏。
1.7.2 孤儿进程
如果父进程比子进程先退出则此时子进程就变成了孤儿进程,那么它的ppid又会变成谁呢?
我们发现ppid由22331变成了1,那么这个1是什么呢?
这个1就是我们的操作系统。
1.8 进程优先级
进程对于软硬件资源来讲总是1对多的概念,那么就存在多个进程竞争同一个资源,那么有竞争就存在先后顺序,而重要的进程更需要先进行,因此就存在优先级的概念。
1.8.1 查看系统进程
ps -la 用来观察进程状态
我们可以观察到几个信息:
- PID:进程的代号
- PPID:该进程的父进程的代号
- PRI:该进程的优先级,值越小优先级越大
- NI:代表这个进程的nice值
1.8.2 PRI和NI
- PRI的值越小越被执行,那么加入了nice值以后,pri(new) = pri(old) + nice
- nice的范围是-20到19,一共40个级别
- pri的初始值是80,因此pri的范围为[60,99]
1.8.3 修改优先级
- 输入top指令
- 进入top后,输入r指令,输入进程的pid,修改nice值
PRI - NI的值永远是80
1.9 环境变量
变量分为环境变量和本地变量
1.9.1 与环境变量相关的命令
- echo:查看环境变量
- export:设置一个新的环境变量
- set:显示本地的shell变量和环境变量
- env:显示所有的环境变量
- unset:取消特定的环境变量
1.9.2 常见的环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录
- SHELL:当前shell
1.9.3 为什么执行可执行代码需要添加./?
是因为我们的可执行文件没有添加在PATH环境变量中,因此需要加上当前路径./,以便于查找
如果想不添加./,可以使用export PATH=$PATH:可执行文件所在路径
当将当前路径添加到PATH中就可以不添加./来执行可执行文件。
1.9.4 通过代码三种方式获取环境变量
1. 直接输入env获取所有的环境变量
2. 使用命令行的第三个参数
main函数除了已知的argc和argv以外还有env
3. environ
也可以将环境变量输出来
1.9.5 通过系统调用来获取环境变量
1. getenv
可以通过调用getenv函数,传入要获取环境变量的名称即可
我们可以发现两个PATH的值是相同的。
1.10 进程地址空间
我们fork创建出来的父子进程,子进程共享父进程的代码,我们可以测试一下:
我们可以看见父子进程打印出来的数据是一样的,也证实了我们的结论。
可是如果当我们将g_val的值发生改变后,那么他们两个值还会相同吗?
我们发现当子进程对g_val进行修改时,同一个地址有两个不同的值,而这种情况是不可能出现的,因此这个地址肯定不是物理地址,而是虚拟地址。
那么这里的原理是什么呢?
这里就要涉及地址空间这个概念,由于内存是无法阻止你进行修改数据的,因此为了安全,我们设置了一个地址空间,用来保护我们的内存不被损害,而这里就出现了进程地址空间来帮助我们管理内存,进程地址空间通过页表和物理内存进行解耦。
我们之后的操作都是先对于进程地址空间进行操作,如果该操作是违法的,则就不会作用在实际物理内存上。
那么进程地址空间和该现象有什么关系呢?
由于fork后的父子进程,子进程大部分信息都是来自于父进程,因此在进程地址空间上的地址也相同,通过页表也映射在物理内存上的同一个位置。
因此刚开始子进程和父进程共有一套代码和数据,而如果其中一个进程要修改该数据,就会发生写实拷贝,将共有的代码和数据复制一份,然后在复制的数据上进行操作,这样就不会损害未进行数据修改的进程的数据。
这就是为什么g_val修改时,父进程的g_val仍保持原样的理由。
进程地址空间的优点:
1.进程管理和内存管理解耦
2.让进程以统一的视角来看待内存,降低编码成本
这个第二个优点是怎么来说明呢?
如果没有进程地址空间,那么每一个进程的代码上局部变量,全局变量,常代码等所处内存的位置就不是固定的,而在进行编码的时候,会造成极大的成本,而存在了进程地址空间,每个变量和代码等所处在地址空间上的位置都是处于一个固定的位置,而只需要实现使用页表来映射在实际物理内存的位置即可,减少了编码成本。