目录
一、冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
下面是冯诺依曼体系结构图:
我们所认识的计算机,都是输入设备、存储器、运算器、控制器、输出设备组成的。
- 输入单元:包括键盘,鼠标,扫描仪,写板,网卡,磁盘等;
- 中央处理器(CPU):含有运算器和控制器等;
- 输出单元:显示器,网卡,打印机等。
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存;
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备);
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取;
- 一句话,所有设备都只能直接和内存打交道。
我们的数据需要先从磁盘加载到内存中,然后由CPU读取并进行计算,将计算的结果再次加载到内存中,最后再由内存写入磁盘,通过输出设备将数据交给我们。
为什么CPU为什么不能直接访问外设呢?
因为输入输出设备称之为外设,外设一般是很慢的,比如说磁盘,相对于内存,他的速度是非常慢的,但CPU的计算速度确是非常快的。就好比从磁盘的读取速度很慢,但是CPU的计算速度却很快,但是整体的速度还是以磁盘的读取速度为主的,所以整体效率就以外设为主。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发 送文件呢?
首先从键盘上读取信息然后加载到内存,再从内存将数据通过一系列操作发送到输出设备上(网卡),然后通过一系列的网络操作将数据发送到朋友的输入设备上(网卡),朋友的电脑再从输入设备中将数据读到内存,然后通过输出设备(显示器)就可以将信息发送到朋友的电脑上。
二、操作系统
1、概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理);
- 其他程序(例如函数库,shell程序等等)。
操作系统是一款进行软硬件资源管理的软件。为什么操作系统要对软硬件进行管理呢?
因为操作系统对下要管理好软硬件资源,对上需要给用户提供良好(安全、稳定、高效、功能丰富等)的执行环境。
操作系统管理的本质:先描述,再组织。
- 描述:通过 struct 结构体对各种数据进行描述;
- 组织:通过 链表 等高效的数据结构对数据进行组织管理。
在计算机中,操作系统就相当于我们的管理者,而硬件驱动就相当于我们的执行者,而软件就是我们被管理者。
首先操作系统是不相信任何人的,正如我们是银行的用户,经常去银行存钱,但银行就信任我们吗?为了避免用户中有人恶意破坏,而对操作系统造成伤害, 所以操作系统并不是暴露自己的全部功能而是以系统调用来访问操作系统。由于系统调用的使用成本可能较高,之后在此基础上便有人进行二次的软件开发而产生了 图形化界面 和 shell 及工具集。
系统调用与库函数
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用;
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
2、设计OS的目的
- 与硬件交互,管理所有的软硬件资源;
- 为用户程序(应用程序)提供一个良好的执行环境。
计算机的体系的结构是层状的,一般不可以跳过某个层
三、进程
1、基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等;
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
我们打开任务管理器便会发现这些正在运行的可执行文件都是一个个进程。
2、描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合;
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
- 在 Linux 中描述进程的结构体叫做 task_struct;
- task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的信息。
操作系统在对我们的进程进行先描述,后组织的时候,会先将我们的程序的共有属性创建一个结构体,然后对我们的每一个进程创建一个结构体对象,这就是先描述的过程。接下来我们的操作系统会使用特性的数据结构(比如链表)将我们的结构体对象组织起来,这就是后组织的过程。然后我们的操作系统对进程的管理就会转换成对特定数据结构的管理。
所以,进程 = 内核关于进程的相关数据结构+当前进程的代码和数据。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程;
- 状态: 任务状态,退出代码,退出信号等;
- 优先级: 相对于其他进程的优先级;
- 程序计数器: 程序中即将被执行的下一条指令的地址;
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器];
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等;
- 其他信息。
3、组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
4、查看进程和终止
查看一个进程的基本信息,我们可以利用命令 ps -axj 列出当前系统所用进程信息;
先测试一段代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("我是一个进程!\n");
sleep(1);
}
return 0;
}
在输入查看进程的命令,如下所示:
输入 ps -axj | head -1 && ps -axj | grep "test",就可以获得表头及带有 test 的进程。
这里我们还需要解释一个概念那就是 PID 和 PPID 的概念,操作系统里指进程识别号,也就是进程表示符。操作系统里每打开一个程序都会创建一个进程 ID,即 PID 。 当然了,PPID 就是父进程的进程 ID 号。
杀死进程
有两种方法:第一种是Ctrl + c 是强制结束进程,第二种是:用命令的形式,kill -9 PID,指定目标进程杀死。
在这里我推荐使用第二种方法。
5、通过系统调用获取进程标识符
- 进程id(PID);
- 父进程id(PPID).
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
int main()
{
pid_t t = fork();
if (t == 0)
{
while (1)
{
cout << "我是一个子进程" << " pid:" << getpid() << " ppid:" << getppid() << endl;
sleep(1);
}
}
else if (t > 0)
{
while (1)
{
cout << "我是一个父进程" << " pid:" << getpid() << " ppid:" << getppid() << endl;
sleep(1);
}
}
return 0;
}
6、通过系统调用创建进程-fork
- 运行 man fork 认识 fork;
- fork有两个返回值;
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝);
- fork 之后通常要用 if 进行分流。
可以看到 fork() 这个函数很特殊,成功创建子进程后居然有两个返回值,给父进程返回子进程pid,给子进程返回 0,如果创建失败那么就返回 -1。
如何理解两个返回值
当 fork() 要对值进行返回的时候,其实在函数的内部创建子进程的工作已经完成,此时已经有了两个进程,两个进程继续执行下面的语句,执行完 fork() 之后自然都会有返回值,这样在我们看来就好像有两个返回值,实则我们在接收返回值时便已触发了写时拷贝(写实拷贝就是当操作系统检测到子进程有写的操作的时候,操作系统就会给子进程分配相应的物理空间),看似相同的 ret 实则储存在不同的空间。
7、进程状态
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里;
- S睡眠状态(sleeping): 意味着进程在等待事件完成 (这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep));
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束;
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行;
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态.
ps -axj 指令是查看进程的信息
(1)R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里;
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
;
}
return 0;
}
运行时,查看进程的状态是R+,说明是运行状态。
后面有个+号表示什么意思呢?
这里+表示该进程是前台运行的,当我们使用ctrl+c的时候能够终止掉该进程,不写+表示的是后台运行的,这时用ctrl+c是无法终止掉该程序的,要用命令杀掉进程来终止。
(2)S睡眠状态(sleeping): 意味着进程在等待事件完成 (这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep));
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int n = 0;
scanf("%d", &n);
return 0;
}
运行时,查看进程的状态是S+,说明是睡眠状态。
(3)T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行;
kill -l :就能显示所有的信号,而 SIGSTOP 信号对应 19 ,SIGCONT 信号对应 18。
当我们输入kill -19 PID ,该进程就会处于停止状态。
当我们输入kill -18 PID ,该进程就会恢复状态。
(4)死亡状态:这个状态只是一个返回状态,是一瞬间的,你可能不会在任务列表里看到这个状态,因为一个进程死亡后就会变成僵尸进程。
(5)僵尸状态:进程死亡后的状态,一个进程死亡后,会处于僵尸状态如果其父进程不回收,会一直占用资源,造成内存泄漏。
8、特殊进程
8.1 僵尸进程
- 僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用) 没有读取到子进程退出的返回代码时就会产生僵尸进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
下面演示一下僵尸进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
while (1)
{
printf("我是子进程,我的pid :%d,我的ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else if (id > 0)
{
//父进程
while (1)
{
printf("我是父进程,我的pid :%d,我的ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
perror("fail");
exit(-1);
}
return 0;
}
运行时,当中途用kill -9 PID 杀死子进程,此时子进程就是一个僵尸进程。
8.2 孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。
看下面这段代码,就是父进程先运行3秒,然后就退出了,此时子进程就是孤儿进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fail");
return 1;
}
else if (id == 0)
{
//子进程
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else
{
//父进程
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(-1);
}
return 0;
}
运行结果对比:
本文要是有不足的地方,欢迎大家在下面评论,我会在第一时间更正。
老铁们,记着点赞加关注!!!