1.冯诺依曼体系
体系的构成 :运算器,存储器(RAM 和 ROM),控制器,输入设备,输出设备
思想 :
1.数据和程序是以二进制代码的形式放在存储器中,存放的位置由地址指定,地址码也是二进制的。
2.控制器是根据存放在存储器中的指令序列即程序来工作的,并由一个程序计数器(指令地址计数器)控制指令执行。控制器具有判断能力,能根据计算结果选择不同的动作流程。
注意:
;a 这里的存储器指的是内存
b 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
c 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
d 所有设备都只能直接和内存打交道。
2.操作系统
操作系统是一个软件,是管理计算机硬件与软件资源的计算机程序,同时也是计算机系统 的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。
操作系统的构成
操作系统内核(进程管理,内存管理,文件管理,驱动管理),其他应用(函数库 ,shell程序等等)
系统调用 : 操作系统提供的函数,称之为系统调用函数
库函数 : 系统在功能调用的使用上,功能比较基础,对用户的要求相对比较高,然后一些有心的开发者(大佬们),封装了系统调用函数,提供了一些封装后的函数供程序员去使用,被封装的函数称之为库函数(stdio.h、stdlib.h、string.h等等)。
操作系统管理软硬件资源的方式:
管理 = 描述 + 组织
描述 :struct task_struct 用结构体来进行抽象的描述
组织 :操作系统是用双向链表来组织进程的PCB
3. 进程模块
进程和程序的区别
程序是一个文本文件,是静态的 ;而进程是程序执行起来的一个实体(要创建一个task_struct结构体)
进程概念
1.进程就是运行起来的程序,程序运行起来需要被加载到内存中。(这是站在用户的角度看待进程的)
2.进程就是操作系统的描述,这个描述叫PCB(process control block)(进程控制块),Linux下 PCB 有自己的名字叫task_struct.而操作系统就是使用task_struct结构体描述进程,使用双向链表来将这些结构体组织起来进行管理。
描述进程 :
标识符: 与进程相关的唯一标识符,用来区别正在执行的进程和其他进程。
状态: 描述进程的状态,因为进程有就绪,阻塞,运行等好几个状态,所以都有个标识符来记录进程的执行状态。
优先级: 如果有好几个进程正在执行,就涉及到进程被执行的先后顺序的问题,这和进程优先级这个标识符有关。
程序计数器: 保存程序执行的下一条指令。
内存指针: 该指针指向程序地址空间 ( mm_struct )
上下文信息: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
cpu密集程序 :程序大量时间都在使用CPU进行计算(逻辑计算和算术计算)
IO密集程序 :程序大量时间都在和磁盘打交道,进行IO
进程的执行方式 : 抢占式执行
并行 : 在同一时间,多个进程,每一个进程都拥有一个CPU进行运算
并发 : 多个进程,只拥有少量CPU,每个进程只能独占CPU一小会,就会让出CPU供其他进程运算。
也就是说,当一个CPU暂时空闲的时候,没有拥有CPU且需要运算的所有进程会同时向该CPU靠拢,进行抢占式执行,以便拥有CPU进行运算 ,但是他只能用CPU一小会,就要让出CPU供其他进程完成自己的运算。
组织进程 :
操作系统是用双向链表来组织进程的PCB,所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。
通过系统功能调用创建进程
利用 fork 创建子进程(两个返回值,代码共享,数据独有)
pid_t fork(void); //pid_t ==>int 类型
子进程创建失败 --> 返回值为 -1
子进程创建成功(返回两次) --> 返回0 返回给子进程
返回值大于0 (返回子进程的 pid ),返回给父进程
fork 返回值大于0的情况返回给父进程 的原因 :
因为如果一个父进程在创建一个子进程的情况下又创建了一个子进程,并且两个子进程在返回值都是0的情况下返回给父进程,那么父进程就无法判断这是哪一个子进程结束的返回值,所以子进程将自己的 pid 返回给父进程,父进程就可以分辨出返回的子进程是哪一个。
而对于子进程来说,fork返回给0给子进程,但是子进程的pid 不会是0,之所以fork返回0给子进程,是因为子进程可以随时使用getpid() 来获取父进程的pid,也可以用getppid() 来获取自己进程的pid ,所以返回值可以是0。
fork 之后父子进程除非采用同步的手段( vfork() ),负责不能确定父子进程是哪一个进程最先执行,因为进程执行的方式是抢占式执行。
#include<stdio.h>
#include<unistd.h>
int main()
{
//创建子进程
//系统调用函数 fork()
pid_t pid=fork();
if(pid < 0)
{
//没有内存时可能会导致此结果;
//创建PCB 是需要消耗内存的
perror("fork\n");
return 0;
}
else if(pid == 0)
{
//child
printf("i am child :[%d] - [%d]\n",getpid(),getppid());
}
else //pid > 0
{
//father
printf("i am father :[%d] - [%d]\n",getpid(),getppid());
}
//获取当前进程的pid,使用getpid
//获取当前进程的父进程的pid,使用getppid
return 0;
}
在fork之后,子进程和父进程是在fork()指令的下一行开始执行的,首先,如果子进程还是从fork() 指令处开始执行,就会陷入无限fork() 中,无法停止;其次,程序在执行时,所依靠的是程序计数器来取指令的,而程序计数器的用处就是来保存下一条指令,所以子进程直接从程序计数器所指的指令处开始执行。
进程状态
R : 运行状态(Running) 并不意味着程序一定在运行中,也可能是在指令队列缓冲器中。
R+ 前台进程 && R 后台进程
./[可执行程序名称] & -->可以将该进程变成后台进程
fg -->可以把一个进程放到前台来运行,
利用栈后进先出的特性,
弹出最后创建的一个后台进程来变成前台进程。
S : 睡眠状态(Sleeping) 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡(interruptible sleep))。
D : 磁盘睡眠状态(Disk sleep) 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T : 暂停状态(stopped) 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
t : 跟踪状态(tracing stop) gdb调试时,所调试的程序就会产生一个跟踪状态
X : 死亡状态(dead) 进程终止的一瞬间,PCB里的进程状态就会被改成X状态,然后被释放掉
Z : 僵尸状态(Zombie) 进程已经退出,但是PCB没有释放的状态,当子进程退出时,父进程没有回收子进程的退出状态,这个时候子进程退出的消息父进程没有接收,子进程就成为一个僵尸进程(“子进程此时是死的”),此时使用kill强制删除命令也无法删除该进程。
僵尸进程的危害 --> 内存泄漏
由于子进程变成僵尸状态,相当于,子进程的PCB在内核中还存在,并且在双向链表中挂着(使用 ps aux 命令还可以看到该进程),也就是说,PCB 的资源还没有释放,也就是说内存被泄漏掉了。如果有大量僵尸进程产生,则内存泄漏的越多。
结束进程
kill -9 [进程pid] --> 强制删除
孤儿进程
孤儿进程不是一种进程状态,而是一种进程种类的名称。
产生原因 : 是由于父进程先于子进程退出,导致子进程需要将自己的退出状态信息返回给一个进程。这是操作系统的1号进程(Init进程),Init进程本身就会创建许多进程,而本身Init进程创建的进程我们不能称之为孤儿进程。
解决或防止僵尸进程的方法:
草率的做法 : 如果产生了僵尸状态的进程,则将僵尸状态的父进程杀掉,那么僵尸状态的进程变成了孤儿进程,这时孤儿进程就会被1号进程将进程的返回状态信息回收掉,同时在内核当中释放该孤儿进程的PCB
4.环境变量
环境变量 一般是指在操作系统中用来指定操作系统运行环境的一些参数,为了让操作系统在执行时更加愉快的变量
常见的环境变量
PATH : 查看可执行程序的环境变量
HOME : 保存当前用户的家目录的环境变量
SHELL : 保存当前所使用的shell的环境变量
LD_LIBRARY_PATH : 程序运行时,库文件的搜索路径的环境变量
LIBRARY_PATH : 程序编译的时候,库文件的搜索路径的环境变量
常见的命令
env : 查看当前操作系统中的环境变量以及环境变量的值
echo $[环境变量的名称] :查看特定的环境变量的值
set : 查看操作系统中所有的环境变量
系统下的环境变量
/etc/bashrc
当前用户的环境变量
~/.bashrc
~/.bash_profile
export : 设置一个环境变量
export [环境变量名称] = $[环境变量名称]:[新增的环境变量名称]
在文件中修改
vim ~/.bash_profile
export [环境变量的名称] = 环境变量的值
source [修改过的环境变量文件] :告诉操作系统我们修改过这个文件,
操作系统重新加载一下就可以在命令中修改。
在命令行中修改
临时修改,只在当前shell中是有效的,在其他的shell当中是没有效的
就算重新开一个终端也是没有效的。
环境变量的组织方式
操作系统环境变量的变量的类型
char* env[] --数组中每一个元素都是char*,字符指针数组以NULL来结尾
使用代码获取环境变量(环境变量具有全局属性,可以被子进程继承)
//从main函数的参数里获取
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(i=0; i < argc; i++)
{
printf("argv[%d] = [%s]\n",i,argv[i]);
}
for(i=0; env[i]; i++)
{
printf("env[%d] = [%s]\n",i, env[i]);
}
return 0;
}
//利用库函数获取
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("environ[%d] = [%s]\n",i, environ[i]);
}
return 0;
}
//通过系统功能调用获取或者设置环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* path = getenv("PATH");
printf("%s\n", path);
return 0;
}
5.程序地址空间
在C语言中我们称之为程序地址空间,而Linux下称之为进程虚拟地址空间。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
int g_val = 10;
if(pid < 0)
{
perror("fork");
return 0;
}
else if(pid == 0)
{
//child,子进程肯定先跑完,也就是子进程先修改
//完成之后,父进程再读取
g_val=100;
printf("child [%d]: [%d] : [%p]\n", getpid(), g_val, &g_val);
}
else
{
//parent
sleep(3);
printf("parent [%d]: [%d] : [%p]\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
运行之后我们发现变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但他们的地址值是一样的,说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做虚拟地址,而我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,这块就涉及一个写时拷贝的概念。
写时拷贝 : 当数据发生修改的时候,才重新分配一个物理内存,并将页表当中的映射关系改掉
物理内存: 存储数据时需要依靠的介质
逻辑地址: 进程虚拟地址空间,也就是人为规定的,不能存储数据的.
页表
页表的作用是将虚拟地址映射成物理地址
页表的存储在汇编中就是 段基址 * 16 + 偏移量,段基址是页内偏移,块号是偏移量。
分页式
前提:会将虚拟地址分成一页一页的格式,会将物理内存分成一块一块的格式。
分页的原因:假设我们有一个16M大小的存储空间,因为计算机存储的时候,地址都是随机分配的,如果很不凑巧,这段空间的正中间的位置分配一个8M大小的数据,那么之后要是再存储一个5M大小的数据就会出现存不下的问题,这就造成了内存的浪费。
分页式的存储就和书差不多,把知识点都分成一页一页的,然后每一块知识点都有一个目录,可以通过目录很快的找到这一块的位置,然后在这一页中找知识点,唯一有区别的就是,在计算机中,每一块物理内存的大小都是一样的。
块号: 根据页号在页表中的映射去查找的块的标号
假设虚拟地址为5000,块的大小是4096,如何计算物理地址?
页号 = 虚拟地址 / 块大小
页内偏移 = 虚拟地址 % 块的大小
块的起始地址 = 块号 * 大小
物理地址 = 块的起始地址 + 页内偏移
页号 : 5000 / 4096 = 1
页内偏移 :5000 % 4096 = 904
块的起始地址 :5 * 4096
物理地址 : 5 * 4096 +904`
分段式
将虚拟地址映射成物理地址的结构不是页表了,而是段表。它的存储是将一个数据段直接放在物理内存中。
先根据段号从段表中找到对应的段的起始地址,再通过 段内偏移 + 段的起始地址 =物理地址。
分段式和分页式对比
分页式数据存储效率高,分段式效率低。
分段式对程序很友好,可以通过段表的结构,找到虚拟地址空间当中的一段。
段页式
存储时采取了段表结构和页表结构。
先根据段号来找到段表中页的起始地址,然后找到对应是那一页,再根据页号找到该页表中的块号,最后通过块号算出块的起始地址,块的起始地址 + 页内偏移 = 物理地址。