写在前面
我们现在算是开始接触到网络编程,后面的内容和我们之前学的语言有点不一样,需要更深的理解和记忆.今天的博客是关于计算机的基本结构和进程的初步认识.
冯诺依曼体系结构
学习计算机就绕不开这个知识点,我们需要有一个基本的了解。
冯诺依曼体系结构包含 以下几个部分,其他的组件都是在这个基础上发展的
- CPU (运算器 + 控制器)
- 存储器(内存)
- 输入设备 键盘,摄像头,网卡,硬盘...
- 输出设备 显示器,网卡,硬盘
我们有的时候会看到自己的电脑上存在一个叫GPU(显卡),这个为何不存在冯诺依曼体系结构中呢?实际上,我们上面所说的都是计算机必不可少的部分,GPU有没有都一样.
浅谈CPU
这个可以是人类科技的最巅峰的表现,不过现在CPU不是很贵.我们来看看我电脑的CPU,不是很好
主频
下面我标记的就是主频,他衡量的CPU执行素的的快慢,数字越大,执行速度越快
谈到这我们就可以说一下摩尔定律了(不是我们化学上的那个),他的大概意思是每隔18个月,我们芯片(CPU)的集成算力就提升一倍,这是一个经验规律.这是由intel公司的一个联合创始人摩尔提出的.现在这个规律已经濒临失效了.我们芯片单个计算单元到了一个瓶颈了,我们可以把计算单元做的很小,但是不能够做的无限小.现在的量子计算机可能会打破这一瓶颈.
说一下GPU
GPU就是我们所说的显卡,他和CPU的功能是一样的,只不过是专业方向的芯片,CPU是通用性的
计算单元
一个CPU包含了很多基本的计算单元,这些计算单元就是一个个门电路.
所谓的门电路就是针对一个比特位进行逻辑运算,这些门的运算和我们之前学习的逻辑或等是一样的
- 非门
- 与门
- 或门
- 异或门
存储器
我们先来看看存储器是什么,有的人可能把它当作内+外存,也有的说只是内存,这个看个人理解吧,我推荐看作只是内存.我们开始有点疑惑,对于计算机而言,不就是接收数据,处理数据,放出数据吗,这样的我们好象是不需要存储器.
是的,从功能上来说,我们是不需要存储器的,但是它还是存在了,主要从下面两个角度来分析的.
技术角度
CPU处理数据的速度远大于输入输出设备的速度,也就是说它们的速度跟不上,根据木桶原理,计算机的效率受到了限制.人们就开始像,我们是不是可以把一些数据提前直接放在一块内存中,用的时候再拿出来,这就是存储器的原因.我们还是有点疑惑,这样当存储器里面的数据用完后,不还是要收到限制吗,稍微用思维想一下就可以知道.所以人们又出现了局部性理论,假设我们的代码执行在第10行,我们们有理由相信下一次可能是第11,12或者第8,9行.所以操作系统提前或者一起把这块都拿到存储器中,这样就提高了效率.
成本角度
是不是存在比冯诺依曼体系效率更高的结构?s是的,但是历史没有选择它们.想一想,我们如果在CPU里面加上很大的寄存器,这样的效率可是很高的,那么我想问问这种成本是要多高,寄存器可不是一般的便宜,那么计算机就变成了奢侈品了,所以好用又便宜冯诺依曼体系被我们推崇.
操作系统
现在我们还是需要理解一下OS.OS是一款软件,它是管理硬件的.我们先来说一个不太恰当的说辞,几乎所有的硬件都只是简单的被动的执行运算,不能主动的完成某些功能,这里面OS+CPU才是真正的大佬.
什么是OS
我们先来看一下操作系统的定位.
先来举一个银行的例子.在银行里面存在桌子板凳一类的硬件,还有银行工作人员,更有一个行长,这里面就可以类比了.
我们理解到了OS是管理硬件的软件,那么接口是什么?要知道银行系统是不会相信任何人,操作系统也是,所以对于用户来说,你不需要了解原理,只需要知道怎么用就可以了,我们用的就是结构,回到正题,Linux是由C语言写的,接口就是函数,我们使用接口本质就是调用函数.
管理
上面我们谈到了管理一词,这是OS最主要的功能.我们需要好好的理解什么是管理.这里面我举一个例子,在一所高校中存在三种角色.校长,辅导员,学生.校长是管理学校的,他是通过辅导员来管理学生的.这就是管理的意思.
那么这里面我们就有问题了,校长都没有见过我,他管理我?这里我们就要明白了,管理一个人需要和他见面吗?不需要,只要他知道你的数据就可以了.管理的本质就是不要直接面对面管理对象,只要我拿到对象的全部数据就可以了,本质是对数据的管理.
描述 + 组织
管理核心理念是描述+组织,那么这两个究竟是什么?人类认识世界就是通过属性来的,我们抽象出世间的已切,并把他们通过某种方式组织起来.
对于一个校长,他想要了解张三同学的情况,那么这个学校的张三那么多,他可不知道哪个才是自己想要找的,所以我们肯定需要通过特征来排除,要知道Linux是由C语言写的,而C语言可以描述人特征类型当仁不让的就是结构体所以我们用一个结构体来记录学生的特征.这就是描述.
那么我们已经把学生的信息保存好了,如果校长想要看看这个学期绩点最低的几个学生,那么他不能一个一个去查吧,这里面数据结构就体现出作用了,我们通过一个双向链表来组织所有的学生,这样对学生的管理就变成了对链表的增删查改.这就是组识.
OS再理解
我们需要在理解一下操作系统.现在我们已经知道了OS是管理硬件的一种软件,当然它也可以管理软件,这里先按下不表.
我们先来说一个例子,再C/C++中,我们使用printf和cout,这是标准宝库里面的一些函数,它的作用是打印数据,向哪打印数据?显示器,那么显示器是硬件吗?是的,也就是说程序可以控制硬件吗?这里就不是了,想一想,我们的printf/cout都是接口,是OS对外提供的,它可不想让你去直接管理硬件,你只要把自己的需求告诉OS,OS帮你管理,程序是没有资格管理硬件的.
OS如何提供服务
OS是通过系统调用的形式对外提供接口的.
我们知道了OS像银行是不相信任何人的,也就是说我们只能使用一些普通的接口,但是在现实生活总会出现一些年纪比较大的爷爷奶奶,他们不会到银行取钱,这时候银行就又出现另外一种人,接待.你把自己的需求,要去多少钱和接待说一下,他帮你去办,最终你拿到钱就可以了.
这里面就出现了几种角色,这里的接待就像是图形化界面或者命令行解释器,爷爷奶奶就是现在的编程小白,他们不懂计算机,但会基本使用,知道点击什么就可以做什么功能.平常会自己取钱的人,就像是初级的编程者,会一些语言,知道lib里面函数的调用进而间接的对接口进行使用.现在我们还要再进一个阶段,我们是不是可以直接调用OS的接口,越过lib,当然不是全部越过去.
进程
我们先说一个粗糙的理解,一个运行的程序就是一个进程.我们知道,自己的可执行程序是在硬盘中的,当它运行时被加载到内存中,这时候他还不是一个进程,不能说进入一个学校,就可以说自己是这个学校的学生.当我们用一个结构体描述了这个程序,他们两个相加才可以理解成一个进程.
PCB
首先我想说的,无论哪个系统说用PCB描述一个进程这句话都是对的,而在Linux中,进程还有另外一个描述,task_struct.这个就像媒婆和王婆的区别.
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct
等下我们就可以看看Linux里面的结构体的属性,大家可以先看一下,后面会一一解释.
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
创建一个进程
简略的说,一个运行的程序就是一个进程,我们这里可以给一个死循环,先看一下.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
查看进程
我们先来看一下如何查看Linux的进程,这里面需要用到一个指令,大家先会用就可以了,之后在解释.
[bit@Qkj 08_10]$ ps xaj
我们一看这么多进程,哪一个说我们想要的,这里需要使用管道和grep,基本指令的博客已经谈过了.
[bit@Qkj 08_10]$ ps xaj | grep 'mybin'
这里面我们已经看到存在了一个进程叫做./mybin,但是这里面就存在可一个问题,另外一个是什么?我们看到了一个grep,这个也是我们在调用指令的时候出现的另外一个进程,你写的程序被调用了叫进程,别人也是,如果我们不想看到这样的结果还可以这么做.
[bit@Qkj 08_10]$ ps xaj | grep 'mybin' | grep -v gr # grep -v gr 表示 匹配到了 gr 这关键字 都不要
杀掉一个进程
我们之前都是使用一个热键CTRL+c来杀死一个现在也可以通过一个指令,后面他们的区别等会解释.
[bit@Qkj 08_10]$ kill -9 18205 # -9 发送9号信号,这里先不谈 后面跟上 pid,pid在下面谈
pid
这里我们就先开始PCB里面的一些属性,上面我们截图里面可是一点都看不懂啊,就知道一个进程名.我说的具体点,这样便于我们理解.
pid是一个进程的唯一标识符,是process id 的简写.就像我们在现实生活中每个人都有一个身份证号,它可以唯一标记一个PCB.我们在说一个可以查看继承的方式.
我们去根目录里面去看一个一个文件夹,这个文件夹实时的记录了进程的pid,实时就是进程跑着的话就纪录,否则不记录.
我们查看一下这个文件夹,你就会发现蓝色的数字都是pid.
好了,现在我们已经知道了pid,那么如何把自己的和这个文件里面的pid对应起来呢?这是一个问题,不过先不要急,我们需要扩展一下第一种产看进程的方法,希望这里可以给进程的的属性带上属性名称.
[bit@Qkj 08_10]$ ps xaj | head -1 # 查看 首行的 上一行
这里我们就可以查看属于自己进程的pid了,很容易
好了,这里面我们还需要谈一下下面的东西,什么是实时,看好了.如果进程正在执行,proc文件里面出现了一个pid命名的文件
只要是该进程结束了,这个文件夹就会消失,这才是实时.
好了,现在我们需要进入进程对应的文件夹里面看点东西,为了后面的事情做铺垫,这里面大家可以先不用了解.
当前目录
我们在C语言中学到过创建一个文件,如果不知名路径就会在当前路径下创建一个,之前我们说过当前路径就是源文件所在的路径,但是这种所法是不对的,准确来说,是进程所在的工作路径是当前路径.
我们先来测试一下,我们这个打开一个写的文件.
#include <stdio.h>
#include <unistd.h>
int main()
{
FILE* f = fopen("file.txt","w"); // 没有 就去创建
while(1)
{
printf("I am a process\n");
sleep(1);
}
fclose(f);
return 0;
}
我们创建一个文件夹,把可执行程序移动到这个文件里面吗,在这里执行.
这里我们就会在创建一个进程,而且进程的工作路径也发生的了改变,那么去该路径下看看是不是创建了一个文件.
好了,确实出现了,所以我们就知道了当前路径是进程所在的路径,可不是什么源代码所在的路径.
得到pid
我们不能每次都要去通过一个指令去看自己的pid吧,这样也太麻烦了。网络编程为我们只提供了一些接口。我们以后用的就是这种.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process pid : %d\n",getpid());
sleep(1);
}
return 0;
}
ppid
ppid是父进程的pid,全称parent process id.可以这么理解,一般情况下进程存在一个父进程我们多试几次,你就会发现一个问题,我们每一次运行同一个程序得到的进程的pid都是不一样的,这是OS需要重新为我们开辟空间
但是如果我们多次看一下自己的ppid,你就会发现一个很有趣的事情.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process pid : %d ppid : %d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
那么这里的我们就开始疑惑了,为何这个ppid是不变的,这个父进程对应的是啥?我刚才退出一下xshell,这个父进程会发生变化,不过原理还是一样的。
我们看到了这个父进程是bash,我这里说一个结论,我们在命令行上所有的指令几乎都是bash的子进程.至于bash怎么创建,它的父进程是谁,这里先不谈.
fork
我们这里就有点问,有没有不是使用可执行程序,也就是**./**,是不是右一些接口可以帮助我们创建.有的,这里有一个fork函数可以供我们创建子进程.
这里我们就有些疑惑了,我们从来没有遇见过可以返回两个数的函数,这个fork函数好特殊啊,这里为什么会这样,先不说,我们现在只需要知道fork可以帮助我们创建一个进程,我们先来看看.
既然我们已经知道了,fork可以创建一个子进程,我们有理由相信printf会执行两次.
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
printf("hello bit id = %d\n",id);
sleep(1);
return 0;
}
很好,这里就是可以确定fork确实可以创建一个进程,但是这里面还存在一个问题,你是如何确定这个新创建的进程是子进程呢?这里我们还需要查看一下进程的信息.
int main()
{
pid_t id = fork();
while(1)
{
if( id == 0 )
{
// child
printf("我是子进程 pid :%d ppid : %d\n",getpid(),getppid());
sleep(1);
}
else
{
// parent
printf("我是父进程 pid :%d ppid : %d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
从这里我们就可以看出,fork创建了一个子进程,父子进程共享后面的代码,这也就解释了我们printf为何可以打印两次.根据返回值的不同,我们是可以控制这两个进程的。
但是这里还是存在两个关键问题,我们一个id打印的时候没有修改,为何出现了两次不一样的值?fork是如何实现这个机制的?最关键的是,为何是给父进程返回pid,给子进程返回0,这三个问题我们现在只回答两个,第三个等着后面的地址空间在说吧。
为何是给父进程返回pid,给子进程返回0?我们先来回答这个问题
在现实生活中,一个孩子肯定对应一个父亲,但是一个父亲可不止一个儿子,父亲给儿子取了名字,最主要的作用就是区分他的几个儿子,也就是说父进程必须存在可以标识子进程的方案,这个就h是fork后给父进程返回子进程的pid,帮助父亲能够快速的识别孩子.那么又为什么给子进程返回0呢?子进程最重要的是看看自己是不是被创建成功了,这个只是一个标记,对于子进程,找到父进程很容易,成本很低.
fork为何会有两个返回值
既让有两个返回值,那么他肯定返回两次.我们先来谈一下子进程是如何被创建的.
首先fork之后肯定要创建一个子进程,也就是说多了一个task_struct,这后面就是OS的作用了,OS要描述和组织,这里不谈.这里面存在一个问题,对于子进程它的属性从哪里来?大部分是通过继承父进程的,那么代码呢?它和父进程共享同一片代码,执行的逻辑是一样的,当然数据肯定存在些差异.这里面我们还要补充知识,对于一个函数,在执行return语句的时候,它的核心功能已经完成了,return只是一个收尾的.
那么fork可就有的说了,fork这里面核心的代码执行完了,可以理解为return执行了两次.
进程状态
进程的状态这里我们要好好说说,都是干货,先去网上找一些图片,你会发现它们是真的复杂,而且有些书谈的很官方,这里我们要想办法把它们拉到尘埃中去,让我们真真切切的感受和理解究竟什么是进程的状态.
所谓的状态我们可以理解为反映一件事物的情况,对于进程也是如此.在Linux中,进程的状态本质就是一个整数,每一个状态对应的值是不一样的,我们先来拨开本质.
在Linux中我们只谈四种进程的状态的大类,这里先初步认识下.
- 运行态
- 终止态
- 阻塞态
- 挂起态
运行态
这里我们还要退一下边角的知识,我们知道OS通过task_struct结构体来描述进程,通过一个链表来组织进程.,但是这不意味着哦们没有其他的类似数据结构的方式来组织,这里就有一运行队列需要我们考虑,关乎着运行态的概念理解.
我们知道,自己电脑一般的CPU个数远远小于自己运行进程的个数,那么一些运行的进程可能会放在一个运行队列中,那么我们就疑惑了,运行态是在CPU运行的状态还是在这个运行队列中状态,究竟是哪个是运行态?
这里我要解释一下,只要一个进程的task_struct在这个运行队列中(代码等在另一个地方),这个进程就处于运行态,当然在CPU里面的肯定也是运行态.
终止态
这个就比较好理解了,结束了肯定是这个进程不用了.我想问的是如果一个进程处于终止态是不是这个进程就已经被释放了,还是这个进程的空间还可能存在,只不过这个进程永远不会再再次运行了?.我们想一个例子,你去外面下馆子,吃完饭了,准备付钱,你叫了一下老版,老板回应你了,但是生意实在是太好了,抽不开手,但是对于老版来说,你这个顾客已经没事了,也没有什么要求了,等下过来把钱收了就可以了.对于CPU也是如此,如果一个进程要结束,那么肯定要把该进程所处的空间给释放了,但是CPU实在太忙了,它想着我先把你置为终止态,反正你也不用了,等我有点闲的时候就去把空间释放了就可以了.
阻塞态
阻塞态和后面的挂起态都是比较难以理解的,这个还有需要考虑一下其他的硬件,我们要好好的分析一下.一个进程要运行起来,可不只需要申请CPU的资源,它还有可能需要申请类似磁盘,网卡等这样的资源,那么这里就开始存在问题了.要知道这些硬件的数据处理效率可是远远低于CPU的效率,记住这个基本的理论,后面要用.
我们前面说过,OS是管理硬件的,那么管理,我们肯定要描述硬件,OS会把这些硬件做成一个类似于task_struct的结构体,这个结构体其他的我不想谈,只谈一点,存在一个task_struct* wait_queue,好了就是一个类似于运行队列的的等待队列,这就有意义了.
现在我们就可以谈谈阻塞态了,当一个进程准备运行的时候,CPU已经准备好资源了,但是这个进程如果还需要申请硬件的资源,这就开始出现了问题,要知道硬件的资源可是很慢的,我这便CPU都准备好了,抬头一看,我去,这硬件怎么这么拉,操作系统心里面一想,别说了,这个进程你还是离开运行队列吧,你这硬件实在是太慢了,我把你这个进程放在你这还没有准备好的硬件对应的wait_queue队列里面,啥时候准备好了,我就把这个进程放在运行队列里面.那么处于wait_queue里面队列就处于阻塞态.
挂起态
挂起态这里面我们需要总另一个角度来讨论,不过我们需要这么谈一下,有的书可能会把挂起态看作阻塞态的一种特殊情况,这个也对,但是这里我们把它们分开.
现在我们就可以谈谈挂起态了,不过我们需要解释一下什么是swap分区,在我们的硬盘中,不仅仅存在C盘,D盘一类的,极有可能存在一个名为swap的分区.Swap分区在系统的物理内存不够用的时候,把硬盘内存中的一部分空间释放出来,以供当前运行的程序使用.
我们还知道电脑的进程是很多的,那么内存总有一天会填满,如果内存被填满了,我们又想添加一个急着用的进程,那么OS该怎么办?OS开始去内存里面开始去看看是不是存在,短期不会调用的进程,也就是一些处于等待资源的,我们一看确实存在,我们想是不是可以把这个进程里面的代码数据放在swap分区,我们把sack_struct保留内存当中,给新进程腾一点空间,那么这个只保留sack_struct进程的状态就是挂起状态.
Linux进程状态
我们已经谈过理论了,这里需要和实际结合,而且这里面还有很多事情要做,而且还需要看看如何查看它们的状态.
你们会发现这Linux的状态可不是有点多,而是非常多,最关键的是我们这里还需要把这些实际进程与理论相结合,不过大家不要急,我们一一解释.
R 状态
R是运行态,并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里 .
我们开始演示一下查看进程的状态.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
这里面开始出问题了,我们这个进程不是一直再运行吗,为何好象打印的是sleep状态,这就有点疑惑了.我们可以从这个角度理解,打印函数可是非常快的,而且我们每次打印一次就会休眠1秒,这就意味着我们很多时候是出于S状态的,也就是阻塞态.我把sleep屏蔽掉.再看看现象.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
}
return 0;
}
你会发现我们这里面还是存在问题,我们可是把sleep给屏蔽了,为何还是S.我问一个问题,printf往哪打印数据,这里肯定是显示器啊,那么显示器是硬件吗?是的,它的资源调度的慢,这就不是个阻塞态对上了吗,所以进程有很大的时间处在阻塞态.
说实话,我们得到一个运行态的进程确实有点困难,这里给一个死循环,里面什么都不做,这样就可以得到一个运行态了.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
}
return 0;
}
S 状态
S可以认为是sleep的缩写,这里是阻塞状态,前面我们已经演示过S状态了,一般情况下,只要是程序调度了硬件(CPU不算),有极大可能进程较多时间处于阻塞态.
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
}
return 0;
}
D 状态
D状态是深层睡眠的状态,这个状态不能被杀掉.D状态不是太好演示,这里我们简单的理解一下.
这里面我们举一个例子,看好了,如果一个进程要往磁盘里面打印500MB的文件,好了,我把这个任务告诉磁盘,你写吧,我在这等着,现在这个进程处于阻塞态,操作系统一看,这有个占据很大资源的进程,我OS把他给杀掉,这也就是我们平常可能遇到软件卡退的原因,主要就是造作系统觉得你这个进程占据的资源太大,我给你杀掉,那么这时候OS也觉得这个进程有点大,我把你杀掉,那么磁盘一直在写数据,无论你写入成功还是没写成功,我们总要给原本的进程返沪一个信号吧,磁盘等会一看,好家伙进程好象被杀掉了,我该给谁说结果.如果我们写入成功还好一点,要是没写入成功,如果这500MB是银行的客户资料,你就会知道什么是痛苦了.这里面S状态就不行了,我们需要更深层次的睡眠,这就是D状态.
X 状态
X就是死亡状态,像资源等都被释放掉了,没什么可以说的.
Z 状态
进程如果要结束一般是不会直接释放资源的,而是处于Z状态.在现实生活中也是如此,一般一个人死亡,像警察,医院会做一系列工作后才会正式宣布人已经死亡,进程也是如此.这个状态是Linux独有的状态.
我们这里有点疑惑,为何要进入Z状态,感觉有点多此一举,反正这个进程肯定会被释放掉.我们先来解释一下为何一个进程会被创建出来.我们肯定是需要完成任务的,那么当进程结束后,我们是如何知道这个任务是不是被完成了,也就是说现在你还不能被释放掉,必须把情况给我说明白,之后才能被释放.
Z状态就是维护退出信息,把退出的信息告诉父进程或者OS,至于怎么做的我们先不谈.我记得Linux内核存咋退出信息.我们之前main函数的返回值也是放在这里面,这个到进程控制在细谈.
僵尸进程
处于Z状态的进程叫做僵尸进程,当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 我们看看能不能模拟出来.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cout = 5;
while(cout)
{
printf("我是子进程 ,还存在%d s\n",cout--);
sleep(1);
}
printf("我是一个僵尸进程,等待被回收\n");
exit(0);
}
else
{
while(1)
{
printf("我是父进程\n");
sleep(1);
}
}
return 0;
}
我们这里有一个检测的指令,大家会用就可以了,让他一直检测这个进程的信息.
[bit@Qkj 08_11]$ while :; do ps xaj | head -1 && ps xaj | grep 'mybin' | grep -v gr;sleep 1;echo "####################################"; done
现在我们开始看僵尸进程的状态,这里你就会发现子进程变成了一个Z状态的进程.
僵尸状态危害
长时间存在僵尸状态,父进程不会回收它,导致里面的资源不会被释放,这就构成了内存泄漏.至于后面如何回收,后面再谈.
孤儿进程
既然谈过僵尸进程了,这里也把孤儿进程说一下吧孤儿进程很是形象,就是父母找不到了,孩子在孤儿院。对应进程也是如此,如果一个进程的父进程死掉了,那么这个子进程就变了孤儿进程.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程\n");
sleep(1);
}
}
else
{
int cout = 10;
while(cout)
{
printf("我是父进程,还剩 %d s\n",cout--);
sleep(1);
}
printf("父进程已经 死亡\n");
}
return 0;
}
这里你会发现我们的子进程变成了一个孤儿进程,它的父进程没有了,所以被1号进程给收养了,所谓的1好进程就是操作系统.那么操作系统为何要收养这个孤儿进程,就是为了以后这个孤儿进程死掉后释放资源.
我们发现父进程好象是直接退出的了,没有经过Z状态,这是为什么呢?父进程是bash的子进程,当父进程退出后,bash是存在可以释放父进程资源的方法,就把它直接释放了,故我们看到的是越过了Z状态.
这里我们还会看到一个情况,这个孤儿进程的状态是S,我们这个不担心,但是你往上面翻,上面的所有进程状态后面都带了一个+号,这个存在些区别.
后台进程
状态后面不带+号的是后台进程,这个进程是不能够被ctrl+c杀掉的,这里需要使用kill指令.算是补充上面的一个坑.同样后面带+号的是前台进程,可以被我们杀掉.
T 状态 & t 状态
还剩下,两个状态,某种意义上它们两个算同一个进程状态,再新的Linux内核中好象都是T.这里我们只是简单的认识一下它们两个.
T 状态
T状态时是暂停状态,我们在日常生活中碰见过暂停,比如说听一首歌,突然有些事,这里可以暂停一下.进程也是如此.
我们先来看看kill\指令的选项,这里不要求记忆,但是需要理解.
[bit@Qkj 08_12]$ kill -l
这里面有很多,我们现在只需要了解三个.
- 9 杀掉进程
- 19 暂停进程
- 18 恢复已暂停的进程
t 状态
t进程是也是暂停状态,只不过它是调试的时候遇到的断点的状态.
总结
到这里我们已经把Linux状态全部谈完,这里总结一下.我们需要把理论的状态和实际的状态组合到一起,下面是一张图片.
优先级
所谓的优先级是我们排队使用某些东西,就像我们在食堂吃饭,食物是够的,但是我们却不能一拥而上,原因就是窗口有点少,所以我们需要排队.对于CPU也是如此,我们电脑的的CPU个数远远低于进程的个数,那么进程也要排队.我们就想问哪个进程可以先进入CPU,哪个不先进入?这就是调度器的事了.这就是进程的优先级.我们可以查看进程的优先级.关于优先级我们不惜细谈.
查看优先级
我们直接进入任务管理器,top.,找到相应得到pid就可以了.
[bit@Qkj 08_12]$ top
我们会发现,Linux的优先级分为两个部分,其中PRI 就是优先级
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
修改优先级
这里我们不建议这么做,但是需要了解一下这里面的原理.这里我们就可以来看看什么事NI了,我们通过修改NI来间接的修改优先级.
注意这个优先级需要在超级用户权限下进行.
- 使用 top,进入任务管理器
- 进入top后按“r”–>输入进程PID–>输入nice值
我们想要给NI一个-100的值,你认为自己的PRI可以变得很低,但是实际情况也是如此吗.
我们看看你就会发现.为何NI的值是-20,我们我们明明给的是-100啊,这里就要谈了,造作系统很不想让你修改优先级,所以设置了超级用户权限,你还是要改,我就把NI的设置到一个范围,尽量让ide修改不破坏调度器的平衡,这里的NI取值[-20,19].也就是说我们的优先级的范围就是[60,99].
这里我们还要说一个结论,新的优先级等于老的PRI加上NI的值,我们看一看.
你们就会发现不对啊,我们重新给了NI是10,原来的PRI是60,这里按照结论新的应该是70啊,为何事90.这里是因为每一次我们修改优先级,进程的PRI都会被重置为80.也就是很大程度上老的PRI就是80.
竞争性 & 独立性
这里我们谈一下其他的概念,都是比较容易理解的.
竞争性
我们都知道一般的电脑只有一个CPU,一个CPU只能运行一个进程,但是我们的电脑上跑的进程可不是就几个,那么进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级,这也是优先级出现的原因.
独立性
这个我们还没有办法好好解释,这里先给大家一个结论.每一个进程都有自己独立的空间和资源,进程与进程之间互不干扰.这就是如果你在打游戏和听歌,发现游戏软件崩了,但是却不会影响你的听歌软件,至于这个机制是如何实现的,这里我们先不谈.
并行 & 并发
这两个概念是计算机两种不同的实现机制,一般还是并发的比例比较大点.
并行
如果你的计算机配置很高,存在多个CPU,这里以两个举例子.存在两个CPU,那么操作系统可以同时运行两个进程,每一个COU都有一个对应的运行队列.
并发
并发进程是如果的的电脑的只有一个CPU,我们的进程只能一个一个来这个CPU里面执行.
不要以为一个进程只能在CPU当中跑完才可以下来,我们大部分的操作系统都是分时的.系统会给每一个进程一个时间片的概念,再一次调度周期中,我们对进程进行交叉调度,让进程的代码得到相应的推进,这就是并发.
我们这里就开始疑惑了,如果我们进程是不断被切换的,那么这个时间是不是被有点长.是的,这个时间确实有点长,但是这个长是对于操作系统而言的,对于我们人来说这就很短了.这也是我们平常觉得多个进程在"同时"执行.
进程调度
我们刚在谈了并发和并行,这里看看操作系统是如何进行进程的调度的.首先我要说一下抢占式式执行,对于进程 来说,资源就这么多,CPU的个数也是有限的,那么如果出现了一个优先级高的进程,操作系统就会把原本正在执行的进程给拉来下,换成这个优先级高的进程执行,这就是抢占式执行的含义.
进程调度
这里我们需要好分析一下进程是如何调度的,里面可能涉及到哈希表的内容,这里我们先来看看是不是存在不按优先级的进程的存在?这里是的,我们知道运行队列也是队列,它支持先进先出的特性,如果一个进程进入运行队列,那么他也严格按照这种特性来走.
我们在想如果一些进程的优先级一样该怎么办?这里就需要用到哈希表的特性了,我们把进程的优先级分成若干部分,遇到优先级和他相匹配的就把他挂在相应的的地方.
到这里我我们就可以知道了进程是如何存在的,当OS觉得要调用进程的时候,他就会去这个哈希表里面遍历,但是我们知道这里遍历一个哈希表还是有点慢的,所以我们又想出了一个位图,这里的位图就比较简单了.我们先来解释一下.我们都知道int是四个字节,也就是三十二位.我们是不是可以这么干:如果存在了一个进程要执行,就把这个进程放在这个哈希表,又把进程的优先级对应的位改成1,到时候我们只需要遍历这个位图,看看哪一位是1就可以了.
我们先去看看内核是如何是实现的,看一下逻辑.我们会发现进程的哈希表存储140种优先级的进程,位图是5个int类型的数组,总共32*5 = 160个二进制位,够表示哈希表了.最为关键的是这里面存在两个一摸一样的结构,其中结构1可以作为正在执行的进程,结构2看作后面出现新进程先加入到结构2中,等着结构1执行完了,它们两个一交换就可以了.
进程切换
这里谈一下进程切换,算是这篇博客最后的内容了.在谈着这个问题之前,我们先来看看CPU里面的寄存器.在CPU中存在两种不同类型的寄存器,这里我们先不解释.
- 可见寄存器
- 不可见寄存器
我们看一下下面的代码,我们都知道a是一个存在栈区的局部变量,那么我们为何可以得到a里面的数据.
int func(){
int a = 10;
return a;
}
这个我们在函数栈帧的博客里面已经谈过了,造作系统会把这个值放在寄存器中,这种寄存器就是可见寄存器.同样还存在一个不可见寄存器,这种寄存器是保存进程的信息的.我们知道进程在执行一段时间后就会脱离CPU,那么这个脱离寄存器的的进程肯定需要保存进程的信息,这里面的信息就叫做上下文,上下文是保存在不可见寄存器当中.等着下一次该进程再次被执行,里面的上下文在被释放出来.这里我要提一下,寄存器的数量是不多的,但是不是一个寄存器只能保存一个上下文.