操作系统?进程!!!
一、引言
从现在我们将开始学习进程相关的知识,它是操作系统实现多任务并发和高效资源管理的核心载体,没有进程,电脑就无法同时运行多个程序或稳定处理任务
二、冯诺依曼体系结构
截至目前,我们所认识的计算机都遵循如下的冯诺依曼体系结构:

计算机都有一个个的硬件组成
- 输入单元:键盘、鼠标、扫描仪等
- 中央处理器(CPU):含有运算器及控制器等
- 输出单元:显示器、打印机等
重要的,关于冯诺依曼体系结构要强调:
- 这里的存储器是指的内存
- CPU只能对内存进行读写
- 外设也只能直接访问内存
- 总的来说,所有设备都只能直接与内存打交道,冯诺依曼体系结构是以内存为核心的
三、操作系统相关概念
概念
任何计算机系统都包含一个基本的程序,称为操作系统(OS)。笼统的,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(函数库等)
为什么要设计操作系统?
- 管理硬件资源:CPU、内存等硬件资源是有限的,如果没有操作系统的调度,就会出现多个进程抢占资源的情况,可能使某些程序饥饿,甚至还会出现数据覆盖的情况,使程序出错,效率低
- 简化用户操作:普通用户很难精通硬件的底层原理,没有操作系统时,所有人都要和01指令打交道,但是有了操作系统,它会提供抽象接口,使得用户使用更加方便
- 支撑程序运行:如果没有操作系统,所有应用程序都需要自己实现相关的软硬件交互逻辑,但是有了操作系统,提供了统一的系统调用。简化了开发难度,同时实现了软件底层的统一
定位
- 操作系统是一款完全用于统筹管理的软件
什么叫管理?
所有管理都是描述+组织
- 先描述被管理对象:进程被描述为进程控制块+属于进程的代码和数据
- 在组织被管理对象:将进程的PCB放在对应的不同状态的队列中,等待对应事件发生
操作系统从用户程序到底层硬件的交互过程

如上图,用户调用操作系统提供的系统调用,操作系统进行处理信息并且与硬件的驱动程序进行交互、管理、调度,最终实现了用户与硬件的交互
上图中的库与系统调用是什么?
- 操作系统是一个封闭的整体,它是不相信用户的,如果用户对操作系统之下直接操作是危险的,但是操作系统的设计就是为了简化用户操作,所以操作系统会向外暴露一些基础的接口,这些接口被称作系统调用
- 系统调用太过基础,对于一般的开发者与用户来说,使用的门槛还是太高,所以一些开发者对系统调用再次进行封装,就称为库
四、初识进程
什么叫进程
简单来说,进程就是进行中的程序,从操作系统层面,将一个进程描述为:进程控制块+进程对应的数据和代码
进程的描述
进程信息被存放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,一般称之为PCB,Linux操作系统之下的PCB叫做:task_struct
task_struct内容分类:
- 标识符:描述本进程的唯一标识符,用来区别其他进程
- 状态:任务状态等
- 优先级:进程优先级
- 上下文数据:进程执行时处理器中的寄存器中的数据
- 其它信息
查看一个进程
通过ps、top等工具可以查找
初识系统调用
- fork:创建一个进程
- getpid:获取进程id
- getppid:获取父进程pid
使用上面几个系统调用我们便可以完成下面一份代码,用于创建一个子进程并且通过pid以及ppid判断父子进程之间的关系
int main(){
pid_t id = fork();
if(id > 0){
//父进程
printf("i am parent process, my pid:%d, my ppid:%d\n", getpid(), getppid());
}
else if(id == 0){
//子进程
printf("i am child process, my pid:%d, my ppid:%d\n", getpid(), getppid());
}return 0;
}

上面就是这段程序的运行结果了,可以看出,子进程ppid就是父进程的pid,在这里我们初识了系统调用,后面我们会深入的了解相关知识
五、进程状态
CPU的资源在计算机中至关重要,而CPU的核心功能之一又是用于调度,这时候区分一个进程的状态就至关重要了,例如:如果一个进程还在等待事件发生,那么我们就调度器遇到它是就会跳过,如果一个进程已经就绪,那么当CPU空闲时就会调度它,这样就会提高CPU的运行效率,避免资源浪费和调度混乱
- 运行:
是指一个进程正在被调度或者它现在在运行队列里,例如下面一段代码:
while(1){
int a = 1, b = 2;
int c = a + b;
}
进程状态监控:

可以看到,上面代码的运行状态是R,这就代表着该进程是处于运行状态的或是在运行队列中的,这也很好理解,这段代码一直在进行运算,自然是处于运行状态了
- 阻塞:
是指一个进程正在等待一些事件的发生或是I/O写入,例如下面一段代码:
while(1){
printf("i am a sleeping process\n");
}
进程状态监控:

可以看到,这个进程的状态是S,也就是睡眠状态,事实上,在操作系统层面上,这就是阻塞状态,虽然该进程是以while(1)的方式在执行的,但是由于要向显示屏上打印,显示屏的访问速度对于CPU来讲太慢了,这个时候对于CPU来讲,这个进程就是大部分时间在等待显示屏的打印,微不足道的一部分时间在运行,所以对外显示的这个进程状态就总是S状态了
之前提到过,冯诺依曼体系架构是以内存为核心的,那么在向显示器终端进行写入的过程中,内存究竟是承担怎样的角色呢?大致的过程是这样的:
- 首先由用户进程进行系统调用
- 然后转到内核态,向内存中的输出缓冲区进行写入
- 接下来由驱动程序将输出缓冲区的内容以终端可以识别的信号发送给显示器
另外,进程的关键信息(PCB)也是保存在内存中的,当然,还包括进程的数据等,综上,可以发现在这个过程中内存真的占据了关键地位
同时,Linux操作系统中还有D状态,称为不可打断睡眠,它的本质仍然是阻塞状态,但是由于它等待的事件比较重要,比如:磁盘I/O流等,这些时间比较重要,所以即便是kill命令都不能杀死
- 挂起:
当内存十分不够用时,这个时候一些阻塞状态的进程会被操作系统强制暂停,将他们的进程上下文保存起来,让他们的PCB继续在阻塞队列中而进程的数据和代码被还回磁盘
另外:进程还有一些例如:停止(T)、死亡(X)状态等,但这些状态要么不容易出现,要么转瞬即逝,所以在这里就不做演示,并且这些状态见名知义
- 僵尸状态:
僵尸状态是一种特殊的状态,它的形成是因为子进程提前退出而父进程没有处理子进程退出的相关信息,僵尸进程会一直等待父进程读取退出值,否则将一直保持僵尸状态,例如下面一段代码:
int id = fork();
if(id == 0){
//子进程
sleep(5);
exit(0);
}
else if(id > 0){
//父进程
sleep(30);
}
else{
//错误处理
//.......
}
进程状态监控:

观察上面的监控就可以看出,在进程开始的前5秒,父子进程都处于睡眠状态,但是在子进程推出之后,由于父进程没有处理子进程的退出值,这时候子进程进入了僵尸状态
写到这里才想起来,id为什么会同时进入if和else还没有解释,这与后面将会讲解的进程地址空间有关系,在后面讲完就会恍然大悟
- 僵尸状态进程的危害:
- 进程进入僵尸状态之后,如果父进程一直不做处理,那么操作系统就要一直维护僵尸进程的退出信息,占用了CPU资源
- 同时,内存中还要一直维护进程的PCB进程信息控制块,如果一直不做处理的话,PCB的内存将会泄漏
- 如何解决僵尸进程的问题?
- 在后面讲完进程的退出时讲
- 孤儿进程
孤儿进程不是一种状态,而是相反的,如果子进程还没有结束而父进程提前推出了,那么这个时候,就没有父进程来关心子进程的返回信息了,这时候pid为1的进程也就是shell(可以理解为操作系统)就会领养该子进程,这个子进程就叫做“孤儿进程”
