ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

冯诺伊曼体系结构和操作系统进程的概念

冯诺伊曼体系结构和操作系统进程的概念

冯诺-依曼体系结构

冯·诺伊曼体系结构(von Neumann architecture)是 1945 年冯·诺伊曼在《EDVAC 报告草案》中提出、奠定了现代通用计算机基础的体系结构,核心思想是存储程序(stored-program)。

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

部件作用
运算器 ALU算术/逻辑运算
控制器 CU取指、译码、发控制信号、管理时序
存储器存放指令和数据(统一编址)
输入设备把外部信息送入计算机
输出设备把结果送出
运算器 + 控制器 = CPU,再加上主存,构成“主机”;输入输出设备统称外设。
核心特征
  1. 存储程序:程序(指令)和数据一样以二进制形式存在同一存储器里,可像数据一样被读写、修改——这是与早期“插线板编程”机器的根本区别。
  2. 指令顺序执行:CPU 按 PC(程序计数器)指向的地址逐条取指、译码、执行,默认顺序推进,遇跳转才改变流向。
  3. 指令与数据共用存储器和总线:同一时刻只能要么取指令、要么取数据(总线分时复用)。

工作原理:取指—译码—执行循环
PC → 取指令 → 译码 → 取操作数 → 执行 → 写回 → PC 更新 → 循环
这就是进程时看到的“CPU 一条条执行指令”的底层模型。

主要优点与缺陷

  • 优点:通用性强,程序可存储、可加载、可修改,硬件不必为每个任务重新设计。
  • 冯·诺伊曼瓶颈:CPU 与存储器速度差距大,且指令和数据共享一条总线,取指与取数争带宽,限制吞吐。缓解手段包括 Cache、流水线、多级存储、预取等。
CPU不能直接与输入设备和输出设备连接也是因为CPU和它们的速度差距太大了. 差了大概6次方. 所以引入了储存器, 先将输入设备的要处理的数据加载到内存, CPU处理后将数据输出到内存中等待数据设备的调用.

操作系统

概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  1. 内核(进程管理,内存管理,文件管理,驱动管理)

  2. 其他程序(例如函数库,shell程序等等)

目的

  1. 方便性:提供友好接口,用户无需关心硬件细节。
  2. 有效性(效率):提高资源利用率与系统吞吐,如多道程序、并发、缓冲、虚存。
  3. 可扩充性:结构清晰,便于添加新功能/新硬件。
  4. 开放性:遵循标准,便于互操作。
  5. 更本质的目标:并发、共享、虚拟、异步——让多个任务安全、高效地共用一台机器。

功能

功能内容
进程管理进程创建/撤销、调度、同步互斥、通信、死锁处理。你学的 fork、进程状态、父进程回收都属于这块
内存管理分配回收、地址映射、虚拟内存、分页/分段、页面置换、内存保护
文件管理文件的组织、目录、读写、权限、磁盘空间分配(Linux “一切皆文件”)
设备管理驱动、缓冲、中断处理、设备分配与 I/O 调度
用户接口命令行 shell、图形界面、系统调用(编程接口)
其他保护与安全、网络通信、错误检测与处理

理解管理

相当与先对需要处理的数据, 描述出来(即使用结构体struct或class类,将需要处理的数据储存起来), 再对这些数据进行组织(使用学习的数据结构或STL容器, 将整体的数据组织联系起来)

系统调用和库函数概念

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。

  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

这里就体现了操作系统对于用户是不信任的, 但是又想让用户调用操作系统对硬件进行向下管理, 所以就有了系统调用的接口, 但是由于系统调用的接口对于用户还是太复杂, 就对接口进行适当封装形成了库.

这里就像我们生活中的银行, 我们将钱存入银行, 其实就是将钱放入银行金库. 可实际上我们并没有去银行的金库将钱放入. 这也是银行对我们的不信任.(群众里面有坏人) 所以实际上银行设计了有ATM和人工窗口, 这些接口对钱进行调用. 可是对于年迈的老人来说这些银行的流程太复杂, 所以好的银行大厅就会有一些银行的工作人员对前来办理业务的人进行帮助.(这就相当与封装了系统调用的接口)

进程

一、进程的概念

  • 程序 vs 进程:
    程序是磁盘上的静态可执行文件(代码+数据);
    进程是程序的一次运行实例,是系统进行资源分配和调度的基本单位。同一个程序跑多次就是多个进程。

  • 组成:进程 = 程序代码 + 数据 + PCB。Linux 用task_struct描述 PCB,记录 PID、PPID、状态、优先级、地址空间、打开的文件、信号、 CPU 上下文等。

  • PID 与 PPID:每个进程唯一 PID;

  • PPID 指向创建它的父进程。PID 1 是 systemd(或 init),没有父进程,负责收养孤儿。

  • 地址空间:每个进程有独立的虚拟地址空间(代码段、数据段、堆、栈、共享库),互不干扰。

PCB 在 Linux 里是task_struct
PCB 是概念名,Linux 的具体实现就是struct task_struct(定义在include/linux/sched.h,几千行)。每个进程对应一个task_struct实例,由alloc_task_struct()分配。

task_struct不包含代码和数据的字节内容,它装的是元数据 + 指针:

struct task_struct { pid_t pid, tgid; // 身份 volatile long state; // 状态 R/S/D/Z struct mm_struct *mm; // 地址空间描述(代码/数据在哪) struct files_struct *files; // 打开的文件表 struct fs_struct *fs; // cwd、root struct signal_struct *signal; // 信号处理 struct task_struct *parent, *children, *sibling; // 家族链 struct list_head tasks; // 挂到全局进程链表 ... };

代码和数据实际存放在该进程的虚拟地址空间里(用户态内存),由 mm_struct + 页表描述、映射到物理页。mm_struct 里再指向一串 vm_area_struct(代码段、堆、栈、共享库各一段)。

内核实际是“一组 struct 通过指针连成数据结构”

task_struct 不是孤立的,它同时挂进多种组织:
全局双向链表 tasks(遍历所有进程)
父子链表 children/sibling(pstree 看到的树)
PID 哈希表(按 PID 快速查找)
CFS 调度器的红黑树(按 vruntime 排序)
等待队列等

二、Linux 下查看进程的基本操作

  1. ps—— 静态快照(最基础)
ps # 当前终端的前台进程 ps aux # 所有进程,BSD 风格:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND ps -ef # 所有进程,System V 风格:UID PID PPID C STIME TTY TIME CMD ps -ef | grep nginx # 过滤某程序 ps -u lcb # 指定用户 ps -p 102617 # 指定 PID ps -o pid,ppid,stat,cmd -p 102617 # 自定义列 ps aux --sort=-%cpu # 按 CPU 降序 ps -eLf # 含线程(LWP) ps -ef --forest # 树状显示父子关系
  1. top / htop —— 动态实时
top # 实时刷新;按 P 按CPU排序、M 按内存、k 杀进程、q 退出 htop # 更友好(需安装),支持鼠标、树状、搜索 F3
  1. 按名字查找
pgrep -a nginx # 输出 PID + 命令行 pidof nginx # 只输出 PID pgrep -af "python.*test" # 匹配完整命令行
  1. pstree —— 进程树
pstree -p # 显示 PID,能看清 sshd -> bash -> test 的父子链
  1. /proc —— 内核暴露的进程信息
ls /proc/<PID>/ # 该进程的各类信息 cat /proc/<PID>/status # 状态、UID、内存、线程数 cat /proc/<PID>/cmdline # 启动命令行 ls -l /proc/<PID>/exe # 指向可执行文件 ls -l /proc/<PID>/cwd # 当前工作目录 ls -l /proc/<PID>/fd # 打开的文件描述符
  1. 作业控制(当前 shell 内的进程)
jobs # 查看后台/暂停作业 ./test & # 后台运行 Ctrl+Z # 暂停到后台 bg %1 / fg %1 # 转后台继续 / 调回前台
  1. 终止与调整
kill <PID> # 发 SIGTERM(15),礼貌请求退出 kill -9 <PID> # SIGKILL,强杀(不可捕获) kill -l # 列出所有信号 pkill -f nginx # 按名字/命令行杀 renice -n 5 -p <PID> # 调整优先级
  1. 其他常用
who / w # 当前登录用户及终端 lsof -p <PID> # 进程打开的文件/端口 ss -tlnp # 端口与对应进程

fork()创建子进程

通过上面的知识我们了解到了程序没一次运行就会创建一个进程, 那么我们能不能在程序运行时创建一个进程呢? 这时我们就要用到fork()了.

我们在Linux下运行这个段代码

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){intret=fork();if(ret<0){perror("fork");return1;}elseif(ret==0){//childprintf("I am child : %d!, ret: %d\n",getpid(),ret);}else{//fatherprintf("I am father : %d!, ret: %d\n",getpid(),ret);}sleep(1);return0;}

如果以我们学习C语言的理解看到if-else语句,肯定只会有进入其中一个代码段执行, 但我们运行后发现,ret == 0和ret > 0这两个代码段的代码都执行了.

1.为什么fork给父子返回各自的不同返回值? 2.为什么一个函数会返回两次??

本质:子进程是父进程在 fork 时刻的“克隆”
fork()由内核的copy_process()完成:分配新的task_struct,复制父进程的地址空间、文件表、信号设置等,然后让新进程加入调度队列。子进程从 fork() 返回处开始执行(返回值 0),不会重走 fork 之前的代码。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进