1.冯诺依曼体系结构
结论:
--- CPU不和外设直接打交道,和内存直接打交道。
--- 所有的外设,有数据需要收入,只能载入到内存中;内存写出,也一定是写道外设中。
--- 为什么程序要运行必须加载到内存?
写好的程序存储到磁盘上,cpu要执行代码访问数据,只能从内存中读取(体系结构规定)。
--- 进度条代码:为什么已经调用了printf函数,但是数据没有被打印出来?
显示器是外设,你的代码加载到内存cpu跑,跑完之后不是直接写入到外设的,而是写给内存,交给它一段时间后刷新,才被写入到外设输出出来。
--- 计算机怎么知道输入设备中有数据呢?
各种外设和cpu在控制之间存在交互。
--- cpu中的控制器是用来响应外设的一些请求,它去告诉cpu;还有一些芯片专门用来把外设数据搬到内存,数据就绪状态的捕捉;cpu中的运算器进行算术运算和逻辑运算。
2.操作系统
操作系统是一个进行软硬件资源管理的软件,为用户提供稳定的、高效的、安全的执行环境。操作系统内包括进程管理、文件系统、内存管理、驱动管理。 管理者对重大事情做决策,但不需要和被管理者直接交互。决策需要有依据,依据的来源是数据!
计算机如何管理硬件?
用struct结构体描述硬件各属性,然后用链表或其它数据结构将各硬件组织起来。
3.什么是进程?
一个运行起来(加载到内存)的程序---进程。 同一时期有很多磁盘上的程序要加载到内存上。操作系统要对这些进程进行管理,引入PCB的概念进行对进程的描述---进程控制块struct task_struct{进程属性},进程ID、属性、状态、优先级、数据......。当程序加载到内存里,系统给每一个进程匹配一个进程控制块,操作系统遍历链表(存储单元为进程控制块),将死亡状态的进程清除,将优先级高的进程加载到CPU---对进程的管理转化成了对链表的增删查 。管理的理念---先描述再组织。
---进程演示,进程在调度运行的时候,具有动态属性!
---在Linux下,当程序加载在内存中,然后将myproc二进制文件删除掉,进程不会关闭!一个进程在运行时一定加载的是磁盘上的某个程序,如何知道进程加载的是哪个磁盘上的程序,系统上标识
---getpid()获取子进程、getppid()获取父进程,重启进程每次子进程id每次发生变化,但是父进程id每次都不变。我们在登陆时,操作系统给指派了一个shell,命令行上启动的进程一般它的父进程没有特殊情况的话都是bash!./myproc这个任务交给子进程去跑,哪个子进程出问题了不会影响到父进程,但是如果父进程出问题了命令行解释器就会崩掉。
---fork()创建子进程;fork是一个函数,在它执行前:父进程,执行后:父进程+子进程,fork()之后的代码,fork()函数的返回值;pid_t,同一个变量id,会给父进程返回子进程的id,给子进程返回0。被父进程和子进程共享!可以根据返回id不同让父子进程执行后续不同部分的代码。
4.进程状态:
1.运行状态:一般一个cpu会维护一个进程的运行队列,进程入队列---让进程的PCB结构体去排队。队列的存储数据类型是task_struct,在运行队列的所有进程都叫做运行状态(包括正在运行/在运行队列)。cpu运行处理某一个进程时,根据pcb去内存中查找储存着对应的code/data块去调用运行。进程的状态就是它的一个属性,在task_struct中存储。
2.阻塞状态:进程不只是只占用CPU资源,当cpu正在执行某个进程时,发现它需要调用某个外设资源时,由于外设速度很慢,就会先将它放到某外设队列去排队,这个状态就叫阻塞状态。当在外设处理完毕后,操作系统将它的状态改为R,重新放到运行状态中。所谓的进程不同的状态,本质上是进程在不同的队列中等待某种资源。
3.挂起状态:由于内存的空间有一定的限制,阻塞状态的任务又需要等待很长时间,d操作系统就会先将该任务的code/data换出保存在磁盘的某个区域,保留它的task_struct。阻塞状态不一定会挂起(内存不够才会挂起),只要不是运行状态都有可能被挂起。当阻塞状态结束,操作系统将该进程资源重新加载到内存,运行状态改为R,放入cpu的运行队列。
4.内存数据的换入换出:将进程的相关数据,从磁盘加载到内存、从内存保存到磁盘某个区域。
5.Linux下的进程
在实际的操作系统中,阻塞状态、挂起状态这些不会暴露给用户,只会提供用户有用的信息。
---前台进程:状态后面带+,一直在会在显示器打印,不能再键入shell命令,用ctrlC可以终止。
---后台进程:不带+,一直在显示器打印但是可以键入别的命令也会夹杂打印,用ctrlC不能终止,只能用kill -9 pid杀死该进程。
1.查看进程运行状态R:运行状态时间很短,经常会进入S状态排队。
2.查看进程休眠状态S:当程序调用需要访问外设时,就会进入休眠状态,阻塞状态的一种。
3.查看进程深度睡眠状态D:
4.查看进程暂停状态T:一个正在运行的程序,kill -19 pid暂停状态变为T ,kill -18 pid继续运行。
5.查看进程追踪暂停状态t:当代码文件进入gdb调试时,调试的过程等待输入指令时就是追踪暂停状态。
6.查看进程将亡状态Z:fork用父进程创建子进程,退出子进程,继续运行父进程,此时子进程并没有被回收,就是僵尸状态。进程结束会释放data/code,但是还没有释放PCB,等待回收后才能回收PCB,不回收会造成内存泄漏。
7.孤儿进程:父进程如果提前退出,子进程就会被操作系统“领养”,它的PPID变为1(操作系统进程)。例:fork用父进程创建子进程,然后杀死父进程,子进程的PPID就会变成1。
6.进程优先级(简单了解)
1.什么叫做优先级?
先/后获得某种资源的权利。
2.为什么会存在优先级?
资源有限,要访问资源的进程很多。
3.Linux优先级特点---很快
---PRI:老的优先级,不会被改也不能被改。
---NI:取值范围【-20,19】。通过top修改,sudo top ,PID,新Nice值,uit退出。
在Linux当中,一个进程的 最终优先级=老的优先级 + nice;Linux支持进程运行中,通过修改nice值修改优先级,大部分情况下nice默认为0。我们可以把一个进程运行起来后调整他的优先级。
7.进程的相关特性
进程之间具有竞争性、独立性--运行期间互不干扰。
任何时刻,只有一个进程正在运行;多个进程在多个CPU下并行运行
Windows下:一个cpu好似多个进程都在跑,时间片轮转:不管进程执行完花多长时间,每次给定一段时间让它占领cpu,时间到了继续返回运行队列排队,采用轮转的方式使得多个进程可以同时推进,这种方式叫做并发。通过进程切换的方式可以在一个时间段内让多个进程“同时运行”。
进程切换:
cpu有一套寄存器:取指令、分析指令、执行指令。指令--代码中
例:pc/eip寄存器:永远保存当前正在执行指令的下一条指令的位置。
当我们的进程在运行时,一定会产生很多的临时数据,比如加法运算的中间结果。这份数据属于当前进程。cpu内部虽然有一套寄存器硬件。但是,寄存器里面保存的数据,是属于当前进程的。
进程在运行时占有cpu,但不是占用到进程结束。进程在运行时都有自己的时间片。
上下文保护和上下文恢复:暂且认为将A进程运行中产生的临时数据暂时保存在PCB中,剥离当前的进程,轮转下一个进程。等到A进程继续轮转时,首先恢复它数据,加载到寄存器中。
8.环境变量
环境变量是操作系统为了满足不同的应用场景而预先在系统中设置的一大堆全局变量,这些变量在整个系统中一直都会被其它进程访问到。
command not found
要执行一个指令或程序,先找到这个程序。 ./myprocess -》 ./表示当前路径
将我们的程序拷贝到系统安装指令下,就可以不加./直接运行了。sudo cp myprocess /usr/bin/
没经过测试的程序最好不要安装在bin下,sudo rm myprocess /usr/bin/
添加到环境变量中:echo $PATH输出系统的环境变量,pwd查询当前路径,将当前路径添加到环境变量中,export PATH=$PATH:/ / /。$PATH将老的环境变量先加载进来再添加新的,直接添加自己的环境变量的话会导致整个PATH文件被覆盖。
当自己的可执行程序的路径也被添加到环境变量中后,也可以用which查询
cd ~,vim /etc/bashprofile -> vim /etc/bashrc配置文件中环境变量的设置,在系统启动后加载到内存
---env是一个外部命令,用于列出当前系统的所有环境变量及其赋值。
---PATH是一个环境变量,用于指定操作系统在命令行中搜索可执行文件的目录列表。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define USER "USER"
#define MY_ENV "myval"
int mai()
{char *who = getenv(USER);if(strcmp(who,"root")==0)
{printf("user:%s\n",who);printf("user:%s\n",who);printf("user:%s\n",who);
}else printf("权限不足!\n");return 0;
}
int main()
{char *myenv = getenv(MY_ENV);if(NULL==myenv){printf("%s,not found\n",MY_ENV);return 1;}printf("%s-%s\n",MY_ENV,myenv);return 0;
USER环境变量最大的意义,可以标识当前使用Linux的用户
bash就是一个系统进程,mycmd也会变为一个进程(会由fork创建),是bash的子进程,由于环境变量的全局属性,子进程也能访问环境变量;但对于自己创建的myval,只在当前bash下创建,由当前bash产生的子进程都不能访问它。本地变量也可以用set添加: ls显示的是该目录下的文件,shell在当前目录变化时会改变配置文件PWD
9.命令行参数
main()函数可以带参数吗?可以带几个参数?可以带什么参数?
int main(int argc, char *argv[ ] ) ;
int min(int argc,char* argv[])
{for(int i=0;i<argc;i++){printf("argv[%d]->%s\n",i,argv[i]);}return 0;
}
在我的进程的上下文中,获取环境变量的三种方式:
1.getenv,更加推荐。拿到的是等号后面的内容,其它两种是整行输出。
int main()
{char* myusr = getenv(USER);printf("%s\n",myusr);return 0;
}
2.char *env[ ]
int main(int argc,char* argv[],char* env[])
{for(int i=0;env[i];i++){printf("%s\n",env[i]);}return 0;
}
3.extern char **environ,指向env的那张表,存储的系统中的环境变量
int main()
{extern char **environ;for(int i=0;environ[i];i++)//这里为什么降维了?{printf("%s\n",environ[i]);}return 0;
}
补充:putenv(),改变或者添加一个环境变量
stat 获取文件的所有属性
10.程序地址空间
多进程在读取同一个地址时,读出来的值不同!-> 这里的指针指向的地址,不是物理地址,是虚拟地址。
#include<stdio.h>
#include<unistd.h>
int global = 100;
int main()
{pid_t id = fork();if(id<0){printf("error!\n");return 1;}else if(id==0){int cnt = 0;while(1){printf("我是子进程! pid:%d,ppid:%d,global:%d,&global:%p\n",getpid(),getppid(),global,&global);sleep(1);cnt++;if(cnt==10){global = 300;printf("子进程已经更改了global!\n");}}}1,17 else{while(1){printf("我是父进程! pid:%d,ppid:%d,global:%d,&global:%p\n",getpid(),getppid(),global,&global);sleep(2);}}return 0;
}
在global被改变后,子进程打印300,父进程打印100?---发生了写时拷贝
地址空间的本质:是内核的一种数据结构 mm_struct //以32位为例
---地址空间描述的基本空间大小是字节
---32位下能够表示 2^32次方个地址->只要保证唯一性即可
---2^32*1字节 = 4GB空间范围
---每一个字节都要有唯一的地址,给每一个字节用32bit位的地址表示
为什么 存在地址空间
---1.如果让进程之间直接访问物理内存,万一进程越界非法操作呢?
非常不安全。页表拦截非法操作!
---2.地址空间的存在可以更方便的进行进程和进程数据代码的解耦,维持进程独立性这样的特征。
写时拷贝:任何一个进程尝试写入时,操作系统先进行数据拷贝,更改页表映射,然后再让进程进行修改。操作系统,为了保证进程的独立性,做了很多工作! ---通过地址空间和页表,让不同的进程,映射到不同的物理内存处。
---3.再一次理解地址空间
在磁盘中的可执行程序里面,有没有地址呢?(还没有加载到内存的时候),这个地址是什么地址?
---调C库函数的时候需要链接,链接的过程就是将库中需要用到的函数的地址加载到程序中形成可执行程序,这个地址叫做逻辑地址。
---虚拟地址空间,不是只有操作系统会遵守对应的规则,编译器也要遵守!编译器在编译代码时,会按照虚拟地址空间规则划分代码区/数据区(堆和栈运行时才会产生),编译器按照虚拟地址空间的方式同时对我们的代码和数据进行编址。当程序被加载到物理内存中的时候,该程序对应的指令和数据,都天然的具有了物理地址!