前言
在了解进程概念之前,还有很多东西需要我们先了解,以助我们更好地学习以后的知识
大部分都是概念的东西,大部分大概有个印象就好了
冯诺依曼体系结构(硬件方面)
当代计算机的设计都是按照冯诺依曼体系结构设计(大概结构为下图)的
那么它是有什么魔力呢?可以让那么多的计算机都按照这个结构设计
首先我们要知道,cpu处理数据的能力是最快的,内存次之,外设(磁盘等)最慢
真因此价格也是从高到低的
一台电脑从理论上来说可以全部由cpu组成,因为几乎每个硬件都有存储数据的能力,但是那样计算机的价格就太高了
如果要使计算机被广泛推广并使用,一定要价格合适且效率不错,冯诺依曼体系结构就做到了这点
我们首先要了解一下木桶效应,简单的来说就是一同水的容量多少取决于最小的那块木板
所以如果cpu直接和外设交互,就会导致整机效率过低
一般都是外设的数据加载到内存中,cpu与内存交互并处理数据,然后再返回给内存,再传到输出设备中去
有了以上的了解,我们就能明白为什么以前总是听说程序在运行前要被加载到内存中去
因为程序=代码+数据,本质上就是一个文件,被存储到磁盘中,想被cpu处理就要被加载到内存中
粗谈操作系统
什么是操作系统?
简单来说操作系统就是进行软硬资源管理,并为用户提供安全,稳定,高效的使用环境的一款软件(内存中第一个加载的软件)
我们先来看下三段(操作系统,驱动程序,硬件设备)之间是怎么进行资源管理的
操作系统,驱动程序,硬件设备可以形象地理解为校长,辅导员,学生
如果直接由校长直接管理学生那校长的负担会很大,但是如果只用管理几个辅导员,由辅导员去管理学生,校长的工作量就可以骤减了
先抛出一个结论:管理的本质不是管理一个人,而是管理数据!!!
校长原本愁与管理学生,但是学了编程以后创建了struct结构体,里面包含学生的各种属性,然后还有指针将每个结构体相连成链表,这样对学生的管理就变成了对链表的增删查改
管理方法:先描述(属性赋值),再组织(数据结构)!!!
人是通过属性认识世界的;重要的属性的集合就代表这个人
谈完了下三层,我们再来看上三层(为用户提供高效稳定安全的使用环境)
用户有很多,有的可能是普通用户,有的可能是开发者,有的可能是破坏者
所以操作系统为了安全的环境,实行一刀切政策,不信任任何用户,任何用户只能通过系统调用来使用操作系统
这和银行很像,银行不信任任何客户但是仍然为我们提供服务,所以设置了防弹玻璃保护自己,设置了一个小窗口(系统调用)用来处理业务
但是系统调用的学习成本太高,不利于用户使用,站在开发者的角度上,可以将系统调用封装成各种函数接口,但是一个函数由不同的人来写就有不同的函数接口,所以开发者将函数接口封装成各种各样好用的函数,也就是库,以后的开发者就不用自己再写了,直接调用即可
注意:想访问底层的os数据或者硬件,一定要贯穿整个层状结构
进程
进程和程序很容易混淆,进程是在内存中的程序吗?当然不是!
进程=可执行程序+内核数据结构(PCB)
进程在内存中,操作系统理所应当也要对其进行管理,怎么管理呢?先描述再组织!
描述就是各种属性加上拷贝可执行程序的代码,组织就是将其组织成链表结构
当然一个进程的pcb可以不止在一张链表(一个等待队列中)里,也可以在其他的数据结构中
这样对进程的管理,就变成了对链表的增删查改——本质就是一个建模的过程
我们在Linux中平时敲的指令以及各种运行的程序都是进程!!!!
查看进程
每个进程都要有自己的工作目录
进程的信息可以通过 /proc /进程id 来查看
getpid()——获取进程id
getppid()——获取父进程id
也可以通过ps(ps axj)和top这种用户级别软件来查看(一般配合grep指令和管道进行操作)
grep指令:grep 文件名——找出含有文件名名字的文件
grep -v 文件名——找出不含有文件名的文件
通过系统调用fork创建进程
在外出旅行的时候我们应该试过在车上一边下载视频一边播放视频以打发时间,这里就是两个进程在同时工作,也反映了创建子进程的原因——让子进程去做和父进程不一样的事,以更高地服务我们
格式:pid_t fork()
在fork后一般用if进行分流,fork会给父进程返回子进程的pid,给子进程返回0
接着我们抛出几个问题,下面再一一解释
1.fork干了什么
2.为什么fork有两个返回值
3.为什么fork的两个返回值,会给父进程返回子进程id,给子进程返回0
4.fork之后,父子进程谁先运行
5.如何理解同一个变量却有不同的值(待解决...)
首先有了上面进程的概念,fork创建子进程后,子进程是没有代码和数据的,系统会以父进程的pcb为模板拷贝一份pcb出来,这时父子进程共享代码。
但是如果子进程要修改数据的话也会影响到父进程的数据,所以这里的数据采用写时拷贝的方法进行存储
既然共享了代码,所以fork之后,父子进程会执行fork以下的代码,如果要让他们干不同的话,就要用if进行分流
那fork之前的代码呢?fork之前的代码子进程也是可以看到的
在一个函数中,return是一个语句吗?当然是!所以父子进程都会return一次,也就给了fork两个返回值(这里先这样理解,真实情况是os通过寄存器做到的返回两次)
因为父与子是1:n的关系,所以要给父进程返回子进程的id以便知道孩子有哪些,子进程没有孩子所以返回0
那么谁先运行呢?这个由os,pcb中的调度信息(优先级,时间片)等等决定
父子进程是相互独立的,父进程挂掉不会影响子进程,子进程挂了不会影响父进程
关于最后一个问题要学了后面的内容才能更好的理解
进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态
怎么知道呢?通过访问pcb里的一个变量数据——所以进程的状态修改本质上就是修改pcb里的存储进程状态的变量数据,并将其pcb列入不同的队列中
运行状态
如字面意思,就是各种资源已经准备好了,在运行队列当中了,随时可以被调度了
只要在运行队列中的进程,都是运行状态
每个cpu中都一定会设置一个运行队列
阻塞状态
我们写的代码,或多或少都要访问一些资源(键盘,磁盘,网卡等),但是当资源没有就位的时候,代码就无法继续往下运行,这个时候我们的进程就卡住了,
挂起状态(阻塞挂起)
当位于阻塞状态的进程越来越多时且os内存资源严重不足的时候,我们的电脑就会很卡,为了不让os挂掉,我们就要清理掉一些内存空间
所以当os内存资源不足的时候,处于阻塞状态的进程的代码段和数据会被交互到磁盘中的swap区域,以清理内存空间
当进程被os调度的时候,被置换出去的代码和数据又要重新被加载回内存中
linux进程的具体状态
R (running)
S (sleeping)
D (disk sleep)
T (stopped)
t (tracing stop)
X (dead)
Z (zombie)
R——其实就是运行状态,但是当代码中有大量的io操作的时候,即使代码正在运行也很难查出R状态,因为io太慢了,一直在等资源就位
状态后面的+表示前台进程——即该进程运行的时候bash的命令行无法运行,并且可以ctrl c终止
如果想要进程以后台进程的方式运行,要在后面加个&
后台进程可以输入命令行,但是不能ctrl c,如果想要终止后台进程,就要用到 killed -9 PID来终止进程
S——浅度睡眠
为阻塞状态的一种,会对外部的信号作出响应,可以被killed终止
进程在访问资源的时候,可能资源还没就位,就会被设为S状态
D——深度睡眠
为阻塞状态的一种,磁盘休眠状态,不会对外部的信号作出响应,不可以被killed终止,os也没资格
故事:假如os内的一个进程正在向磁盘写入客户的数据,但是os内已经没多少内存了,假如写入数据失败了,但是这个进程又被killed掉了,客户的信息丢失了,那么谁来负责呢?所以设置为D状态,不能被终止,除非拔掉电源
T——暂停状态(stopped)
当进程要访问一个资源的时候,可能暂时不让进程访问,就将其设为T状态——killed -SIGSTOP PID,继续运行killed -SIGCONT,暂停后自动会被换成后台进程
t——debug的时候追踪断点,在断点处让进程暂停
僵尸进程
我们为什么要创建一个进程?让一个进程来帮我们完成某项工作
我们要不要知道一个进程完成的怎么样?当然要!
所以进程在退出的时候,要有一些退出信息,表明自己把任务完成的怎么样(main函数通常都是return 0,return 1 /2/3会怎么样)
进程在退出的时候,退出信息会由os写到退出进程的pcb当中,可以允许代码和数据被释放,但是不允许pcb立即被释放,因为要让父进程或者os获取pcb里的退出信息,要是父进程或者os一直不获取呢,那就是所谓的僵尸状态(Z)了
如果一个进程Z状态了,父进程一直不回收它,他就要一直存在?是的,所以会造成内存泄漏(pcb在内存当中)
孤儿进程
当子进程的父进程直接退出了,子进程的pcb里的退出信息由谁来获取呢?这个时候就要由os接管子进程了,这里子进程被称为孤儿进程
进程的优先级
在系统中进程有那么多个,而cpu只有一个,那么哪些进程优先被调度就取决进程的优先级
进程的优先级是什么呢?——其实就是PCB里的一个变量PRI
PRI(new)=PRI(old)+nice——PRI(old)默认为80,nice范围为-20到19,可以理解为对优先级的修正
优先级越低就越先被cpu调度
为什么nice值有范围呢?如果有人一直恶意修改一个进程的nice值让其PRI一直都是最低的,那么这个进程总是要被调度,其他进程得不到调度,这是不公平的
进程之间的相关概念
独立性:每个进程相对之间都是独立的
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(每个进程经过一段时间(时间片)就会被从cpu上剥离下来,让下一个进程被调度)
进程的切换
这里我们以Linux2.6内核进程调度为例,不同系统的进程切换和调度方法可能不一样
我们电脑的cpu只有一套,而进程却有很多个,在调度进程的时候会使用寄存器,这些寄存器保存了一些进程的信息——进程的硬件上下文,在进程被切换的时候,系统将寄存器保存的这些信息拷贝到进程的PCB中,然后切换进程,再将新进程的PCB中的信息写入寄存器,以进行下一步操作
在进程切换的时候,就会设计进程如何被调度的问题
在linux2.6内核中,优先级分为实时优先级(优先级高的执行完再执行优先级低的,例如车载系统)和普通优先级。【0,99】为实时优先级,【100,139】为普通优先级,我们这里看普通优先级就好了
cpu维护了两个队列,分别为运行队列和过期队列,还有两个指针active和expired指向运行队列和过期队列
运行队列中的queue的数组下标代表优先级的大小,下标越小优先级越高
将当前进程都纳入运行队列中后开始调度,如果这个时候又有较高的优先级进程要被调度是不会插入运行队列中的(因为这样插队就会导致优先级低的一直无法被调度),而是会被纳入过期队列中相应的位置
而运行队列中被调度的进程等到时间片结束后,就会被从cpu上剥离,然后也按照优先级的顺序插入到过期队列中的相应位置,然后调度下一个进程
如此反复直到运行队列为空,然后两个指针交换一下
那么怎么判断队列为空呢?每次都遍历一遍的代价有点大,所以这里采用位图的方式——bitmap
32*5=160个比特位,如果哪个位置有进程相应的比特位就为1,不然为0
nr_active则是判断该队列中有没有进程
所以大概的流程就是先看nr_active是否有进程,有的话然后通过位图判断位置,让cpu从那个位置向下进行调度,如此反复,这样就实现了接近O(1)的调度算法
———————————————————————————————————————————
至此进程的相关知识就总结的差不多了,感谢大家支持,我也会继续输出linux相关知识