冯诺依曼体系结构
冯诺依曼体系结构是现代计算机硬件体系结构,包括输入设备、存储器(内存)、输出设备(显示器)、运算器和控制器,注意:中央处理器(CPU)包含运算器和控制器等
(1)不考虑缓存情况,CPU能且只能对内存进行读写,不能访问外设,因此输入数据后放到存储器中进行缓冲
(2)硬件行为决定软件行为,所有设备都是围绕存储器工作的。
例如qq聊天过程:
输入设备输入数据,CPU读取数据首先从内存中去读,如果内存中没有,就去辅助存储器(例如硬盘,比如qq发文件)读,把硬盘的数据读入到内存,然后才会被CPU读取到。然后发送数据(发送设备是网卡,网卡什么时候从内存中取数据,是程序控制的),到达对端电脑,对方通过网卡接收数据,放到内存中,CPU进行处理,放回内存,控制显示器从内存中拿数据显示出来。
系统调用和库函数:操作系统会通过驱动系统来管理软硬件资源,它不会完全暴露给上层,对上层以接口对用户提供服务,这个接口称为系统调用接口,而系统调用接口成本比较大,因此库函数对系统调用接口进行了二次封装。
进程
- 进程的概念
就是一个正在执行的程序,操作系统将运行中的程序描述起来,通过描述来实现对程序的运行调度,这个描述信息就是操作系统调度一个程序运行的实体,因此在操作系统中进程就是运行中程序的描述pcb—进程控制块;
为什么要有PCB?
因为操作系统要管理进程,要管理进程就要先描述,再组织,要描述就要有PCB
那么进程和程序的区别是什么?
(1)程序是放到磁盘的可执行文件,进程是指程序运行的实例;
(2)进程是动态的,程序是静态的;
(3)进程是程序的执行,通常进程不可以在计算机之间迁移,而程序通常对应着文件,静态可以控制;
(4)进程是暂时的,程序是长久的;
(5)进程是一个状态变化的过程,程序可以长久保存;
那么在Linux操作系统下的PCB就是task_struct(一个结构体),它的内容分类包含:
标识符(PID):描述本进程的唯一标识符、状态、优先级(交互式程序一般优先级要求是最高的)、程序计数器(程序将被执行的下一个指令的地址)、内存指针
上下文数据(程序上次正在处理的数据)、I/O状态信息、记账信息(一个进程在CPU上的运行时间)、其他信息,即程序上次正在处理的数据。
CPU的分时机制:每个程序在CPU上运行都有一个时间片,时间片运行完毕则调度切换,时间片就是程序在CPU上运行的这段时间。 - 查看进程
/proc目录保存进程信息
通过ps命令查看,它的选项包括:-aux(信息更详细)/-ef(查看所有进程信息)
例如:
[Daisy@localhost ~]$ ps -ef
显示所有进程信息
例如要打印第一行和匹配到loop字符串,使用:
[Daisy@localhost ~]$ ps -ef | head -n 1 && ps -ef | grep loop
UID PID PPID C STIME TTY TIME CMD
Daisy 6422 5467 0 16:48 pts/4 00:00:00 grep --color=auto loop
可以看到显示出了第一行并且匹配到了loop字符串,&&表示相与的意思
- 通过系统调用获取进程标识符
使用接口getpid()来获取进程标识符
例如我们先写一个Makefile文件,然后生成了一个10-12可执行文件,./进行运行成为一个进程,通过getpid()查看他的进程标识符,例如vim 10-12.c:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("pid:%d\n",getpid());
return 0;
}
make后./运行10-12文件,结果是
[Daisy@localhost LinuxCode]$ ./10-12
pid:8579
发现他的pid是8579,然后使用ps -aux | grep 10-12可以查看进程标识符也是8579
通过getppid()获取父进程id。
例如:
[Daisy@localhost LinuxCode]$ ./10-12
ppid:8068
使用ps axj | head -n1 && ps axj | grep 10-12
[Daisy@localhost LinuxCode]$ ps axj | head -n1 && ps axj | grep 10-12
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
8068 8692 8691 8068 pts/2 8691 R+ 1000 0:00 grep --color=auto 10-12
发现父进程ID也是8068,而且进程不管是退出还是重新运行,父进程ID并不发生变化,因为它是bash
[Daisy@localhost LinuxCode]$ ps -aux | grep 8068
Daisy 8068 0.0 0.3 116692 3412 pts/2 Ss 18:40 0:00 -bash
使用kill -9 来杀掉这个进程,这时进程结束
- 通过系统调用创建进程fork
例如:
vim 10-12.c,编写代码
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before!\n");
fork();
printf("after!\n");
printf("I am a process:pid:%d,ppid:%d\n",getpid(),getppid());
}
make后然后./进行运行,结果是:
[Daisy@localhost LinuxCode]$ ./10-12
before!
after!
I am a process:pid:9065,ppid:8809
[Daisy@localhost LinuxCode]$ after!
I am a process:pid:9066,ppid:1
证明fork可以创建子进程,在创建子进程之后,父子进程代码共享(子进程因为拷贝了父进程PCB里边的很多数据,因此与父进程内存指针以及程序计数器都相等,所以运行的代码以及运行的位置都一样),数据各自私有一份(进程运行时具有独立性,因为父子进程是两个进程,保持独立性),但是父子进程谁先运行不一定(取决于操作系统的调度系统来决定的),但是可以稍加控制,例如:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before!\n");
fork();
printf("after!\n");
printf("I am a process:pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
运行结果是:
[Daisy@localhost LinuxCode]$ ./10-12
before!
after!
I am a process:pid:9135,ppid:8809
after!
I am a process:pid:9136,ppid:9135
可以看出父进程先运行
总结:fork有两个返回值,给父进程返回子进程的pid,给子进程返回0(返回值若为-1,说明创建子进程失败),例如:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id<0)
{}
else if(id==0)//child
{
printf("I am a child : pid:%d ppid:%d\n",getpid(),getppid());
}
else
printf("I am a parent : pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
结果是
[Daisy@localhost LinuxCode]$ ./10-12
I am a parent : pid:9252 ppid:8809
I am a child : pid:9253 ppid:9252
- 进程状态
1、R运行状态:并不是进程一定在运行中,而是进程要么是在运行中,要么在运行队列中
2、S睡眠状态:表示进程在等待事件完成,又叫做可中断睡眠
3、D磁盘休眠状态:又叫不可中断睡眠状态,进程通常等待IO的结束
4、T停止状态
5、X死亡状态 - 僵尸进程
即处于僵死状态的进程,表示进程已经退出,但是资源没有完全释放
僵尸进程的产生:子进程先于父进程退出,为了保存退出原因,因此资源没有完全被释放,因此在子进程退出时,操作系统会通知父进程,让父进程获取子进程的退出原因,然后释放子进程的所有资源,但是如果当前父进程并没有关注子进程的退出状态,子进程称为僵死状态,这就是僵尸进程
例如:
[Daisy@localhost LinuxCode]$ touch test.c
[Daisy@localhost LinuxCode]$ vim Makefile
[Daisy@localhost LinuxCode]$ vim test.c
编译test.c代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return 1;
}
else if(id>0)
{
printf("parent[%d] is sleeping...\n",getpid());
sleep(30);
}
else
{
printf("child[%d] is begin Z...\n",getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
编译在另一个终端下启动监控,
当前终端如图:
另个终端:
发现 < defunct >那一行状态信息是Z+,就是僵尸进程。
僵尸进程的危害:资源泄漏
如何避免僵尸进程产生:进程等待
注意:僵尸进程就算是使用kill -9都无法杀死。僵尸状态处理:退出父进程
- 孤儿进程
即父进程先于子进程退出,子进程没有从父进程获取自身的退出状态,子进程就成为了孤儿进程,所有的孤儿进程都被一号(init)进程所收养,父进程成为一号进程(init/systemd进程)(孤儿进程退出后不会成为僵尸进程)
例如:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
}
else if(id==0)
{
printf("child pid = %d,ppid=%d\n",getpid(),getppid());
sleep(8);
printf("child pid = %d,ppid=%d\n",getpid(),getppid());
}
else
{
sleep(3);
printf("father pid =%d,ppid=%d\n",getpid(),getppid());
}
return 0;
}
此代码首先让父进程进入3秒的休眠期,然后子进程打印出第一句后,进入8秒的休眠期,此时子进程的父进程id是父进程的进程id,三秒之后父进程退出(孤儿进程前提),8秒之后,子进程退出时,它的父进程id已经变成了1,也就是孤儿进程会被一号金城所收养
代码运行结果就是
[Daisy@localhost ~]$ ./file
child pid = 11338,ppid=11337
father pid =11337,ppid=11151
[Daisy@localhost ~]$ child pid = 11338,ppid=1
注意:孤儿进程在系统后台运行
- 进程优先级
即CPU资源分配的先后顺序
例如:
[Daisy@localhost LinuxCode]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 9961 9957 0 80 0 - 29173 do_wai pts/1 00:00:00 bash
0 R 1000 10370 9961 0 80 0 - 37235 - pts/1 00:00:00 ps
UID代表执行者身份、PID代表进程代号、PPID父进程的代号、PRI代表这个进程可执行的优先级,值越小越早执行、NI代表这个进程的nice值
PRI与NI:PRI即进程的优先级,NI就是nice值,表示进程可被执行的优先级的修改数值,nice的取值范围是==-20至19==,共40个级别
命令top然后按“r” 输入进程PID号,然后 输入新的nice值(必须在root下才能修改)
注意:程序分为CPU密集型和IO密集型,其中IO密集型不需要修改优先级;并且一般不建议修改优先级
- 其他概念
竞争性:系统进程数目众多,而CPU资源只有少量甚至只有一个,因此进程之间具有竞争属性。
动态性:进程的实质是一次程序执行的过程,有创建、撤销等状态的变化。而程序是一个静态的实体。
结构性:进程拥有代码段、数据段、PCB(进程控制块,进程存在的唯一标志)。也正是因为有结构性,进程才可以做到独立地运行。
独立性:多个进程运行,要独享各种资源,多进程运行期间互不干扰。
并行:多个进程在多个CPU下分别,然后同时进行运行,这个必须有多个 CPU 才行
并发:在一个时间段内多个进程同时推进,依赖多进程间的切换(进程切换会保留自身的硬件上下文信息,在下一次恢复再拿出来)(操作系统可以运行多个进程,但是不能在同一时刻多个进程运行)
环境变量
环境变量是系统提供的一种变量,设置系统运行环境参数信息的变量,让系统运行环境的配置更加灵活方便,并且可以通过一个环境变量向进程传递参数,在系统当中通常具有全局属性,它和进程没有直接关系。
- 环境变量相关命令:
(1)echo:显示某个环境变量
(2)export:设置新的环境变量
(3)env:显示所有环境变量,例如:
可以看出env命令显示所有环境变量
(4)unset:清除环境变量 - 常见环境变量:
(1)PATH:在shell中帮用户查找命令,它可以不用带路径来进行命令的执行,当不想用路径来执行命令时,可以将自己的路径放在环境变量后,但是要记得备份(使用 export PATH=$ PATH: 路径名这个命令来进行操作),例如:
[Daisy@localhost LinuxCode]$ ls
Linux1 Makefile test
[Daisy@localhost LinuxCode]$ pwd
/home/Daisy/LinuxCode
[Daisy@localhost LinuxCode]$ export PATH=$PATH:/home/Daisy/LinuxCode/test
[Daisy@localhost LinuxCode]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Daisy/.local/bin:/home/Daisy/bin:/home/Daisy/LinuxCode/test
执行export PATH=$PATH:/home/Daisy/LinuxCode/test命令后,通过echo $PATH查看环境变量发现在其后确实跟上了/home/Daisy/LinuxCode/test路径,这时就可以不用带路径来执行命令,也可以将PATH清空,使用export PATH=’'命令即可(清空后任何命令就无法执行,重新启动虚拟机(xshell)即可)。
(2)HOME
它的作用是执行当前用户登录所处的主工作目录,也就是默认的目录,例如当用户名是Daisy时,pwd发现我的主工作目录是/home/Daisy,echo $HOME发现也是/home/Daisy
[Daisy@localhost ~]$ clear
[Daisy@localhost ~]$ pwd
/home/Daisy
[Daisy@localhost ~]$ echo $HOME
/home/Daisy
然后切换到root用户下,执行上述操作,得
[root@localhost ~]# pwd
/root
[root@localhost ~]# echo $HOME
/root
发现他们的主工作目录是/root,表明HOME环境变量是执行用户的主工作目录。
Linux也可以指定本地环境变量,例如:
[Daisy@localhost ~]$ myenv=1000
[Daisy@localhost ~]$ echo $myenv
1000
如果要将它导成环境变量,使用export myenv即可,然后env命令来查看一下,发现存在了env这个环境变量,如图:
可以使用unset命令来清楚掉,然后使用env查看一下,例如:
[Daisy@localhost ~]$ unset myenv
[Daisy@localhost ~]$ env
这时就发现没有了env这个环境变量。
(3)SHELL:当前Shell,值通常是/bin/bash
- 通过代码获取环境变量
(1)命令行第三个参数
例如touch myenv.c文件,然后编写Makefile规则,例如Makefile中为:
myenv:myenv.c
gcc $^ -o $@
.PHONY:clean
clean:
rm myenv
然后vim myenv.c,例如:
#include <stdio.h>
int main(int argc,char* argv[],char* env[])//argv表示命令行参数,它指向可执行程序的名称,env放环境变量
{
int i=0;
for(;env[i];i++)
{
printf("%d:%s\n",i,env[i]);
}
return 0;
}
然后make一下,然后./myenv,得到
这时打印出的进程myenv是一个环境变量,进程环境变量创建到了当前目录下,因为环境变量有一个属性是当前目录下。因此从上述可以看出环境变量的组织方式就是一个字符数组,然后存储环境变量。
(2)通过第三方指针environ获取
例如touch myenviron.c,然后vim myenviron.c:
#include <stdio.h>
int main(int argc,char* argv[])
{
extern char* *environ;
int i=0;
for(;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
由于环境变量的组织方式如图:
因此在声明environ时,它是一个二级指针。,最后vim Makefile,然后make后./运行myenviron,得到
可以看出获取到了环境变量。
- 通过系统调用获取或者设置环境变量
使用getenv函数,例如重新vim myenv.c:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
return 0;
}
将上一次的myenv文件通过make clean删除,然后重新make,./运行myenv,得到的是PATH环境变量的内容,得到:
[Daisy@localhost LinuxCode]$ ./myenv
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Daisy/.local/bin:/home/Daisy/bin
可以看出得到了PATH环境变量的内容
- 环境变量通常具有全局属性
例如重新vim myenv.c,例如:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("MYENV"));
return 0;
}
然后先将上一次的myenv make clean,重新make,./运行myenv,得到
[Daisy@localhost LinuxCode]$ ./myenv
段错误(吐核)
发现出现错误,因为此时并没有MYENV这个环境变量,那我们创建一个MYENV环境变量,先MYENV=1000,但是这时他只是一个本地变量,然后export导出环境变量,然后通过env查看一下是否有MYENV这个环境变量例如:
[Daisy@localhost LinuxCode]$ MYENV=1000
[Daisy@localhost LinuxCode]$ echo $MYENV
1000
[Daisy@localhost LinuxCode]$ export MYENV
[Daisy@localhost LinuxCode]$ env
结果发现如图
的确有MYENV这个环境变量,此时重新make,./运行myenv,得到
[Daisy@localhost LinuxCode]$ make
gcc myenv.c -o myenv
[Daisy@localhost LinuxCode]$ ./myenv
1000
说明环境变量是可以被子进程继承下去的,因为没有用export时,运行myenv程序没有得到MYENV的内容1000,在export导出环境变量后,可以得到MYENV的内容1000,因为这时MYENV已经成为了环境变量,系统环境变量列表中存在,因此他能够被子进程继承,因此它具有全局属性。
程序地址空间
例如:
编译这段代码:
#include <stdio.h>
#include <stdlib.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id==0)
{
sleep(1);
printf("child: %d,%p\n",g_val,&g_val);
}
else
{
sleep(2);
printf("father:%d,%p\n",g_val,&g_val);
}
return 0;
}
make之后运行得到:
child: 100,0x601044
father:100,0x601044
发现父子进程的g_val的值和地址都是一样的,因为子进程按照父进程为模板,父子进程没有对变量进行任何修改,那么如果将代码改为
#include <stdio.h>
#include <stdlib.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id==0)
{
sleep(1);
g_val=200;
printf("child: %d,%p\n",g_val,&g_val);
}
else
{
sleep(2);
printf("father:%d,%p\n",g_val,&g_val);
}
return 0;
}
得到的结果是:
child: 200,0x601044
father:100,0x601044
发现父子进程的g_val的值发生改变,但是地址还是一样,所以可以得出父子进程输出的变量不是同一个变量,但是地址相同,所以这个地址绝对不是物理地址,在Linux下,这种地址叫做虚拟地址,操作系统必须负责将虚拟地址转化为物理地址;
因为直接访问物理内存,:进程之间内存访问,缺乏控制,内存的利用率比较低(每一个程序都要求一块连续的内存),操作系统为每一个运行中的程序都创建了一个虚拟地址空间,
因此可以来分析下图:
它不是内存,叫做进程地址空间,刚刚打印出来的地址是地址空间上的地址,所以进程地址空间又叫虚拟地址空间。
举一个例子:有一个富人,他具有10亿的财产,他有5个孩子(孩子之间并不知道别的孩子的存在),他对每个孩子都说是自己唯一的继承人,而每个孩子想要花费一小部分富人的财产,只要在承受范围内,富人都会给,站在每个孩子的角度认为自己都是唯一的继承人,拥有这10亿财产,他们不知道有其他孩子的存在,而在上帝视角这5个孩子是竞争的关系,虚拟地址空间就是这个道理,富人就相当于操作系统,10亿的财产相当于物理内存,而富人给每个孩子的承诺相当于虚拟地址空间,这5个孩子相当于5个进程。
有多少个进程就有多少个地址空间,地址空间是一个结构体,描述的是一个一个的区域
如图:
可以看出虚拟地址空间需要映射到物理内存,通过页表+MMU(内存管理单元)来进行映射到物理内存
总结:
为什么要有虚拟地址空间?
(1)保护物理内存
(2)保证进程是独占资源的
虚拟地址和物理地址的概念:
CPU通过地址来访问内存中的单元,如果CPU没有MMU(内存管理单元)或者MMU没有启用,CPU在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片接受,这叫做物理地址;
如图:
如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址,MMU将这个地址翻译成另一个地址发到CPU芯片外部地址引脚上,也就是虚拟地址映射成物理地址,如图:
虚拟地址和物理地址地址的分离可以给进程带来便利性与安全性,虚拟地址和物理地址必须建立一一对应的关系,才可以进行正确的地址转换;记录对应关系最简单的办法就是将对应关系记录在一张表中,为了让翻译速度较快,这张表必须加载到内存中,不过,这种记录方式比较浪费,因此Linux采用了分页方式来记录对应关系,分页就是以更大尺寸的单位页来管理内存,在Linux中,通常每页大小是4kb,通过getconf PAGE_SIZE可以获取当前内存页大小。
32bit位分页机制下虚拟地址由32bit组成的,常规4kb分页,32bit的虚拟地址被分成3个域:目录(最高10位)、页表(中间10位)、偏移量(最低12位),如图:
总结如下:其实虚拟内存映射到物理内存有三种方式,即操作系统进行内存管理的三种方式:
分段式:
虚拟地址的组成方式:段号+段内地址
借助段表:物理段起始地址:获取到物理段起始地址后+段内地址,这种方式对程序员比较友好,将程序的地址根据使用性质不同进行分段管理;
分页式:虚拟地址的组成方式:页号+页内偏移 、
借助页表映射到物理内存块号,获取到物理内存块号之后*块大小+页内偏移,提高了内存的利用率;
段页式:将虚拟地址进行分段管理,段内采用分页式管理,虚拟地址的组成:段号+段内页号+页内偏移
借助段表进行找到段内页表的起始地址,借助段内页号找到页表项,根据页表项可以找到物理块号;
交换分区的作用:
虚拟地址是64位,也就是大小为2^64,而物理内存只有8G,也就是大小为2 ^35,当物理内存不够用时,就会将物理内存中的数据交换到磁盘的交换分区上存储(将对应页表的缺页中断位进行置位),空出的内存运行当前数据;
内存置换算法:最久未使用算法(LRU),最少未使用算法等;