文章目录
- 1. 理解冯诺依曼体系结构
- 1.1 简单见一见冯诺依曼
- 1.2 进一步认识
- 1.3 为什么一定要有内存的存在?
- 2. 操作系统
- 2.1 概念
- 2.2 设计OS的目的
- 2.3 OS的核心功能
- 2.4 如何理解“管理”二字?(小故事版)
- 2.5 系统调用和库函数概念
- 3. 进程简述
- 3.1 基本概念
- 3.2 描述进程 —— PCB
- 3.3 task_struct
- 3.4 查看进程
- 3.5 第一个系统调用函数
- 3.6 创建进程
- 3.7 进一步探索
- 3.7.1 三个问题:
- 3.8 补充
1. 理解冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器。这些计算机底层的硬件大多遵循冯诺依曼体系结构,那么冯诺依曼体系结构到底是什么呢?为什么要遵循冯诺依曼设计出来的体系结构,而不用其他体系结构呢?
1.1 简单见一见冯诺依曼
计算机是由一个个的硬件设备组成的,冯诺依曼体系结构下主要是这几种:
- 输入设备:键盘、鼠标、话筒、摄像头……
- 输出设备:显示器,打印机、投影仪、绘图仪……
- 中央处理器(CPU) = 运算器 + 控制器
- 存储器,也就是内存
其中输入与输出设备称为外设,存储器也叫内存,而外存也就是磁盘之类的。
1.2 进一步认识
我们知道,软件运行前,都是在磁盘中的,而软件运行则必须被加载到内存中,因为上述结构规定,CPU获取,写入只能从内存中来进行。此外软件运行,一般都有输出结果,也会对磁盘进行写入操作。
- 什么叫软件运行?—— 即CPU执行我们的代码,访问我们的数据!
- 什么叫加载与写入?—— 可以看成是一种IO(Input/Output)操作,也就是数据从一个设备“拷贝”到另一个设备。从中也可看出体系结构的效率是由设备的拷贝效率来决定的。
- 怎么理解IO?—— 要理解IO得站在内存的角度来思考,比如说我是一个内存,软件运行会被加载,此时我接收输入设备的数据,这个过程就叫做Input。软件运行中会对磁盘写入和对显示屏等设备进行输出数据,那这个过程就叫Output。
格外强调:
-
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。
-
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
-
- 即CPU在数据层面只和内存打交道,而外设只和内存打交道。也就是说所有设备都只能直接和内存打交道。
1.3 为什么一定要有内存的存在?
如下图,我们去掉内存后的体系结构是这样的:
似乎这样来看,我们自己设计出来的体系结构也不是不行。首先确实是可以这样设计的,但缺点是成本太大了。我们知道CPU的速度是最快的,其次是内存,最后是磁盘。因为CPU内部有寄存器这个硬件元件,在冯诺依曼体系下,它能够很有余力的去往内存缓冲区读取数据然后返回给CPU进行处理,而在我们这个体系结构下,为了应对外设与CPU之间的速度不对等,那我们只能把外设设备全部用寄存器给重组一遍,妥妥土豪行为。具体的,计算机世界里的存储结构大致呈现金字塔状,如下图所示:
给出结论: 当代计算机选择遵循冯诺依曼体系结构,实则是一种性价比行为的表现。也就是说当代计算机,是性价比的产物,正式因为有了性价比,才有计算机走入寻常人家,才有了操作系统以及与其相关生态的发展,才有了如今的互联网时代!
2. 操作系统
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
如下图所示:
换而言之,操作系统是一个基本的程序集合,是一款进行软硬件管理的软件。
2.2 设计OS的目的
- 对下,与硬件交互,管理所有的软硬件资源(这不是目的,是OS的手段)
- 对上,为用户程序,即应用级程序,提供一个良好的执行环境(这才是OS的目的)
上图理解:
-
- 计算机软硬件体系结构是个层状结构,数据只能在层与层之间相邻流动,不能一下跨越多层
-
- 访问操作系统必须使用系统调用——其实就是函数,只不过是系统提供的
-
- 在我们的程序中,只要你判断它访问了硬件,那么它必然贯穿整个软硬件体系层
-
- 库(如lib库)可能在底层封装了系统调用
2.3 OS的核心功能
在整个计算机软硬件体系架构中,操作系统的定位就是:一款纯正的搞“管理”的软件
2.4 如何理解“管理”二字?(小故事版)
以学生,辅导员,校长之间的管理为例。在此例中,校长类似于管理者,学生类似于被管理者。从计算机的角度来看,校长是操作系统,辅导员是驱动程序,学生则是各个硬件。那么故事正式开始了。
你是一名A大学的校长,你的日常工作就是管理这个校园内的所有学生,但学生的数量是非常多的,你不能挨个去访问每个学生的情况,所以你招募了一些打工仔,给了他们一些职位,这个职位就叫辅导员。而你要做的事就是针对学生情况给辅导员们开会,你在会议上去颁布和通过一系列有关学生的文件,也就是说,你对某些事件拥有决策权。而辅导员们要做的就是根据你在会议上下发的文件去具体落实到学生中,也就是说辅导员对事件具有执行权。某天,你想对学生的学习情况有个基本的了解,此时你下发文件,要求辅导员们去统计各个学生的学习情况。然后,你让辅导员们把统计上来的数据都给放到excel表格中,表格中一行就是一个学生,分别顺序的对应者学生的各科情况。此时你对学生的管理就变成了对excel表格的管理,而这个过程可以抽象成先描述,再组织。也就是说,你发的文件是一个描述的过程,描述了你想要做的事以及想要这件事达到的效果是什么,而辅导员们根据下发的文件去统计学生的学习情况并填写在excel表格中的这一过程,称之为组织过程。于是你通过了下发文件这一先描述的过程,然后再让底下打工人具体实施这一文件内容的再组织的过程,你最终达到了自己想要的结果。
进一步,由现实世界向计算机世界靠拢,此时你依旧是A大学的校长,只不过你在当选校长之前是学习计算机的,你特别精通C语言,你发现excel表格固然很好,但这终究是别人开发的程序,你担心你们学校清北的料子被别人知道然后被挖走,于是你决定不用excel表格来管理学生数据,此时你发现C语言中有个结构体类型似乎非常适合管理学生数据,你只需先创建一个结构体类型,然后在结构体内部设置变量诸如:学生的姓名、学号以及各科的成绩等,此时你再根据辅导员们收集上来的数据,挨个去创建学生结构体对象,为了方便快速找到下一个学生对象,你在结构体内部还设置了一个指向下一个学生对象的结构体指针next,于是你用了一个链表把各个学生之间给链接了起来。最后你对学生的管理,就变成了对链表的管理。这个过程也可以抽象成先描述,再组织。你在设计结构体时就是一个 “先描述” 的过程,然后具体创建学生结构体对象,再把他们链接起来,就是一个 “再组织” 的过程。
总结: 无论在现实生活,还是在计算机世界里,你去做什么事都是 “先描述,再组织” 的过程。在计算机中,是由操作系统去实施管理硬件功能:
- 先描述起来,用struct结构体
- 再组织起来,用链表或其他高效的数据结构
2.5 系统调用和库函数概念
-
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
-
- 系统调用在使用上很基础,需要去了解操作系统,对用户的要求比较高。所以,一些人就把部分的系统调用进行适度封装,从而形成了库函数。有了库,就很有利于更上层用户或者开发者进行二次开发。
-
- 操作系统要向上提供对应的服务,是通过系统调用的方式来提供的。换个角度,用户使用操作系统,只能是通过系统调用接口的方式来使用。也就是说,操作系统不相信任何的人,这同时也是操作系统对自身的一种保护。
3. 进程简述
3.1 基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体
3.2 描述进程 —— PCB
PCB的概念:(先描述)
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 术语上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct。
task_struct —— PCB的一种 (在具体的操作系统下,具体的再组织)
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的属性信息。
如下图:
总结: 进程 = 内核数据结构对象 + 自己的代码和数据;
具体来说,进程 = PCB(task_struct) + 自己的代码和数据
3.3 task_struct
内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下⼀条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I∕O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息。
具体以链接形式给出:task_struct
组织进程:
Linux中进程的组织是依靠双向链表,把一个个的PCB(task_struct)给管理起来的。
3.4 查看进程
-
- 进程的信息可以通过/proc的系统文件夹来查看
比如说我还可以具体的查看PID为1的进程内容:
- 进程的信息可以通过/proc的系统文件夹来查看
-
- 利用工具来查看
- top:可以实时、动态的查看当前进程的情况;
- ps: 可以用来查看系统当前的进程状态,ps查看的进程信息是当前的一个快照,是静态的。
top:
ps:
从上述现象中,我们不难发现,其实系统中的指令和工具的运行,实质上,就是创建了一个进程。也从侧面说明了进程是操作系统进行资源分配的一个基本的单位。
3.5 第一个系统调用函数
- 进程id(pid)
- 父进程id(ppid)
- 我们可以通过一个系统调用函数来获取进程的标示符,即getpid()。现在我们用man getpid 命令来查看一下它的使用。
现在我们已经了解了它如何使用,话不多说,直接打开vim 写下如下代码,看看运行效果。
运行效果:
再运行多次:
发现每次运行时,子进程的pid一直呈现递增的趋势,但其所对应的父进程却一直没变,那么它的父进程到底是谁呢?仔细阅读的小伙伴已经发现了,父进程的pid与ps运行下的其中一个bash进程的pid完全一样,如下图:
直接给出结论:实际上,我们运行shell程序与远端服务器进行联系时,所呈现出了的命令行窗口也是一个进程,它就是bash进程。bash是一个命令行解释器,可以说,在命令行窗口下,我们大多数自己运行的程序,其父进程都是bash进程,而对应程序的进程,也就是子进程,可以说是由父进程进行创建而来的,怎么理解父进程创建子进程呢?接着看。
3.6 创建进程
可以用系统调用函数——fork,来自主创建进程,先来认识下fork。
看看fork的返回值:
编写如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
// 创建进程,并查看
int main()
{
int ret = fork();
printf("我是一个进程,我的pid是:%d!,我的返回值是:%d\n", getpid(), ret);
return 0;
}
运行:
提出问题: 为什么程序中只有一条输出语句,也并没有循环控制语句,结果到头来却输出了两句呢?
解答:其实新创建的子进程没有自己的代码和数据,它被父进程创建出来,它是与父进程共享同一份数据和代码,如下图:
fork调用前只有父进程,调用后,子进程被创建,由于程序执行的顺序性,当子进程被创建时,此时产生了进程分流,也就是说在fork语句后,父与子进程各自执行他们的后续代码,也就是共享的那部分代码,此时两个进程就会各自分别执行printf语句,各自进行输出,最终呈现两个输出语句。
3.7 进一步探索
如下代码:
int main()
{
int ret = fork();
if (ret < 0)
{
perror("创建进程失败");
return 1;
}
else if (ret == 0)
{ //child
while (1)
{
sleep(1);
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}
}
else
{ //father
while (1)
{
sleep(1);
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
}
return 0;
}
运行:
3.7.1 三个问题:
- 为什么fork要给父子返回各自不同的返回值?
- 为什么一个函数会被返回两次?
- 为什么一个变量,既会等于0,又会大于0?从而导致if else 两个分支语句同时成立?
解答:
-
- 众所周知,父进程是可以创建子进程的,甚至创建多个子进程,那么父与子之间的对应关系就是 1:n 的数量关系。实质上这样设计只是为了更好的区分父子进程,并由父进程记录子进程以便后续操作。子进程返回0意思是只需要标记子进程被成功创建了,而父进程返回子进程的pid,可以直接用于后续操作(如等待子进程结束、向子进程发送信号等)。
-
- 依旧是上述进程分流与共享代码导致的,如下所示:
- 依旧是上述进程分流与共享代码导致的,如下所示:
-
- 我们之前说,子进程被新创建时会与父进程共享代码与数据。记住,这只限于新创建时,当后续代码中,如果子进程对父进程中的某个数据进行了修改,此时它们之间这个数据就不是数据共享了,但代码依旧共享,因为代码只读,具有常量性。换而言之,把父子任何一方,去进行了数据修改,OS会把被修改的数据在底层拷贝一份,让对应的目标进程去修改这个拷贝!这种策略叫做写时拷贝。
得出结论:进程间都是独立的,即进程具有独立性。
写时拷贝详见C++系列中的string类的深度剖析下与其模拟实现
3.8 补充
- 实质上,我们执行的所有的指令,工具,自己的程序,运行起来,全部都是一个进程!
- 有了进程的创建,自然也有进程的销毁,比如你写了个死循环程序在命令行窗口一直跑,你可以直接使用Ctrl+c来杀掉进程,也可以使用kill命令加上对应进程的pid来杀死进程。
Ctrl+c方式:
kill命令方式:
- 关于进程中的cwd与exe,如下:
当我们把在右边xshell下执行rm命令删除exe文件后:
‘