一、冯诺依曼体系结构
冯 • 诺依曼体系结构核心原理为:用户输入的数据先放到内存当中,CPU 读取数据的时候就直接从内存当中读取,CPU 处理完数据后又写回内存当中,然后内存再将数据输出到输出设备当中,最后由输出设备进行输出显示。
参考来自:冯诺依曼体系结构-CSDN博客
0x01 计算机组成
1.输入设备: 键盘,磁盘,网卡,显卡,话筒,摄像头等
2.输出设备: 显示器,磁盘,网卡,显卡,音响等
3.存储器(内存)
4.运算器&&控制器(cpu)
0x02 存储器分级结构
二、 操作系统
0x01 什么叫操作系统?
操作系统是一款专门针对软硬件资源进行管理工作的软件
操作系统对下管理好软硬件资源
对上为普通用户提供良好的运行环境,为程序员提供各种基本功能
此时就会出现一个问题:操作系统如何提供各种基本功能呢?
因为操作系统不相信任何用户,所以会采用系统调用接口去完成各种基本功能的调用
0x02 操作系统的作用是什么?
以管理好软硬件资源的方式,给用户提供稳定的,高效的,安全的运行环境
0x03 如何进行管理?
先描述被管理对象,再把多个管理对象之间产生联系,使用特性的数据结构组织起来 --- 先描述,再组织(可以将对目标的管理转化成对数据的管理)
比如从校长的角度,校长想要给一个学习成绩好的学生奖励,但是有一个学校有很多的学生,那么校长如何进行管理呢?此时就可以将每一个学生的数据聚合起来,形成学生的属性,然后再通过数据结构,将多个学生的聚合数据之间产生关联,这就叫先描述,再组织
三、进程
0x01 初看什么叫做进程?
初步来说进程是加载到内存的程序
0x02 那么如何管理进程呢?
当然也是先描述,再组织,任何进程在形成之时,操作系统要为该进程创建PCB --- 进程控制块
0x03 为什么要有PCB?
因为管理进程的方式是先描述再组织,要描述进程,那么就需要使用结构体来描述进程的相关属性
0x04 什么叫做PCB?
PCB: 一个结构体类型,在Linux系统中,PCB具体就是
struct task_struct
{
//进程的所有的属性
}
0x05 如何看见进程
①运行程序后
PID:进程号,当程序运行时就会有当前进程的进程号
②结束程序后
CTRL+C后,就结束了进程
提示:
ps axj : 查看所有进程
head -1: 只显示结果的第一行,即每一列的列名
曾经我们所有的启动程序的过程,本质上都是再系统上面创建进程
0x06 再看进程
进程加载到内存中,有了进程控制块,那么所有进程管理任务与进程对应的程序毫无关系,与进程对应的内核创建的的该进程的PCB强相关,就相当于当你面试的时候,面试官不是看你长的有多帅,有多美,而是看所收集到的数据来进行筛选,所以你是否能进面,就与这些数据强相关
0x07 如何获取PID?
①getpid()
头文件:
#include<sys/types.h>
#include<unistd.h>
介绍:
PID是task_struct的内容之一,是描述本进程的唯一表示符,用来区别其他进程
②获取PID
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("pid: %d\n",getpid());
sleep(1);
}
return 0;
}
③关闭进程的俩个方式
(1) CTRL + C
(2) kill + 9 进程号
0X08 如何获取PPID?
①获取PPID
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("getppid: %d\n",getppid());
sleep(1);
}
return 0;
}
0x09 状态
①退出码&退出信号
0x10 上下文
① 上下文数据
进程执行时处理器的寄存器中的数据
② 进程切换
①CPU中存放着寻多寄存器,寄存器中保存着当前正在运行的程序的临时数据
②运行队列run_queue中的每一个结点都是一个task_struct ,通过指针找到相应的代码和数据
③每次有结点是运行状态的时候就可以加载到CPU中进行运行,但是进程的代码可能不是很短时间就能运行完成
④可以假设,规定每个进程单词运行的时间片是5ms,那么第一个进程在CPU中运行5ms之后,就得从CPU中拿出,再去运行队列之后进行重新排队,重新进行运行
⑤所以再单CPU的情况下,用户感受到的多个进程同时在运行,本质是通过CPU的快速切换完成的
从上述可以得出一个结论:
进程在运行期间是有切换的,进程可能存在大量的临时数据,而这些临时数据是在CPU的寄存器中保存
但是又会得出一个疑惑点:
CPU里面的寄存器只有一套
这时就要提到保护上下文和恢复上下文的操作了:
当一个task_struct结点在CPU运行之后,那么此时它要重新去排队了,那么此时CPU寄存器中所保存的临时数据就会被保存到task_struct中,这就是保护上下文
而当这个task_struct结点又到它运行到CPU中时,那么之前保存的数据又会被重新加载到CPU的寄存器当中继续使用,这就叫恢复上下文
可以更通俗的讲就是:让你去做其他的事情,但是又不耽误现在要做的事情,当你要回来继续做这件事时,可以继续做这件事情从上述可知,通过上下文,我们能感受到进程是被切换的
四、查看进程
0x01 通过/proc系统文件夹查进程信息
进程启动之后会在/proc目录下形成目录,以自身的PID号作为目录文件名,形成对应文件夹,当这个对应的进程退出时,这个文件夹就会消失
0x02 查看当前进程所对应的属性
0x03 查看当前正在执行的程序
提示:
cwd:表示当前工作目录,当执行命令或打开文件时所参考的相对路径
exe:表示我们当前正在执行的程序
五、创建子进程
0x01 创建第一个子进程
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int ret = fork();
if(ret > 0)
{
printf("hello \n");
}
else
{
printf("world\n");
}
return 0;
}
从之前所学中我们可以猜想,执行的结果必然是if和else中的一个,但是结果真的是这样吗?
从结果中可以看出,竟然是if和else都执行了,这是为什么呢?
这是因为创建了子进程
从上面的代码中我们可能还有些疑惑,这到底是什么意思,那么我们再来看个代码:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int ret = fork();
while(1)
{
printf("PID:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
从上述代码和结果中我们又可以看出,俩个进程的PID号竟然是不一样的,因为一个是父进程,另一个是子进程
0x02 理解fork()创建子进程
①创建的子进程与在操作系统角度创建进程的方式是没有任何差别的,也是系统里多了一个进程,即多了一份与进程相关的内核数据结构task_struct和数据代码,但是此处的数据代码会继承父进程的代码数据,
②内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct③fork()之后,子进程和父进程代码是共享的,但代码只有一份,是不可以被修改的
④数据其实也是共享的,但这要考虑到修改的情况
0x03 写时拷贝
在 Linux 中,调用 fork
()创建子进程时,只是读取的时候共享一份,当需要修改的时候,并不会将父进程的所有数据复制,而是会找一块空间,将父进程该页数据复制一份给子进程 ,以此来保证进程之间的独立性
参考:【Linux】写时复制(CopyOnWrite)|写时拷贝|rcu_bandaoyu的博客-CSDN博客
0x04 fork()返回值
我们创建子进程,就是为了和父进程干不一样的事情,那么这就需要fork()的返回值来完成
创建子进程失败: <0创建子进程成功: 给父进程返回子进程的PID, 给子进程返回0
0x05 父子进程可以做不同的事情
#include<iostream>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id > 0)
{
while(true)
{
std::cout << "parent:" << getpid() << " "<< getppid() << std::endl;
sleep(2);
}
}
else if(id == 0)
{
while(true)
{
std::cout << "child:" << getpid() << " "<< getppid() << std::endl;
sleep(2);
}
}
else
{
//如果进程创建失败,什么也不做
}
return 0;
}
此时,我们会有一个问题,fork()之后,父子进程谁先运行?
这个是不确定的,这是有调度器所决定的
六、 进程的状态信息
0x01 进程的状态信息在哪里呢?
在task_struct中
0x02 进程状态的意义
方便操作系统快速判断进程,完成特定的功能,比如调度,调度本质上也是一种分类
0x03 运行状态(R状态)
运行状态并不意味着进程一定在运行中,表明进程要么是在运行中,要么是在运行队列中,即当前进程已经被放在了运行队列当中,随时等待CPU进行调度
#include<iostream> #include<unistd.h> int main() { while(true); //没有等待外设,只是排队CPU资源,比较快,所以一直处于运行状态 return 0; }
提示:
R+:"+"表示在前台运行,ctrl +c进行停止
没有"+"则处于后台运行: 如果想要后台运行进程,则./test &,ctrl +c是停止不了的,只有通过kill + 9 + 进程号停止
0x04 睡眠状态&磁盘休眠状态(S状态&D状态)
当完成某种任务的时候,任务条件不具备,需要进程进行某种等待,也就是当进程等待外部设备条件时,就会放入等待队列,这时的状态就是S状态或者D状态
S睡眠状态: 意味着进程在等待时间完成,可以被操作系统消除,也可以叫做可中断睡眠状态
#include<iostream> #include<unistd.h> int main() { while(true) { std::cout << "hello world" << std::endl; } return 0; }
为什么代码一直在运行,确实S状态呢?因为正在打印到显示器上,外设的速度是比较慢的,等待外设就需要花费时间的,只是看起来比较快,其实进程一直是在处于休眠状态
D磁盘休眠状态: 这个状态的进程通常会等待IO结束,即当进程在等磁盘处理数据并返回,但是此时进程什么事也没做,此时操作系统想要消除这个进程,但是如果消除了这个进程,那么磁盘处理好数据后那返回给谁呢?,所以这个进程不能被消除,所以D状态也可以叫做不可中断睡眠状态
0x05 挂起状态&唤醒进程
我们把从运行状态的task_struct(run_queue),放到等待队列,就叫挂起状态(阻塞)
从等待队列,放到运行队列,被CPU调度就叫做唤醒进程
提示:
①进程在运行的时候,有可能因为运行的需要,可能会在不同的队列里
②在不同的队列里,所处的状态也可能是不一样的
0x06 停止状态(T状态)
可以通过发送 SIGSTOP 信号给进程来停止( T )进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
提示:①kill -l :列出所有可用信号
②查看正在运行的进程的PID
③kill -信号编码 进程号
④进行查看是否处于T状态
0x07 死亡状态(X状态)
这个状态只是一个返回状态,你不会在任务列表里看到这个状态回收进程资源 = 进程相关的数据结构 + 代码和数据
0x08 僵尸状态(Z状态)
僵尸状态是为了辨别退出死亡的原因,而这些原因也是数据,保存在task_struct中,一个进程退出并不是直接进入死亡状态,而是先进入僵尸状态,再进入死亡状态
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << " I am child,running\n" << endl;
sleep(2);
}
}
else
{
cout << "parent do nothing\n" << endl;
sleep(50);
}
return 0;
}
进行监控进程:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; sleep 1;echo "###################" done;
当进程正在运行时,父进程一直处于运行状态,但是此时子进程被干掉,但是没有得到父进程的回收,此时子进程所处的状态就是僵尸状态
0x09 孤儿进程
当父进程被干掉,但子进程还在运行,此时的子进程就叫做孤儿进程,孤儿进程的父进程会变成操作系统
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << " I am child,running\n" << endl;
sleep(2);
}
}
else
{
cout << "parent do nothing\n" << endl;
sleep(10);
exit(1);
}
return 0;
}