Linux系统篇(一):从零入门操作系统:冯诺依曼体系到进程的完整理解 计算机是现代生活与编程学习的基础而操作系统更是连接硬件与软件的核心桥梁。想要真正明白程序如何运行、系统如何工作就需要从底层原理开始了解。本文将带大家循序渐进认识操作系统相关知识理清计算机运行逻辑读懂进程、系统调用等基础概念帮助我们更清晰地理解计算机工作本质。1. 冯・诺依曼体系结构计算机的骨架冯・诺依曼体系是现代计算机的底层逻辑它定义了计算机的五大核心部件和数据流动规则也是我们理解操作系统的基础。核心组成•输入设备键盘、鼠标、网卡、摄像头等负责将数据传入计算机•输出设备显示器、声卡、打印机等负责将数据从计算机传出•存储器内存数据的临时中转站CPU 只能直接与内存交互•运算器负责算术和逻辑运算是CPU 的计算核心•控制器指挥协调各部件工作控制指令的执行流程关键规则•程序必须先加载到内存软件运行的前提是程序从磁盘加载到内存这是体系结构的硬性规定。•CPU 只和内存打交道运算器和控制器仅能直接访问内存外设与内存之间的数据交互由操作系统协调完成。•数据流动的本质是 “拷贝”数据从一个设备到另一个设备本质上是在不同存储介质间的复制过程。性能视角的理解计算机的性能瓶颈本质上由设备的 “带宽” 决定寄存器 高速缓存 内存 存储器 磁盘 / 外设速度从上到下递减成本也随之降低。操作系统的核心工作之一就是通过调度和缓存尽量让数据在高速设备中停留减少低速 IO 的等待。2. 操作系统软硬件之间的 “大管家”操作系统OS是一组管理软硬件资源的程序集合它是硬件的管理者也是上层软件的服务提供者。核心组成•狭义的操作系统内核负责进程管理、内存管理、文件管理、驱动管理四大核心功能•广义的操作系统内核 外壳程序如 shell 系统调用 系统库 第三方工具例如我们现在使用的安卓系统它就是用Linux内核加外壳shell包装得到的设计 OS 的核心目的•对下管理硬件统一管理 CPU、内存、磁盘、外设等资源解决多程序竞争资源的冲突问题•对上提供服务为用户和应用程序提供稳定、安全、易用的接口屏蔽硬件底层的复杂细节•提高资源利用率通过合理调度让 CPU 和 IO 设备尽可能并行工作减少资源闲置1.简单故事介绍我们可以用一个校园故事把校长、辅导员、学生和操作系统的管理逻辑完整串起来清晨的校园里校长操作系统内核坐在办公楼里制定着全校的资源规则与调度秩序手里握着分配教室、图书馆、网络和水电的最终决定权保证学校的一切资源都不会被滥用。辅导员系统服务 / 中间层则守在教学楼接收学生们的各种申请。学生应用进程们带着自己的目标来上课、自习、开班会 —— 他们需要教室CPU 时间、座位内存、黑板显示设备和网络接口外设但谁也不能直接闯入校长办公室只能先向辅导员提交申请。我们可以把这张图的管理逻辑用操作系统的视角串成一个完整的故事校长就像操作系统内核只需要握着第一个学生信息节点的地址就能通过这条单向链表遍历并管理全校所有学生辅导员则像内核里的进程调度模块负责维护这条链表的日常更新—— 新生入学就创建新节点结构体并链入链表学生毕业就摘除节点学生状态变化就更新节点信息而最底层的学生就像一个个应用进程只需专注自身任务所有管理操作都通过辅导员和节点完成内核无需直接接触每个学生就能高效完成对全校资源与秩序的管控。2.总结计算机硬件1.先描述用struct2.再组织起来用链表或其它高效的数据结构3.系统调用和库函数操作系统为了安全将运行空间分为用户态和内核态用户程序无法直接操作硬件必须通过系统调用向内核发起请求。1.通俗理解银行柜台模型用户程序就像银行客户操作系统内核就像银行柜台客户不能直接进入金库操作必须通过柜台系统调用办理业务系统调用是用户态进入内核态的唯一入口也是操作系统提供给用户的服务接口2.系统调用与库函数的关系库函数如 C 标准库是对系统调用的封装为用户提供更易用的接口例如printf()最终会调用write()系统调用将数据输出到标准输出设备库函数运行在用户态系统调用会触发用户态到内核态的切换4. 进程程序运行的动态实体程序是存储在磁盘上的静态文件而进程是程序运行起来后在内存中的动态实体1.进程的核心定义•进程 程序代码 数据 进程控制块PCB•每个进程都有自己独立的地址空间包括代码段、数据段、堆栈段•操作系统通过struct task_structLinux 下的 PCB来描述和管理进程2.进程控制块PCB内容分类•标示符: 描述本进程的唯一标示符用来区别其他进程。•状态: 任务状态退出代码退出信号等。•优先级: 相对于其他进程的优先级。•程序计数器: 程序中即将被执行的下一条指令的地址。•内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。•上下文数据: 进程执行时处理器的寄存器中的数据。•I/O 状态信息: 包括显示的 I/O 请求分配给进程的 IO 设备和被进程使用的文件列表。•记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。•其他信息3.组织进程在Linux内核的源码中可以看到所有运行在系统里的进程都以task_struct双向链表的形式存在内核里进程的关键属性•PID进程 ID操作系统中进程的唯一标识符类似进程的 “身份证号”•PPID父进程 ID创建当前进程的进程 ID类似进程的 “父 ID”•进程状态运行态、就绪态、阻塞态等描述进程当前的运行情况•资源信息进程打开的文件描述符、内存地址、CPU 寄存器状态等struct task_struct { // 1. 进程标识核心字段 pid_t pid; // 进程ID pid_t tgid; // 线程组ID主线程PID 进程ID struct task_struct *parent; // 父进程指针 struct list_head children; // 子进程链表头 uid_t uid; // 真实用户ID gid_t gid; // 真实组ID // 2. 进程状态与调度 volatile long state; // 进程状态核心字段 int prio, static_prio, normal_prio; // 优先级 unsigned int rt_priority; // 实时进程优先级 struct sched_entity se; // 调度实体关联调度类 // 3. 内存管理核心 struct mm_struct *mm; // 用户态地址空间普通进程有效 struct mm_struct *active_mm; // 内核态地址空间所有进程有效 // 4. 文件与IO struct files_struct *files; // 文件描述符表fd数组 struct fs_struct *fs; // 文件系统信息当前目录、根目录 // 5. 信号处理 struct signal_struct *signal; // 信号相关全局信息 struct sighand_struct *sighand; // 信号处理函数集合 sigset_t blocked; // 阻塞的信号集 // 6. 上下文与栈 void *stack; // 进程内核栈指针 struct thread_struct thread; // CPU寄存器上下文切换时保存/恢复 // 7. 时间统计 cputime_t utime, stime; // 用户态耗时、内核态耗时 };4.查看进程在Linux中输入下面指令•ls /proc•top•ps都可查看5.通过系统调用获取进程标识符•1.进程IDPID•2.父进程IDPPID#includestdio.h #includesys/types.h #includeunistd.h int main() { printf(我的进程%d\n, getpid()); printf(我的父进程%d\n, getppid()); return 0; }6.通过系统调用创建一个进程1.创建进程 forkforkfork() 会创建一个新进程返回值有三种情况•返回 小于 0创建失败•返回 0代表当前是子进程•返回 大于 0代表当前是父进程返回值是子进程的 PID代码示例#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() 后面的代码继续往下走。2.杀进程 kill 9 进程标识符3.进程的深入理解#include stdio.h #include sys/types.h #include unistd.h int main() { int ret fork(); if(ret 0) { perror(fork file!\n); return 1; } else if(ret 0) { printf(我是一个子进程\n); } else { printf(我是一个父进程\n); } return 0; }一个变量两个返回值因为有两个进程4.关键特性父子进程的执行顺序fork()之后父子进程谁先执行由操作系统的调度器决定顺序是不确定的父子进程共享代码段但数据段是独立的修改数据不会互相影响5.进程的独立性写时拷贝机制父子进程之间的数据看似共享实际上是通过 ** 写时拷贝Copy-On-Write, COW** 机制实现了真正的独立性。写时拷贝的工作原理•fork()刚创建子进程时父子进程的页表指向同一块物理内存数据段是只读共享的•当父进程或子进程尝试修改数据时操作系统会为修改方复制一份数据副本•复制完成后父子进程各自的页表指向自己的物理内存互不干扰#include stdio.h #include unistd.h #include sys/types.h int main() { int val 100; pid_t pid fork(); if (pid 0) { // 子进程修改val的值 val 200; printf(子进程val %d地址%p\n, val, val); } else { sleep(1); // 等待子进程先执行 printf(父进程val %d地址%p\n, val, val); } return 0; }