目录
1、进程的基本概念
2、进程控制块 - PCB
task_struct内容分类
3、查看进程
通过ps命令查看进程
通过proc查看进程
4、通过系统调用获取进程标示符
5、通过系统调用创建进程-fork初识
6、进程状态
操作系统进程状态
linux进程状态
僵尸进程
孤儿进程
僵尸进程的危害
进程状态总结
7、进程优先级
基本概念
查看系统进程
PRI and NI
通过top命令修改进程优先级(ni)
8、linux中的四个重要概念
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 假设我们在磁盘上写了一个test.c的文件代码,最终经过编译链接形成test.exe可执行程序。这个可执行程序也是一个文件,当前在磁盘当中,根据冯诺依曼体系,如果我们想要运行这个可执行程序,我们就需要将其从外设(磁盘)加载到内存中,然后CPU再和内存交互。而加载的过程就叫做此程序运行起来了,但是我们并不能说其为进程,因为加载到内存中只不过是将代码和数据加载进去,此程序只是被搬到内存中,它依旧是一个程序。
操作系统里面可能同时存在大量的进程,所以操作系统要把所有的进程管理起来,对进程的管理本质就是对进程数据的管理。这里就又用到了结构体和数据结构。
⭐:严格意义上的进程=可执行程序+该进程对应的内核数据结构
操作系统里面存在大量的数据,操作系统需要把所有的进程管理起来,而对进程的管理,本质就是对进程数据的管理,依旧是先描述,再组织。
先描述:
- 当一个程序加载到内存时,除了加载了代码和相关数据,操作系统还要为了管理该进程创建对应的数据结构,而在linux操作系统中描述进程是用一个叫task_struct的结构体,进程的所有属性数据全部放入此结构体中。当我们有多个进程的时候,相对应的操作系统也就创建多个task_struct结构体。
再组织:
- 这里我们将所有的task_struct用双链表进行链接起来,此时对进程的管理也就变成了对内核数据结构的管理。
在操作系统中我们把描述进程的结构体叫做PCB(进程控制块),而linux操作系统下的进程控制块叫做struct task_struct,以此来描述进程结构体。
task_struct主要包含以下内容:
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/ O状态信息:包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息..............
我们现在创建一个可执行程序mytest,它现在位于磁盘上,现在我们将程序运行起来,此时程序就变成了一个进程。
我们该如何查看现在的进程呢,主要有以下两种方法:
- 通过ps命令查看进程
- 通过proc查看
通过ps命令查看进程
ps ajx | grep 'mytest'
ps ajx命令会把你系统中所有的进程显示出来,而随后的命令其目的是只把名为mytest的进程显示出来:
此刻我们发现我们要的是mytest进程,但为什么又出现了grep的进程呢?
- 我们自己写的代码,编译成为可执行程序,启动之后就是一个进程,同样别人写的代码,启动之后也是一个进程。例如先前学到的ls、pwd、touch、grep、chgrp……指令,这些指令就是别人写的,存在目录/usr/bin/里头, 这些指令在执行后也会成为进程,这也就是为什么上面会把grep显示出来。
那么我们如何只显示mytest的进程呢?
输入下面指令:
ps ajx | grep 'mytest' | grep -v grep
此命令就是把带有grep字符的进程屏蔽掉,只展现出来mytest进程。
通过proc查看进程
我们都知道在根目录下有许多的路径:
我们要注意上面的proc目录,它是一个内存文件系统,里面存放的是当前系统实时的进程信息。下面我们进入此目录里面看看:
每一个进程在系统中都会存在一个唯一的标识符,就如同每个学生都有一个自己的专属学号一样,而这个唯一标识符在linux中称为pid(process id),而上面框框所圈住的蓝色数字即为进程的pid。
我们可以查看进程的所有title列名称,我们输入下面指令:
ps ajx | head -1
这一行显示出来的就是所有的属性名,提取好了属性,我们就可以创建mytest的进程,然后显示mytest进程的具体信息。
ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v grep
我们知道proc存放的是当前系统实时的进程信息,现在我们已经知道了进程mytest的pid(13261),下面我们就在proc目录里面查询一下:
我们知道proc里面存放的是进程的实时信息,那如果我们结束进程mytest,那么我们就不能在proc里面查询到mytest了:
而当我们再次运行起mytest程序时,结果如下:
此时我们会发现mytest先前的pid失效了,由此我们知道,当一个相同的程序重新启动那么其就是一个新的进程,会给该进程重新分配一个pid。
下面我们具体来看一下进程的两个属性:
ls/proc/27120 -al
- cwd表示进程当前的工作路径。
- exe表示进程对应的可执行程序的磁盘文件。
如果我们用fopen创建一个文件,那么此文件默认就在当前路径,而当前路径指的就是当前进程所在的路径,进程会自己维护当前路径。
⭐:如pid,当前路径等这些都是进程的内部属性,都在进程的进程控制块PCB(task_struct)结构体中。
通过系统调用获取pid需要用到系统调用函数getpid和getppid来分别获取进程pid和父进程ppid。
下面我们来看一下系统调用函数:
man 2 getpid
下面我们运行一下该程序:
我们可以看到mytest进程的pid为32321,下面我们通过ps命令来查看该进程的相关信息,验证一下结论是否正确。
- 按下ctrl + c
- 使用命令:kill -9 pid
下面我们来看一下进程的ppid:
我们可以看到当我们每重启一个进程的时候,其pid是会不断的发生变化的,但其父进程ppid不会发生变化。下面我们来看一下其父进程究竟是什么:
我们知道,我们创建进程是用 ./可执行程序,那么有没有一些函数调用或者系统调用能让我创建子进程呢?下面我们来认识一下创建子进程的系统调用接口fork:
man 2 fork
下面我们来看一下它的返回值:
我们可以看到fork有两个返回值,如果子进程创建成功,那么就把子进程的pid返回给父进程,把0返回给子进程,如果创建失败,把-1返回给父进程。
下面我们用测试代码来证明一下:
上面是我们的测试代码,我们知道如果按照常规思路,上面的printf应该只执行一次,下面我们看一下测试结果:
我们看到printf执行了两次,这完全不符合C语言的逻辑!!!!
下面我们再来看一下更奇怪的现象:
测试代码:
测试结果:
我们可以看到同一个id值,使用打印,没有修改,却打印出来了不同的值!!!
下面我们来验证一下是否创建了子进程:
我们知道这段代码会分别按照父进程和子进程的代码逻辑执行两次,下面我们来看一下执行结果:
我们可以看到执行结果和我们预期的一样。
- fork之后,父进程和子进程会共享代码,一般都会执行后续的代码—解释printf为什么会打印两次的问题
- fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断,让父子执行不同的代码块。
⭐:两个重要问题:
1.fork为什么给父进程返回子进程的pid,给子进程返回0?
- 一个父进程可能有多个子进程,但一个子进程只能有一个父进程。父进程必须有标识子进程的方案,从而控制子进程。而这个方案就是给父进程返回子进程的pid。
- 子进程最重要的是要知道自己创建成功了,同时子进程找父进程的成本非常的低,只需要getppid即可,所以我们只需要给子进程返回0即可。
2.为什么fork会返回两次?
在我们fork以后,os使系统多了一个进程。
- 父进程:task_struct+进程代码和数据
- 子进程:task_struct+进程代码和数据
- 子进程的task_struct对象内部的数据基本上是从父进程继承下来的。
- 子进程和父进程执行相同的代码,fork之后,父子进程代码共享,而数据要各自独立。也正是数据的不同使其有不同的返回值从而执行不同的代码。
当我们调用一个函数,当这个函数准备return的时候,这个函数的核心功能已经完成,子进程被创建,并将子进程放入运行队列, 此时父子进程都存在且代码共享,那么return就顺理成章运行两次,自然也就有了两个返回值。
进程状态本质上是一个整数,在进程的task_struct中(int status)
- 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
下面我们将以面到点,先从操作系统层面讲进程状态,最后细化到linux层面。
对于操作系统,我们分运行、终止、阻塞、挂起这四个状态来进行讲解,当我们对这四个状态了解过后,就会对上图有清晰的认知。
1.进程运行:
- 进程只要在运行队列中就叫做运行态,运行态代表进程已经准备好,随时都可以被调度。
2.进程终止:
- 进程终止态指的是进程还在,只不过永远不运行了,随时等待被释放!
问:进程都已经终止了,为什么操作系统不立马释放对应的资源,而要去维护一个终止态?
- 释放也要花费时间,操作系统是一个“大忙人”,等操作系统闲暇的时候它就会把进程给释放了。
3.进程阻塞:
- 一个进程在使用资源的时候可不仅仅是在申请CPU资源,进程可能申请更多的其他资源,如:磁盘、网卡、显卡、显示器资源、声卡/音响等等。
- 如果我们申请CPU资源暂时无法得到满足,我们进程是需要在CPU的运行队列中进行排队的,那么同样的当我们申请其他慢设备的资源也是需要排队的!!!(task_struct在进程排队)。
- 当进程访问某些资源(磁盘、网卡等),该资源如果暂时没有准备好或者正在给其他进程提供服务,此时:
1. 当前进程要从runqueue中移除
2. 将当前进程放入对应设备的描述结构体中的等待队列中。
(操作系统对进程的管理任务)
⭐结论:进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列 中进行排队,此时进程的代码并没有运行,进程所处的状态叫做阻塞。
4.进程挂起:
- 挂起和阻塞有点类似,挂起最终也是卡住了,不过挂起和阻塞在操作系统的定义上还是有点区别。进程挂起就不会申请CPU资源,也就意味着该进程此时处于一个非运行状态。
- 此时操作系统就要帮我们进行辗转腾挪,将那些短期内不会被调度(你等的资源,短期内不会就绪)进程,它的代码和数据依旧在内存中!就是白白的浪费空间!此时os就会把该进程的代码和数据置换到磁盘上!如下图所示:
- ⭐结论:当我们把短期内不会被调度的进程的代码和数据置换到磁盘中后,释放掉这块空间,此时内存就多出来了这块空间的容量,可以短期内让其它进程使用。操作系统通过此种方法让进程只在内存中留下PCB,剩下的代码和数据全部置换到磁盘上面(swap分区),这样的进程就叫做进程挂起。(往往内存不足的时候,伴随着磁盘被高频率访问)
linux操作系统的源代码当中对于进程状态有如下定义:
/* * The task state array is a strange "bitmap" of * reasons to sleep. Thus "running" is zero, and * you can test for combinations of others with * simple bit tests. */ static const char * const task_state_array[] = { "R (running)", /* 0 */ "S (sleeping)", /* 1 */ "D (disk sleep)", /* 2 */ "T (stopped)", /* 4 */ "t (tracing stop)", /* 8 */ "X (dead)", /* 16 */ "Z (zombie)", /* 32 */ };
1.运行状态-R
- 一个进程处于运行状态(running),表明这个进程要么是在运行中,要么是在运行队列里。
示例:
此程序编译运行以后进入死循环,此程序也没有访问其他外设资源,此进程一直在cpu的队列里面,下面我们来看一下进程的状态:
我们看到当我们的代码不访问外设,只访问cpu资源,那么就是R状态。
2.浅度睡眠状态-S
示例:
我们运行此代码,然后查看进程状态:
- 我们这里执行了printf语句,打印信息我们需要访问外设(显示器),但是想要访问外设的进程很多,操作系统会把你的进程PCB放到显示器的等待队列里面,当轮到你的时候操作系统会把你唤醒 ,此时我们就可以打印了。这就是S睡眠状态,对应的就是上文操作系统层面上的阻塞状态。
⭐:S睡眠状态又叫做浅度睡眠和可中断睡眠。
- 浅度睡眠:当进程处于S状态,它可以随时被操作系统唤醒。
- 可中断睡眠:当进程处于S状态,它可以随时被kill掉。
3.深度睡眠状态-D
- D状态也是一种阻塞状态,只不过这种阻塞状态与传统阻塞状态不同。在linux中,当我们等待的是磁盘资源,那么我们进程阻塞所处的状态就为D状态。
实例分析:
- 假如我们内存中有一个500MB大小的进程,我们需要将进程的代码和数据写到磁盘上,磁盘在写入这500MB的过程中,进程在内存中默默的等待磁盘完工,此时进程所处的状态就为S状态,进程不会被运行,等待磁盘资源就绪给其反馈结果。但是在进程等待的过程中,如果内存中的进程越来越多,操作系统也就越来越忙,S状态进程只占内存不干事,操作系统就会将它kill掉(一般服务器压力过大,OS会终止用户的进程)。这时候当磁盘写完数据(不管写入成功还是失败)呼叫进程的时候发现进程已经不在了。
无论磁盘读取成功与否,此数据都失效了,因为对应的进程被OS杀掉了,那么谁来背这个锅 ?
- 操作系统:当内存不够,OS有权去杀掉进程,这是属于OS的权利。
- 进程:进程将数据给磁盘是进程应该做的,进程需要等待磁盘的反馈结果。
- 磁盘:磁盘也只是做了它该做的,将数据写到磁盘里面。
综上:OS、进程、磁盘这三者都没有错误,所以设计操作系统的人为了使这个进程不被操作系统kill掉,于是就设计出了D状态,也就是深度睡眠状态,也叫做不可被中断睡眠。此时进程在等待磁盘的过程中就由浅度睡眠S转变为深度睡眠D,就不会被OS杀掉,我们想要解决掉D状态,就需要我们关机重启或者拔电源解决。
4.死亡状态-X
- 死亡状态是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,你不会在任务列表里看到这个状态。
5.僵尸状态-Z
- 假设张三和李四一起外出跑步,跑着跑着李四突然倒了,张三赶紧打110和120,医生判定李四已无生命迹象,此时医生任务已经完成,但警察还要调查李四的死因,是他杀还是自杀。在警察调查李四的死因过程中,李四所处的状态就为僵尸状态。
⭐:当linux中的一个进程退出的时候,一般不会直接进入X状态(资源可以立马被回收), 而是进入Z状态。
为什么进程要进入Z状态呢??
- 进程被创建出来,一定是因为有任务让这个进程执行,当进程退出的时候,一般需要把进程的执行结果告知给父进程或者OS以此让我们得知任务的完成结果。
- 进程进入Z状态,就是为了维护退出信息,可以让父进程或os读取(通过进程等待来进行读取)。最后才能进入X状态。
6.暂停状态 - T/t
- 在Linux中,我们可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
示例:
如果我们想要将进程暂停,输入下面命令:
kill -l
我们可以看到第18个信号是使进程继续,第19个信号是使进程暂定。
- 暂停进程: kill -19 "进程pid"
- 继续运行进程:kill -18 "进程pid"
- t也是暂停状态,和T不一样的是它代表的是进程被调试的时候,遇到断点所处的状态,追踪状态。
前面我们提到过,当一个进程正在等待其退出信息被读取的的状态叫做僵尸状态。处于僵尸状态的进程叫做僵尸进程。
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
运行该代码以后,我们可以通过下面指令来实现对进程信息的检测:
while :; do ps ajx | head -1 && ps ajx | grep process | grep -v grep; sleep 1; echo "—————————————————————————————————————————————————————————————————"; done
通过对上面进程的pid和ppid观察,我们得知,pid为26726的是父进程,pid为26727的是子进程。运行5s后子进程退出变为Z状态。此时进程就变为了僵尸进程。
我们将僵尸进程代码修改为如下代码:
运行此程序查看进程信息:
通过上图我们可以看出子进程为8133,父进程为8132,可是当我们父进程8132退出的时候,父进程为什么没有变成僵尸状态呢??
- 父进程8132的父进程是bash,bash会自动回收它的子进程,也就是这里的父进程8132。这里看不到僵尸进程是因为父进程8132被它的父进程bash回收了。
子进程退出的时候父进程要通过某种方式回收子进程,但这里的父进程8132被bash自动回收,但这里的子进程8133还没退出,如果我们的子进程8133退出,那谁来回收子进程呢??
- 这里操作系统扮演了干爹的角色,如果父进程提前退出,那么子进程就会被1号进程(操作系统)领养,我们把这种被领养的进程称为孤儿进程。
⭐:当我们父进程退出的时候,状态由S+变为S,S+代表的是前台进程,S代表的是后台进程,并且这里使用ctrl+c并不能结束进程,我们需要手动kill进程。
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!这样就会导致内存泄漏。
- 进程在操作系统中的运行状态,对应linux操作系统中的运行状态R。
- 进程在操作系统中的终止状态,对应linux操作系统中的僵尸状态Z和死亡状态X。
- 进程在操作系统中的阻塞状态,对应linux操作系统中的浅度睡眠状态S和深度睡眠状态D。
- 进程在操作系统中的挂起状态,对应linux操作系统中的S、D、T状态。
优先级vs权限:
- 优先级是进程获取资源的先后顺序。比如你中午去食堂吃饭排队,排在前面优先级高先打到饭,排到后面优先级低,后打到饭。但是总能吃到饭。
- 权限是你能不能的问题。比如我们想要看某部电影,但需要开通VIP,普通用户不能观看,因为你没有那个权限。
为什么要存在优先级:
- 在我们系统里面,进程占大多数,而资源始终是少数。CPU只有一个,可是进程有n个,在系统里面竞争资源是常态,所以我们要存在优先级来确定前后。
总结:
- cpu资源分配的先后顺序就是指的进程的优先级。
- 优先级高的进程有优先执行权利。配置进程优先级对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
在linux或者unix系统中,我们运行一个无限循环的process文件,并用ps –l命令则会类似输出以下几个内容:
ps -l
下面我们解释一下上面的几个重要信息:
- UID : 代表执行者的身份
- PID : 代表这个进程的标示符
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的标示符
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
Linux下用PRI(priority)和NI(nice)来确认优先级:
- PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高,越大代表进程优先级越低。
- NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,只能通过修改ni值来更改优先级。
- Linux下默认进程的优先级是80。
- PRI值越小越快被执行,那么加入nice值后,将会使PRI变为:PRI(new)=PRI(old)+nice.当我们每次设置优先级,这个old优先级都会恢复为80。
- 当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值。
- nice其取值范围是【-20,19】,所以pri的取值范围是【60,99】。分别40个级别。
修改进程优先级就是在修改ni值,这里有两种方法:
- 通过top命令修改进程的nice值。
- 通过renice命令修改进程的nice值。
⚠:需要注意的是进程的优先级不能随意修改,它会打破调度器平衡。需要我们非要修改进程优先级,就需要使用超级用户root修改。
我们这里只讨论一种方法:使用top命令修改进程优先级
1.创建进程process,修改其优先级:
2.输入sudo top命令:
3.按下' r '键,此时会要求你输入待调整nice值的进程的PID。
4.输入后按下回车,此刻会要求你输入调整后的nice值。
5.修改后,我们再用ps - al查看是否已经修改:
我们再次修改process的优先级:
⭐:linux不允许无节制的修改优先级,根据我们前面所学到的知识我们知道,尽管我的ni值设置为-100,但是ni最低是-20。而PRI = PRI(old)+nice = 80 - 20 = 60。我们每次设置优先级,old优先级都会恢复成80,这也正是为什么我们后续把ni设置为10,而PRI变成90的原因。
竞争性
- 系统进程数目众多,而CPU资源只有少量甚至一个,所以进程之间是具有竞争属性的,为了高效完成任务,更合理竞争相关资源,便就有了优先级。
独立性
- 多进程运行,需要独享各种资源,多进程运行期间互不干扰。进程运行具有独立性,不会因为一个进程挂掉或者异常,而导致其他进程出现问题。
并行
- 多个进程在多个CPU下分别同时进行运行,被称为并行。
并发
- 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
⚠:多个进程在你的系统中运行 != 多个进程都在你的系统中同时运行。
- 不要以为进程一旦占据CPU,就会一直执行到结束,才会释放CPU资源!我们遇到的大部分操作系统都是分时的!!!多个进程在一个运行队列中,操作系统在一次调度周期内会给每一个进程赋予一个时间片的概念!
- 对于上图,我们假设进程1进入CPU运行,假设操作系统给进程1分配10ms时间,当10ms时间到了,那么进程1就暂停运行,操作系统会把进程1从CPU上剥离下来,然后调度进程2,以此类推............假设运行队列中的进程有5个,且操作系统给每个进程都是分配10ms,那么在1s内,这五个进程平均每个进程要调度20次。
⭐:在一个时间段内,多个进程都会通过切换交叉的方式,让多个代码在一个时间段内都得到推进,而这种现象我们就称作为并发。
补充概念:
抢占式内核
- 操作系统难道就是简单的根据队列来进行先后调度的吗??有没有可能突然来了一个优先级更高的进程呢???对于正在运行的低优先级进程,如果来个优先级更高的进程,我们的调度器会直接把进程从CPU上剥离,放上优先级更高的进程,这就是进程抢占。
进程的优先级 | 队列
- 在我们的进程队列中,操作系统是允许不同优先级的进程存在的。且相同优先级的进程,是可以存在多个的。在这里我们借用指针数组的实现方法来实现根据不同的优先级,将特定的进程放入不同的队列中!!!
进程间的切换
- cpu的内部存在各种各样的寄存器,可以用来临时保存数据。而寄存器又分为可见寄存器和不可见寄存器。当进程在被执行的过程中,一定会存在大量的临时数据会暂存在CPU内的寄存器中。当我们要进行下一个进程而本进程还没结束的时候,我们就需要将本进程的历史数据拿走。
⭐:我们把进程在运行中产生的各种寄存器数据,叫做进程的硬件上下文数据。
- 当进程被剥离,需要保存上下文数据。
- 当进程恢复的时候,需要将曾经保存的上下文数据恢复到寄存器中。
- 上下文数据保存在该进程的task_struct中。