文章目录
Ⅰ.冯诺依曼体系结构
大多数我们常见的计算机,都使用的是冯诺依曼体系结构 如下图; 因为冯诺依曼体系可以拉近cpu与外设信息交换之间的差距 ,通过借助一个储存器,简单理解为内存;举个例子,就比如教授很有学问,对于学渣就是遥不可及根本没办法接触并认识,但是你可以通过一个导师介绍认识
具体怎么实现的呢?可以简单的理解是外设的输入较cpu是很慢的,在你输入的期间,cpu就可以运行许多程序了,所以cpu就说"你把你的数据和代码等放到一个地方,等你输入完成了,我一下子就给你运行完毕";那么当另一个程序来的时候,cpu仍然这样说,这样子cpu一下子就可以运行很多程序(其实还是那么多程序,但是只不过是无需等待外设的输入消耗的时间了);当cpu运行完毕后,仍然不会等待外设的输出,因为外设太慢了,所以cpu对外设输出设备说:“我把结果放到了某某地,到时候你忙完,自己去某某地拿吧”
这样子就可以大幅度减小两者间的差距,通过内存的储存;这就是冯诺依曼体系结构
Ⅱ.操作系统
操作系统的总体框架:
如何理解操作系统呢?
- 其实操作系统也是一个软件,就是一款用计算机语言写的一个程序;
- 所以你怎样使用计算机语言对一个事物呢?思来想后只能将这用一系列的类型描述起来,比如将其抽象出来,用结构体描述起来,然后使用一种数据结构比如:链表组织起来,然后转化为对链表的增删查改等一系列操作,就可以完成对底层的管理
Ⅲ.进程PCB
抽象出来进程就是:操作系统中的一个结构体 + 该程序的代码和数据
为什么需要一个结构体?
那么我们先思考一下,如果很多程序都仅仅只把代码和数据加载到内存,那哪个先经过cpu的计算呢?又到什么时候结束呢?这些代码和数据会不会有乱组合的情况呢?
所以每一个程序都需要一个管理它的结构体就是 进程 ;进程不仅仅是代码和数据,还需要一个管理代码和数据的信息,其中需要有状态,上下文信息,指针指向代码和数据,优先级,标识符…
1.查看&管理 进程
查看:
ps + axj + 进程的名字
我们如果需要指定某个进程可以配合grep使用,关键字查找功能
ps axj | grep test.c
ps axj | head -1 可以将进程头一行取出,里边是目录条例信息
最终 :
ps axj | head -1 && ps axj | grep test.c
管理:
kill -9 test.c 可以直接杀掉运行中的进程
kill -19 test.c 可以暂停该进程
kill -18 test.c 可以恢复暂停,使其继续运行
2.父子进程
a. 查看父子进程
将head -1取出就是上边照片 ;先抛出两个概念:
pid : 系统为了区别进程,将每一个进程编号,就是pid
ppid : 是该进程的父进程
// 在程序代码中获取该进程的编号需要调用接口getpid(),
// 该函数在<unistd.h>头文件中;而返回值在<sys/type.h>中
pid_t pid = getpid();
// 同理获得父进程的编号
pid_t ppid = getppid();
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 printf("pid:%d ppid:%d\n",getpid(),getppid());
8
9 return 0;
10 }
这就说明这个进程的编号是8765,而他的父进程是31068,这个时候问题就来了,main函数不只有一个进程吗?哪来的父进程?答案是此父进程是操作系统,因为操作系统也不会直接执行该进程(操作系统也怕出错),便将此差事交给自己创办的子进程也(在此情况就是8765)
b. 在代码中创建子进程
首先介绍一个创建进程的函数 fork();
使用方式是:在代码中使用fork()函数,如果返回值为零就是进入了子进程的模块,返回值大于零就是父进程的模块,当fork()之后,在此语句的后边的所有代码都被子父进程共享,所以可以使用if区别子父进程,分别实现不同的功能(暂且先死记硬背,待文中后边的虚拟地址空间我再介绍为什么一个函数可以有两个返回值)
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 printf("i am process pid : %d , ppid : %d\n",getpid(),getppid());
8
9 pid_t id = fork();
10 if(id == 0)
11 {
12 printf("i am child, pid : %d , ppid :%d\n",getpid(),getppid());
13 }
14 else
15 {
16 printf("i am parent, pid : %d , ppid : %d\n",getpid(),getppid());
17 }
18 return 0;
19 }
可以看出子进程的ppid 指向父进程的pid ;而父进程的ppid则指向操作系统;这样便学会了如何创建子进程
3.进程状态
a. R 运行态
运行态:顾名思义。进程在运行中,实际上可能在运行队列中,还可能在cpu中
+ 号表是前台运行( 可以ctrl + C 与 kill - 9 test.c 杀掉进程),没有的话就是后台运行(只能使用 kill -9 test.c 杀掉进程,使用ctrl + C 无效)
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 printf("pid : %d ppid : %d\n",getpid(),getppid());
8 while(1)
9 {}
10
11
12 return 0;
13 }
b. S 浅度睡眠态
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 printf("pid : %d ppid : %d\n",getpid(),getppid());
8 while(1)
9 {
10 sleep(1);
11 }
12
13
14 return 0;
15 }
此外在使用类似printf,cout,scanf,cin与外设相关接口函数时,该进程也是属于休眠状态,因为操作系统运行进程超级快(可以参考前面所说的,与外设相比),当运行完毕时,只有外设还在忙着输入输出,此时操作系统已经在休息状态了,所以呈现的是休眠状态
c. D 深度睡眠态
前言:深度睡眠态此时出现可能有点突兀,但是为了文章的顺序逻辑性,箭在弦上不得不发,如果不理解,可以二看这个。
深度睡眠这个状态是在操作系统内部操作系统自主控制完成的,也是与外设交互,但不同的是此外设不再是我们在用户层
次能够看到的了,是在操作系统内部,比如操作系统内存严重不足时,将代码与数据暂时挂起到外设磁盘中,此时进程与外
设的交涉,进程所处的就是深度睡眠态,此时用户是看不到的,但是 浅度睡眠 在用户层次是可以看到的
总结一下深度睡眠与浅度睡眠,两者都是在与外设的交互中的状态,不同的时,浅度睡眠在操作系统错误时,用户可以看到(比如该输入 a =10;用户输入一个 100),此时可以直接杀掉进程,再次启动;而深度睡眠就不一样了,深度睡眠是操作系统自己为了使自己不犯错,给进程与外设(用户不可见的外设)交互时,自己为了防止自己犯错,给该进程设置了一个 D 状态,此状态不可被杀,除非该进程自己完成了与外设的交互,或者强行断电终止
d. Z 僵尸态(僵尸进程)& 孤儿进程
父子进程,当子进程完成时,此时父进程仍在继续运行的时候,此时子进程就呈现出了僵尸态,也叫僵尸进程
此时子进程的信息(所有信息包括异常结束信息)需让父进程得知,因为子进程本身就是父进程分派过去的任务,最终任务的完成与否都得将结果告知父进程
为了呈现这个状态,我们模拟一种场景:父子进程都在while死循环中,kill掉子进程
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 printf("i am process pid : %d , ppid : %d\n",getpid(),getppid());
8
9 pid_t id = fork();
10 if(id == 0)
11 {
12 while(1)
13 {
14 printf("i am child, pid : %d , ppid : %d\n",getpid(),getppid());
15 sleep(1);
16 }
17 }
18 else
19 {
20 while(1)
21 {
22 printf("i am parent, pid : %d , ppid : %d\n",getpid(),getppid());
23 sleep(1);
24 }
25 }
26 return 0;
27 }
kill掉之后,只有父进程运行,子进程呈现Z状态
那么kill掉父进程,又会是什么场景呢?还是此代码,再次模拟一个场景
将父进程kill掉
此时的 child 进程未变,但是ppid直接变成了1,原因是将父亲kill掉,此进程就变成了孤儿了,简称孤儿进程,但是此进程最终的信息也得让 操作系统得知,要不然 进程该乱套了,而此时操作系统就说,既然没父进程了,那我我就领养了吧,所以1就可以理解为就是操作系统
而孤儿进程此时也从 S+ 变成了S(无加号,也就是后台运行)无法ctrl + C 终止进程,若想结束只能kill掉或者断电
e. T/t 暂停态
kill -19 test.c
kill -18 test.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 while(1)
8 {
9 printf("i am process pid : %d , ppid : %d\n",getpid(),getppid());
10 sleep(1);
11 }
12
13 return 0;
14 }
f. X 死亡态
- 一个进程结束的时候是非常短暂的,是无法被捕捉的,所有该进程基本无法模拟出来
g. 阻塞状态
- 当使用scanf需要读入数据时,该进程就处于阻塞状态,需要等待外设的输入,这就是阻塞状态
h. 挂起状态
挂起态可细分多种:比如运行挂起,新建进程是挂起,阻塞挂起…
其实万变不离其宗旨:就是节省操作系统的空间,减小操作系统的内存压力
此处以阻塞挂起为例:当需要输入数据时,若用户迟迟不输入,操作系统便有权利将此进程的信息唤出到磁盘中,当用户输入完毕后需要运行的时候,再将此进程的信息唤入到内存中运行;这样做的目的可以保证操作系统有足够多的内存空间运行其他进程,增大内存的利用效率,本质就是以时间换取空间
4.进程的优先级
前面介绍了是以单个进程为例,那么操作系统中那么多的进程是如何协调调度呢?谁先运行?怎样找到下一个进程呢?
其实操作系统中对进程的管理使用的是数据结构,比如说链表
先看模拟查看进程优先级的场景代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 while(1)
8 {
9 printf("i am process pid : %d , ppid : %d\n",getpid(),getppid());
10 sleep(1);
11 }
12
13 return 0;
14 }
查看进程的指令是: ps -al
看结果:其中 PRI 就是该进程的优先级, NI 的作用是调整该进程的优先级
PRI的默认值是80,NI的范围是[-20,19]所有PRI+NI的范围是[60,99]即操作系统中有 99 - 60 + 1 = 100 个运行队列(代表不同的优先级队列),每个队列中可以有许多个进程(因为优先级相同的可能有许多进程)如果不理解,可以先跳到末尾看大O(1)调度算法
接下来,操作一下修改优先级,输入指令的顺序为:
top
r
pid
NI值
还是上边的代码,看结果
所有此时该进程的优先级就被拉低了,因为PRI越小的越早执行,默认规定,优先级小的先进行
Ⅳ.命令行参数&&环境变量
1. 命令行参数
前言:我们写的函数都可以选择传参数,所以我们就想,main函数能不能传参呢?其实是可以的,那么谁调用main函数呢?那么谁给main函数传参呢?main函数的调用是在命令行中运行的,那么此操作相当于给函数传参了,所以可以猜想可以在命令行中对main函数传参
先看代码:count代表参数个数,而s是一个字符指针数组,里边存储的是每个参数的指针
1 #include<stdio.h>
2
3 int main(int count,char* s[])
4 {
5 for(int i = 0;i<count;i++)
6 {
7 printf("s[%d]->%s\n",i,s[i]);
8 }
9
10
11 return 0;
12 }
有人会疑惑你给main函数传参也没用啊!!!接下来看另一段代码
1 #include<stdio.h>
2 #include<string.h>
3
4 int main(int count,char* s[])
5 {
6 if(count != 2 )
7 {
8 printf("请输入选项:[1,2,3]其中一个\n");
9 return 1;
10 }
11
12 if( strcmp(s[1],"1")==0 )
13 {
14 printf("执行1操作\n");
15 }
16 else if(strcmp(s[1],"2")==0)
17 {
18 printf("执行2操作\n");
19 }
20 else if(strcmp(s[1],"3")==0)
21 {
22 printf("执行3操作\n");
23 }
24 else
25 {
26 printf("输入错误!\n");
27 }
28 return 0;
29 }
可以发现,这样也可以实现switch case的判断,从更高的角度来看,就可以将1实现为免费版的软件,将2实现为消费版的软件,用于区别两者,其上边就是命令行参数的基本介绍
2. 环境变量
前言:我们运行自己写的可执行程序时,需要使用 ./test.c 才能使该可执行程序跑起来,但是运行操作系统的ls,pwd,touch…就无需指明路径?为什么呢?因为操作系统将其路径设置为了环境变量,操作系统会默认去其中查找ls,pwd…的可执行程序
- 可以使用echo $PATH 查找环境变量:默认路径查找
- 可以使用env 查看全部的全局变量
- 在代码中获取环境变量的三种方式:
1.getenv(“PATH”)
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 char* path = getenv("PATH");
7 printf("path: %s\n",path);
8 return 0;
9 }
2.命令行参数
1 #include<stdio.h>
2 #include<stdlib.h>
3 // 命令行参数必须是0,2,3个参数,如果想查看env只能将前两个带上
4 int main(int count,char* s[],char*env[])
5 {
6 int i = 0;
7 while(env[i])
8 {
9 printf("env[%d]->%s\n",i,env[i]);
10 i++;
11 }
12 return 0;
13 }
3.char** environ
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 extern char** environ;
7 int i = 0;
8 while(environ[i])
9 {
10 printf("env[%d]->%s\n",i,environ[i]);
11 i++;
12 }
13 return 0;
14 }
Ⅴ.虚拟地址空间
先看一段代码和现象,抛出问题,然后解决问题
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int val = 100;
6
7
8 int main()
9 {
10 pid_t id = fork();
11 if(id == 0 )
12 {
13 while(1)
14 {
15 printf("i am child ,&val: %p,val: %d\n",&val,val);
16 sleep(1);
17 }
18 }
19 else
20 {
21 while(1)
22 {
23 printf("i am parent,&val: %p,val: %d\n",&val,val);
24 sleep(1);
25 }
26 }
27
28
29 return 0;
30 }
这个现象正常,接下来继续看
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int val = 100;
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0 )
11 {
12 int count = 3;
13 while(1)
14 {
15 printf("i am child ,&val: %p,val: %d\n",&val,val);
16 sleep(1);
17 if(count==0)
18 {
19 val =300;
20 }
21 count--;
22 }
23 }
24 else
25 {
26 while(1)
27 {
28 printf("i am parent,&val: %p,val: %d\n",&val,val);
29 sleep(1);
30 }
31 }
32
33 return 0;
34 }
是不是很诡异,地址一致,但是val缺不同,这就不得不谈谈虚拟地址空间和写时拷贝了
1. 虚拟地址空间&页表
虚拟地址空间的存在的目的是为了操作系统以一种相同的视角处理内存中的数据,否则东一个进程西一个进程进来了,随便把代码放到内存中,操作系统在查找时便不便于搜索查找,但是如果使用某种映射关系,将不同的代码和数据按照属性映射到一张表中,然后使用一种新的编号对其编号,然后呈现给操作系统,每个进程都是如此,那么操作系统是不是就可以使用较低的成本去管理调用了,所有这个表就是页表
- 这样操作系统查找变量时去虚拟地址空间中查找变量
- 查找到变量后去页表中找真正内存的储存位置
- 而我们%p打印的地址,都是虚拟地址,真正的地址是不会暴露给用户的
- 这样做可以减少错误操作,因为如果没有页表,用户便可以直接对内存中的数据读写,但是如果有页表,页表可以检测该内存是否属于该进程,属于一道屏障保护的作用
- 还可以节约操作系统去真正内存中查找数据的时间
2. 写时拷贝
当父进程创建子进程的时候,子进程与父进程公用一段代码与数据,所有子进程会将父进程的虚拟地址空间与页表拷贝一份自己使用,当操作系统发现子进程或者父进程某一方想对这个公共的val进行修改时(此处以子进程为例),操作系统会开辟一块空间先拷贝一份val的值,然后对其修改,让子进程页表中的与val对应的真实空间指向新开辟的空间但是此时对外呈现的地址依旧与父进程的地址一样,这就是写时拷贝
- 可以节省空间,本身需要全部数据拷贝一份,但是现在只需将需要写的数据进行拷贝,最坏的情况(全部写即全部修改)就是之前的工作量,所以这肯定节约空间
- 但是对用户呈现的还是全部拷贝一份,只有进行修改,才会真正的开辟空间进行