一、冯诺依曼体系的介绍
- 冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。而我们现在常见的计算机,大多都遵守冯诺依曼体系,因为这样的计算机的成本更低,效率更高。如下是冯诺依曼体系结构图:
输入设备:包括键盘、鼠标、麦克风、扫描仪、写字板、硬盘、网卡等,平常计算机供我们执行操作的外设。
存储器:说直白点就是内存,但我们日常可能也会有一个被我们误解的"内存"就是存储内存,也就是我们手机上的储存空间256g等,这里有专业的名称,存储内存成为外部存储 ,也就是硬盘,光盘,和u盘等等。内部存储器是插在主板上的内存条。
中央处理器:就是我们日常电脑里的cpu,而cpu的核心组成部分是运算器和控制器,运算器是用来处理算术运算和逻辑运算的,控制器就相当于计算机的指挥官根据内存中的指令指挥硬件完成相应操作,如今的cpu更加复杂了,在这张图列出来的只是核心的组成部分。
输出设备:就是我们现在用到的显示器、耳机、打印机等,就好比我们用输入设备-键盘进行打字,而计算机需要通过输出设备进行反馈给我们,我们才知道我们在打字。
**注意:**冯诺依曼体系放在概念上很难理解,我们试着放在我们现在用的QQ发信息来理解一下,这里补充一下,输入设备和输出设备中还包含网卡。
二、操作系统(简称OS,全程Operator System)
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统,就是有了操作系统我们使用计算机的时候才有这种图形化界面。笼统理解操作系统包括:内核(进程管理、内存管理、文件管理、驱动管理)和其他程序(例如函数库】shell程序等等)。
2.2 设计操作系统的目的
对上,为用户和用户程序(应用程序)提供一个良好的执行环境,我们现在有的这些图形化界面执行程序就是操作系统的便利,我们想打开一个程序直接就双击点击那个图标即可 。
对下,与硬件交互,管理所有的软硬件资源。
2.3核心功能
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的"搞管理"的软件。而操作系统对软硬件的管理是根据数据进行管理,例如用户要执行游戏,那么就会双击点击游戏图标,然后游戏的进程就放到内存里面,然后操作系统通过图形API抽象GPU硬件,游戏调用API,然后由驱动转换为GPU(也可以大体称为显卡)指令,这样就做到操作系统对硬件进行管理。
2.3.1 "先描述再组织"的介绍
上面的这一套管理方法可以用:"**先描述,再组织"**来总结。
先描述指的是:操作系统涉及的各个管理模板及其功能
再组织指的是:将这些模块按逻辑分类,形成层序化的结构。
这样子说可能有点难理解,我们从小方面进行理解:我们学的C++、java都是有类和对象这一个概念的,我们实例化出来的对象有自身的属性,给该对象赋予属性的过程就是先描述,例如这个对象是男的还是女的,年龄是多少,住哪里。再组织就是,我们要如何管理这些信息,我们可以使用数据结构,例如链表来管理这些对象的信息等等。
那么再放到操作系统上来, 底层硬件的数据好比对象,然后这些数据会以某种数据结构类型例如链表进行管理,那操作系统只需要对链表进行管理,也就完成对硬件进行管理的操作了。
为了加深记忆,再放到现实生活上来。在我们现实生活上,国家对我们每个人的管理也是通过数据进行管理,我们每个人都有对应的身份证,这就是先描述,然后在当地的档案库或者派出所的数据库里存放着我们每个人的个人信息,这就是再组织。而警察想对某个人进行管理的时候,只需要在数据库进行查找即可,也就是进行增删查改操作,而不需要直接去找到某个人面对面询问交流。
2.4 系统调用和库函数概念
2.4.1 理解系统调用
**操作系统为用户态运行的进程与硬件设备之间进行交互提供了一组接口,这组接口就是所谓的系统调用。**但是操作系统是不相信任何用户的,就像我们可以在Windows系统上安装东西下载东西打开游戏等等,这是操作系统给我们提供的服务,而操作系统不相信用户是因为任何用户都可能(有意或者无意)破坏系统稳定性、安全性或隐私性,例如用户随意对操作系统的内部函数进行修改的话可能会导致系统崩溃等等。
举个日常生活常见的例子就是银行系统。银行为我们提供的服务和操作系统为我们提供服务类似。例如:我们想去银行存取钱,我们就要去取号然后去柜台窗口办理各种手续,而不是直接去金库那里拿钱,我这举个游戏应用的例子,例如在赛博朋克2077中就是使用多线程渲染画面,就是用一些系统函数例如CreateThread()创建进程等等。
系统调用实质上就是函数调用,操作系统例如Linux/Windows/Macos的底层都是C语言写的,而系统函数实质上都是C函数。但系统调用在使用上功能比较基础,对用户的要求更高,所以有的人就会对系统函数进行包装,形成库函数,举个例子就是C语言的printf,底层依赖系统函数的write()。

三、进程的基础认识
3.1 进程的概念
进程 = 内核数据结构对象+自己的代码和数据。
首先我们先来解释一下"自己的代码和数据",在我们用C++或者别的语言编写代码并运行起来时,在我们的磁盘就会形成一个可执行文件,当我们双击该可执行文件(程序)时,这个程序会加载到内存中。
然后是内核数据结构对象(PCB )。在我们刚刚把一个可执行文件加载到内存中的时候,操作系统会为它创建一个内核数据结构对象,而该对象存储的是:代码地址、数据地址、id、优先级、状态等等,还有指向下一个PCB的指针。如下图所示:

注意:这两个加起来才算是进程。
3.2 描述进程-PCB(process control block)
PCB :进程控制块(也就是上面所说的内核数据结构对象),这里我们再对其进行比较详细的描述一下。当我们让一个程序加载到内存中后,操作系统要为刚刚加载到内存的程序创建一个结构体,而这个结构体就是用来记录进程信息的。
task_struct-PCB的一种
在Linux中描述进程的结构体叫做:task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到内存里并包含进程的信息。
task_struct里的内容信息:
因为进程是多个处于内存中的,所以我们需要一些标识符、优先级等等东西来记录,举个例子,当我们打开游戏的时候就会发现其他进程的占用是非常非常小的,因为游戏现在的优先级是最高的。
3.3 通过系统调用获取进程标识符
编写一段代码:
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
sleep(1);
printf("我是一个进程!, 我的pid: %d\n", getpid());
printf("这是父进程的pid:%d\n", getppid());
}
return 0;
}
我们可以通过getpid()获取当前进程的id,getppid可以获取父进程的id,如下图所示:
如果说我们想通过查文件的方式查找进程的信息我们可以在命令行输入:ps ajx | head -1 && ps axj | grep 文件名
3.4 通过系统调用创建进程-fork(初识)
如果我们想创建一个进程,我们可以使用fork()函数。fork()函数通过系统调用创建一个与原来进程几乎完全相同 的进程(可以理解为代码完全相同) 。在调用fork()函数的进程里,该进程会被称为父进程 ,调用fork()函数后生成的进程被称为**子进程,**此时父子进程是共存的,他们一起向下执行代码。
需要注意的是: 调用fork函数之后,父进程和子进程 是同时执行fork函数之后的代码 的,而fork函数之前的代码是由父进程**单独执行。**可能这些有点拗口,这里来个代码演示:
cpp
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 int ret = fork();
8 printf("Hello proc: %d!, ret: %d, parent:%d\n", getpid(), ret, getppid());
9 sleep(1);
10 return 0;
11 }
3.4.1 fork()函数的返回值问题
上面提到了**父进程和子进程是同时执行fork函数之后的代码的,**那么当进程执行的时候,我们怎么判断哪个是父进程,哪个是子进程呢?这就是fork()函数返回值解决的问题。
fork()有三个返回值:
1、在父进程中,fork返回的是创建子进程的ID。
2、在子进程中,fork返回的是0。
3、如果出现错误,fork返回一个负值。
还是刚刚的代码:
cpp
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 int ret = fork();
8 printf("Hello proc: %d!, ret: %d, parent:%d\n", getpid(), ret, getppid());
9 sleep(1);
10 return 0;
11 }
执行结果为:

这里我们可以看到有两个printf,第一个printf是父进程执行的,我们上面提到,父进程调用的fork的返回值是子进程的id,第二个printf是子进程执行的,我们就看到子进程的id和父进程的ret是相同的,验证了这个说法,然后第二个printf的parent就是父进程的id,验证了fork出的进程和主进程是父子关系。
上面讲完了fork返回值有什么,现在来讲一下**"fork函数有两个返回值"**的问题。可能我们会好奇,怎么一个函数会返回两次呢?这里我们以一段代码来开始:
cpp
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 int ret = fork();
8 if(ret < 0)
9 {
10 perror("fork");
11 return 1;
12 }
13 else if(ret == 0)
14 {
15 printf("I am child : %d!, ret: %d\n", getpid(), ret);
16 }
17 else
18 {
19 printf("I am father: %d!, ret: %d\n", getpid(), ret);
20 }
21 sleep(1);
22 return 0;
23 }
程序输出的结果如下:
。
我们看到,if-else判断语句在没有循环语句或者递归的情况下走了两次,这个其实我们在上面就已经提到了,即:"调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。"如果不是从fork函数之后开始运行的话,那么子子孙孙(子进程)无穷尽也了"如下图所示:
END!