冯诺依曼体系结构
cpp
--》输入设备--》存储器--》输出设备--》
⬆ ⬇
运算器
丨丨
控制器
其中,控制器对输入设备和存储器、输出设备、运算器都进行控制
输入设备,可以包括磁盘(ROM),键盘,甚至是可触屏;存储器,这里不是指的磁盘,而是内存(RAM),cpu只和内存打交道,不只是cpu,所有设备,都只能和内存直接打交道;输出设备,通常是屏幕等
中央处理器(cpu),包括运算器和控制器
冯诺依曼体系结构的基本原理,就是存储程序和程序控制。计算机,就是按照指定的指令执行流程完成对程序的控制
cpp
寄存器《--- 三级高速缓存 《--- 主存 《--- 本地二级存储(磁盘)《--- 远程二级存储
操作系统
operation system,对上,为应用程序提供一个良好的运行环境;对下,管理硬件资源
一言以避之,操作系统就是一款搞管理的软件。
操作系统,狭义上,指的是内核,包括文件系统、内存管理、进程管理、驱动管理等;广义上,还要再加上shell、运行库、glibc、预装的的系统级软件等等。
所谓管理方式,操作系统并不直接管理硬件,其实管理的是描述硬件的数据,即"先描述,再组织",将这些数据通过结构体的方式描述包装起来,在通过数据结构进行维护存储
进程
概念
- 程序的一个执行实例
- 担当分配系统资源的实体(cpu时间,内存)
- PCB + 自己的程序代码和数据
- 进程是动态的,进程不与程序或者作业一一对应。(程序,指令的集合,存储在程序文件中;作业,程序的一次运行)
PCB
Process Control Block,在linux中,叫做task_struct,其中维护着进程的各种信息,比如
- 标识符,pid进程之间相互区别的标识符
- 优先级,Pri,Ni
- 程序计数器,程序中即将被执行的下一条指令的地址
- 内存指针,指向内存中程序代码、相关数据、共享内存块的指针
- 上下文数据,当一个程序时间耗尽,需要程序将寄存器中的数据拿走保存在内存中,当再次获得cpu时间后能够接着被切换走的状态继续运行,这些数据就叫做上下文数据
- 状态,包括运行状态和退出信息等
运行状态
在linux中,运行状态主要有以下几种
-
R,表示处在运行队列中,或者正在运行
-
S,标志可中断休眠,通常是正在等待事件完成
-
D,磁盘休眠,不可中断休眠,通常是在等待I/O事件
-
T,停止状态,通常是接收到了SIGSTOP,进程可以发送SIGCONT继续运行
-
X,死亡状态
-
Z,僵尸进程,本质是已经死掉的进程,但"没死干净"的原因是父进程不去接受程序的退出信息,导致cpu运行队列中的位置一致被霸占着,造成进程描述符泄露(在运行队列中,维护着active队列和expired队列,长度都为140,099为实时优先级的进程,其他为普通优先级,和nice值的-1920的取值相对应,通过位图的大O(1)调度算法实现调度,所以一个占着位置不释放,就会造成资源泄露
除了僵尸进程,还有一种进程叫做孤儿进程,是一个进程在运行时,他的父进程意外退出,导致没有进程来接受其退出信息,这是该进程会被一号进程systemd领养,资源由一号进程全部回收,运行在后台。孤儿进程的产生一般具有目的性,比如我们需要一个程序运行在后台
查看运行状态
- 查看proc文件夹,其中包含了进程的信息
- top指令
- ps指令,a-所有用户的所有进程;u-以用户为中心显示,包括cpu、内存情况;x,包括没有控制终端的进程,如后台运行的守护进程;j,显示进程组信息、会话id、父进程信息等
进程运行关系
- 竞争:cpu资源是有限的,所以为了系统的有序运行,进程之间会有优先级,以合理竞争cpu资源
- 独立:进程之间都有自己独占的资源,才能在多进程运行中互不打扰
- 并行:多个进程在多个cpu上同时运行,互不打扰
- 并发:多个进程在同一个cpu上运行,通过进程切换的方式,实现多个进程共同推进
环境变量
环境变量的组织方式
每个进程都会获得一个字符指针数组,其中每个字符指针都指向一个以'\0'结尾的字符串,其中保存着环境变量的值,最后一个数组成员是NULL
在代码中获取环境变量的方式
- 通过main函数的第三个参数获取
cpp
int main(int argc, char* agrv, char** env){}
- 通过第三方变量environ
cpp
extern char** environ
- 通过getenv
cpp
char* user = getenv("USER");
虚拟地址空间
来看下面一段代码
cpp
int a = 10;
int main()
{
pid_t pid = 0; if((pid = fork()) < 0) {perror("fork"); exit(1);}
if(pid == 0){cout << ++a << endl;}
else{cout << a << endl;}
return 0;
}
可以发现,父子进程打印的结果并不相同,说明两段代码指向的内存"并不是同一块内存",但是去查看a的地址又是相同。
这是因为用户能够直接使用的都是虚拟地址空间,只有操作系统能够接触到物理地址空间。
在task_struct中,指向了一个mm_struct,其中包含了进程的虚拟地址空间信息,例如栈的起止位置、堆的起止位置、数据段和代码段的起止位置等。再经过页表的映射,就和物理地址空间中的每一个字节都形成了一一对应的关系,这一过程,就通过内存管理单元MMU实现
那么为什么不直接使用物理地址呢?
- 安全性,如果直接使用物理地址,那么内存中操作系统相关代码以及所有的用户代码都是可以被估计位置的,这样恶意程序就可以设法修改其内容,导致程序崩溃 2. 起止位置不确定,如果从物理地址零地址或者其他地址开始使用的话,那么往后程序拷贝内容的起点都是不确定的,还需要遍历内存或者其他不必要的步骤进行确定 3. 效率问题,在内存空间不足时,会将不紧要的程序拷贝到swap分区,这样使用物理地址的话,就需要将一整个程序的所有内容全部拷贝走。
页表和分页机制的意义
- 页表和分页操作都经由操作系统之手,所以能够保护所有程序的合法的内存数据(这也是为什么一旦程序越界访问就会立刻段错误,被操作系统杀死)
- 在进程看来,内存空间是有序的,在操作系统看来,理论上可以分配物理空间的任意位置给任意程序,这样就实现了进程管理和内存管理的解耦合
fork
- 操作过程:用户调用fork后,操作系统开辟新的内存块和内核数据结构给子进程,随后拷贝部分父进程的内核结构中的内容给子进程(环境变量等),随后将子进程添加到系统的进程列表中(前面提到的cpu的runqueue),之后,调度器开始进程调度
- 退出方式,例如主函数中的return 0,或者使用exit(), _exit()
exit和_exit的区别
前者还完成了后续处理工作,比如执行析构函数、刷新缓冲区、关闭所有流,后者就是直接强制退出,哪怕造成资源泄露
进程等待
如果不对进程等待,就会造成前面提到的僵尸进程的问题。等待进程的方法主要有两种,一种wait(),另一种是waitpid
cpp
pid_t wait(int* status); //阻塞等待
pid_t waitpid(pid_t pid, int* status, int option); //可选择等待进程pid,-1表示等待任意一个推出的进程,也可选择是否阻塞等待,如果等待未成功返回0
对于waitpid和wait,常用的几个宏:
- WIFEXITED(status),查看进程是否退出
- WEXITSTATUS(status),提取进程退出码
- WNOHANG,表示非阻塞等待
在这里插入代码片