目录
前言
本节主要学习以下内容:
- 认识冯诺依曼系统
- 操作系统概念与定位
- 深入理解进程概念,了解PCB学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害
- 了解进程调度, Linux进程优先级,理解进程竞争性与独立性,理解并行与并发
- 理解环境变量,熟悉常见环境变量及相关指令, getenv/setenv函数
- 理解C内存空间分配规律,了解进程内存映像和应用程序区别, 认识地址空间
一、冯诺依曼体系结构
1.1 冯诺依曼体系结构是什么
冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
冯诺依曼体系结构如图:
- 输入设备:键盘、摄像头、话筒、磁盘、网卡...
- 输出设备:显示器、音响、磁盘、网卡...
- 存储器:内存
- 中央处理器(CPU)由运算器和控制器组成
- 运算器:集成于 CPU,用于实现数据加工处理等功能的部件。
- 控制器:集成于 CPU,用于控制着整个 CPU 的工作
1.2 冯诺依曼体系结构为什么这么设计
1.2.1 思考
有没有想过冯诺依曼体系结构为什么不这么设计?而是设计多了一个储存器,也就是内存?
想要了解清楚,请往下阅读!
1.2.2 了解一下计算机的存储分级
在此之前先了解一下计算机的存储分级,其中寄存器离 CPU 最近;L1、L2、L3 是对应的三级缓存;主存通常指的是内存;本地存储(硬盘)和远程储存通常指的是外设。
储存器的特点:离 CPU 更近的,存储容量更小、速度更快、成本更高,如主存往上的;离 CPU 更远的,则相反,如本地磁盘。如上图,它们呈现如金字塔形状。
1.2.3 解释
我们对木桶原理都有了解,要去衡量木桶能装多少水,并不是由最高的木片决定的,而是由最短的木片决定的。
而我们的中央处理器CPU 就是如木桶原理一样。
假设 CPU 直接访问磁盘,也就是如下图,那么它的效率可太低了。假设这里 CPU 是速率是纳秒级别的,则磁盘是速率毫秒级别的,这两个之间的速率就相差了 100 万倍,(1s=10^3ms(毫秒)=10^6μs(微秒)=10^9ns(纳秒)),当一个快的设备和一个慢的设备一起协同时,最终的效率肯定是以慢的设备为主。所以硬盘的存在严重拖慢了 CPU 的速率,整个计算机体系的效率就一定会被拖累。
所以,这时候就需要有一个介质来联通 CPU 和 硬盘之间的通道,这个介质就是冯诺依曼体系结构中的存储器,也就是我们的内存。这下理解冯诺依曼体系结构为什么这么设计了吧。
有了内存的存在,计算机的整体效率就会大大的提高。依旧假设这里 CPU 是速率是纳秒级别的,则磁盘是速率毫秒级别的(1s=10^3ms(毫秒)=10^6μs(微秒)=10^9ns(纳秒),CPU 与内存的速率就只是相差了 1000 倍左右,内存与硬盘之间的速率也是差了 1000 倍左右,CPU 只与内存进行交互,不与外设(磁盘之类)进行交互,这里计算机的短板就不再是磁盘了,所以计算机的整体速率就会大大提升。
----------------我是分割线---------------
那有人又问了,既然这个存储器(内存)这么好,为什么不把计算机的磁盘内存全部换成内存或者是寄存器?
首先明确内存是一直需要通电的,没有电的供应它里面存储的所有数据都会丢失,而像磁盘之类的没电了数据依旧存储在磁盘里面,放上个十年也没有事。第二,要明白内存是要钱的,越靠近 CPU 的内存价格越昂贵。假设把计算机的磁盘内存全部换成内存或寄存器,从技术角度可以的,但是生产出来的计算机就会特别昂贵十几万起步那种,这种计算机的价格普通人接受不了,这种产品就不会进行大量的传播。要知道:凡是被广泛传播的产品,它的价格一定是便宜的,质量是可以的,价格普通人可以接受。就好比几千万的车,你身边的人为什么不开呢?原因就是它价格昂贵,普通人接受不了,那么这种产品就不会被大范围的传播。
而冯诺依曼的这种体系结构通过添加一块内存,几乎可以达到和你全部换成内存的计算机速率一致,还能以较低的成本生产出来,这样的产品就会被世界范围内的人所接受。换句话说,就是因为它便宜,你才愿意用它、买得起它,冯诺依曼体系结构就是这个道理。
1.3 往下要明确几点
- CPU 读取数据(数据+代码),都是要从内存读取。站在数据的角度我们认为 CPU 不和外设直接进行交互。
- CPU 要处理数据,要先将外设的数据加载到内存。站在数据的角度,外设直接和内存打交道。
- 我们把数据搬到内存的过程叫做 input。CPU 完成数据的运算,不能直接把计算结果直接打到外设上,而是把计算结果刷新到内存中,再有内存把数据刷新到外设中。内存把数据刷新到外设中这个过程叫做 outputoutput
- 我们将数据输入再到输出这个过程就叫做 IO
- 程序要运行,必须要加载到内存中。程序就是文件,而文件就在磁盘中。
- 那为什么程序要运行必须要加载到内存中呢?因为冯诺依曼体系结构的特点决定的!!
这里暂且简单认识,后面会详细讲。
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道
1.4 冯诺依曼实例
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。
从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?(注意这里的计算机都遵循冯 • 诺依曼体系结构,且这里不谈网络,不考虑细节,只谈数据流向)
(1)假设你给qq朋友发送 hello,你是发送数据的一端,请解释数据是如何流向的?(不考虑细节,只谈数据流向)
解释:你此时从键盘上输入 “hello”,此时 “hello” 会回显到你的显示器上(不考虑细节)此时“hello” 会储存在寄存器(内存)中,然后内存传给 CPU ,经过 CPU 处理写回你的内存里,然后由内存刷新到外设(这里不在是磁盘,而是网卡),然后上传到网络上,经过网络你朋友的设备中的输入设备(网卡)先识别到这条消息,传到内存,内存再传给 CPU 计算,计算完成写回内存里,再由内存刷新到你朋友的输出设备(这里是显示屏),到这一条数据的流向就完成了。
(2)发送的是文件呢?
文件依旧是数据,与上面类似,不在解释。计算机的数据流向基本都是这样的,进行类比即可理解
(3)数据为什么这么流向?
是由你的硬件和冯诺依曼体系结构决定的
----------------我是分割线---------------
二、操作系统(Operating System)
2.1操作系统概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库, shell程序等等)
2.2 为什么要有操作系统
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
总的来说就一句话:对下管理好所有的软硬件,对上给用户提供一个稳定高效的运行环境。
2.3 计算机体系及操作系统的定位
计算机体系:
操作系统的定位:
在整个计算机软硬件架构中,操作系统的定位是: 一款纯正的“搞管理”的软件
那问题来了,什么是“管理”呢,如何理解“管理”?
----------------我是分割线---------------
2.4 如何理解 "管理"
2.4.1 学校例子(用于理解概念)
现实中我们做事情无非是 a、做决策 b、做执行。比如你想吃一碗粉,首先你进行做决策去哪家餐馆,做了决定后你走向你想去的餐馆,走这个过程就是做执行。
假设在学校里一般有这三种角色(进行了简化)
- 校长(管理者)
- 辅导员(执行者)
- 学生(被管理者)
这里还要明白一点:管理者和被管理者可以不直接沟通。比如,你的大学校长(管理者)没有和你直接见面,却可以随时管理你。
比如拿奖学金与否、挂科与否,表现如何,学生信息如何,校长都可以随时管理。比如说评选奖学金,校长在系统中筛选好某系某级综合成绩排名前 3 的学生来发奖学金,这时校长把 3 位同学的辅导员叫过来,并要求开一个表彰大会来奖励 3 位同学,然后辅导员就开始着手执行工作。再比如,校长要开除一名学生,则校长只要知道这个学生挂了多少科,有没有达到学校的标准,没达到标准就可以开除他了,开除就找辅导员执行这项工作。
校长为什么能管理你,为什么连你的面都没见过就能管理你,因为你的个人信息在学校的系统里面,也就是说校长管理学生的本质是通过 “ 数据 ” 来进行管理的。
这说明:拿到被管理者的核心数据,来进行支持管理决策,这才是核心。
- 这里的校长(管理员)——就相当于操作系统
- 辅导员(执行者)——相当于驱动程序
- 学生(被管理者)——相当于软硬件
上述说明:管理是对管理对象的数据的管理
----------------我是分割线---------------
理解了上面,那又有一个问题来了,校长是如何拿到数据并对它进行管理呢 ?
假设只有一个学生,这个学生产生的各种数据,校长都可以随便对这些进行管理。
那如果有5万学生呢,这些学生每天产生的数据全部扔给校长,校长又怎么管理呢?直接对数据管理明显就是不可能,这种无序的数据是没有意义的。校长就定义一个结构体,这个结构体包含了一名学生的所有信息,有多少学生就定义这个结构体数组有多大。
创建这个结构体就是对被管理者进行描述,再根据描述类型定义对象,可以把对象组织成数组,那么对学生数据的管理工作就变成了对数组的增删查改。核心就是:先描述,再组织(先描述就是用C语言用结构体进行封装,C++的类就是一切皆对象)
如果把数组改成链表再组织起来,对学生的管理就转化成了对链表的增删查改, 如果把数组改成二叉树,红黑树...等等各种数据结构再组织起来,就变成了对某种数据结构的管理。
管理的核心思路就是: 先描述,再组织(简称六字真言)
同理,操作系统对软硬件的管理, 就变成了对数据结构的管理,反过来也说明操作系统内部一定存在大量的数据结构和算法。
这里我解释有点绕,水平有限,hhh...
----------------我是分割线---------------
2.4.2 银行例子(类比系统)
假设有一个银行系统,类比如上。
银行工作的时候,整个封闭的银行就放出了几个柜台,供业务工作使用,要办理业务的人来了就到柜台去办理。那为什么银行给你提供服务是以柜台窗口的形式进行?为什么柜台前还有一块很厚的玻璃?答案是:不相信你呗。这也说明银行这套体系预先把你当坏人,把所有人都预先当做坏人,有这种保护措施同时也将银行的风险降到了最低,还能给银行提供了一个稳定安全的服务。
进行类比,如果计算机把操作系统中的各种东西(内存管理,文件系统,进程管理,驱动管理...等等)暴露给用户,就好比银行门户大开,工作人员没有玻璃阻隔跟你面对面交流,银行的钱直接就放到桌面上,上层的人(客户)想拿钱就随时拿,这就给银行带来很大危险(随时遭受抢劫..)。这说明不把操作系统暴露给用户就是为了保证操作系统的安全性,操作系统也把所有人当做坏人。用户就直接去柜台办理。类比,计算机用户也需要办理业务,操作系统也需要给用户提供各种服务,提供的服务也是通过柜台来办理,这里的柜台就是各种系统接口(system call)
这里的各种系统接口就是操作系统提供的各种函数。
银行用户有需求想办理业务,银行提供各种服务,就如LInux是用C语言写的,系统接口的本质:就是用C语言提供的函数(操作系统相关的,不是我们C语言的库函数)。我们把这种系统接口(system call)就叫做系统调用。
到此结束,总结一下:
- 操作系统不信任任何人
- 对外提供服务是以接口的形式提供服务
- 只能使用系统接口访问操作系统,其他任何方式都不行
直接使用操作系统的接口成本会比较高,所有在操作系统之上还提供了一个服务层,这个服务层就好比 shell外壳、图形化界面,这个服务层用于方便小白用户使用操作系统。操作系统之上还提供了一个第三方库,第三方库就是对系统接口的封装,如C库、C++库...,第三方库方便开发人员使用。
2.5 系统调用和库函数概念(总结)
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发
----------------我是分割线---------------
三、进程(process)
3.1 基本概念
在Windows下我们查看进程在任务管理器中
- 我们自己启动一个软件,本质上就是启动了一个进程。
- 在 Linux 下,执行一个命令,运行一个程序 ./xxx ,其实就是在系统层面上创建了一个进程
- 一个被加载到内存中的程序,就已经不能叫做程序了,是叫一个进程
操作系统里面是存在大量进程的,也可以同时加载多个进程,Linux 也是如此。那存在这些大量的进程要不要被管理呢?答案是必须的
Linux 是如何管理大量的进程的呢?
答案是六字真言:先描述,再组织
程序加载到了内存,操作系统该怎么调度呢?操作系统怎么知道哪个执行完了,怎么知道哪个的优先级需要调整,怎么知道哪个执行到一半需要切换?操作系统完全不知道。
操作系统目前没有能力管理这些进程,因为这些加载进来的程序只有代码和数据,没有任何描述自身结构,所以操作系统需要对进程管理就需要对进程先描述,再组织管理。
先描述:操作系统为了管理进程,对加载到内存的每一个程序,也就是对每一个进程申请了一个PCB的结构体,(PCB下面讲),每一个PCB结构体都保存了每一个进程的所有属性
什么是属性呢?
C++中的类一切皆对象,C语言的 struct 封装,C/C++对一个事物的封装一定包含了该事物的所有属性。
我们人认识世界,是通过“属性”来认识的,比如我对一个事物进行描述:有一个事物他会汪汪叫,会帮人们看家护院,这时我们潜意识就会想到狗,是不是呢?只要是被人认识的东西,就一定会被人抽象换成各种属性,然后就能用计算机语言描述出来。所以PCB这个结构体一定包含了进程所有的属性
那问题又来了,属性是数据吗?
答案是属性也是数据。
属性和程序内的代码和数据有关系吗?
其实它是两套概念,曾经在Linux 指令中讲过:文件 = 内容+属性,可执行程序本质就是一个文件,把可执行程序加载到内存中本质只是加载了可执行程序的内容,加载到内存里就叫进程而不再叫程序了,对进程管理还需要大量的数据结构,大量的数据结构叫做PCB结构,用PCB结构来描述进程所有的属性,其中PCB结构的属性和进程所有的内容是没有太大关系的,这个PCB结构体的属性包括:这个进程在
哪里,是谁启动的,什么时间启动的,有没有唯一的标识,有没有唯一的ID,优先级是什么....这就是进程的属性
因为PCB结构,一个个PCB结构体连接起来,所以对进程的管理到最后,变成了对PCB结构体链表的增删查改 。
每加载进来一个程序,也就是每增加一个进程,都会添加一个新的PCB结构体
到最后解释一下,什么是进程呢?
进程 = 对应的代码和数组 + 进程对应的PCB结构
为什么存在PCB结构体?
操作系统要管理进程,要管理进程就要:先描述,再组织
什么是PCB结构体?
没有谈,下面解释
----------------我是分割线---------------
3.2 描述进程 - PCB
什么是PCB?
课本上称之为PCB(process control block),PCB就是一个结构体,不同的操作系统中PCB的名字叫法不同。
在 Linux 操作系统下的PCB结构体叫做是: task_struct
task_struct 跟PCB的关系就跟王婆和媒婆的关系一样,王婆是一个具体的媒婆,task_struct 也是一个具体的PCB结构体
3.2.1 task_struct - PCB的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
3.2.2 task_ struct内容分类
这里的简单了解,先有个概念,后面详细讲
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
3.3 组织进程
可以在内核源代码里找到它。Linux 所有运行在系统里的进程都以 task_struct 链表的形式存在内核里,这个链表是以双链表呈现
----------------我是分割线---------------
3.4 查看进程
默认查看自己终端的进程
ps
查看系统所有的进程
ps axj
这里我写了个死循环,方便观察进程
intmain() { while(1) { printf("hello Linux\n"); sleep(2); } return0; }
运行程序
查看我们想要查看的程序
1. ps axj | grep '查看进程名' 2. //例如: 3. ps axj | grep 'mytest'
把头部标签带上
ps axj | head -1 && ps axj | grep 'mytest'
其中,PID就是进程ID(process ID),其他后面解释。
那有人好奇了,你说 10983 是 mytest 的进程ID,那下面的13696 进程又是什么?
别忘了你执行了 grep 命令,一个命令的执行也是一个进程,所以 13696 进程是 grep 命令的进程ID
如果结束 mytest 这个进程,当然你查就查不到啦
进程的信息可以通过 /proc 系统文件夹查看,把所有进程以文件系统的形式展示
ls /proc
查看单个进程详细信息
1. ls /proc/[进程ID] -al 2. //例如 3. ls /proc/[20190] -al
其中的 cwd 为当前工作目录(每个进程的都会有一个属性,来保存自己所在的工作路径),exe 则是该进程的可执行程序所处的位置
查看单个进程简略信息(所有属性),上面是详细的,这里简略
ls /proc/进程ID
要查看更多 ps 选项用法,直接 man 就行了
man ps
----------------我是分割线---------------
3.5 通过系统调用获取进程标示符
3.5.1 查看进程ID(PID)
通过系统调用获取 PID 标示符为:getpid
man查看一下详细信息,其中的头文件与C语言无关,pid_t 是无符号整数,相当于用于定义一个变量的类型,直接使用它即可(如:定义一个变量id,pit_t id = 0)
修改代码,通过系统调用查看pid
intmain() { while(1) { printf("pid: %d\n", getpid()); printf("hello Linux\n"); sleep(2); } return0; }
运行一下,可以查看PID
进行比对,命令行与系统调用的PID一样
除了在自己的终端关闭进程,在别的终端也可以杀掉进程,命令 kill,-9为发送九号信号(暂且不讲,后面学)
1. kill -9 [PID] 2. //例如 3. kill -9 18530
回车后,就发现右边终端的进程被杀掉了
每个进程都会有自己独立的 PID,不会出现重复的PID
----------------我是分割线---------------
3.5.2 查看父进程ID(PPID)
进程是有父子关系的,PID 的父进程叫做 PPID
通过系统调用获取父进程ID标示符为:getppid
同样 man查看一下详细信息,其中的头文件与C语言无关,pid_t 是无符号整数,直接使用它即可
修改代码,通过系统调用查看ppid
进行比对,一致
父进程PPID永远为 bash,子进程PID都为bash 创建的 。类比:父进程相当于王婆,子进程相当于王婆手下的一个实习生
比如,把自己终端的 bash (PPID)杀掉,自己所处的终端就不能执行命令了,每个终端都有自己的 bash,为shell 外壳程序创建
这里我在自己的终端杀掉这个bash终端它直接强制退出了,我就无法演示了。如果在另一个终端杀掉自己终端的bash,之后自己终端所有命令都无法执行。
四、通过系统调用创建进程 - fork 初识
4.1 创建子进程 - fork
man 查看一下 fork 函数,头文件是<unistd.h>,fork的作用是创建一个子进程(child process)
fork 的返回值有两个
1、创建子进程失败返回 -1
2、成功:a.给父进程返回子进程的PID b.给子进程返回 0
先记住 fork 有两个返回值
----------------我是分割线---------------
4.2 测试 fork
测试代码
intmain() { printf("I am parent process\n"); fork(); printf("you can see me\n"); sleep(2); return0; }
把 fork 注释掉,运行结果,只执行一次 printf
不注释 fork ,运行结果, printf 被执行了两次
打印了两次,这是为什么?它只有一条语句,可是在 fork 之后被执行了两次,这是怎么回事?
其实是 fork 函数的原因,fore 给进程创建了一个子进程,fork 之后的代码是父子共享的,父进程和子进程各执行一次打印
再进行测试
intmain() { printf("I am parent process\n"); pid_tret=fork(); printf("ret: %d\n", ret); sleep(1); return0; }
结果打印了两次,一个变量怎么可能是两个值?
实是创建子进程成功:a.给父进程返回子进程的PID b.给子进程返回 0(其他后面解释)
在以前学C/C++的时候,我们没见过两个死循环同时执行,也没见过 if 和 else 同时执行
测试代码
intmain() { pid_tid=fork(); if(id<0) { //创建子进程失败perror("fork"); } elseif(id==0) { //child process(task)while(1) { printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); } } else { while(1) { //parent processprintf("I am parent, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); } } return0; }
fork 之后有两个不同的执行流 ,两个进程的返回值不同从而决定了执行不同条件的代码,子进程的ppid 是父进程的 PID
这里的现象仅仅是因为使用 fork 产生的特殊情况
----------------我是分割线---------------
4.3 一个感性的问题
fork 为什么给子进程返回 0,给父进程返回子进程的PID?
这里没有官方的解释,这里只给出这个问题的一个感性认识。
一个子进程永远只有一个父进程,但父进程可以拥有多个子进程。比如,一个孩子只有一个父亲,而父亲可以有多个孩子。
进程多了就要有进程的标识符,没有事不行的。就好比一个父亲他有三个孩子,父亲想叫其中的一个孩子,得叫孩子的名字吧,不叫孩子怎么知道叫哪一个孩子,总不能说:孩子,你过来一下。这样叫哪知道是哪一个,同比进程也是如此,得有一个认得出你的标识符。给子进程返回 0,给父进程返回子进程的PID就是类似情况
----------------我是分割线---------------
4.4 为什么会有两个返回值
创建子进程也新建了一个 task_struct 结构体,这个 task_struct 以父进程的task_struct 为模范创建,但又不完全相同。
CPU 想要运行一个进程,要经过复杂的数据结构和算法,形成一个运行队列,从运行队列里面选择进程执行。
CPU 运行一个进程,本质是从 task_struct 形成的队列中挑选一个task_struct 来执行它的代码。
一个函数已经准备 return,核心代码已经执行完了。那也说明子进程也已经加载到运行队列里面了,CPU可以随时调度,父进程与子进程已经同时存在运行队列里面了,父进程执行完成子进程也差不多完成了,父子各自执行自己的返回值,父进程和子进程执行完当然就有两个返回值了。
返回两次不代表一个变量会记录两个值
----------------我是分割线---------------
父子进程被创建出来,哪一个程序先运行?
答案不一定,都有可能,由操作系统调度器决定
到这里只 fork 的基本概念就差不多完成了
----------------我是分割线---------------
文章先到这,下篇即将更新