系统编程:
进程概念->进程控制->基础IO->进程间通信->进程信号->多线程
进程概念
冯诺依曼体系结构----现代计算机硬件体系结构
冯诺依曼体系结构----现代计算机硬件体系结构
计算机五大硬件单元:输入设备:键盘输出设备:显示器存储器:内存-外存---固态接口类型SATA SATA3 PCI-E(目前最好)运算器:CPU—主频2.5GHz,主频越大代表时钟震荡周期越高,代表1s中处理的指令也就越多控制器:CPU所有设备都是围绕存储器工作的
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存 不考虑缓存情况,
- 这里的CPU能且只能对内存进行读写,
- 不能访问外设(输入或输出设备) 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊 天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发 送文件呢?
硬件决定了软件的行为
操作系统—管理
操作系统:
一个软件安装在计算机硬件上
目的:
为了让计算机更加好用—功能:合理统筹管理计算机上边的软硬件资源
管理:
先描述使用pcb描述进程,使用双向链表将pcb串起来进行管理,再组织。
库函数与系统调用接口的关系:
封装关系:库函数封装了系统调用接口,是上下级的调用关系
进程概念----进程是什么
进行中的程序
linux是一个多任务操作系统,表示有大量的程序需要被cpu调度运行,这时候cpu使用了分时技术,分别轮询处理每一个进程,在进程程序切换调度时,需要记录运行信息,因此操作系统在调度进程在cpu上运行时,使用pcb对运行中的程序进行描述,通过调度pcb完成对进程的调度,因此进程是pcb。
pcb对运行中程序进行描述
每一个运行的程序都是pcb
在操作系统角度,操作系统通过pcb来控制一个进程的运行,这个pcb也叫进程描述符,描述了一个运行中的程序
Linux操作系统下的PCB是task_struct结构体(用双向链表进行组织的)
什么时task_struct结构体
参考链接
task_struct结构体中的内容
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
内存指针
pcb中有一个指针指向了当前要运行的程序
cpu通过pcb内存指针知道代码在什么位置,然后加载到内存上面
cpu分时机制
不会体会到卡顿的原因,调度进程时,不会一直在一个进程上面运行,轮询调度pcb 。
每个都执行一段时间,切换速度很快。
每个进程只运行很短的一段时间(时间片)
程序计数器
即将执行的指令的地址
上下文数据
cpu正在处理的数据是什么
标识符PID
每一个进程都有一个ID
进程状态
当前进程处于什么状态
优先级
前台进程(交互式进程)优先级更高
批处理(后台进程)
IO状态信息
每一个进程里面都会打开很多的文件,打开文件就要进行管理
记录描述文件,所以需要保存下来这些信息
记账信息
一个进程大致在cpu上运行了多长时间
进程查看
ps
-ef
-aux 查看系统所有进程信息
- /proc 保存系统中正在运行的程序信息
- pid_t getpid() 获取调用进程的pid
- 根目录下的proc/目录存放的就是当前操作系统上面正在运行中的程序的运行信息
例如
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{ while(1){ sleep(1); } return 0;
}
通过系统调用获取进程标示符
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{ printf("pid: %d\n", getpid()); printf("ppid: %d\n", getppid()); return 0;
}
进程创建
创建进程就是创建pcb
用fork创建进程
fork()—通过复制调用进程(父进程)创建一个新的进程(子进程)
子进程与父进程完全相同
head line 打印了一次
tail line 打印了两次
复制了父进程的pcb(意味着和父进程拥有一样的内存指针,程序计数器,上下文数据):
和父进程运行相同的代码,相同的运行位置,
处理一样的数据
父子进程代码共享,数据独有 。
同一个内存区域,打印的值相同
子进程创建成功都是从下一步指令开始运行
如何分辨父子进程:通过返回值
父子进程不一定谁先运行,要看cpu调哪个pcb
父进程:
返回子进程的pid,pid>0
子进程:
返回0
失败:
返回-1
为什么要创建子进程?意义何在?
- 分摊压力,cpu资源足够的情况父子进程同时处理数据,效率高
- 希望子进程完成其他的任务
进程状态
普遍的系统的三种状态
就绪,运行,阻塞
Linux进程状态:
- 运行态(R)
- 可中断睡眠态(S)
- 不可中断睡眠态(D)
- 停止态(T)
- 僵死态(Z)
- 死亡态(X)
- 追踪态(t)
加号代表前台进程
cpu使用率非常高,什么原因?
死循环。
杀死进程
kill 进程ID
普通杀死进程杀不死停止态进程
要用强杀
kill -9 进程ID
僵尸进程:
处于僵死态的进程----进程退出后,资源没有完全释放(没有完全退出)
强杀都杀不死
如何产生?
子进程先于父进程退出,将自己退出原因保存在pcb中,操作系统检测到子进程退出,因为父进程有可能关注退出原因,所以不敢随意释放所有资源,通知父进程子进程的退出,但是这时父进程可能正在打麻将,没有关注到这个通知,导致子进程退出了
但是资源一直没有 释放,处于僵尸进程,处于僵死状态,成为僵尸进程。
危害:资源泄露,一个用户能够创建的进程是有限的,导致新进程创建失败
处理:干掉父进程
如何避免:
进程等待
孤儿进程:
父进程先于子进程退出,子进程成为孤儿进程,运行在操作系统后台,父进程成为1号进程(被领养)
孤儿进程的使命就是不断奋斗最后成为守护进程
守护进程/精灵进程
特殊的孤儿进程 一个特殊的孤儿进程(脱离终端,脱离登会话的孤儿进程)
进程优先级
通过一个评级来决定一个进程的cpu资源优先分配权
为了让计算机运行的更加合理
(因为进程的性质各有不同—批处理/交互式)
查看:
ps -l
修改:优先级无法直接修改,但是可以通过修改NI的值,来调整PRI的值
PRI=PRI+NI
renice程序运行后修改 (nice的范围(-20~19))
Renic -n ni_val -p pid
nice程序运行时指定
nice -n ni_val ./main
优先级调整更多的是针对cpu密集型程序(对cpu资源要求比较高)
磁盘密集型程序因为本事呢对cpu资源要求不是很高,因此大多数情况下,没必要调整
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI :代表这个进程可被执行的优先级,其值越小越早被执行 NI :代表这个进程的nice值
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,在Linux下,就是调整进程nice值 nice其取值范围是-20至19,一共40个级别需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进 程的优先级变化。可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice
top 进入top后按“r”–>输入进程PID–>输入nice的值
竞争性:
系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
独立性:
多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行:
多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发:
多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发
环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
就是内存解释
和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
常见环境变量:HOME SHLL PATH
通过第三方变量environ获取
**int argc 参数个数
char argv[] 字符串指针数组放的是参数
char env[] 这个字符串指针数组所保存的就是环境变量
#include <stdio.h>int main(int argc, char *argv[], char *env[]){ int i = 0; for(; env[i]; i++){ printf("%s\n", env[i]); } return 0; }
通过系统调用获取或设置环境变量
#include <stdio.h> #include <stdlib.h>int main()
{ printf("%s\n", getenv("PATH"));
return 0; }
环境变量通常是具有全局属性的
环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h> #include <stdlib.h>
int main()
{ char * env = getenv("MYENV"); if(env){ printf("%s\n", env); } return 0;}
直接查看,发现没有结果,说明该环境变量根本不存在
导出环境变量 export MYENV=“hello world”
再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?
子进程崩溃了,对shell本身没有影响
程序地址空间
为什么要用虚拟地址空间+页表:保持进程独立性+充分利用内存+内存访问控制
段页式内存管理:段号+段内地址+页内偏移
段式内存管理:段号+段内地址
页式内存管理:页号+页内偏移
#include <stdio.h> #include <unistd.h> #include <stdlib.h>
int g_val = 0;
int main(){ pid_t id = fork(); if(id < 0){ perror("fork"); return 0; }else if(id == 0){ //childprintf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); }else{ //parent printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } sleep(1); return 0; }
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变 量进行进行任何修改。可是将代码稍加改动:
#include <stdio.h>#include <unistd.h>#include <stdlib.h>int g_val = 0;int main()
{
pid_t id = fork(); if(id < 0){ perror("fork"); return 0; } else if(id == 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统一管理
OS必须负责将 虚拟地址 转化成 物理地址 。
地址:内存区域的编号
-----进程的虚拟地址空间—内存描述符----mm_struct
操作系统通过mm_struct这个结构体给进程描述了一个虚拟的地址
如何描述:
mm_struct{
ulong size;
ulong code_start;
ulong code_end;
ulong data_start;
ulong data_end;
}
为什么要使用虚拟地址空间虚拟地址空间+页表
通过页表进行映射,页表可以进行标记,当前地址是可读还是可写
提高内存利用率
对内存访问进行控制
保证进程独立性
虚拟内存的方式
写时拷贝技术:提高子进程创建效率
父进程创建了子进程,但是并没有直接给子进程开辟内存,拷贝数据,
而是跟父进程映射到同一位置,
但是如果内存中数据发生的改变,那么对于改变的这块内存,
需要重新给子进程开辟内存,并且更新页表信息。
进程O(1)调度方法
一个CPU拥有一个runqueue
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!实时优先级:0~99(不关心)
活动队列
时间片还没有结束的所有进程都按照优先级放在该队列 nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个 比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列
过期队列和活动队列结构一模一样过期队列上放置的进程,都是时间片耗尽的进程 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
-
active指针永远指向活动队列
-
expired指针永远指向过期队列
-
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在 的。
-
没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活 动进程!
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增 加,我们称之为进程调度O(1)算法