🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》《算法入门》
⛺️技术的杠杆,撬动整个世界!
1.冯诺依曼体系结构
输入设备:键盘,鼠标,话筒,摄像头...网卡,磁盘(磁盘是外存)
输出设备:显示器,磁盘,网卡,打印机...
所有输入输出设备都是外设
将读写的操作我们称为IO:input/output(站在内存角度)
CPU:运算器+控制器
存储器:内存
问题一:软件运行的时候必须先加载到内存吗?
程序运行之前,软件是在磁盘中作为一个文件存储,CPU获取,写入,只能从内存中来进行,CPU执行我们的代码,访问我们的数据,加载到内存就是让数据从一个设备"拷贝"到另一个设备,体系结构的效率是由设备的拷贝效率决定的。
CPU在数据层面,只和内存打交道,外设和内存打交道。
冯诺依曼体系架构包含4个关键组件:
存储器:用于存储指令(程序代码)和数据;
控制器:(CPU核心组件):负责从存储器中取出指令,解析指令并协调其他组件工作;
运算器(CPU核心组件):执行指令中的运算逻辑(如加减,逻辑判断等);
输入输出设备:负责与外部交互(磁盘读写,键盘输入,屏幕输出)。
如何理解数据流动: (场景A给B用微信发信息)
A:
把微信的软件(可执行程序)加载到内存中的存储器,然后键盘输入信息,将信息从输入设备流动到存储器,把数据经过运算器控制,为了保证数据安全,加密,在运算器中将输入的信息转换为一堆乱码结构,由CPU计算完成之后写回内存,然后再通过微信将数据打到我们自己体系结构的输出设备,网卡(输出设备);
B:
然后这个信息通过网络,传输到B的计算机的输入设备里面,如果B打开了微信,此时微信就在计算机的内存(存储器)中,这条信息通过输入设备(网卡)传输到存储器中,运算器解析加密的信息,然后通过输出设备(显示器)显示到B的计算机微信界面。
编辑好的文件没有发出去,是先存储在磁盘上,当我们要发送文件到QQ平台,本质是将文件拖拽到QQ平台,也即是搬到内存中,然后执行QQ文件的加密封装写回存储器,然后通过输出设备网卡,上传到网络,网络传输,这个文件再通过对方冯诺依曼体系的网卡(输入设备),输入设备将文件读取到存储器,通过内存中的运算器将文件解析解密最后显示到对方的输出设备(磁盘),最后显示到对方的磁盘。
所以这个流程是:通过冯诺依曼体系结构将本地磁盘的文件拷贝到对方的磁盘。
2.操作系统
概念:一个基本的程序集合,称为操作系统(OS),操作系统是一款进行软硬件管理的软件
操作系统包括:
- 内核:进程管理,内存管理,文件管理,驱动管理;
- 其他程序:比如函数库,shell程序等
操作系统的设计目的:
对下:与硬件交互,管理所有的软硬件资源;
对上:为用户程序(应用程序)提供一个良好的执行环境。
- 软硬件体系结构:
(层状)高内聚(相同逻辑的代码在同一地区)低耦合。 - 访问操作系统,必须使用系统调用---就是函数,但是是系统提供的
- 我们的程序,只要判断出来它访问了硬件,那么它必须贯穿了整个软件硬件体系结构!
- 库可能在底层封装了系统调用。
理解操作系统
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的"搞管理"的软件。
怎么理解管理:
操作系统通过驱动程序管理底层硬件;
管理者和被管理者,怎么管理?
(操作系统)根据数据进行管理,由中间者(驱动程序)拿到这个数据(底层硬件的数据)
先描述再组织!
操作系统把网卡硬盘,显示器,把每个底层硬件都封装到一个类里面,包含硬件的名称,硬件的状态,硬件相关的各种信息,在操作系统中构建一个struct device的类,然后每一个设备都要对应一个struct device对象,所以操作系统管理硬件转化为了对硬件信息的增删查改。
操作系统对进程怎么管理的?
操作系统对每一个进程定义struct结构体对象,然后把进程相关的各种属性都放在这个结构体里面,里面要添加属性就要用链接节点全部链接起来,把对进程的管理转换为对链表节点的增删查改。
怎么理解系统调用?
用户 / 程序无法直接访问操作系统内核 (内核是受保护的核心层,直接访问风险高、成本大),必须通过 "中间层"+"系统调用接口" 间接与内核交互 ------ 这是操作系统的安全与易用性设计。
操作系统为不同场景的用户(普通用户、开发者)提供了不同的 "中间层",最终都通过系统调用接口访问内核:
1. 普通用户(命令行操作)
操作路径:用户输入指令 → Shell(外壳程序) → 系统调用接口 → 操作系统内核
- 用户 :输入命令(比如
lsmkdirrm)。 - Shell(如 Bash) :是用户与内核的 "命令行交互程序"(属于用户态程序),负责解析用户指令,并调用对应的系统调用。
- 系统调用接口 :内核对外暴露的 "官方入口",Shell 通过它让内核执行具体功能(比如
ls对应 "读取目录" 的系统调用)。
2. 开发者(编写程序)
操作路径:开发者写代码调用库函数 → 库函数(部分) → 系统调用接口 → 操作系统内核
- 开发者 :在代码中调用库函数 (比如 C 语言的
printfopenfork)。 - 库函数(如 C 标准库):是开发者的 "工具包",分两种情况:
- 部分库函数封装了系统调用 (比如
printf底层调用write系统调用,实现向屏幕输出); - 部分库函数是纯用户态逻辑 (比如
strcpy字符串复制,不需要内核参与)!
操作系统要向上提供对应的服务;操作系统不相信任何用户或人!
我们接触到的所有Linux/windows/macos都是用C语言写的,有C语言一定有C函数,函数有输出参数和返回值,这个输出参数就是用户输出到操作系统,返回值就是操作系统返还给用户,用户和操作系统之间进行某种数据交互。
在开发角度,操作系统对外会表现为一个整体,但是会暴露出来自己的部分接口,供上层开发者使用,这部分由操作系统提供的接口叫做系统调用;
系统调用在使用上,功能比较基础,对用户的要求来说也相对比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而有了库,就很有利于更上层用户或者开发者进行二次开发。
3.进程
什么是进程?
来比较系统地理解:
当我们创建一个可执行程序(文件),这个文件最开始是存储在我们的磁盘上的,cmd(
cmd.exe是 Windows 操作系统内置的可执行程序 (后缀为.exe),作用是接收用户输入的文本命令,解析并调用对应的系统功能 / 程序,是用户与 Windows 内核交互的 "命令行入口"。)
打开这个可执行程序,在内存中启动这个可执行程序,于此同时操作系统也在内存中,可执行程序cmd的代码和数据都存储在内存中,由CPU进行处理运作,但是我们有一些可执行程序打开之后关闭,就不能继续在CPU中占用空间,这里就涉及到一个进程问题,CPU对所有在内存中的可执行程序进行管理;
首先要创建一个结构体(PCB),这个结构体包含所有可执行程序的属性,如下:
struct XXX(所有操作系统的统称叫做PCB)
{
代码地址
数据地址
id
优先级
状态
...
struct xxx* next;//创建指针用于指向下一个数据
}
然后无数个正在运行的可执行程序由这个结构体进行封装并且通过指针next链接起来,形成一个链表结构,这个链表结构就是进程列表。所以操作系统要管理所有的进程列表,本质上也就是对这个PCB链表的增删查改。
在操作系统内会有这样的进程列表,其中的每一个结构体内部都封装了代码和数据。
所以进程=内核数据结构对象+自己的代码和数据
补充:CPU和操作系统的关系:
CPU 和操作系统是 "硬件核心" 与 "软件管理者" 的依存关系 ------ 操作系统通过控制 CPU 执行指令,让硬件发挥作用;CPU 则是操作系统的 "执行载体",没有 CPU,操作系统无法运行。
所有操作系统的结构体我们叫做PCB,这个结构体就叫做进程控制块,在Linux操作系统下就是struct task_struct,进程的所有属性,都可以直接或者间接通过task_struct找到。
所以进程=PCB(task_struct)+自己的代码和数据
再来理解:
当在磁盘中打开一个可执行程序cmd,这时候会将该可执行程序加载到内存中,读取到该程序的代码和数据,于此同时,操作系统会创建一个PCB进程控制块用来封装这个可执行程序的代码和数据,在CPU中运行的时候,CPU是拿取到这个PCB进程块,而不是直接拿取这个可执行程序的代码和数据,当程序运行结束之后我们关闭程序,同时CPU将该信息报到操作系统里面,操作系统将对应的该程序的PCB节点进行删除,同时删除其代码和数据,这就是关闭程序==(本质上就是操作系统对内部的PCB进度控制块链表的删除)。==
接下来写一个程序来理解:
[keda@VM-0-4-centos lesson12]$ vim myprocess.c
[keda@VM-0-4-centos lesson12]$ make
gcc -o myprocess myprocess.c
[keda@VM-0-4-centos lesson12]$ ll
total 20
-rw-rw-r-- 1 keda keda 75 Nov 17 20:11 Makefile
-rwxrwxr-x 1 keda keda 8464 Nov 17 20:21 myprocess
-rw-rw-r-- 1 keda keda 165 Nov 17 20:21 myprocess.c
[keda@VM-0-4-centos lesson12]$ ./myprocess
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
I am a process! ,my pid : 2944
^C
[keda@VM-0-4-centos lesson12]$ cat myprocess.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
sleep(1);
printf("I am a process! ,my pid : %d\n", getpid());
}
}
my pid用来查看进程,如果我们在进程运行的过程中删除掉这个可执行的程序,会发现这个进程还是在运行,为什么?
因为这个可执行程序删除了,意味着在磁盘上删除了,但是这个程序在运行的时候,是拷贝了一份在内存中,它的代码和数据已经完全被拷贝到内存中,开始运行的时候内存分配了PCB进程控制块,并且在CPU中运行。所以目前是不影响这个可执行程序的进程。但是后续会出现问题。
进程在运行过程中会生成进程对应的可执行exe文件,如果在文件编辑过程中写fopen("/a/b/c/d.txt","w"),进程会记录下自己的当前路径,然后会有cwd,cwd后面跟目前的文件存储路径。
研究父进程:
可以看到子进程的数字在不断变化,但是父进程的数字始终不变,这是为什么?

现在我们在另一行查一下父进程:
[keda@VM-0-4-centos lesson12]$ ps ajx | head -1 && ps axj | grep 21115 | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
20974 20976 20976 20976 pts/0 21115 Ss 0 0:00 -bash
20976 21114 21114 20976 pts/0 21115 S 0 0:00 su keda
21114 21115 21115 20976 pts/0 21115 S+ 1001 0:00 bash
这里我们查看父进程:
bash是一个命令行解释器,本质是一个进程,
-bash意思是远程登陆的。
OS会给每一个登陆用户,分配一个bash,所以我们执行的所有命令ls,pwd,mkdir,touch...这些都是bash
用代码创建子进程的方式:
[keda@VM-0-4-centos lesson12]$ vim myprocess.c
[keda@VM-0-4-centos lesson12]$ make
gcc -o myprocess myprocess.c
[keda@VM-0-4-centos lesson12]$ ./myprocess
父进程开始运行:pid :23747
进程开始运行:pid :23747
进程开始运行:pid :23748
[keda@VM-0-4-centos lesson12]$ cat myprocess.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("父进程开始运行:pid :%d\n", getpid());
fork();
printf("进程开始运行:pid :%d\n", getpid());
}
从这里我们可以看到,父进程运行结束之后,子进程运行的时候同时打印出两份,一份是父进程的信息,另外一份是子进程自己的信息。
所以子进程没有自己的代码和数据,用的只是父进程的代码和数据。
这里我们就有一个疑问了:子进程没有自己的代码和数据,用的只是父进程的代码和数据,那为啥还会有父子进程的概念?
答:子进程并非完全 "共用" 父进程的代码和数据,而是在创建时通过 "复制" 获得独立副本(或共享只读部分),父子进程是完全独立的执行实体 ------ 这正是 "父子" 概念存在的核心原因。在没有程序新加载的前提下。