进程基础
在学习进程之前,首先要有一定的计算机硬件和软件基础。
硬件基础:冯·诺依曼体系结构
如图,是计算机在硬件上的体系结构。
下面举出一些常见的输入输出设备(有些设备只作输出设备,或者只作输入设备;而有些设备既能作为输出设备,又能作为输入设备)
输入设备:话筒、摄像头、键盘、网卡、磁盘,鼠标等等
输出设备:声卡、显卡、网卡、磁盘、显示器,打印机等等
这些设备连接在一起的目的:设备之间的数据流动。本质是设备之间频繁的数据拷贝!所以说,对数据进行拷贝的整体速度,是决定计算机效率的重要指标。
经典场景:程序在运行的时候,必须把程序先加载到内存。
程序文件里都是指令和数据,这些指令和数据最终是要让 CPU 来执行的,而程序文件在生成后会存储在磁盘中,磁盘中的文件要先加载到内存,才能和 CPU 进行交互。所以,程序在运行的时候,必须先将程序加载到内存。
软件基础:操作系统
操作系统是计算机上一个进行软硬件资源管理的软件,同时它也是计算机开机同时第一个被加载的软件,它可以为用户提供一个稳定、高效、安全的运行环境
管理:根据数据做决策(操作系统)
被管理:执行管理者的决策(软硬件)
计算机对数据管理的建模:先将堆数据的管理场景转化为对特定数据结构的增删查改,将具体问题,转化为计算机级别的建模,先描述,再组织
那么到底什么是进程呢?
用程序来举例子,一个可执行程序的本质是二进制文件,保存在磁盘中,在运行时要将程序先加载到内存中;计算机上的软件,如微信,qq,本质上也是一个.exe的可执行程序,在启动时都要先加载到内存中。
这些可执行程序,在被加载到内存中后,就会变成一个个进程!
加载到内存后,如何对这些进程进行管理呢?
对每一个进程,都由一个 内核PCB 和 可执行程序 组成,这个内核PCB也被称为进程控制块(process ctrl block)。
这个内核PCB可以看做一个结构体,这个结构体中包含进程的状态、优先级、标识符、内存指针等包含进程信息的所有属性字段。对进程的管理,可以看做对每一个进程PCB的管理,所有对进程的控制和操作,都只与进程的PCB有关,与进程的可执行程序无关!
总结:对进程的管理,可以粗略的看做在多个PCB对象形成的链表中,对一个个结点进行增删查改
几乎所有的指令,都是程序,加载到内存运行起来后也要变成进程。CPU的主要工作就是在内存中取指令 -> 分析指令 -> 执行指令 这个周期内进行循环。
CPU中也存在着一个 存储器PC ,PC里的指令指针 IP/EIP等存储正在执行指令的下一条指令的地址,指令指针指向哪一个进程的代码,就表示哪一个进程即将调度!判断、循环、函数跳转指令的本质,就是修改储存器PC里的指令指针指向。
进程状态
创建一个进程的,系统中就多了一个进程!在Linux中普通进程都有它的父进程,每个进程都会有一个编号叫 pid ,每次启动进程的 pid 几乎都会变化,因为这个进程是全新的进程!
下面代通过 父进程创建子进程的过程 来测试新创建的进程
fork()函数,头文件 #include<unistd.h>
fork()函数用来创建子进程,有两个返回值,给子进程返回0,给父进程返回子进程的id。(在Linux中,可以用同一个变量名,表示不同的内存)
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); if(id == 0){ printf("我是子进程,my_pid:%d,my_ppid:%d,return id:%d\n",getpid(),getppid(),id); exit(0); } else{ printf("我是父进程,my_pid:%d,my_ppid:%d,return id:%d\n",getpid(),getppid(),id); } }
子进程被创建,其实是以父进程为模板的:子进程会将父进程的字段信息浅拷贝过来,利用写时拷贝(写的时候再进行开空间深拷贝,否则就浅拷贝)来优化创建进程的效率。
运行结果:
一个进程崩溃了,是否会影响其他进程的运行?
仅从运行上来说是不会的,任意进程之间具有独立性,互相不影响。
测试:父进程崩溃了,是否会影响子进程的运行?
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); while(1) { if(id==0) { printf("我是子进程,my_pid:%d,my_ppid:%d,return id:%d\n",getpid(),getppid(),id); sleep(1); } else { printf("我是父进程,my_pid:%d,my_ppid:%d,return id:%d\n",getpid(),getppid(),id); sleep(1); } } return 0; }
使用 kill -9 指令杀死父进程
可以看到杀死父进程后,子进程依然还在运行,但它的 ppid 发生了改变,变成了‘孤儿进程’。
进程排队
就算进程放在了CPU上,进程也不会是一直在运行的!
CPU中有时间片的概念,即CPU会给当前进程分配一个时间片的时间(极短)来运行,后面的进程按开启时间顺序进行排队,一旦时间片上的时间耗尽,当前进程依然没有运行结束,CPU就会进行切换进程,未完成的进程继续入队,等待下一次调度。这种方式公平、高效,可以有效防止进程阻塞或者挂起而造成CPU资源的浪费!
宏观上来看,我们可以同时打开多个软件,仿佛有很多程序在同时运行。但是实际上,CPU每次只能处理一个进程的内容,只是轮换的时间极短,我们没有发现而已。
进程阻塞
上面提到了进程阻塞的概念,那么进程阻塞一般是什么样的场景呢?
如我们运行一个C语言程序,在程序启动的那一刻,就被加载到内存上,形成了一个进程。这个程序中有需要使用者用键盘输入的部分,但使用者迟迟没有输入。在等待的这段过程中,这个进程看似在运行中,但CPU不可能一直等待着使用者输入。这时候进程就会将自己设置为阻塞状态,根据不同的阻塞类型排入不同的等待队列进行排队,等待软件或硬件资源就绪后再继续运行。
所以总结下来:当进程在等待软硬件资源的时候,且资源没有就绪,进程就会将自己设置为阻塞状态,处于阻塞状态的进程会根据等待资源的不同而被连入多个等待队列中!
进程挂起
当计算机资源吃紧的时候,操作系会对一些优先级不高的进程设置为挂起状态,并将其移到外存,等到条件允许了再将这个进程调回内存中。
僵尸进程
每一个进程的创建都是为了完成某些用户需要的工作的,所以这些进程必须有结果、有数据(进程状态)。进程退出后,它的这些进程状态还需要它自己维持住,供上层读取。
当某个进程死亡(退出)后,它的进程状态没有被他的父进程读取,那这个进程目前的状态,就是僵尸状态。僵尸状态的进程,会一直存在,内存得不到释放,会造成内存泄漏!
孤儿进程
一个进程的父进程先于子进程死亡(退出),那这个子进程就会被1号进程过领养,变成一个孤儿进程。