进程
- 一.冯诺依曼体系结构
- 二.操作系统
- 1.管理的概念
- 2.系统调用和库函数概念
- 三.进程
- 1.先描述
- 2.再组织
- 四.Linux里的PCB
- 1.概念
- 2.理解当前路径
- 3.PID
- 1.kill指令
- 2.获取自己的PID
- 4.初识fork函数
- 五.进程状态
- 1.一般意义上的
- 1.运行
- 2.阻塞
- 3.挂起
- 2.Linux内核里的状态
- 1.运行态
- 2.阻塞态
- 3.暂停态
- 4.僵尸状态
- 5.孤儿进程
- 六.进程优先级
- 1.概念
- 2.查看进程优先级
- 3.top调整优先级
一.冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:
1.输入单元:包括键盘, 鼠标,扫描仪, 写板等 。
2.中央处理器(CPU):含有运算器和控制器等 。
3.输出单元:显示器,打印机等。
关于冯诺依曼,必须强调几点:
1.这里的存储器指的是内存。
2.不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。
3.外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
4. 一句话,所有设备都只能直接和内存打交道。
二.操作系统
1.管理的概念
总结:操作系统通过驱动程序来获取数据进而实现对软硬件的管理。
但如果操作系统管理所有数据的话,很显然数据量太大了,但由于所有的硬件都有公共属性,所以可以将它们组合成对象,最终操作系统就可以从对数据的管理变成对数据结构的管理了。我们把这种方式称为先描述再组织。
结论:在操作系统中,管理任何对象,都可以转化成对某种数据结构的增删查改。
操作系统就是一款搞管理的软件。
2.系统调用和库函数概念
根据上文可以知道显示器是硬件,那么我们平常写c语言里的printf将数据打印到显示器上是怎么实现的呢?毫无疑问是必须通过操作系统的。库函数与系统调用一定是上下层的关系。
三.进程
简单理解:一个已经加载到内存里的程序叫做进程(任务)。进程=PCB+自己的代码和数据。
1.先描述
一个操作系统可以,不仅仅只能运行一个进程,可以同时运行多个进程。就意味着操作系统必须将进程管理起来,那么它是如何管理的呢?
核心思路先描述再组织。任何一个进程,在加载到内存成为真正的进程的时候,操作系统都要先形成描述进程(属性)的结构体对象–PCB(进程控制块)。本质上PCB就是进程属性的集合。而操作系统是用c语言写的,所以PCB是用struct封装的。
在加载进程时,不仅要把你自己的代码和数据加载到内存里,还要把PCB也加载进去。而这两者相结合才能成为一个进程。操作系统对进程进行管理就可以直接管理PCB。
2.再组织
对多个进程进行管理
PCB是一个struct对象,里面可能存放着进程编号,进程状态,优先级,其他PCB指针等属性,而操作系统对多进程管理就可以通过PCB指针,形成链式结构,像管理链表一样进行增删查改,这就是再组织。
四.Linux里的PCB
1.概念
Linux下的PCB是task_struct。task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_struct内容分类
2.理解当前路径
接下来简单写一个程序
接着在运行后新开一个标签页来查看进程(ps:如果下面进程PID有变化是正常现象,因为每启动一次程序,PID大概率会改变)
可以使用ps指令或者查看proc目录来查看进程。
这里面的蓝色字体全部都是目录,每一个数字就是当前进程的PID。可以看到myprocess的PID是26117,接着进入这个进程。
进入进程
而圈着的就是当前路径。一般我们创建一个文件只写文件名,它会默认将文件放在当前路径下。因为进程的PCB里包含了当前路径的信息,而在打开或者创建文件时,默认会将当前路径拼接在文件前面。
3.PID
上文提到PID是进程的唯一标识符,那么PID有什么作用呢?
1.kill指令
可以使用kill-9将指定进程强行停止。
2.获取自己的PID
PID存放于task_struct里,而task_struct是Linux里的一种数据结构,而操作系统并不信任用户,不能跳过操作系统直接访问task_struct,必须通过系统调用接口。而这个调用接口是getpid。
PPID是父进程
当运行一个进程时,bash会将该进程变成自己的子进程,但即使子进程终止也不会影响bash进程。
4.初识fork函数
测试
我们可以发现fork之后的代码被执行了两次。我们再次阅读手册,可以看到fork的返回值是paid类型,它的作用是创建一个新的进程。如果调用成功,它返回的一个子进程的PID给父进程返回0给子进程。那么为什么它有俩个返回值呢?接着改进代码再测试。
结果是不断打印父进程和子进程,说明再同一份代码里,id>0和id=0是同时成立的,也说明有两个死循环在同时运行。简单理解,在执行了fork指令后,该进程产生了一个分支,父进程是它自己,子进程是分支。
分析
第一个问题。返回不同的返回值是为了进行区分,让不同的执行流执行不同的代码块。一般而言,fork之后的代码父子共享(参考上文被执行了两次的代码)。一个父进程可以有多个子进程,为了能够识别子进程,所以给父进程返回子进程的PID用来标识子进程的唯一性。而一个子进程只能有一个父进程,故不需要进行标识。
第三个问题。fork创建了一个子进程,而进程=内核数据结构+代码和数据。所以首先要创建自己的task_struct,而它的创建是以父进程作为模板,再对部分属性进行修改。子进程创建时没有代码和数据,只能访问父进程的代码和数据,这就是为什么fork之后父子进程代码共享。而为什么要创建子进程呢?是为了让父子进程执行不同的代码块从而进行协同,所以在fork函数设计上具有了不同的返回值。
第二个问题。首先fork是一个系统调用函数,在执行它时会进入到操作系统内创建子进程。而在创建子进程时,会进行创建子进程PID,填充子进程内容等操作,将这一系列操作完成后会进行返回。关键是在进行返回时子进程已经创建完毕而代码又是父子共享,所以会有两次返回。
第四个问题。一个概念,在任何平台进程在运行时具有独立性。而又由于数据能够被修改(代码不能修改),所以不能让两个进程共享同一份数据。但由于子进程没有任何数据,所以它有需要将父进程数据进行拷贝。但子进程并不一定会使用父进程里的数据,如果全部拷贝势必会造成资源浪费,所以操作系统做了一些修改。在子进程刚创建时代码和数据都是共享,当子进程需要修改数据时,操作系统额外开辟一块空间,在新空间里写入,这种技术被称为父子进程间在数据层面上的写时拷贝。所以在进行return写入的时候,子进程进行了写时拷贝,这样就保证了返回值不同。
总结
fork创建了一个子进程,而子进程在创建之初与父进程共享代码和数据。由于父和子是两个进程会执行两次return,在两次返回时对PID发生写时拷贝,让父子进程的PID变成不同的值,所以后续我们就可以通过对不同的PID进行分流,让它们执行不同的代码块。
回到前文的bash进程,bash是通过fork函数创建子进程来运行。
五.进程状态
1.一般意义上的
以下主要介绍运行,阻塞,挂起三个状态。
1.运行
Linux内部可以同时有多个进程,每个进程通过双链表进行链接,这样链接起来的队列叫做运行队列。所以只要我们能够找到头节点,就能够调用所有进程。那么毫无疑问每个cpu里需要有一个头节点,它会维护一个runqueue(运行队列)的结构体,队列里包含了很多属性,最重要的就是head和tail。这样如果我们需要运行一个进程,直接将它拿到头部就可以了。
而多个进程毫无疑问会强占CPU资源,这时就会有一个调度器(一种函数)得到所有进程的参数之后就可以更好的进行资源分配了。
运行态
凡是处于运行队列里的进程都被称作运行态(R态)。
一个进程只要放到CPU上去那是否意味着必须跑完才会被CPU放下呢?
当然不是。每个进程里都有一个叫做时间片的概念。比如时间片是10ms,那么该进程最多在CPU里运行10ms,然后放到队列尾部。最终呈现的效果就是,在一个时间段里,每一个进程都会被调用。对于这种情况,又被叫做并发执行。在这段时间里必然存在着大量将进程拿去和放下的操作,这种情况叫做进程切换。
2.阻塞
每个CPU都连接着各种各样的外设,不论是什么硬件,操作系统都会先描述再组织。
接下来有一个进程要从键盘里读取数据,当前该进程等待键盘输入数据,但我却一直不按下键盘。那么这个进程就绝对不能放到运行队列里,因为想要访问的软硬件资源没有就绪。这个进程就会被链接到键盘资源里,如果又有一个进程需要从键盘读取资源,那么这个进程又会链接到上个进程后面。这个链接起来的队列就是waitqueue(等待队列)。如果键盘输入了数据,那么该进程就会自动进入运行队列了。
阻塞态
总结:把这种等待某种特定设备的进程称为该进程的阻塞状态。每一个设备都有等待队列。
3.挂起
阻塞挂起态
如果操作系统内存资源严重不足了,那么它会保证正常运行的情况下,省出内存资源。而阻塞队列在等待过程中里面是没有数据的,那么此时操作系统就有可能将等待的进程的代码交还到外设里,只保留PCD,这个过程就叫做内存数据的换出;而当该进程的资源就绪后,CPU又会将代码拷到运行队列里,这个该过程叫做换入。而在这个过程里,代码和数据并没有在内存中,这个状态就叫做挂起状态。
2.Linux内核里的状态
一个进程的状态与它的代码和数据无关。
Linux内核代码定义的状态
1.运行态
R态就是运行态,一个例子
有个很奇怪的现象,明明程序在运行,但它显示的却不是R态而生S态。接下来把打印都注释掉。
这时这个进程就变成R态了,是怎么回事呢?这是因为CPU的速度很快,所以我们在进行printf打印的时候,CPU大部分时间都是在等待外设资源,所以我们看到的是等待态。这里的R+代表的是当前进程在前台运行。
2.阻塞态
S和D态都是阻塞态
S态被称为浅度睡眠,可以被直接唤醒。D态在Linux里被称为深度睡眠,处于不可被唤醒状态,它也不可被操作系统所响应。
3.暂停态
T和t状态就是暂停态
可以使用Kill-19暂停进程,kill-18重新运行。之前的kill-9是强制结束进程。
4.僵尸状态
Z状态
1.僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
2.僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
3.所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
exit
这个函数的作用就是让一个正常运行的进程直接终止,具体参数现在知识储备不足,之后会更新。
一个例子
在第5秒的时候,有一个进程从S态变到了Z态。并且可以看到后面有个defunct(失效的)单词。对于这种已经失效但在等待父进程回收的状态就叫做僵尸状态。
进程一般退出的时候,如果父进程没有主动回收子进程信息,那么子进程会一直让自己处于Z状态,进程的相关资源尤其是task_struct不能被释放。这样就会造成内存泄漏问题。
5.孤儿进程
这次让父进程先退出看看会发生什么
我们发现过了一会,父进程消失了,子进程还在。并且该子进程的PPID还变成了1。这里的1是什么呢?
其实就是systemd,也就是操作系统本身。所以可以得出,如果父进程先退出,那么子进程的父进程会被改为1号进程(操作系统)。对于这种进程我们称为孤儿进程,该进程被操作系统领养。
为什么要被领养呢?因为孤儿进程也需要被释放。
六.进程优先级
1.概念
是什么
优先级与权限的区别。权限决定的是能不能,而优先级是谁先谁后的问题。
为什么
因为资源是有限的,进程是有多个的,注定了进程之间有竞争—竞争性。
操作系统必须保证大家进行良性竞争,就必须确认优先级。如果进程长时间得不到CPU资源,该进程代码长时间无法执行–进程的饥饿问题
基本概念
1.cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2.优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能(谨慎)。
3.还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整 体性能。
2.查看进程优先级
优先级可以调整。那么我们是否可以任意更改nice值,大大的提高优先级呢?技术上是可以实现的,但Linux的调度器设计者并不想让用户过多的参与优先级的调整,所以nice值只能在【-20,19】之间。优先级默认是从80开始。
3.top调整优先级
进入root用户,再输入top
按r,输入要调整进程的PID
输入调整的值
更改成功