硬件与软件的桥梁:冯诺依曼体系、操作系统和初始进程的深度解析

发布于:2025-07-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

今天终于正式进入了进程的范畴,前面的linux操作也是真的很多哦。

在学习进程之前,我们来了解一下冯诺依曼体系结构。我们都知道现代计算机的基础是冯诺依曼体系结构。但是,大多数人可能并不觉得它有多重要。

一、冯诺依曼体系结构

在这里插入图片描述

冯诺依曼体系结构是现代计算机的基础,它奠定了几乎所有通用计算机的设计框架。冯诺依曼包含四大部分,五大单元

四大部分:输入设备,存储器,输出设备,中央处理器

五大单元:输入,输出设备,存储器,运算器,控制器

首先来看看什么是存储器存储器指的就是内存,磁盘,ssd(闪存)是外存

中央处理器也叫CPU,CPU包含运算器,控制器

运算器:算术运算,逻辑运算

控制器:执行代码,进行逻辑控制的

输入设备:键盘,摄像头,磁盘,网卡...

输出设备:显示器,磁盘,网卡,打印机...

我们暂时对冯诺依曼体系结构有了一个初步的认识。为了更好的理解冯诺依曼体系结构,提一个问题,我们都知道可执行程序必须得先加载到内存,这是为什么呢

先给结论从运行效率上来说,输入输出设备(外设) << 内存 << CPU的

有一张可以很清楚描述的图,我们来看一看。

在这里插入图片描述

我们应该知道,离CPU越近,它的效率越快,但是它的造价也会越高。比如说,你拿500块钱去买内存可能还买不到好的内存,但是买磁盘能买1TB的,越靠近CPU造价越高,空间越小。在数据流动的时候,它的规则是不允许CPU直接访问外设,因为CPU外设太慢了,而是先把输入设备里的数据放在内存里,CPU从内存里拿数据,处理完在写回内存,内存再把数据写到输出设备里

结论1CPU在数据层面,不和外设打交道,只会和内存打交道

外设不和CPU直接打交道,只和内存直接打交道

一个问题:为什么CPU不和外设打交道,为什么外设不和CPU打交道,它们都要和内存打交道呢

直接把输入设备的数据给CPU,CPU把数据给输出设备,不就可以了吗?但我们不想这样做。不知道大家有没有听过木桶原理。

木桶原理:就是说,这个桶能装多少水,取决于这个桶最低的那根竹板

在这里插入图片描述
也就是说,如果我们直接让输入输出设备和CPU打交道,那么计算机的效率就由输入输出设备来决定了。这是万万不行的。这时候就有人说了,那冯诺依曼体系结构里不是也有输入输出设备吗,计算机的效率也是由输入输出设备决定的吧。那你想一想,为什么可执行程序必须得先加载到内存里呢可执行程序得先加载到内存里本质上叫做预先加载,在加载过程中CPU不就可以做其它事情了吗,等到运行可执行程序的时候,CPU不就可以直接访问内存里的数据了

结论2内存的本质是外设和CPU之间的缓存,计算机的效率是以内存的速度为主

所以,可执行程序必须得先加载到内存里,是由冯诺依曼体系结构决定的

在这个过程当中,我们把输入设备里的数据搬到内存当中叫做input,内存里的数据刷新到输出设备上叫做output,简称IO,所以IO的本质是外设和内存进行数据交换

我们在写代码时,都用过scanfprintf函数吧。这两个函数是在干什么呢scanf函数是从键盘上读取数据到内存里那么这时候就有人要问了,scanf函数是怎么把键盘上输入的数据读到内存里的呢?不要忘记了,我们的程序是先加载到内存里的printf函数是将数据进行格式化输出到输出设备上。因此scanf和printf函数也叫做IO函数。

在计算机中,数据流动就是从一个设备流动到另一个设备,它的本质就是拷贝。那么计算机的整体效率其实就是设备间拷贝的效率

提几个问题:

1、打开文件(fopen)是在干什么呢

本质上是将外设里的数据加载到内存里,然后交给CPU处理,等到要将数据写到文件里时,再将数据写到内存里,刷新给输出设备

2、我在云南,朋友在北京,通过QQ发送在吗,解释一下,整个数据流的流动

我从键盘上(输入设备)输入数据,加载到内存里(因为启动了QQ,程序加载到了内存里),再交给CPU进行加密,将处理后的结果写回到内存里,再刷新给显示器(输出设备),此时你自己就看到了自己发送的消息,网卡(输出设备)通过网络,将数据发送给对方的网卡(输入设备),数据流动到内存里(对方也启动了QQ),再经过CPU进行解密,写回到内存里,刷新给显示器,此时对方就看到了你发送的信息

发送消息的本质是基于冯诺依曼体系结构,从我的键盘文件拷贝数据到对方的显示器文件

二、操作系统

1. 理解操作系统

对操作系统的初步认知这里就不做介绍了,不了解的可以看这篇文章:指尖上的魔法:优雅高效的Linux命令手册

提个问题:

我们使用计算机,大部分情况是在使用计算机的什么呢

今天我们写了一个C语言的程序,调用了printf,scanf函数,printf函数将数据格式化输出到显示器上,显示器是硬件资源吧,scanf函数从键盘获取数据到内存里,键盘是硬件资源吧。所以我们是通过软件来访问计算机的硬件资源

计算机的硬件资源是有限的,而用户的需求是无限的,这就要求必须要把硬件资源管理好,才能以有限的资源来应对无限的需求。

操作系统是一款进行软硬件资源管理的软件。既然要管理,那么什么是管理者呢

举个例子:在学校里,人们的身份大致可以分为校长,辅导员,学生。校长属于管理者,学生就属于被管理者,那辅导员呢?辅导员属于决策的执行者。

在生活中,人们所做的事情大致可以分为两类,做决策,做执行。做决策的人才是管理者,做执行的人不是管理者。学生在学校几点上课,几点吃饭,几点睡觉,人家都把你安排的明明白白的。校长根本不用见学生的面,就可以管理学生。

结论1管理者要管理被管理者,见面不是必要条件

他都不用见你的面,就把你安排的明明白白的。那么,是怎么做到的呢

比如说,毕业后你进入了一家互联网公司,在最终评绩效的时候,你的组长给你打了一个C,你很不服啊,就去找组长理论,说凭什么给我打C,你的组长说,人家一年写了2万行代码,有50个bug,你一年写了200行代码,有500个bug,bug比你的代码行数都多,你还好意思说。发奖金的时候,老板看到你的绩效打了一个C,就知道你平时肯定没有好好干活,一天就知道摸鱼。

结论2见面不是本质,获取你身上的有效信息,是管理的必备要素

管理本质是对被管理对象,相关有效数据的管理

管理者都不用见被管理者的面,他是怎么获取被管理者的信息呢?是通过辅导员拿到被管理者的信息的。

结论3辅导员协助校长,获取被管理者的有效信息

现在,学校要评选奖学金,他就要根据学生的数据进行判断,应该给谁发放奖学金呢?如果只有一个班的学生,那校长就可以拿着一张Excel表格看哪位学生的成绩优秀,选择给哪位学生发放奖学金。但是如果学生人数有50000人呢,难道校长要拿着每一个班的Excel表格去看吗,那不得把校长累死呀。

还没成为校长之前,他是一名C程序员(大部分操作系统都是由C语言写的),现在他要对学生的数据进行管理,所以现在这位校长就要动脑筋了,每个学生都要有基本的信息,包括成绩,班级,姓名,电话号码…,这些都是学生数据的属性,虽然每位学生属性的内容不一样,但是每位学生都要有对应的属性。所以对每位学生属性数据进行管理,不就是给每位学生创建一个结构体变量,对学生数据的管理就转化为了对50000个结构体变量的管理吗。

校长要想找到每个班里学习成绩最好的,那不就是需要对50000个结构体变量进行访问吗,那这效率也太低了吧,所以,校长又动脑筋了,他给结构体里添加了一个结构体指针(struct stu* next),以前要手写50000行代码,现在只需要用指针将他们的数据进行关联不就可以了。每一个节点对应一名学生的基本信息。从此以后,校长管理所有的人就直接转化成为了对链表的管理

上面的故事可以高度抽象为先描述(将所有的学生数据变成一个个结构体变量),在组织(用结构体指针将一个个结构体变量进行关联)。

管理的本质是对数据进行管理

管理的做法先描述,在组织

校长就相当于操作系统,学生就是硬件,辅导员就是驱动程序

操作系统要管理硬件,也必须遵守先描述在组织。而且操作系统内可不仅仅只有硬件管理,还有进程管理,文件管理,内存管理...也就是说,操作系统内一定有大量的数据结构

在这里插入图片描述

2. 系统调用

举个例子:银行系统有自己的金库,桌椅板凳,电脑…这些东西都不会随意被群众进行访问或者损坏,因为群众当中有坏人,会影响银行系统的安全。但是,它又要给群众提供服务,所以,银行必须给群众提供窗口,让工作人员按照群众的要求进行操作。这就保证了银行安全的同时又能够对外提供服务。

同样的道理,操作系统中有进程管理数据,文件管理数据,内存管理数据…,如果随意的让外部程序访问,万一出现问题了怎么办,比如野指针,修改数据。我们不允许外部程序直接访问操作系统,所以操作系统为了给上层用户提供一种安全的访问方式,就有了系统调用。

系统调用其实就是一个个函数

系统调用也属于操作系统,既然是函数,可能就有参数和返回值

结论系统调用是为了让用户安全的访问操作系统

也就是说和操作系统交互,只能使用系统调用。这时候又要衍生出问题了。如果我写了一个C程序,使用printf函数向显示器上打印了一个hello world,本质上不就是向硬件上写数据了吗,而操作系统作为软硬件资源的管理者,是不是说我们将来要向显示器上写数据,必须通过系统调用来访问操作系统然后才能进入显示器,向显示器进行写入吗?是的,没错。这时候有人就要说了,我以前写程序的时候,没用过系统调用,用的都是C语言的printf、C++的cout?先把这个问题放下。

举个例子:你是A学校的学生,属于学霸级别,B学校的校长想让你替他们的学校去参加数学竞赛,他可以直接来征求你的同意吗,这肯定是不行的。他必须要去征求A校长的同意,然后才能带你离开。

B校长要的是你,他却要去征求A校长的同意。对于操作系统而言,你写了一个程序要访问硬件,不能直接让程序访问,这是做不到的,必须要通过操作系统才能访问硬件。

换句话说,你写的printf、cout其实并没有直接访问底层硬件,它是通过操作系统访问的。那么它是怎么做到的呢

故事:在银行存钱,取钱的时候,有时候还是挺麻烦的,你可能要去取号,排队,万一这个人是个老大爷,他不仅耳背,还不识字,这会大大的增加老大爷的使用成本,这时候大堂经理过来了,他帮老大爷填写单子,存钱等工作,大堂经理就给老大爷把问题解决了。

大堂经理其实就像是一层软件层,这个软件层就是各种语言的库。总的来说,使用系统调用是有成本的,这就要求程序员要对各种系统调用的原理有一定的了解。可是我作为一个上层的应用程序员,不一定对操作系统有足够的了解,这会大大的增加程序员的成本。没关系,他可以使用库里面的方法。所以,库当中的方法就是对系统调用进行了封装

库和系统调用就是上下层的关系

注:不是库中的每一个方法,底层都封装了系统调用。如果访问了硬件资源,那它底层大概率封装了系统调用

计算机的使用者,除了开发者,还有纯正的小白。他们可什么都不懂,所以就有开发者基于软件层进行二次开发应用,像图形化界面,shell…。

三、进程

1. 进程概念

对于进程的概念有一个说法:运行起来的程序就叫做进程(当然这是不完整的)。

启动应用,就要求我们的可执行程序必须预先加载到内存里,但是我们也可以启动多个应用啊,也就是说,系统当中,会存在大量的进程。那么这些进程要不要被操作系统管理起来呢答案是要的怎么做先描述,再组织。所以在操作系统层面,叫做struct PCB(Process Control Block进程控制块)。既然已经描述了,那么怎么组织呢?

系统里可能会有大量的进程,那么每个进程都会有一个struct PCB,通过链表的方式进行关联。也就是说,对进程的管理转变为对链表的增删查改

在这里插入图片描述

所以,什么是进程呢

进程 = PCB(内核数据结构) + 代码和数据

在linux上进程叫做struct task_struct

可能有小伙伴听说过进程排队,进程排队是在干什么呢

进程排队本质其实是让你的pcb节点,进行排队

再本质就是从一个数据结构,把节点拿走,放入新的队列数据结构中

在这里插入图片描述

所有运行在系统里的进程都以task_struct 双链表的形式存在内核里

要想学好进程,就必然少不了学习进程的相关属性。接下来就来看看进程属性吧。

2. 进程属性

. 标示符:描述本进程的唯一标示符,用来区别于其它进程

例子:每个班的学生,在考试的时候,都会按照编号来排座位,根据编号来区分每个同学。

. 状态:任务状态,退出代码,退出信号等

. 优先级:相对于其它进程的优先级

. 程序计数器(PC):程序中即将被执行的下一条指令的地址

例子:程序在编译,链接形成可执行程序,当加载可执行程序时,会把它放入内存里并分配地址,在执行可执行程序时,会从第一条地址处开始执行。程序计数器是CPU里的一个寄存器,而执行可执行程序时,地址是在内存里的,PC里会存放被执行指令的内存地址,IR寄存器从PC寄存器里取数据,此时PC指针会自动存放下一条指令的内存地址。

. 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

. 上下文数据:进程执行时处理器中的寄存器中的数据

例子:比如说征兵的时候,大二的你要去当兵,你被选上之后不能给辅导员连招呼都不打,直接就走,那等你回来的时候恐怕都被退学了。所以你应该给你的辅导员说一声,这时候他会对你的档案进行保留,等你当完兵回来了,再从大二继续开始上学。对你的档案进行保留的过程就叫做上下文数据,不能让你从大一开始重新上学。

而进程也是会被进行切换,调度的。在CPU里有许多寄存器,但我们称之为一套寄存器。这些寄存器保存的就是进程的临时数据。当前进程被切换调度的时候,就要对该进程的上下文数据进行保留,再度被切换回来时,就要对该进程上下文数据的恢复

. I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表

. 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

. 其他信息

现在,我们已经对进程属性有了一个大致的了解。接下来的日子里,我们就要学习进程的相关属性了。

先来补充一点知识。

查看进程可以通过 /proc 系统文件夹查看

接下来认识一下第一个系统调用。

pid_t getpid(); //获取当前进程的标示符

我们写一个程序来验证一下。

在这里插入图片描述

终止程序后,是查不到该进程的
在这里插入图片描述

. 进程会记录自己对应的磁盘二进制可执行文件
在这里插入图片描述

. 进程启动的时候,默认工作路径就是自己可执行程序所处的路径
在这里插入图片描述

为什么要说这个呢?还记得C语言的文件操作吗?我们在fopen的时候,有时候并不需要带上路径。这是为什么呢就是因为进程会自己记录所处的工作路径,这就是原因。

证明:
在这里插入图片描述

那如果我们改变一下工作路径呢?结果又会怎样。

int chdir(const char* path); //更改工作目录,成功了返回0,失败了,返回-1

在这里插入图片描述

可以看到,新建的文件就在改变的目录下。

使用 ls /proc查看进程的效率太低,所以介绍一个新的命令。

ps axj | head -1 && ps axj | grep "myproc" 

在这里插入图片描述

grep命令本身也是一个可执行程序文件,加载到内存时也会成为进程,同理ps命令也会成为进程我们要查看的进程是myproc,为什么grep进程也被过滤出来了呢因为grep进程也包含了关键词myproc,所以被过滤出来了

在这里插入图片描述

这样做就只把myproc进程过滤出来了。

pid是当前进程的标示符,那么ppid是什么呢答案ppid是当前进程的父进程的标示符

pid_t getppid(); //获取当前进程的父进程的标示符

在这里插入图片描述

pid在不断发生变化,是因为每次启动进程时,系统都会为该进程重新分配标示符那父进程ppid怎么一直不变呢?他是谁啊

在这里插入图片描述

我们在命令行中,启动命令/程序的时候,都会变成进程。该进程的父进程(ppid)是bash,bash是谁呢?不知道还有没有人记得shell,bash就是linux中的外壳程序

在linux系统中,增多进程是通过父进程创建子进程的方式让linux系统中的进程变多的

也就是说,我们的代码都是由父进程创建子进程来执行的那么父进程是如何创建子进程的呢

pid_t fork(); //创建子进程

写一个简单的程序来创建一个子进程。

在这里插入图片描述
在这里插入图片描述

看到这里,相信大家有很多的疑问。怎么会打印两次呢

先来看看fork函数的返回值吧

在这里插入图片描述

成功的话,给父进程返回子进程的pid,给子进程返回0;失败的话,给父进程返回-1并且设置错误码

此时此刻,相信大家的疑问更多了吧。

1、为什么给父进程返回子进程的pid,给子进程返回0呢

2、一个函数怎么可能有两个返回值呢

3、返回值怎么可能既大于0又等于0呢

先来解决第一个问题。为什么返回值是这样的呢

在创建子进程的时候,我们只能创建一个吗?当然不是了。也就是说,父进程与子进程的比例是1:n了。那也就意味着子进程的父进程是唯一的,子进程想找父进程是很容易的,但子进程是有多个的,父进程想找子进程是不容易的,因此父进程需要拿到子进程的pid。在后面的文章中会对这个问题进一步解释。

解释第二个问题之前,先解释上面的程序为什么会打印两次呢

进程 = struct task_struct(内核数据结构) + 自己的代码和数据

那么,父进程创建子进程,子进程也需要有自己的struct task_struct和子进程自己的代码和数据。但是父进程的代码和数据是从磁盘文件里加载到内存里的那么子进程自己的代码和数据又从何而来呢

在linux系统中,创建一个新的子进程,子进程的struct task_struct也要被初始化,以父进程为模板进行初始化

子进程的代码和数据默认共享父进程的代码和数据个别数据会做出修改(进程在数据层面要保证独立性)

也就是说,父进程和子进程执行的是同一份代码,那也就解释的清楚为什么会打印两次了。因为父进程和子进程都会执行一次代码,所以会打印两次

现在再来看看第二个问题。为什么一个函数会有两个返回值呢

答案其实已经出来了。父进程和子进程都会执行一次fork函数,所以会有两个返回值

既然父进程和子进程看到的是同一份代码,那我们就可以写一个打破大家认知的程序了。

在这里插入图片描述
在这里插入图片描述

一份代码既执行了else if又执行了else,这足够奇怪了吧。相信大家也能够解释这个原因。这是因为父进程和子进程两个执行流分别执行了不同的语句

至于第三个问题,以目前的知识无法解答。在后续的文章里会做出解释。

今天的知识分享暂时先到这里吧,觉得不错的点点赞。

在这里插入图片描述