冯诺依曼体系结构
这里谈论的体系结构指的是计算机组成
常见的计算机,如笔记本,不常见的计算机,如服务器,大部分都遵守冯诺依曼体系
计算机,都由一个个的硬件组件组成
输入单元:如键盘,话筒,摄像头,usb,鼠标,磁盘/ssd,网卡,显卡等
输出单元:如显示器,打印机,磁盘,网卡,显卡等
这里的存储器指的是内存
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
即所有设备都只能直接和内存打交道
CPU处理数据的能力非常快,然后是内存,再是各种外设(如磁盘-->永久存储介质)
在数据层面上,当代cpu一般不与外设直接进行交互,原因何在?
答案是会导致整机效率降低
在数据层面上,cpu优先和内存直接打交道
所以有这么一个问题:
程序在运行之前,为什么必须要先加载到内存?
因为程序=代码+数据,最终都要cpu来执行处理,cpu需要读取到这些代码和数据,而cpu只和内存有“数据(二进制)”层面的交互,生成的可执行程序.exe本质就是一个文件,在磁盘(外设)中保存,故而需要先加载到内存
操作系统
概念(是什么)
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
1 内核(进程管理,内存管理,文件管理,驱动管理)
2 其他程序(例如函数库,shell程序等等)
操作系统就是一款软件,进行软硬件资源管理的软件
设计OS的目的(为什么)
1 与硬件交互,管理所有的软硬件资源
2 为用户程序(应用程序)提供一个良好的执行环境
操作系统将软硬件资源管理好(手段)给用户提供良好(稳定,高效,安全)的使用环境(目的)
如何理解 "管理"(怎么办)
操作系统内部一定会存在大量的数据对象和数据结构,那么如何进行管理呢?
管理的本质是管理数据,就像在学校中,真正的管理者是校长,作为学生并不会与校长有过多的接触,但是我们依然被管理的井井有条,是因为校长手里有学生的信息,如姓名,成绩,在校表现等,所以管理的本质不是管人,而是管理数据
计算机管理硬件:先描述再组织
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发
一般一个用户向要访问非常底层的os数据或者访问硬件,必须贯穿整个层状结构
如我们日常频繁使用的c语言中的printf,c++中的cout,都是访问了硬件的,那么必定调用系统调用, 则printf,cout必定封装了系统调用
操作系统提供系统调用的部分原因是因为不相信用户(这里的用户指狭义上的用户,即开发者)
因为群众中也有坏人,但是操作系统必须为用户提供服务,就像生活中的银行,它不相信我们但必须给我们提供服务,从而银行会有一个一个的窗口, 以提供服务,那么操作系统提供的接口(系统调用)就类似于银行的窗口
但是即使银行提供了窗口,群众中依然存在不熟悉银行办事流程的人,所以银行也会有一定的工作人员为不熟悉银行业务流程的人提供相关服务,那么对系统调用适度封装后形成的库就类似于为不熟悉银行业务的小白提供服务的工作人员。
这样库函数vs系统调用,就是上下层关系
进程
基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
更深刻的理解是:进程=可执行程序+内核数据结构(--->方便os对进程进行管理)(如PCB)
可执行程序加载到内存之后就是进程了吗?当然不完全是啦!
在可执行程序加载到内存之前,老大哥操作系统就已经加载在内存了
不可否认的是,操作系统内可能会同时存在非常多的“进程”,操作系统要管理它们,就要遵循”先描述再组织“,操作系统使用c语言写的,要管理这些”进程“,就要先描述这些”进程“,怎么描述?
用struct结构体!来描述该可执行程序的各种重要属性
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct是PCB的一种,在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
到目前为止,我们已经初步有了进程的框架,那么task_struct的核心字段有哪些?
进程(任务)对应的标识符:pid(process id)
父进程id:ppid 命令行中父进程一般是命令行解释器
我们运行的所有指令,软件,自己写的程序,最终都是进程
下面抛出一个问题:
在linux中,登录之后,命令行启动的进程,父进程一直不变,那么
这个父进程是谁? 是bash(命令行解释器)我们命令行启动的进程都是bash的子进程
获取该进程的pid:getpid()
获得该进程的父进程id :getppid()
一个进程的PCB(task_struct)里包含了该进程的pid和其父进程的id(ppid)
查看进程
除却上面图片中用ps查看进程信息的方法以外,还可以这样查看:
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
/proc:动态的目录结构,存放所有存在的进程,目录的名称以该进程的pid命名
我们可以看到pid为10714的进程里有exe:一个进程能找到自己的可执行程序,如上图中我生成的可执行程序是mycode
pid为10714的进程里有cwd(当前工作目录):默认情况下,进程启动所处的路径,就是当前路径
当然啦,只是默认情况下,要想更改当前工作目录可以使用chdir()
通过系统调用获取进程标示符
进程id(PID)
父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
通过系统调用创建进程-fork初识
linux中创建进程的方式:
1 命令行中直接启动进程--(手动启动)
2 通过代码来创建进程
3 fork之后通常要用if来分流,让父子进程执行不同的代码,做不一样的事情
系统调用fork(),就是通过代码创建进程
启动一个进程本质就是系统多一个进程,OS要管理的进程就多了一个,进程=可执行程序+task_strcut对象(内核对象)
创建一个进程就是系统中要申请内存,保存当前进程的可执行程序+task_strcut对象,并将task_strcut对象添加到进程列表中
fork有两个返回值,成功的话给子进程返回0,给父进程返回子进程的pid
失败则返回-1
可以看到pid为16869的进程 是pid为16870的父进程,二者是父子关系
结合目前来看:只有父进程执行fork之前的代码,fork之后父子进程都要执行后续的代码
fork作为一个函数竟然有两个返回值?
fork代码的一般写法:
我们为什么要创建子进程?因为我们想要子进程协助父进程完成一些工作,比如边下载边播放
创建子进程就是为了让子进程和父进程做不一样的事情,执行不一样的代码
怎么保证能够做到上面的要求呢?可以通过判断fork的返回值,区分谁是父进程谁是子进程,然后让它们执行不同的代码片段
下面来处理几个问题:
1 为什么fork的两个返回值,给父进程返回子进程的pid,给子进程返回0?
因为父:子=1:n 父进程是唯一的,但是子进程可以有多个,父进程要管理众多子进程就必须有区分子进程的唯一标识符,而子进程只需要确定创建成功即可
2 fork之后,父子进程谁先运行?
答案是不确定
因为创建完成子进程只是一个开始,在这之后,系统的其他进程,包括父进程,子进程,接下来要被调度执行的,当父子进程的PCB都被创建,并在运行队列中排队的时候,哪一个进程的PCB先被选择调度,哪个进程就先运行,这个有操作系统自主决定,由各自PCB中的调度信息(时间片,优先级等)+调度器算法共同决定
进程的独立性首先是表现在有各自的PCB,进程之间不会相互影响,代码本身是只读的,不会影响,数据父子进程可能会修改,子进程的PCB以父进程为模板
所以父子进程代码共享,数据要各自开辟空间,私有一份(采用写时拷贝)
所以,fork之后父子进程会执行一样的代码
3 为什么fork会有两个返回值?
我们通常认为若一个函数已经执行到return了,那么它的核心工作也就完成了
fork也是一个函数,会创建子进程,并将子进程放入到调度队列中运行
且fork之后代码共享,即fork函数中核心工作完成后,就已经有子进程了, return也是代码,return也会被共享! 父进程被调度就要执行return,子进程被调度也要执行return
4 如何理解一个变量会有不同的值?
id是fork的返回值,怎么可能同一个变量,同一个地址,会有不同的内容?
这里只能得出一个结论:这个地址绝对不是物理地址
另外,进程之间是互相独立的,互不影响,无论是什么关系
若是父进程被kill掉,子进程依然可以运行没有任何问题,反之亦然
进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep)),也叫做浅度睡眠,浅度睡眠会对外部信号做出反应,可以被kill掉
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,也叫做深度睡眠,不可以被kill掉,os也没资格
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。进程为什么要暂停?在进程访问软件资源的时候,可能暂时不能让进程进行访问,就将进程设置为T
t(tracing stop):debug调试程序的时候,追踪程序,遇到断点,进程就暂停了
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
Z(zombie)-僵尸进程:僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
简单来说,进程状态就是PCB中的一个字段,是PCB中的一个变量,int status
现在有#define NEW 1 #define RUNNING 2 #define BLOCK 3……
PCB->status = NEW
if(pcb->status==NEW),pcb放入什么运行队列之类的
else if(pcb->status==BLOCK),pcb放入什么阻塞队列之类的
……
进程状态变化的本质:
1 更改PCB中status这个整型变量
2 将PCB列入不同的队列中
所谓状态变化,本质就是修改status整型变量
1 运行状态
只要是在运行队列中的进程,状态都是运行状态,表明进程已经准备好了,可以随时被调度
每一个cpu都会在系统层面维护一个运行队列
2 阻塞状态
我们的代码一定或多或少会访问系统中的某些资源,比如:磁盘,键盘,网卡等各种硬件设备
如scanf() cin>> 本质就是我们需要从键盘上去读取数据,若是我们在键盘中输入了数据,那代码顺利执行下去,但若是我们一直不输入,那么键盘上的数据就没有就绪--->我们进程要访问的资源没有就绪(操作系统是要知道设备的状态的,且一定是最先知道它所管理的设备的状态变化的)--->进程代码无法继续向后进行,这就是进程阻塞了
操作系统要管理它手下的各种设备,就要先描述再组织:
当一个进程阻塞了,我们会看到什么现象?
1 进程卡住了
2 PCB没有在运行队列中,并且状态不是running,CPU不调度该进程了
该进程会进入键盘设备的阻塞队列中,同时将status修改成阻塞状态
3 挂起状态
如果一个进程当前被阻塞了,那么这个进程在它所等待的资源没有就绪的时候,该进程是无法被调度的,当如果此时os内的内存资源严重不足了怎么办?
将内存数据置换到外设,针对所有阻塞进程,不用担心慢的问题,因为这是必然的,当前的主要矛盾是让os继续执行下去,当进程被调度,曾经被置换出去的进程的代码和数据又要被重新加载回来