文章目录
1. OS的服务理解
冯诺依曼是计算机的逻辑结构,下图是对计算机理解的层状结构:
操作系统为什么要给我们提供服务?计算机和OS设计出来就是为了给人提供服务的!
想printf或者cout这样的函数是没有资格向硬件写入数据的,因为硬件是通过OS一系列驱动程序管理的,所以打印函数肯定又要通过操作系统的一系列程序才能到底显示器等硬件上!
2. OS如何提供服务
操作系统是不相信任何人的!不会直接暴露自己的任何数据结构,代码逻辑等其他数据相关的细节!!
操作系统通过系统调用的方式对外提供接口服务的!
Linux操作系统是用C语言写的,这里所谓的接口本质就是C函数!
我们学习系统编程,本质就是在学习这里的系统接口。
还是银行的例子,对于老人来说,与窗口内的工作人员交流存在障碍,这时就有接待人员来询问老人的需求,并帮助老人完成操作。
对于编程小白来说就有图形化界面和命令行解释器这样的东西帮助他们来完成对操作系统的接口调用。
对于初级工程师就有lib这样的库来帮助他们完成系统调用并进行一系列开发操作。
Windows的系统接口和Linux的系统接口一样吗?
肯定不一样,那我们写的C语言代码为什么都能在两个平台上运行了,实际上就是lib库在起作用:在Linux下就调用Linux的系统接口函数,在Windows下就调用Windows的系统接口函数。这不就是多态嘛。
3. 见一见系统接口
我们自己写的代码,编译成为可执行程序启动后就是一个进程!别人写的程序(ls pwd touch等)也算进程,只不过执行速度太快我们观察不到而已。
查看进程的第一种方法:
ps axj
ps axj | grep '可执行程序' //筛选''里的进程
ps axj | grep '可执行程序' | grep -v grep
第二种方法:
在ls /路径下有一个proc:内存文件系统,它会存储当前系统实时的进程信息!
每个进程在系统中都会存在一个唯一的标识符:pid!就像我们学号一样!
重新启动的程序pid也会更新。我们以前所说的当前路径也是在进程中维护的!如果我们把可执行程序move到另一个路径下,那么当前路径也会进行更新!
ps ajx | head -1 && ps axj | grep 'mytest' | grep -v grep //查找''的pid
ls /proc/pid -d //会显示进程的很多属性
ls /proc/pid -al //可以看到当前工作路径和可执行程序所在的路径
那么pid、当前路径这些东西都在哪里?实际上它们都是进程的内部属性!所以它们都在进程的进程控制块PCB(task_struct)结构体中!
3.1 getpid(第一个系统调用接口)
我们以前是通过无脑ctrl+c来杀死进程,现在我们还可以运行以下命令杀死进程:
kill -9 pid //9号信号我们以后再讲
3.2 getppid
ppid代表父进程。创建进程有很多方法, ./可执行程序。
为什么我的父进程不变?是谁呢?
几乎我们在命令行上执行的所有指令(你的cmd),都是bash进程的子进程!
3.3 fork()
代码创建子进程 fork()
fork函数是用来创建子进程的,它有两个返回值:
如果创建成功给父进程返回子进程的pid,给子进程返回0,也就是说调用后会有两个进程。为什么?
同一个id值,使用打印没有修改,却打印出来了不同的值???回答不了!进程地址空间中再来回答!
fork如何做到会有不同的返回值?
C语言中if和else可以同时执行吗?有没有可能两个以上的死循环同时运行?
fork之后,父进程和子进程会共享代码,一般都会执行后续的代码–print为什么会打印两次的问题
fork之后,父进程和子进程返回值不同,可以通过不同的返回值判断,让父子执行不同的代码块!!
3.4 两个问题
- fork()为什么给父进程返回子进程的pid,给子进程返回0?
父:子 = 1:n (n>=1)
父进程必须有标识子进程的方案,fork之后,给父进程返回子进程的pid!
子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低!getppid()
- 为什么fork会返回两次?
fork是OS的系统调用函数,fork之后系统多了一个进程
task_struct + 进程代码和数据
task_struct + 子进程的代码和数据
子进程的task_struct对象 内部的数据基本是从父进程继承下来的。
而子进程执行代码,计算数据,子进程的代码从哪里来呢?
和父进程执行同样的代码,fork之后,父子进程代码共享!而数据要各自独立!
这样不同的返回值就能让不同的进程执行不同的代码!
如何理解进程被运行?
return语句也是代码,父子进程共享代码,子进程运行时也会执行该语句!
4. 进程状态
以后凡是说进程,必须先想到进程的task_struct。所谓进程状态实际上就是一些整数,代表不同的含义而已。这些整数就存储在task_struct中。
打个比方:
int status
#define RUN 1
#define STOP 2
#define SLEEP 3
4.1 运行态
每个CPU都会维护(通过task_struct中的指针)一个运行队列。那么运行态是指进程正在CPU上运行,还是进程只要在运行队列中就叫做运行态呢?
答案是运行态是指进程只要在运行队列中就叫做运行态!代表我已经准备好了,随时可以调度!
4.2 终止态
终止状态是指这个进程已经被释放就叫做终止态?还是指该进程还在,只不过永远不运行了,随时等待被释放呢?
答案是指该进程还在,只不过永远不运行了,随时等待被释放
那么问题来了:进程都终止了,为什么不立即释放对应的资源,而要去维护一个终止态呢?
释放要花时间吗?有没有可能,当前的操作系统很忙呢?
4.3 进程阻塞
进程阻塞概念:进程等待某种资源(CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,进程所处的状态就叫做阻塞!
(1) 一个进程使用资源的时候可不仅仅是在申请CPU资源!
(2) 进程可能申请更多的其他资源:磁盘、网卡、显卡、显示器资源、声卡/音响
如果我们申请CPU资源暂时无法得到满足,需要排队的-----运行队列
那么如果我们申请其他慢设备的资源呢 — 也是需要排队的!(task_struct在进行排队)
对进程的管理任务是交给操作系统完成的:当进程访问某些资源(磁盘网卡),该资源如果暂时没有准备好,或者正在给其他进程提供服务。此时:
(1) 当前进程要从runqueue中移除
(2) 将当前进程放入对应设备的描述结构体中的等待队列!
当我们的进程此时在等待外部资源的时候,该进程的代码不会被执行啦!------> 我的进程卡住了 ------> 进程阻塞
而当其他慢设备重新有空时(重新被唤醒),阻塞的进程会从等待队列重新回到运行队列执行代码!
4.4 进程挂起
当操作系统对大量进程进行管理时,如果内存不足了怎么办?
操作系统就要帮我们进行辗转腾挪
短期内不会被调度(你等的资源短期内不会就绪)的进程,它的代码和数据依旧在内存中!就是白白浪费空间!OS就会把该进程的代码和数据置换到磁盘(swap分区)上!这就叫进程挂起!
往往内存不足的时候,伴随着磁盘被高频率访问!
5. Linux下进程状态
static const char *task_state_array[] = {
"R(running)", /* 0 */
"s(sleeping)", /* 1 */ //阻塞状态:浅度睡眠,可中断睡眠,随时可唤醒或者杀掉
"D(disk sleep)", /* 2 */ //阻塞状态:等待磁盘资源,D状态深度睡眠不可被中断
"T(stopped)", /* 4 */
"t(tracing stop)", /* 8 */
"Z(zombie)", /* 16 */
"X(dead)", /* 32 */
};
5.1 S和R状态
我们平时调用printf函数通常都是在等待显示器就绪,所以一般情况都处于S状态(阻塞状态)。
如何模拟一个运行态:只要不访问外设就不会被阻塞,所以单纯写一个死循环或者死循环进行一些计算就可以了。此时就是R状态。
int main()
{
while(1)
{
//10+10;
}
}
5.2 D状态
关于D:深度睡眠,不可被中断睡眠的阻塞状态
假设有一个进程需要向磁盘写入数据,而磁盘写入数据,进程等待磁盘的返回信息(是否写入成功)时:
如果该进程的状态被设置为S状态,当服务器压力过大时,OS是会终止该进程的!所以我们应该把该进程设置为D状态!
D状态就是为了防止操作系统做出这一行为。
所以这三个人都没有错,但是却造成了不好的结果。
如果计算机存在大量D状态的进程可能无法关机,强制断电可能会对软件有所伤害。
5.3 Z和X状态
当一个Linux中的进程退出的时候,一般不会直接进入X状态(死亡,资源可以立马回收),而是进入Z状态,至少PCB不会释放。
为什么?进程为什么被创建出来呢??一定是因为要有任务让这个进程执行,当该进程退出的时候,我们怎么知道,这个进程把任务给我们完成的如何了呢??一般需要将进程的执行结果告诉父进程 OS
进程Z状态就是为了维护退出信息,退出信息会写入PCB(task_struct),可以让父进程或者OS读取的!通过进程等待来读取的!!至于如何等待我们以后再讲!
5.4 如何模拟僵尸进程
如果创建子进程,子进程退出了,父进程不退出,也不等待子进程,子进程退出之后所处的状态就是Z僵尸状态。
我们执行上述代码运行以下命令就可以实时查看该父子进程的状态
while : ; do ps ajx | head -1 && ps ajx |grep process |grep -v 'grep\|worker\|master\|cache'; sleep 1; echo "################################################################"; done
运行程序后大概过了5s,子进程变为僵尸状态,defunct表示失效的。
如果没有人回收子进程的僵尸,该状态会一直维护!该进程的相关资源(task_struct)不会被释放!内存泄漏!
一般必须要求父进程进行回收(后面说!)
僵尸进程的危害:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空
间! - 内存泄漏?是的!
- 如何避免?后面讲
5.5 孤儿进程
如果父进程的父进程(这里假设就是bash)把父进程直接回收走了,那子进程就成为了孤儿进程,以后怎么回收呢?
如果父进程提前退出,子进程还在运行,子进程会被1号进程(操作系统)领养。
状态后面跟+表示这个进程是前台进程,能够ctrl+c掉的都是前台进程。
而孤儿进程没有+号是后台进程不能用ctrl+c掉,得用命令行:
kill -9 pid
5.6 T状态
“T(stopped)” 和 "t(tracing stop)"都是暂停态,具有功能性。其中tracing stop是进程被调试时,遇到断点所处的状态!
运行以下命令就可以模拟暂停态:
kill -19 pid
19信号就是暂停信号
这个时候该进程就变成了暂停状态:
发送18信号可以让该进程继续:
kill -18 pid
我们还可以用gdb打断点观察t状态:
6. 进程优先级
6.1 优先级 vs 权限
权限谈论的是能还是不能的问题!优先级谈论的是能只不过是先还是后!
优先级是进程获取资源的先后顺序!
6.2 为什么存在优先级
排队的本质叫作确认优先级。
那我们为什么要排队?资源不够!!!
系统里面永远都是进程占大多数,而资源是少数!这就直接决定了进程竞争资源是常态!所以一定要确认先后!
6.3 Linux下的优先级概念和操作
Linux下的优先级由PRI和NI(nice值)组成,用ps -al就可以查看。
Linux不允许进程无节制的设置优先级:修改优先级得修改NI值。
prio = prio_old + nice
每次设置优先级prio_old都会被恢复为80!
nice值的取值范围是-20至19,一共40个级别。
所以pri的取值范围就是60至99
修改优先级的命令
sudo top
进入top后按“r”–>输入进程PID–>输入nice值
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
并行:如果存在多个CPU的情况,在任何一个时刻,都有可能有两个进程在同时被运行(就是在CPU上运行)
并发:我的电脑是单CPU的,但是我的电脑中有各种进程都可以在跑啊?
多个进程都在你的系统中运行 != 多个进程都在你的系统中同时运行
不要以为进程一旦占有CPU,就会一直执行到结束,才会释放CPU资源!我们遇到的大部分操作系统都是分时的!
操作系统会给每一个进程在一次调度周期中,赋予一个时间片的概念!
这一个时间段内,多个进程都会通过切换交叉(比如每个进程10ms一个周期)的方式让多个进程代码,在一段时间内都得到推进,这种现象,我们叫做并发!
6.4 问题1
进程运行具有独立性,不会因为一个进程挂掉或者异常,而导致其他进程出现问题!
进程是内核结构(PCB)+代码和数据,那么是如何做到进程具有独立性的?
我们在进程地址空间解决这个问题!
6.5 问题2
操作系统就是简单的根据队列来进行先后调度的吗??有没有可能突然来了一个优先级更高的进程??
抢占式内核!:正在运行的低优先级进程,但是如果来了个优先级更高的进程,我们的调度器会直接把进程从CPU上剥离,放上优先级更高的进程,这个就叫进程抢占.
7 OS的O(1)进程调度算法
活动队列
-
时间片还没有结束的所有进程都按照优先级放在该队列
-
nr_active: 总共有多少个运行状态的进程
-
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
-
从该结构中,选择一个最合适的进程,过程是怎么的呢?
(1)从0下表开始遍历queue[140]
(2). 找到第一个非空队列,该队列必定为优先级最高的队列
(3). 拿到选中队列的第一个进程,开始运行,调度完成!
(4). 遍历queue[140]时间复杂度是常数!但还是太低效了 -
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
到选中队列的第一个进程,开始运行,调度完成!
(4). 遍历queue[140]时间复杂度是常数!但还是太低效了
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
- 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法