本文通过冯诺依曼体系结构(硬件部分)和操作系统(软件部分)为基础来介绍我们应该如何理解进程,为后续的学习做铺垫。
计算机系统的摘要
一、预备知识
1.建立冯诺依曼体系模型
截至目前,我们所认识的计算机,都是由一个个的硬件组件组成。
- 输入单元:包括键盘,鼠标,扫描仪等
- 中央处理器:CPU含有运算器和控制器等
- 输出单元:显示器,打印机等
而这些硬件不管是集成在主板上的各种电路还是拔插式的CPU和外设等都是按照冯诺依曼体系进行走线构造的 。生活中,我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
这里我们重点讲解冯诺依曼体系结构中内存的江湖地位
关于冯诺依曼,必须强调几点:
这里的存储器指的是内存
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
一句话,所有设备都只能直接和内存打交道。
在硬件层面上:内存起到适配作用。由于输入输出处理的速度和CPU处理的速度不一致,内存可以起到过渡缓冲作用,消除速度差,从而实现在硬件利用上达到一种性价比非常高的体系结构
在数据层面上:内存起到数据预加载作用。我们知道输入输出这些外围设备的数据处理速度是比CPU慢很多的,我们还知道磁盘是擅长存储数据的,CPU是擅长处理数据的,如果让磁盘和CPU直接沟通,磁盘传输速度慢,每次只能传输一小部分数据,就势必会浪费的CPU擅长处理数据的优点,所以为了计算机各硬件的最优性,冯诺依曼体系引入了内存,内存的数据传输速度高于磁盘,可以一次性传输大量数据,这样CPU进行数据处理就可以直接从内存读取,大大提高了处理效率。
为什么写的代码要加载到内存?因为这是体系结构的规定。
图示:
那问题来了:你凭什么保证存储器能预加载外设中的大部分数据呢?外设里那么多数据动不动几十个G甚至一个T而内存才几个G你怎么选择哪部分数据要预加载?内存不够了怎么办?凭什么先执行A程序不执行B程序?怎么管理内存分配的?等等问题的答案就是:操作系统会处理这些各种问题(局部原理)
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
理解数据是如何流向的
假如你和你的朋友,QQ发送了一句 在吗? 整个信息是如何在体系结构中流动的?
简单说从打字开始从你的数据外设输入设备流入内存(存储器)进入CPU处理流出输出设备,进入云服务器相似过程处理数据流入你朋友家的计算机冯诺依曼体系结构中处理最终实现显示屏上打印信息
2.理解操作系统
操作系统 — 一款进行软硬件资源管理的软件
管理的本质:先描述(利用struct结构体保存各种数据信息),再组织(增删查改等操作)
请参考这篇文章:如何理解操作系统
二、理解进程
我们看完预备知识,知道了操作系统管理的本质是:先描述,再组织
操作系统会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫作系统调用
1.描述进程 - PCB
首先我们应知道程序的本质其实就是文件,二进制文件,这些文件保存在磁盘中,而当一个程序文件被加载到内存中,我们就称之为进程。
所以我们以前的任何启动并运行程序的行为-- 都是由操作系统帮助我们将程序转换为进程–完成特定的任务。简单点说,就是当你打开一个程序或者一个app,都会转换成一个进程。
在我们的windows任务管理器中就能看到很多正在运行和后台运行的进程,对于这么多的进程,操作系统是一定要进行管理的,那么操作系统是怎么管理的呢?首先我们先看看操作系统怎么描述进程:
我们知道文件=内容+属性。而进程与此类似,操作系统同样为进程赋予了属性信息。
操作系统将进程信息放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct(PCB的一种)
我们这里以linux系统下的PCB来讲解。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
进程是什么:加载到内存的代码和数据和内核关于进程的相关数据结构
这里的task_struct (PCB)就是内核关于进程的相关数据结构,用来描述进程的相关信息
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
2.组织进程
什么是 进程 =内核关于进程的相关数据结构(进程控制块PCB) + 当前进程的代码和数据(磁盘中读取)
我们知道管理的本质是对数据进行管理,管理的逻辑是先描述,再组织,在Linux中,操作系统会通过task_struct结构体将每一个进程的所有属性抽象化描述起来,Linux操作系统再通过双向循环链表的数据结构将数量庞大的进程进行组织,这样的话,管理进程就变成了对进程所对应的PCB进行相关的管理。
我们可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
进程的数据被存放在一个叫做进程控制块的数据结构当中,进程控制块又可以称之为PCB(process control block),进程控制块中包含进程标识符、上下文数据、进程调度信息、进程控制信息、IO状态信息以及进程对应的磁盘代码等,Linux操作系统中进程控制块其实就是struct task_struct结构体,windows操作系统中进程控制块其实就是执行体进程块(struct _EPROCESS )
3.查看进程
一、ls指令
ls /proc/进程的pid
在linux中进程的信息可以通过 /proc 系统文件夹查看,记住进程也可以是一个目录,linux一切皆文件。
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹
二、ps指令
由于ps命令能够支持的系统类型相当的多,所以选项多的离谱!我们这只列举常用的
示例:
ps axo pid,comm,pcpu # 查看进程的PID、名称以及CPU 占用率 ps aux | sort -rnk 4 # 按内存资源的使用量对进程进行排序 ps aux | sort -nk 3 # 按 CPU 资源的使用量对进程进行排序 ps -A # 显示所有进程信息 ps -u root # 显示指定用户信息 ps -efL # 查看线程数 ps -e -o "%C : %p :%z : %a"|sort -k5 -nr # 查看进程并按内存使用大小排列 ps -ef # 显示所有进程信息,连同命令行 ps -ef | grep ssh # ps 与grep 常用组合用法,查找特定进程 ps -C nginx # 通过名字或命令搜索进程 ps aux --sort=-pcpu,+pmem # CPU或者内存进行排序,-降序,+升序 ps -f --forest -C nginx # 用树的风格显示进程的层次关系 ps -o pid,uname,comm -C nginx # 显示一个父进程的子进程 ps -e -o pid,uname=USERNAME,pcpu=CPU_USAGE,pmem,comm # 重定义标签 ps -e -o pid,comm,etime # 显示进程运行的时间 ps -aux | grep named # 查看named进程详细信息 ps -o command -p 91730 | sed -n 2p # 通过进程id获取服务名称
三、top命令
top命令 可以实时动态地查看系统的整体运行情况,是一个综合了多方信息监测系统性能和运行信息的实用工具。通过top命令所提供的互动式界面,用热键可以管理。类似于windows任务管理器。
top(选项)
示例:
top - 09:44:56 up 16 days, 21:23, 1 user, load average: 9.59, 4.75, 1.92 Tasks: 145 total, 2 running, 143 sleeping, 0 stopped, 0 zombie Cpu(s): 99.8%us, 0.1%sy, 0.0%ni, 0.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 4147888k total, 2493092k used, 1654796k free, 158188k buffers Swap: 5144568k total, 56k used, 5144512k free, 2013180k cached
常用
ps axj : 查看所有进程
ps axj | grep XXX: 查看某进程
Ctrl C : 终止进程
4.通过系统调用观察进程
- 获取进程标示符:getpid() 当前进程PID getppid() 父进程PID
- fork()(创建进程)
1 1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main() 6 { 7 // 创建子进程 -- fork是一个函数 -- 函数执行前:只有一个父进程(bash派生的)-- 函数执行后:就会有父进程和父进程创建的子进程。 8 fork(); 9 10 printf("我是一个进程!,我的进程id是%d,我的父进程pid是:%d\n",getpid(),getppid()); 11 sleep(3); 12 13 return 0; 14 }
注意:
- fork 有两个返回值,fork 之后通常要用 if 进行分流
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
我们可以通过一段代码进行观察:
结果:
由程序运行结果可以看到,fork之后,会有父进程和子进程两个进程在执行后续的代码,并且后续的代码被父子进程共享,我们可以通过返回值的不同,让两个进程执行后续共享代码的不同部分。
我们也可以通过这样的手段来让两个进程执行不同的任务,这就是所谓的并发式的编程。
问题1:为什么每次启动pid不一样?
因为操作系统分配id值 ,一般是线性递增分配 考虑到其他进程在分配 所有每次不一样
问题2: 为什么父进程(pid)没有变化 ?
父进程的父进程是bash 命令行解释器,本身本质也是一个进程 所有进程的父进程都是bash