1. 冯·诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯·诺依曼体系。

截止目前,我们所认识的计算机,都是由一个个的硬件组件组成。
输入设备:键盘,鼠标,话筒,摄像头,网卡,磁盘等。
输出设备:显示器,磁盘,网卡,打印机等。
存储器:内存(磁盘就是外存)
中央处理器(cpu):运算器+控制器
首先思考几个问题:
1).软件运行,必须先加载到内存中,那么在程序运行之前,是存放在哪里的呢?
答案是存放在磁盘中,因为程序的本质其实就是文件。
2).为什么要加载到内存中呢?
因为是cpu执行我们的代码,访问我们的数据,而cpu获取,写入只能从内存中来进行。
2. 理解数据流动
数据的流动过程:
数据--->输入设备--->内存--->cpu--->内存--->输出设备--->数据
所以数据流动的本质:数据是从一个设备"拷贝"到另外一个设备。
所以体系结构的效率由设备的"拷贝"效率决定。
cpu在数据层面,只和内存打交道,而外设只和内存打交道。

3. 操作系统(Operator System)
3.1 概念
任何计算机系统都包含一个基本的软件集合,成为操作系统(OS),操作系统是一款进行软硬件管理的软件。
操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等)

3.2 设计OS的目的
- 对下,与硬件交互,管理所有的软硬件资源。也可以认为这不是OS的目的,而是一种手段。
- 对上,为用户程序(应用程序)提供一个良好的执行环境。

补充:
-
软硬件体系结构是层状结构,整个计算机世界都是高内聚,低耦合的!
-
操作系统本身不允许未来的用户直接访问内存,直接读取进程,直接访问文件,直接读取驱动,访问操作系统,必须使用系统调用---------其实就是函数,只不过是系统提供的。
-
printf函数的本质:是用户把数据写到了硬件上(显示器)。我们的程序,只要判断出它访问了硬件,那么它必须贯穿整个软硬件体系结构。
-
库可能在底层封装了系统调用。
4. 理解操作系统
4.1 操作系统的核心功能
在整个计算机软硬件构架中,操作系统的定位是:一款纯正的"搞管理"的软件!
4.2 如何理解"管理"
打个比方:
操作系统就相当于学校校长,负责决策
驱动程序就相当于学校辅导员,负责执行
底层硬件就相当于学校学生,负责被管理
所以要管理,管理者和被管理者可以不需要见面,只需要让管理者拿到被管理者的"数据"即可进行管理。

那么如何拿到数据呢?这就由管理者和被管理者的中间层获取了------驱动程序。
建模的过程就是先描述,再组织,理解了这个过程就能够对任何"管理"进行建模,所以也能够理解为什么C++中有类和STL(容器)了,因为类解决的是描述,而STL解决的是组织的问题。这也就是这个世界的特点!!!
4.3 系统调用和库函数概念
操作系统要向上提供对应的服务,但是操作系统不相信任何用户。举个很贴合的例子就是:银行,银行为了给用户提供服务设置了一个个服务窗口。同样的,操作系统为了给用户服务,提供了"系统调用"功能。因为操作系统基本都是C语言写的,所以系统调用功能调用的基本都是C语言函数,而这些函数通常包含了输入参数和返回值,所以操作系统的本质:用户和操作系统之间,进行某种数据交互。

- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
5. 进程
5.1 基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等。
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 当前理解:进程=内核数据结构PCB(task_struct)+自己的程序代码和数据。
5.1.1 描述进程
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是:**task_struct。**进程的所有属性,都可以直接或者间接通过task_struct找到。
- 对进程的管理,变成了对链表的增删查改!
- 我们历史上执行的所有指令、工具、自己的程序运行起来全部都是进程。
5.1.2 task_struct
内容分类
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据休学例子,要加图CPU,寄存器。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
- 具体详细信息后续会介绍
组织进程
可以在内核源代码⾥找到它。所有运⾏在系统⾥的进程都以 task_struct 双链表的形式存在内核 ⾥。

5.2 基本操作
1. 通过系统调用获取进程标识符
可以通过man指令查看这两个命令的使用方法:

bash
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
我们在命令行运行这个程序的输出结果:

可以观察到,每当重新执行程序时,子进程的标识符(PID)都会发生变化,而父进程的标识符(PPID)则保持不变。这是因为在终端环境中,命令行解释器(Shell,如 bash 或 zsh)通常作为父进程启动并管理用户执行的命令 。当你输入命令(例如 ./a.out)时,Shell 会调用 fork()系统调用创建一个子进程来执行该程序,而自身则作为其父进程继续保持运行。因此,尽管每次运行程序都会生成一个新的子进程,其父进程始终是同一个 Shell 进程。
2. 如果想要查看当前系统中正在跑的进程信息,可以使用如下指令:
bash
ps ajx
如果像查看指定的进程(例如名称为myprocess的进程):
bash
ps ajx | head -1;ps axj | grep myprocess
上面的指令也可以写成:
bash
ps ajx | head -1 && ps axj | grep myprocess
指令ps ajx | head -1指的是查看所有信息中的第一条信息,指令 ps ajx | grep myprocess指的是查看所有信息中名称为myprocess的进程,使用;或者&&可以使这两条指令同时进行,输出结果如下:

可以观察到,输出结果中包含了 grep指令自身对应的进程信息。这是因为在执行 grep命令时,其进程名称中也包含了"myprocess"这一关键字,因此被一并列出。如果希望过滤掉 grip自身相关的进程信息,可以使用如下指令实现:
bash
ps ajx | head -1 && ps axj | grep myprocess | grep -v grep
除了上面这种方式之外,进程的信息可以通过 /proc 系统文件夹查看,这个文件夹中的数据都是内存中的数据,和磁盘没有关系:
bash
ls /proc
-如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

图片中展示的均为 proc文件系统下的目录。其中,每一个以数字命名的目录代表一个特定进程的 PID(进程标识符)。这些数字目录中存储的是对应进程的动态属性信息;一旦该进程退出,系统便会自动将其对应的数字目录移除。若想查看特定进程的数字目录,可使用以下指令:
bash
ls /proc/[pid] -dl
3. 如果想要结束一个进程:
- 使用快捷键:ctrl+c
- 使用命令行指令:
bash
kill -9 [对应的进程pid]
4. 在查看 proc目录下某进程对应的数字目录时,我们会发现其中包含该进程可执行程序的路径(位于 exe文件中)。如果删除磁盘上这个路径指向的可执行文件,该进程并不会立即终止,原因在于其执行代码在启动时已被加载到内存中。此时再次查看进程属性,会发现 exe字段所显示的可执行文件信息已被标记为警告状态,表明对应的磁盘文件已不存在。


此外,我们还可以在进程属性中观察到另一个关键信息:cwd,它表示 current working directory,即该进程的当前工作路径。若需在程序运行时更改进程的当前工作路径,可通过系统调用 chdir() 函数来实现。在代码中调用此函数,即可动态指定程序运行时所处的目录位置。
bash
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 int main()
5 {
6 while(1)
7 {
8 chdir("/home/whb");
9 sleep(1);
10 printf("我是一个进程 !,我的pid为: %d\n",getpid());
11 }
12 return 0;
13 }
14
再重新运行该程序然后查看对应的cwb发现已经发生了变化:

5. 通过系统调用创建进程--fork初始
1. 初识 fork
- 运行
man fork:建议通过查阅手册深入了解fork的官方定义与使用说明。

-
fork 的两个返回值 :这是
fork最核心的特性之一。调用一次,却在父子进程中各返回一次。-
父进程 :返回新创建子进程的 PID(进程 ID,大于 0)。
-
子进程 :返回 0。
-
失败 :返回 -1(通常表示资源不足或权限问题)。
-
-
进程间的关系:
-
代码段 :父子进程共享同一份代码(只读)。
-
数据段 :各自开辟独立空间,私有副本(采用写时拷贝机制,即只有在写入操作时才复制数据,以节省内存)。
-
2. 代码实现:基础版与分流版
基础示例(认识 fork)
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
进阶示例(fork 后的分流处理)
- 注意 :
fork之后通常要用if进行分流,区分父子进程的逻辑。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h> // 用于 perror
int main()
{
int ret = fork();
if (ret < 0) {
perror("fork"); // 错误处理
return 1;
}
else if (ret == 0) {
// Child process
printf("I am child : %d, ret: %d\n", getpid(), ret);
}
else {
// Father process
printf("I am father : %d, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
3. 重点注意
-
为什么 fork 会有两个返回值?
- 因为
fork创建了两个 进程(父和子)。在子进程被创建出来的那一刻,它也开始执行代码,所以父子进程都会执行fork()之后的语句,从而各自得到一个返回值。
- 因为
-
这两个返回值是如何分别给父子进程的?
fork内部机制保证了:在父进程 的上下文中,返回值是子进程的 PID ;在子进程 的上下文中,返回值是 0。内核通过这种方式让它们"相认"。
-
一个变量怎么能同时让 if 和 else if 成立?
-
关键陷阱 :代码中只有一个
ret变量,为什么ret == 0和else能分别执行? -
解释 :看似矛盾,实则是因为
fork之后实际上跑在两个不同的内存空间 (虽然物理页可能共享,但逻辑上是独立的)。父进程的ret存的是子进程 PID(非0),子进程的ret存的是 0。它们虽然变量名相同,但实际上是两个独立的执行流,互不干扰。这个问题需要在讲解完进程地址空间(虚拟内存)后才能彻底解释清楚。
-