前言
计算机体系结构
冯诺依曼体系结构
冯.诺依曼在《第一份草案》文档中描述了自己心中的计算机,并由此确立了计算机结构的五大部件: 运算器、控制器、存储器、输入设备、输出设备;
现在看来,运算器和控制器单元集成在CPU中实现,存储器的容量不断扩大、输入输出设备不断更新,这些部件构成了当代计算机硬件系统的基本组成。
关于如今的冯诺依曼体系:存储器指的是内存,在不考虑缓存的情况下:CPU只能在内存中进行读写操作,不能直接和输入输出设备交换信息;
输入输出设备进行读取和写入信息也只能在内存中进行;
也就是说:所有设备都只能直接和内存打交道。
操作系统(Operator System)
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
设计OS的目的
与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境
定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
进程
进程,什么是进程? 让我们从生活中的例子出发来理解它:
不管我们是在手机上还是电脑上,如果们想要打开QQ,当我们点击软件图标时就会给操作系统(OS)发送相应的运行指令,
OS会将QQ的代码和数据从磁盘加载到内存,我想会有朋友听说过:程序必须加载到内存中才能运行,
这句活是很正确的,不过少了一部分:加载到内存中并且被OS管理起来之后一个程序才变成了进程,才能够真正运行起来。
举个栗子:腾讯是如何确定张三是不是自己的员工的? 是只要张三进过腾讯的办公大楼就是吗,是只要张三住过腾讯分配的员工宿舍就是吗?
都不是,或者说不全是这样,只有当张三的个人档案保存在腾讯的员工档案库时,当可以在档案库中查到他这个人时他才算是腾讯员工,
而那些不在档案库中的,以及曾经在、而如今已经辞职离开的 个人档案已经销毁掉的人,他们都不算是腾讯的员工。操作系统也是如此:只有此时正在被OS所管理的程序才算是进程。
PCB(Process Control Bluck)
那么OS又是如何对进程进行管理的呢? PCB – 进程控制块
我们知道,Linux是用C语言写的,那么当我们需要描述一个人的姓名、性别、年龄、身高等等各种属性是就需要用到结构体(struct),
在Linux中PCB是一个 task_struct结构体,结构体里保存了拉取到内存中的程序的各种属性信息。
(注1:一个进程对应一个task_struct)
(注2:task_struct只是PCB的一种,不同操作系统的PCB会有所不同;task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息)
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等其他信息。
什么是进程:内核对进程管理的相关数据结构 + 当前进程的代码和数据
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
进程信息可以通过 /proc 系统目录查看。
大多数进程信息同样可以使用top和ps这些用户级工具来获取
通过系统调用获取进程标识符(控制符)
#include <unistd.h>
pid_t getpid(void); 返回调用进程的进程ID
pid_t getppid(void); 返回调用进程的父进程ID
(注:pid_t 返回值类型本质上就是有符号整形 – int)
pid:pid就相当于我们的学号、工号,来唯一标识各个进程,进程启动才会分配,进程结束就收回,因此pid可以进行复用。
每次启动同一程序,进程id(pid)都可能不一样。
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("我是一个进程,pid = %d, ppid = %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
通过系统调用创建子进程
调用子进程函数: pid_t fork(); 需要包含头文件 #include<unistd.h>;
若执行成功,给父进程返回子进程的pid,给子进程返回0. 若执行失败,返回-1.子进程是什么我们可能还不太理解,我们先来看一看下面的例子来进行理解吧。
#include<stdio.h>
#include<unistd.h> // sleep(); // getpid(); getppid();
#include<assert.h> // assert(); 断言
void Test02()
{
pid_t ret = fork();
assert(ret != -1);
printf("创建了子进程\n");
if(ret == 0)
{
// 子进程
printf("这是子进程,pid = %d, ppid = %d, ret = %d, &ret = %p\n", getpid(), getpid(), ret, &ret);
sleep(1);
}
else
{
// 父进程
printf("这是父进程,pid = %d, ppid = %d, ret = %d, &ret = %p\n", getpid(), getpid(), ret, &ret);
sleep(1);
}
}
int main()
{
//Test01();
Test02();
return 0;
}
首先我们会发现这次写的函数和我们之前写的C语言程序有很大不同:
该程序既执行了if语句又执行了else语句,
那么为什么同一个变量ret同时保存两个值?(为什么确定是同一个变量呢,因为它们的地址都一样,我们去看电影时一个人一张票,一张票可以坐一个座位,而这里两个人却是同一个座位号,那就说明出问题了,这里我们可以猜测是有一个人走错了房间,它是其他房间的这个座位号 – 实际上也是这样,这里的ret的地址是虚拟地址而不是物理地址,这两个值实际上是存放在其他地方的,具体的大家可以自行了解)。
上面我们了解了一个变量为什么可以保存两个值,那么我们再来思考一下:为什么ret会接受到两个返回值,C语言中一个函数可以有两个返回值吗,那么我们看一下下面的程序:
int Test()
{
int a=10;
int b=20;
return a;
return b;
// 或者这样
// 2. return a && b;
// 3. return a & b;
上面的三种方式可行吗,哪一种可行?
我们来分析一下:
return a;
语句执行完毕后程序就结束了,那么return b;
语句永远都不会执行;return a && b;
这条语句的意思是返回 a 与 b 的逻辑与结果 显然不行;return a & b;
这条语句的意思是返回a 与 b 的按位与结果显然也不行;- 有的朋友会说:返回结构体不就可以了,没错,是可以的,但是返回结构体也是返回一个结构体变量,也只有一个返回值。
因此我们知道C语言是无法同时有两个返回值的。
一个函数为什么会有两个返回值?
– 首先我们需要理解一件事情:我们进行函数调用时,执行到return语句的时候我们的主体部分是什么时候执行完毕了?
或者说,return语句的作用到底是什么?
例如下面我们要进行加法运算:
// 在函数调用处使用a接收返回值
int Test01(int a, int b)
{
int c = a + b;
return c;
}
/* 或者直接传引用
void Test02(int& a, int& b)
{
a += b;
}
*/
我们函数的功能是执行加法运行, 那么在Test01()里我们函数的主体部分是 “c = a + b;”
此时函数的功能已经完成,return的作用是将所求和返回给调用方,告诉它“我已经执行完毕了,这是你要的结果”;
在Test02()里我们函数的主体部分" a += b;" 执行结束后由于返回值是void,我们都不需要返回值。由此我们可以得到一个结论:函数的主体部分在执行到 return 语句的时候就说明已经执行结束了(return还没有执行);
那么我们回到“一个函数两个返回值的问题”,fork的作用是创建子进程,在执行到return语句的时候子进程就已经创建完毕了,
因此,从fork()的 return语句开始父子进程各自会执行一次, 所以会给我们一种一个函数有两个返回值的误导 –
实际上是return 这条语句执行了两次(在父进程和子进程中都执行了一次)。
- fork之后,执行流变成两个执行流;
- fork之后,父子进程谁先执行由调度器决定;
- fork之后,程序的代码区共享,通常由if、else来区分父子进程的执行内容;
- ret有两个值,分别记录父子进程中调用fork()的返回值。
#include<stdio.h>
#include<unistd.h> // sleep(); // getpid(); getppid();
#include<sys/types.h>
#include<assert.h> // assert(); 断言
void Test02()
{
int num = 100;
pid_t ret = fork();
assert(ret != -1);
printf("创建了子进程\n");
if(ret == 0)
{
// 子进程
printf("这是子进程,pid = %d, ppid = %d, num = %d, &num = %p\n", getpid(), getppid(), num, &num);
sleep(1);
}
else
{
// 父进程
printf("这是父进程,pid = %d, ppid = %d, num = %d, &num = %p\n", getpid(), getppid(), num, &num);
sleep(1);
}
}
int main()
{
//Test01();
Test02();
return 0;
}
进程状态
中断进程的指令:kill -9 pid
fork做了什么?
– 创建了一个新的进程,既然创建了进程,就一定会创建新的pcb,子进程会从父进程中继承很多信息,
不过注意,各个进程之间具有独立性,子进程创建好之后,即使杀死了父进程,对子进程也无影响(eg:各个进程之间是独立的 – 关掉腾讯视频不会影响我们看CCtalk直播)。
操作系统是对软硬件进行管理的,因此软硬件都有对应的pcb(task_struct),
一个进程缺少某种资源时其对应的pcb就会离开OS维护的队列,去寻找对应资源,进入资源队列进行等待(此时该进程不占用cpu)。
(类比我们自己写的程序)
当我们写的程序需要scanf输入数据时,程序就会停止下来,黑框框内的光标就会一闪一闪的等待我们输入数据后程序才会继续往下进行。
引入
运行状态:简单说就是当一个进程可以运行的时候它就是处于运行状态;
阻塞状态:阻塞,以我们的日常生活为例:当一条公路车辆多的已经水泄不通,基本上所有车辆都走不动的时候,我们就可以说这条公路此时处于阻塞状态,我们可以理解为:这条公路此时无法“运行”。
运行状态
运行状态的标志为:R
#include<iostream>
using namespace std;
#include<unistd.h>
int main()
{
while(1)
{
cout <<"我是一个进程,pid = " << getpid() << ", ppid = " << getppid() <<endl;
sleep(1);
}
return 0;
}
从上面的示例我们可以得出
- 一个进程看起来一直在运行,但是并不一定会一直处于运行状态;
- R状态是瞬时的;
#include<iostream>
using namespace std;
#include<unistd.h>
int main()
{
while(1)
{
}
return 0;
}
从上面我们可以得出:进程状态与进程此时所在队列有关。
阻塞状态
-
S(可中断休眠)
-
D(不可中断休眠 – 只有在磁盘快满导致数据来不及记录 – 磁盘负载 – 一般不会见到)
-
T(停止状态)
状态后面带 + ,表示在前台运行,可以使用 Ctrl + c 中断,
状态后面不带 + ,表示在后台运行,只能使用 kill -9 pid 指令杀掉。
- t(追踪式暂停):(eg:调试时打断点)
死亡状态 & 僵尸状态::
- Z():进程退出时显示,瞬间状态一般查不到。
休眠状态一般是进程在等待某种资源;
暂停状态一般是进程行为越界,OS不想杀掉进程则会将它暂停。
进程控制优质文章:Linux进程概念和进程状态