🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬
🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🚀 如有不懂,可以随时向我提问,我会全力讲解~
🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!
🔥 你们的支持是我创作的动力!
🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!
🧸 人的心态决定姿态!
🚀 本文章CSDN首发!
目录
0.前言
此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。
大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~
感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!
1. 冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成 。
输入单元:包括键盘, 鼠标,扫描仪, 写板等 中央处理器(CPU):含有运算器和控制器等 输出单元:显示器,打印机等
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道
一幅图加结论带你了解整体概念。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请思考,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
接下来我还是以图的方式给大家展示:
这就是在冯诺依曼角度理解软件行为。
接下来再总体分析一下冯诺依曼体系结构:
2. 操作系统(Operator System)
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
2.2 设计OS的目的
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
2.3 定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的"搞管理"的软件
从操作系统大方向进行构架结构:
2.4 如何理解 "管理"
- 描述被管理对象
- 组织被管理对象
举个简单的例子,比方说:学生在学校要听辅导员的话,辅导员要听校长的话,那么校长就是管理者,辅导员就是执行者,而学生就是被管理者。在某种程度上,被管理者永远都是被管理者,而且被管理者和管理者其实不需要直接沟通,它们可以会通过辅导员这个执行者进行沟通。
上图就是通过例子的形式进行讲解,然后转变为计算机,其实计算机中的运行大概就是这样的,当然里面还有很多是细节需要我们去学习,这里只是让大家更容易理解大致的方向。
2.5 总结:
计算机管理硬件
- 描述起来,用struct结构体
- 组织起来,用链表或其他高效的数据结构
2.6 系统调用和库函数概念
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
3. 进程
3.1 基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
3.2 描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
3.2.1 task_struct-PCB的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
3.2.2 task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
接下来我就对上面的内容分类的概念以图文的方式进行讲解:
3.3 组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
3.4 查看进程
- 进程的信息可以通过 /proc 系统文件夹查看
- 要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
- 大多数进程信息同样可以使用top和ps这些用户级工具来获取。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 sleep(1);
9 }
10
11 return 0;
12 }
3.5 通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("pid:%d\n",getpid());
7 printf("ppid:%d\n",getppid());
8 return 0;
9 }
进程是能够知道自己所处的当前工作目录:
3.6 通过系统调用创建进程-fork初识
- 运行 man fork
- 认识fork fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 fork();
7 while(1){
8 printf("I am a process: pid: %d, ppid: %d \n",getpid(),getppid());
9 sleep(1);
10 }
11
12 return 0;
13 }
请看下面代码:试着回答fork之前的代码是谁执行的呢?
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("I am runing...\n");
7 fork();
8 while(1){
9 printf("I am a process: pid: %d, ppid: %d \n",getpid(),getppid());
10 sleep(1);
11 }
12
13 return 0;
14 }
答案肯定是由父进程执行的,因为fork()本来就是一个创建进程的函数,那么要想调用fork()就创建了进程,所以fork()之前的代码肯定是由当前进程执行,也就是父进程执行。
- fork 之后通常要用 if 进行分流
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("I am runing...\n");
7 pid_t id = fork();
8 if(id == 0){//child
9 while(1){
10 printf("do something!\n");
11 sleep(1);
12 }
13 }
14 else if(id > 0){//parent
15 while(1){
16 printf("call child, do something!\n");
17 sleep(2);
18 }
19 }
20 else{
21 // fork errot!
22 }
23
24 return 0;
25 }
4. 进程状态
4.1 看看Linux内核源代码怎么说
- 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
对进程状态的再次理解:
接下来我给大家先看一下kill命令,然后再演示一下上面刚说的情况:
S状态和T状态的区别:
简而言之T状态再被kill之后,唤醒的时候才知道自己被kill了,而S状态是直接被kill掉的。
4.2 进程状态查看
ps aux / ps axj 命令
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 while(1)
7 {}
8
9 return 0;
10 }
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("Process is running !\n");
7
8 sleep(100);
9
10 printf("Process running done !\n");
11 return 0;
12 }
4.3 Z(zombie)-僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
创建一个僵尸进程的例子:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7 printf("I am running...\n");
8
9 pid_t id = fork();
10 if(id == 0)//child
11 {
12 int count = 3;
13 while(count)
14 {
15 printf("I am child,pid: %d, ppid %d, count: %d\n",getpid(),getppid(),--count);
16 sleep(1);
17 }
18
19 printf("child quit...\n");
20 exit(1);
21 }
22 else if(id > 0)//father
23 {
24 while(1)
25 {
26 printf("I am father,pid: %d, ppid %d\n",getpid(),getppid());
27 sleep(1);
28 }
29 }
30 return 0;
31 }
4.3.1 总结:
僵尸进程是什么:
进程已经退出了,但是它的资源没有被立马回收,此时进程所处的状态叫做Z状态,这样的进程称之为僵尸进程。
为什么要存在僵尸进程:
因为我们必须要保证一个进程跑完,作为启动这个进程的父进程或者是操作系统,必须要知道这个进程退出时把我之前布置给它的任务完成的怎么样了,是把任务执行成功了,还是没有成功,还是指定的时候被kill掉了。
就比方说我给你发了个消息,软件要回馈我这个信息发送没发送出去,要是没发送回去,我再重新发送,要是没发送成功还没有回馈,那么这个软件就是有BUG的。
所以必须要知道子进程退出时的运行结果,所以为了知道运行结果,我们需要父进程或者操作系统来读取子进程的运行结果信息,换句话说,子进程在退出的时候,它的信息并不会立即释放,而是被暂时保存起来,保存到task_struct中,没有人进行读取,这个相关数据就不应该被释放掉。
(至于怎么办后面再说)
4.4 僵尸危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。
可父进程如果一直不读取,那子进程就一直处于Z状态?
是的!- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,
Z状态一直不退出,PCB一直都要维护?
是的!- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!- 内存泄漏?
是的!- 如何避免?
后面说~
4.5 进程状态总结
- 至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程
4.6. 孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为"孤儿进程"
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。
13 while(count)
14 {
15 printf("I am father,pid: %d, ppid %d, count: %d\n",getpid(),getppid(),--count);
16 sleep(1);
17 }
18
19 printf("child quit...\n");
20 exit(0);
21 }
22 else if(id == 0)//child
23 {
24 while(1)
25 {
26 printf("I am child,pid: %d, ppid %d\n",getpid(),getppid());
27 sleep(1);
28 }
29 }
30 return 0;
31 }
5. 进程优先级
5.1 基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
5.2 查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
5.3 PRI and NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
5.4 PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
5.5 用top命令更改已存在进程的nice
- top
- 进入top后按“r”–>输入进程PID–>输入nice值
5.6 其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
6. 环境变量
6.1 基本概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
一般主流的操作系统都有环境变量这个概念,windows中也有环境变量,下面就是我的电脑中的环境变量。
6.2 常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
6.3 查看环境变量方法
- echo $NAME NAME:你的环境变量名称
6.4 测试PATH
我们先来想一个问题,为什么 ls、pwd 这种命令可以直接运行,而我们自己写的程序编译成可以执行后,就不可以直接输入名字运行,而必须写成 ./proc 才可以运行?
这种写成 ./proc 是带了路径的,实际上, ls、pwd 这种程序是在环境变量中的,我们上面也是看到的,在 usr/bin 中那么是不是我们将自己写的程序添加到 usr/bin 中就相当于与添加到环境变量中,也就是说:我们直接在命令行中直接输入程序的名字就可以运行了呢?
答案是肯定的!
这里有两个方法将自己写的可执行文件添加到环境变量中:
1. 首先我们可以看到/usr/bin中有很多很多系统的可执行程序,我们可以把自己的可执行添加进来,这样就可以像 ls 那样直接输入程序名就可以跑。
如果你写了一个特别特别牛的软件,你可以这样给你的系统装一下,当然这种方式我是非常非常不推荐使用,根本原因就是因为:我们的系统中它是会存在各种各样的软件的,而其中你如果把自己的程序如果拷贝到系统的默认可执行程序的可在路径下,那么会污染别人写好的工具集,相当于你在以后输入myproc的时候,就搞不明白我的操作系统中怎么还有一个命令是myproc,还出来这么一个奇怪的字符串 " Hello world ",所以非常不建议这么搞。
如果你尝试了,那么可以用这个命令删除掉,当然,删除的时候一定要注意,千万不要手抖,因为你手一抖,可能你的 /usr/bin 全都没了。
2. 那么上一个方法不推荐,当然这个第二个方法就是推荐的啦~!
就是将自己当前所处的路径导到环境变量里面。
所以就像上面所说:PATH : 指定命令的搜索路径。
换句话说就是我们要执行的时候要加 ./ 就是告诉OS我们要执行的程序在哪里。
当然不要慌,因为只要我们退出XSHELL然后再登录我们的账号,就没问题了。
这说明了什么呢?
PATH就是一个变量,改了PATH有问题么?没有问题!虽然你改了,当我们重新登陆的时候,系统会给你重新配置生成PATH,所以我们在PATH里面随便写,只要我们重新登录一切就恢复了出厂~所以就不需要担心。
6.5 测试HOME
我用root和普通账号分别测试:
它们所默认的工作目录(家目录)是不同的,具体的不同是体现在HOME中。这个很简单。
在windows中是有工作目录的:
在我的XShell中,我们的环境变量也是可以看的:
这个bash是我们系统中的一条命令,这个命令运行起来就是一个进程,然后对我们命令行进行解释。
6.6 和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
这里就有一个好玩的东西:
也就是说,多个用户同时登录的时候,可以通过这个方式去给别人发送消息!
通过这个方式可以查看这个账号有多少个用户在同时登录。
还有一个概念是本地变量,当然这里不深入讲。
然后我们添加了,当然也可以删除:
我们查找不到了,当然也就是被删除啦~!
6.7 命令行函数
我们知道,main函数也是函数,既然是函数就肯定有人去调用它。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main(int argc, char* argv[], char* envp[])
5 {
6 for(int i = 0; i < argc; i++)
7 {
8 printf("argv[%d]: %s\n",i ,argv[i]);
9 }
10 return 0;
11 }
下面我们就来使用一下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main(int argc, char* argv[], char* envp[])
6 {
7 if(argc == 2)
8 {
9 if(strcmp(argv[1],"-a") == 0)
10 {
11 printf("hello world\n");
12 }
13 else if(strcmp(argv[1],"-b") == 0)
14 {
15 printf("hello 兔7\n");
16 }
17 else
18 {
19 printf("hello default!\n");
20 }
21 }
22 printf("argc : %d\n",argc);
23 return 0;
24 }
6.8 环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
6.9 通过代码如何获取环境变量
- 命令行第三个参数
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main(int argc, char* argv[], char* envp[])
5 {
6 for(int i = 0; i < argc; i++)
7 {
8 printf("argv[%d]: %s\n",i ,argv[i]);
9 }
10 return 0;
11 }
这个我刚才在上面的时候已经说过了,这里我就不在说了。
- 通过第三方变量environ获取
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 extern char** environ;
8 for(int i = 0; environ[i];i++)
9 {
10 printf("%s\n",environ[i]);
11 }
12 return 0;
13 }
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
可见,通过这种方式也可以获取环境变量。
6.10 通过系统调用获取或设置环境变量
putenv (后面讲解)
getenv (本次讲解)
我们上面获取环境变量的方式太暴力了,一下全拿到了,但是实际上我们可以通过系统调用获取一个环境变量,因为环境变量都是由环境变量名和内容组成:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5
6
7 int main()
8 {
9 printf("%s\n", getenv("PATH"));
10 return 0;
11 }
6.11 环境变量通常是具有全局属性的
- 环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
环境变量是由系统提供的。
那么我们接下来验证一下,到底是不是系统的环境变量。
那我们再试试本地变量可以看见么:
结论:
环境变量是一个系统级别的全局变量,bash之下所有的进程都可以获取,本质是调用方会给main( ,,envp)传参,我们定义的本地变量,进程是看不到的!
7. 程序地址空间
7.1 程序地址空间回顾
我们在学C语言的时候经常看见这副图。
可是我们对他并不理解!
这里要纠正一下,这幅图人家不叫程序地址空间,而叫 进程地址空间 !进程是要比程序包含的东西大的多的(这个我后面再讲,因为有很多知识我还没讲到,所以这里讲了也看不懂,这里就是提醒一下~!)
问题:
进程地址空间表示的是不是内存?
不是内存!这里就要重新学习下以前我们学过的指针了,因为以前理解的指针多少是有一点错误的~
7.2 通过代码感受一下
接下来我们就感受一下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<malloc.h>
6
7 int g_val = 100;
8 int g_unval;
9
10 int main(int argc ,char* argv[] ,char* envp[])
11 {
12 printf("code addr: %p\n",main);
13 char* str = "hello world";
14 printf("read only addr: %p\n",str);
15 printf("init addr: %p\n",&g_val);
16 printf("uninit addr: %p\n",&g_unval);
17
18 int* p = malloc(10);
19 printf("heap addr: %p\n",p);
20
21 printf("stack add: %p\n",&str);
22 printf("stack add: %p\n",&p);
23
24 for(int i = 0; i < argc; i++)
25 {
26 printf("args addr: %p\n",argv[i]);
27 }
28 int i = 0;
29 while(envp[i])
30 {
31 printf("env addr: %p\n",envp[i]);
32 i++;
33 }
34 return 0;
35 }
29 while(envp[i])
30 {
31 printf("env addr: %p\n",envp[i]);
32 i++;
33 }
34 return 0;
35 }
大家可以观察一下,按照打印的顺序,地址都是由低到高依次打印的,这也就证实了上面那个图。
下面我们继续看:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<malloc.h>
6 #include<sys/types.h>
7 int g_val = 100;
8
9 int main()
10 {
11 pid_t id = fork();
12 if(id == 0)
13 {
14 printf("child: pid: %d, ppid: %d, g_val: %d\n",getpid(),getppid(),g_val);
15 }
16 else{
17 printf("father: pid: %d, ppid: %d, g_val: %d\n",getpid(),getppid(),g_val);
18 }
19
20 sleep(1);
21 return 0;
22 }
有的人可能会说,这不没有问题么,很显然一个全局变量有两个进程。这两个进程对全局变量进行读取,那么访问出来的变量值都是100,没有问题啊~
那么再继续~
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<malloc.h>
6 #include<sys/types.h>
7 int g_val = 100;
8
9 int main()
10 {
11 pid_t id = fork();
12 if(id == 0)
13 {
14 printf("child: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
15 }
16 else{
17 printf("father: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
18 }
19
20 sleep(1);
21 return 0;
22 }
值是一样的,地址也是一样也很正常,没有什么问题,那么再再继续看~
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<malloc.h>
6 #include<sys/types.h>
7 int g_val = 100;
8
9 int main()
10 {
11 pid_t id = fork();
12 if(id == 0)
13 {
14 g_val = 200;
15 printf("child: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
16 }
17 else{
18 printf("father: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
19 }
20
21 sleep(1);
22 return 0;
23 }
那么现在呢?也没问题呢,这不子进程已经将全局变量改成200了么,打印200很正常啊,但是奇怪的是,为什么父进程没有受影响?
有的小伙伴可能认为是父进程执行的,子进程后执行的,所以子进程改了没有影响父进程,那么好,接下来我就让子进程先结束。
那么这是为什么呢?
现在有两个奇怪的问题:
1. 进程已经将全局变量改成200,为什么父进程没有受影响?
2. 为什么子进程已经改了,父进程才来读数据,读到100已经够奇怪了,为什么它们的地址还是一样的?
经过上图的解释,我再统一总结一下:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
OS必须负责将 虚拟地址 转化成 物理地址 。
其实所谓的虚拟地址就是上面程序地址空间的那个图,它们经过一定的方式转换成物理地址,进行物理级别的访问。
8. 进程地址空间
所以之前说"程序的地址空间"是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
这里就是通过父进程的进程地址空间通过页表映射,映射到物理内存中,当我们创建子进程的时候,本质上是系统内部多了一个进程,而且每一次申请一个新进程的时候,操作系统会为当前新进程创建一个属于该进程的地址空间,所以子进程也有一个属于自己的页表,因为在创建的时候是以父进程为模板,也就意味着父子进程使用的代码和数据都是一样的。
因为我们的子进程在对变量进行写入,应不应该影响父进程?
很明显,不应该!!!
这也就将头和尾连接起来了,记不记得我们开始说概念的时候,谈到过进程之间是有独立性的。
所以在更改的时候,在物理内存空开辟一个4个字节的空间,将新的值200写进去,更改映射关系,不再指向父进程数据,而是指向新开辟的空间,所以它们的虚拟地址是一样的,但是打印出来的值是不一样的,这种写入式在发生内存重新申请的技术叫做写时拷贝。(写时拷贝我在我之前的文章中提到过,也讲解过思路,也提供过详细的文章,所以这里我就不讲解了)
我写的string类中写了写时拷贝,大家感兴趣点这个链接去看看。
这样也就实现了在数据上实现了分离!
深入理解
所以我们平常出现野指针了,也就是在虚拟地址空间中野,也野不到物理内存,所以随便野,不要怕,野指针影响的只是你的程序,对系统没一点影响。
好,那么我们再总结一下:
所以要理解一句话:用同样的方式处理每一个进程看待内存的方式。
9. Linux2.6内核进程调度队列
上图是Linux2.6内核中进程队列的数据结构,之间关系也已经给大家画出来,方便大家理解
9.1 一个CPU拥有一个runqueue
- 如果有多个CPU就要考虑进程个数的负载均衡问题
9.2 优先级
- 普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
- 实时优先级:0~99(不关心)
9.3 活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
- 从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
9.4 过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
9.5 active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
9.6 总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!
如上就是 Linux进程概念 的所有知识,接下来要将 Linux进程控制 如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!
再次感谢大家观看,感谢大家支持!