【Linux】七.进程概念篇一《操作系统核心概念一篇通:冯诺依曼体系、OS 设计与进程管理》

【Linux】七.进程概念篇一《操作系统核心概念一篇通:冯诺依曼体系、OS 设计与进程管理》 一.冯诺依曼体系结构1.概念我们常⻅的计算机如笔记本。还有不常⻅的计算机如服务器⼤部分都遵守冯诺依曼体系。目前我们所认识的计算机都是由⼀个个的硬件组件组成输⼊单元包括键盘, ⿏标扫描仪, 写板等中央处理器(CPU)含有运算器和控制器等输出单元显⽰器打印机等1什么是冯诺依曼体系结构冯·诺依曼体系结构由运算器、控制器、存储器、输入设备、输出设备五部分组成。其核心思想是存储程序将程序指令和数据用二进制形式存放在同一个存储器中计算机自动从存储器中逐条取出指令并执行。2在冯诺依曼体系下计算机是如何工作的输入设备输入数据数据线先进入存储器因为存储器本身没有计算的能力这时候CPU会通过一些方式读取存储器的数据通过运算器和控制器对数据进行处理然后将数据通过某种方式返回到存储器最后输出设备再读取存储器当中的信息分析这张图存储器的层次结构中越往上速度越快外设最慢 主存其次 高速缓存 CPU寄存器可以看到CPU 离寄存器最近离高速缓存也很近主存存储器次之所以 CPU 间接从主存中访问数据效率更高。而让 CPU 直接访问外设输入/输出设备肯定效率不高因为 CPU 特别快但输入输出设备运行速度特别慢所以导致效率低。总结的话就是当一个快的设备和一个慢的设备在一起同时合作运行的时候肯定按照慢设备的工作效率为主也就类似于木桶效应木桶效应寄存器是长板CPU运算速度极快大部分指令在1个时钟周期内就能完成。只要数据已经在寄存器里CPU就能飞速处理。这是个“长板”。内存是短板如果数据不在寄存器里必须去内存里取。内存访问延迟大约是100-300个时钟周期。此时CPU只能空转等待Stall。这个漫长的等待时间就是“短板”它决定了这条指令最终完成的总时间。结论一个程序的执行速度不取决于CPU运算有多快寄存器多而取决于它等待从内存拿数据等了多久内存延迟这个短板。3在软件中运行可执行程序文件运行时必须加载到内存里里面这是为什么呢软件运行前中的可执行程序文件在磁盘里因为冯诺依曼的体系结构规定可执行程序时二进制程序cpu获取并执行这些指令必须先将磁盘中的可执行程序加载到内存里面CPU才能执行访问这些命令2.硬件层面的数据流动4在互联网的世界中在软件上面的数据的传输如何进行呢解释在 QQ 上发送消息数据的流动过程电脑联网后我用键盘敲下要发送的消息“在吗”此时输入设备是键盘键盘将该消息写入到内存中CPU 间接从内存中读取到消息。对其进行运算处理后再写回内存此时输出设备网卡从内存中读取消息并经过网络发送到对方的网卡同时输出设备显示器从内存中读取消息并刷新出来显示在我的电脑上。 我朋友的电脑的输入设备是网卡接收到消息后网卡将该消息写入到内存中CPU 间接从内存中读取到消息对其进行运算处理后再写回内存此时输出设备显示器从内存中读取消息并刷新出来显示在我朋友的电脑上。3.关于冯诺依曼知识点强调这⾥的存储器指的是内存不考虑缓存情况这⾥的CPU能且只能对内存进⾏读写不能访问外设(输⼊或输出设备在数据层面上CPU 不和外设输入/输出设备打交道外设只和存储器打交道。可以将存储器理解为是 CPU 和所有外设的缓存而在硬件层面上外设是可以直接给 CPU 发中断的外设(输⼊或输出设备)要输⼊或者输出数据也只能写⼊内存或者从内存中读取。也就是说所有设备都只能直接和内存打交道。4.CPU的工作原理CPU里只有“有电”和“没电”高电平 有电 1低电平 没电 0指令、数字、地址……统统是“有电/没电”的组合计算就是让电“流”过一堆门电从入口流进去经过一堆门与门、或门……从出口流出来流出来的电是什么样由门和流进去的电决定没有“算”的动作就是电在走记住东西就是“把电关住”触发器像个“电的捕鼠夹”时钟边沿来时把输入端的电平“夹住”输出端一直输出被夹住的那个电平直到下次再夹4.CPU工作的五个阶段取指令IFinstruction fetch即将一条指令从主存储器中取到指令寄存器用于暂存当前正在执行的指令的过程。程序计数器中的数值用来指示当前指令在主存中的位置。当 一条指令被取出后程序计数器PC、用于存放下一条指令所在单元的地址的地方中的数值将根据指令字长度自动递增。指令译码阶段IDinstruction decode取出指令后指令译码器按照预定的指令格式对取回的指令进行拆分和解释识别区分出不同的指令类 别以及各种获取操作数的方法。现代CISC处理器会将拆分已提高并行率和效率。执行指令阶段EXexecute具体实现指令的功能。CPU 的不同部分被连接起来以执行所需的操作。访存取数阶段MEMmemory根据指令需要访问主存、读取操作数CPU 得到操作数在主存中的地址并从主存中读取该操作数用于运算。部分指令不需要访问主存则可以跳过该阶段。结果写回阶段WBwrite back作为最后一个阶段结果写回阶段把执行指令阶段的运行结果数据 “写回” 到某种存储形式。结果数据一般会被写到 CPU 的内部寄存器中以便被后续的指令快速地存取许多指令还会改变程序状态字寄存器中标志位的状态这些标志位标识着不同的操作结果可被用来影响程序的动作。备注引用课本定义二.操作系统Operator system)赋予计算机哲学的美誉2.1 概念任何计算机系统都包含⼀个基本的程序集合称为操作系统(OS)。笼统的理解操作系统包括•内核进程管理内存管理⽂件管理驱动管理•其他程序例如函数库shell程序等等强调它是一款软硬件管理的软件广义上的操作系统2-2 设计OS的目的首先回答几个问题问题一1操作系统是什么是一款管理软硬件资源管理的软件问题二2为什么会存在操作系统设计操作系统的目的是啥原因方便用户使用减少用户的使用计算成本目的1对下与硬件交互管理所有的软硬件资源2)对上为⽤⼾程序应⽤程序提供⼀个良好的执⾏环境.2-3 核心功能• 在整个计算机软硬件架构中操作系统的定位是⼀款纯正的“搞管理”的软件2-4 如何理解 管理想想既然操作系统是一款搞管理的软件那他是如何进行管理的究竟管理的是什么呢在这个世界管理主要分三种人决策者执行者被管理者人主要做两件事决策和执行• 管理的例⼦ - 学⽣辅导员校⻓他们三人有不同的身份在平常我们会发现管理者和被管理者是很少见面二者是不直接打交道的。那问题来了二者不直接打交道也不见面那这个命令是怎样执行的校长又是怎么知道学生的情况的呢答案是学生的个人信息在学校的系统当中校长可以调用系统说明你是这个学校的学生同时也方便管理学生比如 24级计科专业有 90 名学生我们想要给其中特定的几名优秀学生发奖学金那是否需要校长跑到该专业学生的宿舍里面挨个询问同学们的各科成绩和学分绩点是多少呢肯定不是的当他想要做发奖学金这个决策时他只需要通过学校的教务系统抽取 24 级计科专业 90 名学生的名单按照学分绩点来进行排名在排名后再根据其它的一些要求综合数据来做出一个决策给前几名同学发奖学金。当校长做完决策后通知计科专业的辅导员过来让他开个表彰大会奖励下这几名优秀同学。辅导员说“好的校长。”此时辅导员就开始做执行这个决策。这就是一个简单的管理过程管理学生就要抽取信息这个抽取信息的过程就叫做“描述学生”。因为操作系统是用c语言写的那么c语言是如何管理学生的是用struct结构体比方管理学校几万人就要有几万个结构体每个结构体变量都保存着学生的信息。// 描述学生 struct student { char name[10]; //名字 char sex; //性别 int age; //年龄 double score; //分数 char addr[100]; //家庭住址 // ... };假如我们想找出成绩最好的同学只需要将其每个同学的成绩拿出来进行比较即可。但如果每个结构体变量之间没有任何关联的话是不方便进行管理的也很难快速找到成绩最好的同学。这个时候就需要将这些结构体变量链接起来就可以在 struct 中包含一些指针信息将所有的结构体变量链接起来此时就形成了一个双链表。校长进行管理学生只要双链表的头指针就行。如果校长想要重新奖章某位学生只需要遍历双链表再将该学生所属的节点从双链表中查找即可假设有新生报到只需要将该学生所属节点插入到双链表中即可。所以校长并不是单独对一个人进行管理的而是将学生的个人信息组织起来对双链表数据结构进行管理。通过双链表对学生进行增删查改的操作也就是所谓的管理学生总结对学生管理就是先描述再组织管理的本质就是对数据结构进行操作描述起来用 struct 结构体。组织起来用双链表或其他高效的数据结构不同的数据结构决定了不同的增删查改的特征和效率也决定了不同的组织和管理方式类比在计算机中校长通常指的是操作系统辅导员可以称为驱动板块学生可以称为软硬件版板块。操作系统不会直接和硬件比如磁盘网卡鼠标底层硬件打交道而是通过驱动程序和硬件打交道那问题来了操作系统怎么去管理硬件呢先描述再组织。所以操作系统先要描述底层的硬件然后形成符合被管理者的数据结构对底层硬件的管理最后变成了对数据结构的管理。举例操作系统要管理磁盘那得要有一个描述硬盘的 struct 结构体就是先描述一个事物通常描述是事物的属性比如磁盘的大小、磁盘的型号等等假如操作系统卸载一个硬件并不是要把这个硬件从电脑中拆走而是直接把这个硬件对应的描述信息给删除掉。所以操作系统为了管理好被管理对象在系统内部运用了大量的数据结构。2-5计算机的层次结构1底层硬件冯诺依曼体系结构。2驱动程序操作系统中默认会有一部分驱动.如果有新外设就需要单独安装驱动程序该驱动程序会通过某种方式将该硬件的信息上报给操作系统告诉操作系统多了这个硬件。驱动程序更多是一种执行者的角色3操作系统最重要的四个功能进程管理、内存管理、文件管理、驱动管理。4系统调用接口操作系统是不信任何用户的任何对硬件或者系统软件的访问都必须通过操作系统的手好比银行是不信任任何用户的但还是得给用户提供服务用户想要取钱存钱都必须经过银行的手所以用户对操作系统中资源的访问都必须调用对应的系统接口。比如在 Linux 中执行命令或运行一个 C 程序就要用到C函数底层都用到了系统接口。系统调用接口本质是操作系统为了方便用户使用操作系统中的资源给用户提供的一些调用接口。但是系统调用接口用起来也不是特别方便。所以一般我们会在系统调用接口上再封装一层比如shell 外壳lib库部分指令这些的底层一般都是封装的系统调用接口。不断的封装的目的也是为了让用户用起来更简单。比如安装 C/C 环境时系统会默认带上 C/C 标准库这些库提供给用户的接口是一样的但是底层可能不一样在 Windows 中调用的就是 Windows 的系统接口在 Linux 中调用的就是 Linux 的系统接口。5用户操作接口底层大都是封装的系统调用接口。2-6库函数和系统调用库函数语言或者第三方库给我们提供的接口。实际上我们使用的函数底层一般就两种情况要么调用了系统接口比如 printf/scanf/vector/list/deque等要么没有调用系统接口比如自己写的 sum 函数自己写的循环,判断语句等。系统调用操作系统提供的接口本质就是用户和操作系统之间进行数据的交互系统调用被封装成库函数判断是否发生系统调用不能等同于是否访问硬件因为很多系统调用只在内核里操作数据结构不涉及硬件。在开发的角度操作系统对外会表现成一个整体但还是会暴露自己的部分接口供上层开发使用这部分由操作系统提供的接口叫做系统调用。系统调用在使用上功能比较基础对用户的要求相对也比较高所以有心的开发者可以对部分系统调用进行适度封装从而形成库有了库就很有利于更上层用户或者开发者进行二次开发。理解就是操作系统是整体黑盒管理硬件系统调用是接口暴露出来的、基础的、门槛高的功能库是封装把基础功能包装得更好用方便上层开发三.进程Process)这个是window任务管理器当中的进程与其意思相同问题操作系统如何进行进程的管理答案先描述再组织操作系统可以一次运行好多个程序吗答案是可以。运行的程序有很多所以OS要将这些程序管理起来这些正在运行的程序称为进程如何管理进程呢答案是先描述再管理操作系统为了方便管理会创建一个描述和控制该进程的结构体这个结构体被称为PCB进程控制块这里面包含了进程的所有信息通过这个PCB就可以找到里面的代码和数据信息在Linux操作系统中struct_task结构体就是进程控制块描述号所有信息之后接着用PCB将所有信息组织起来因为PCB当中有指针可以通过指针将各个结构体链接起来也就是双链表的头指针就可以查找完成PCB所有结构体OS把对进程的管理转换为对数据结构中PCB的管理也就是双链表的增删查改。在磁盘里有我的code.c文件当运行./code操作系统就会发生以上的流程磁盘加载到内存当中为它创建相应的进程申请PCB这里的PCB指向数据和代码然后code.c文件指向code这里面我们要存PCB的原因是OS要对进程进行管理目前对进程的理解进程 代码 数据 与该进程对应的数据结构PCB。3-1 基本概念与基本操作.概念在Linux操作系统当中每次程序的⼀个执⾏实例正在执⾏的程序等同时每一个进程的执行都配上一个ID一般咱们在写的时候用的PID进程号内核观点担当分配系统资源CPU时间内存的实体3-2 描述进程-PCB基本概念进程信息被放在⼀个叫做进程控制块的数据结构中可以理解为进程属性的集合。课本上称之为PCBprocess control block Linux 操作系统下的 PCB 是: task_structtask_struct-PCB的⼀种在 Linux 中描述进程的结构体叫做 task_struct 它是进程实体的一部分是操作系统中最重要的记录性数据结构它是进程管理和控制的最重要的数据结构。每一个进程均有自己的PCB在创建进程时必须建立 PCB伴随进程的生命周期直到进程终止时PCB 将被删除。task_struct 是 Linux 内核的⼀种数据结构类型它会被装载到RAM(内存)⾥并且包含着进程的信息。补充小知识人类认识事物是通过事物的属性而计算机是通过进程的属性去描述和认识进程。然而计算机通过数据结构把要描述的属性组织起来交给操作系统方便管理所以操作系统中充斥着大量的数据结构也就印证了为什么说操作系统是计算机的哲学这个含义3-3 task_ structPCB如何管理进程答案是进程的属性进行管理内容分类进程属性标⽰符: 描述本进程的唯⼀标⽰符⽤来区别其他进程。状态: 任务状态退出代码退出信号等。优先级:相对于其他进程的优先级。程序计数器: 程序中即将被执⾏的下⼀条指令的地址。内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下⽂数据: 进程执⾏时处理器的寄存器中的数据[休学例⼦要加图CPU寄存器]。I/O状态信息: 包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。记账信息: 可能包括处理器时间总和使⽤的时钟数总和时间限制记账号等。理解几个重点概念优先级比如大家去食堂吃饭排队情况就相当于确定了优先级那插队就代表改变优先级程序计数器程序计数器PCProgram Counter是一个CPU内部的寄存器它永远存着下一条要执行的指令在内存中的地址。进程在运行实际上是 CPU 在执行该进程的代码那 CPU 如何得知应该取进程中的哪行指令呢在 CPU 中有一个寄存器叫做EIP这个寄存器通常被称为PC 指针保存着当前正在执行指令的下一条指令的地址。如果说后续不想运行了可以把这个EIP保存在PCB方便后续恢复访问。内存指针CPU 只认识 PCB不认识程序代码和数据。可以通过PCB 结构体中中的内存指针可以帮我们找到该进程对应的代码和数据。上下文数据:CPU寄存器中的数据称为进程的硬件上下文记账信息:操作系统给这个进程记的考勤表 绩效表。干了多少活、等了多久、犯了几次错全记在PCB里。一般默认操作系统去执行默认信息假如有两个优先级相同的进程但一个进程运行50s,一个进程运行10s那下次运行一般运行短的进程注意PCB里记着这个进程干了多少活记账和有没有插队权优先级。操作系统靠这两样东西来公平高效地调度。可以在内核源代码⾥找到它。所有运⾏在系统⾥的进程都以task_struct 双链表的形式存在内核⾥。3-5查看进程1.进程信息可以使⽤top和ps这些⽤⼾级⼯具来获取命令ps ajxa显⽰⼀个终端所有的进程包括其他⽤⼾的进程。x显⽰没有控制终端的进程例如后台运⾏的守护进程。j显⽰进程归属的进程组ID、会话ID、⽗进程ID以及与作业控制相关的信息u以⽤⼾为中⼼的格式显⽰进程信息提供进程的详细信息如⽤⼾、CPU和内存使⽤情况等ps ajx | head - l ps ajx | grep myprocess其中 ps ajx | head - 1 是把ps ajx 输出的信息中的第一行信息属性列出。一般咱们查看信息的话搭配管道使用使用 top 命令实时显示进程process的动态。通过/proc系统文件目录查看。cwd - /home/ubuntu/code/lesson4 前终端的进程运行的目录exe - /usr/bin/bash bash 程序的绝对路径CentOS 里直接用 ll Ubuntu 里需要手动配置别名不然只能用 ls -l这里的echo $$是直接显示我们用户正在运行的进程ID因为ubuntu管理严格有些权限普通用户不可执行就要用sudo提权但centos部分指令默认普通用户也可以执行ps aux | grep sshd 列出系统里所有进程 只留下名字带sshd的进程注意2908666 是 grep sshd 命令的临时进程执行完瞬间就退出了不管在CentOS还是Ubuntu这个目录都会消失报同样的错。所以问题来了既然临时的不能显示那如何找长期的方法一看图方法二1.号进程 systemd 所有Linux系统的根进程sudo ls -l /proc/1这是系统第一个进程开机就启动关机才退出是Linux里最稳的PID2. sshd 主进程远程服务永远活着精准提取sshd主进程PID过滤掉临时进程ps aux | grep sshd | grep -v grep | head -1第一行的PID比如你的 852 就是sshd主进程开机就运行永远不退出查它的信息sudo ls -l /proc/8523. 你自己的 bash 进程普通用户长期用的终端不关闭就永远活着直接拿到当前bash的PIDecho $$直接查不用sudo提权ll /proc/$$只要你不关闭这个终端窗口这个PID就永远存在4. 其他系统级长期服务随便选用这个命令一键列出所有系统级长期进程ps aux | grep root | grep Ss | head -10这些 输出的全是root用户要提权运行的、后台休眠的系统服务全是长期进程随便选一个PID查就行查询1号进程不是临时的长期存在的如果是临时的在我们输入ID的时候就已经文件不存在了3-6系统调用获取进程标示符子进程 IDPID父进程 IDPPID#include stdio.h #include sys/types.h #include unistd.h int main() { printf(pid: %d\n, getpid()); printf(ppid: %d\n, getppid()); return 0; }我圈的数就是运行的结果pit_d是无符号的整数补充shell 是对所有外壳程序的统称而 bash 是某一个具体的 shell命令行解释器。bash 也是许多 Linux 发行版的默认 shell。在执行命令的时候一般情况下往往不是由 bash 来解释和执行而是由 bash 创建子进程让子进程去执行。运⾏ man fork 认识forkfork有两个返回值⽗⼦进程代码共享数据各⾃开辟空间私有⼀份采⽤写时拷⻉复制进程创建一个新的进程#include stdio.h #include sys/types.h // getpid, getppid #include unistd.h // getpid, getppid, fork, sleep int main() { printf(I am a father: %u\n, getpid()); fork(); while(1) { printf(I am a process, pid: %u, ppid: %u\n, getpid(), getppid()); sleep(1); } return 0; }fork 的两种理解站在开发者的角度父子进程共享用户代码代码是只读的不可写而用户数据各自私有一份为了不让进程互相干扰因为二者具有独立性的采用写时拷贝技术。就好比打开 Windows 的任务管理器可以看到有很多进程假如我把微信进程关掉会不会影响到 QQ抖音QQ音乐 进程呢答案是不会总结操作系统中所有进程是互相独立的。为了不让进程互相干扰。注意fork 之后子进程会被创建成功然后父子进程都会继续运行但谁先运行是不确定的由系统调度优先级决定。站在操作系统内核的角度fork 之后站在操作系统的角度系统多了一个进程是的。fork 创建子进程通常以父进程为模板其中子进程默认使用的是父进程的代码和数据写时拷贝。既然多了一个进程OS 就会为子进程创建新的 PCB并把父进程 PCB 中的部分内容拷贝过来。fork 的用法fork 在使用的时候通常要用 if 进行分流让父子进程执行不同的代码实现一个并行的效果。比如父进程播放音乐子进程下载文件通过 fork 的两个返回值来进行分流如果 fork 执行成功在父进程中返回子进程的 pid在子进程中返回 0。如果 fork 执行失败在父进程中返回 -1不创建子进程并适当地设置 errno。#include stdio.h #include sys/types.h // getpid, getppid #include unistd.h // getpid, getppid, fork int main() { printf(我是父进程: %u\n, getpid()); pid_t ret fork(); if (ret 0) { //子进程 while (1) { printf(子进程, pid:%u, ppid:%u\n, getpid(), getppid()); sleep(1); } } else if (ret 0) { //父进程 while (1) { printf(父进程, pid:%u, ppid:%u\n, getpid(), getppid()); sleep(1); } } else { perror(fork); return 1; } return 0; }理解fork的返回值fork 为什么会有两个返回值1 fork()是一个函数它的return ret;是用户代码属于代码段2 fork()在执行return之前子进程已经被创建完成并且放入了调度队列3 父子进程在fork()之后共享代码段只读4 所以return ret;这行代码的机器指令父子进程都能看到、都能执行5 父进程执行return ret; → 返回子进程的PID6 子进程被调度后也从fork()函数返回执行同一行return ret; → 返回0如果 fork 执行成功为什么在父进程中返回子进程的 pid在子进程中返回的是 0 呢每个小孩只有一个亲生父亲而父亲可以有多个孩子。所以儿子找父亲是特别简单的是唯只有一个的而父亲为了更好的找孩子需要给每个孩子起个名字并且记住他。所以在父进程中需要返回子进程的 pid因为得让父进程知道自己的子进程是谁。如何创建多个子进程#include stdio.h #include unistd.h #include sys/wait.h int main() { int i; int n 3; // 想创建3个子进程 for (i 0; i n; i) { pid_t pid fork(); if (pid 0) { perror(fork); return 1; } else if (pid 0) { // 子进程 printf(我是子进程 %d, PID%d, 父进程%d\n, i1, getpid(), getppid()); return 0; // 子进程执行完就退出不再继续fork } // 父进程继续循环 } // 父进程等待所有子进程结束 for (i 0; i n; i) { wait(NULL); } printf(父进程结束, PID%d\n, getpid()); return 0; }结果演示我是子进程 1, PID1234, 父进程1233 我是子进程 2, PID1235, 父进程1233 我是子进程 3, PID1236, 父进程1233 父进程结束, PID1233