程序本质上是一个包含可执行代码的文件,是一个放在磁盘上的静态文件。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中,此时这个程序就被称为进程。所以进程就是一个开始执行但是还没有结束的程序的实例,是可执行文件的具体实现。当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存、设备等)然后进行一系列的复杂操作,使程序变成进程以供系统调用。
综上所述,进程是程序的一次执行过程和资源分配的基本单位。
区分程序和进程:
程序:程序本质是一个放在磁盘上的静态文件。
进程:程序运行起来之后,就叫做进程,静态是动态的,由操作系统管理。
操作系统允许多个进程同时运行,此时就需要操作系统对进程进行管理,管理的核心也是先描述,再组织:所以对进程的管理,会转化成对由进程控制块(PCB)组成的链表的管理(增删查改)。
进程信息包括对应的文件+进程属性。进程属性是方便操作系统对进程进行管理,所以进程信息的大小要比原本的文件要大。
创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。
综上所述,进程就是可执行程序与管理进程需要的数据的集合。
PCB的全称为Process Ctrl Block(进程控制块)。
具体操作系统的PCB名字是不同的,由于Linux操作系统是用C语言进行编写的,因此Linux当中的PCB是由结构体实现的–struct task_struct()
。
task_struct
当中主要包含以下信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程:Pid和PPid。
- 状态: 任务状态,退出代码,退出信号等。并不是所有的进程都是运行的,也有的进程是在等待运行,此时就需要状态信息。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
通过 ps
(process status)命令可以显示当前进程的状态,类似于 windows 的任务管理器。
它的参数比较多,常用的有:
ps -aux
为例:USER: 行程拥有者
PID: 进程的ID号,用来识别进程
%CPU: 占用的 CPU 使用率
%MEM: 占用的记忆体使用率
VSZ: 占用的虚拟记忆体大小
RSS: 占用的记忆体大小
TTY: 终端的次要装置号码
STAT: 该进程的状态
START: 进程开始时间
TIME: 执行的时间
COMMAND:所执行的指令
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
ps aux | head -1 &&ps aux | grep mybin | grep -v grep
:
其中ps aux | head -1
是为了显示第一行属性,ps aux | grep mybin | grep -v grep
是为了过滤出mybin
程序并且屏蔽掉grep
这条命令本身。
在根目录下有一个名为proc
的系统文件夹,这是个动态文件系统。
文件夹当中包含大量进程信息,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc
下,以进程的PID
号为目录名,它们是读取进程信息的接口。
直接查看18014目录就可以查看启动这个进程所有的属性信息了。
使用系统调用函数,getpid()
和getppid()
即可分别获取进程的PID和PPID。
其中,PID是当前进程的ID号,PPID是父进程的ID号。
#include
#include
int main()
{
while(1)
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
sleep(1);
}
return 0;
}
使用ps axj | head -1 &&ps axj | grep mybin | grep -v grep
通过ps查看它的父进程:
可以看到,bash创建了mybin这个子进程,由于进程是有独立性的,子进程挂掉了,父进程bash也不会挂掉。
bash运行原理:bash叫做命令行解释器(在命令行下的所有命令几乎都是bash的子进程),bash只需要接受任务识别任务,然后创建子进程。通过创建子进程,让子进程去完成对应的任务。
这样,每次运行mybin程序虽然PID
会改变,但是父进程IDPPID
是不会变的,因为它是由bash创建的,而bash是不会终止然后重新创建的:
fork是一个系统调用级别的函数,其功能就是创建一个子进程。
可以看到两次输出的PID不相同,说明fork创建了一个新的进程,fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
对于父进程(调用fork函数的进程),fork函数的返回值是子进程的PID,如果子进程创建失败,则在父进程中返回 -1;对于子进程创建成功则是返回0。
使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。
使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。
由于父子进程的fork返回值不同,此时就可以使用if将父子进程分流,这样就可以实现让父子进程执行不同的代码,实现不同的功能:
父子进程都有while死循环,但是由于父子进程是独立的,所以它们可以同时各自执行自己的while死循环,并不存在先调度完父进程然后再调度子进程的情况。这也就进一步证明了子进程的运行和父进程是无关的,只是这两个进程被操作系统调度的顺序是不确定的,取决于操作系统调度算法的具体实现。
要对进程进行监测和控制,首先必须要了解当前进程运行状态的情况,下面是kernel源代码中定义的状态:
/*
* 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 */
};
其中:
R :可执行状态。
S:可中断的睡眠状态。
D:不可中断的睡眠状态。
T/t:暂停状态或跟踪状态。
X:死亡状态,进程即将被销毁。
Z :退出状态,进程成为僵尸进程。
进程的退出状态是会被保存PCB也就是task_struct
中的。
只有在该状态的进程才可能在CPU上运行。处于这个状态的进程的PCB被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。所以处于这个状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里等待被调度。
处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的PCB结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。这样就实现了处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉。
因为这个进程是在等待IO(往显示器输入数据)的。
如果状态中有+
则表示这个进程是在前台运行的,此时可以直接Ctrl+c
终止进程,如果在执行程序时加上&符:./proc &
,则表示将程序放到后台运行,此时无法直接使用Ctrl+c
终止进程,只能使用kill -9 PID
的方式。
处于这个状态的进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
比如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于不可中断睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
包括操作系统在内,任何人都无法杀掉D状态的进程,除非操作系统重启或者该进程拿到数据(得到磁盘的回复)。
可以通过发送 SIGSTOP
信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT
信号让进程继续运行。
t(跟踪状态):当进程被gdb调试的时候,会产生t状态。
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态。
一些书上也会提到进程的三种基本状态:运行态、就绪态和阻塞态(或等待态)。
其中,运行态和就绪态就是R
可执行状态,而阻塞态就是S
可中断睡眠状态。
以下是对这三种状态的解释:
三种状态的转换关系描述如下:
使用echo $?
命令获取最近一次进程退出时的退出码。
进程退出码可以告诉操作系统代码顺利执行结束(0表示正常退出,非0表示不正常)。
对于前台程序通常使用Ctrl+c
来中断,但对于后台进程则必须使用kill
命令完成。
kill
命令是通过向进程发送指定的信号来结束进程的,如果没有指定发送的信号,则默认值为SIGTERM
信号,也就是-15
信号。-15
信号将终止所有不能捕获该信号的进程,至于那些可以捕获该信号的进程则需要使用-9
信号。
使用kill -l
指令列出当前系统所支持的信号集。一共62条(没有32和33):
-15
信号被称为优雅的退出。该信号只是通知对应的进程要进行"安全、干净的退出",程序接到信号之后,退出前一般会进行一些"准备工作",如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。但是,如果在"准备工作"进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。
-9
信号要比-15
信号强硬一点,系统会发出SIGKILL
信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
所以,相比于kill -15
命令,kill -9
在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,可能会导致打开的文件出现错误或者数据丢失之类的错误,所以不到万不得已不要使用kill -9
这种强制结束的办法。如果连kill -9
都没法结束进程,那就只有重启计算机了。
综上,想要杀死进程就有了以下三种命令:kill PID
,kill -15 PID
,kill -9 PID
。其中,kill PID
和kill -15 PID
是等价的,kill -9 PID
是强制杀死进程,不到万不得已不要用。
当一个进程的状态为Z时就为僵尸状态,此时这个进程就被称为僵尸进程。
僵死状态是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码,如果退出信息一直未被读取,则相关数据是不会被释放掉的。
进程被创建的目的就是完成某项任务,那么当任务完成的时候,父进程是应该知道任务的完成情况的,所以必须存在僵尸状态,使得父进程得知任务的完成情况,以便进行相应的后续操作。
综上,存在僵尸状态的原因是为了保持进程基本退出信息,方便父进程读取,获得退出原因。
一般而言僵尸进程可以将不想关的资源比如代码数据等释放掉,但是PCB是会被保留的,进程的退出信息,会放在PCB中。
父进程先退出,那么将来子进程进入僵尸状态时就没有父进程没有读取到子进程退出的返回代码,此时该子进程就称之为孤儿进程。孤儿进程会被1号systemd进程领养,最终由1号进程进行回收:
Linux中除了1号进程所有的进程都有父进程(1号进程相当于所有进程的父进程,类似于多叉树的根节点),如果1号进程也挂掉了,系统也就挂掉了。
进程是有多个的,而CPU的资源是有限的,一个CPU一次只能跑一个进程,因此CPU先跑哪个进程后跑哪个进程就成了问题。类似于排队一样,进程优先级实际上就是进程获取CPU资源分配的先后顺序,优先级高的进程有优先执行的权力。
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
优先级是通过整数来表示的,一般数值越小,优先级就越高。
使用ps -l
指令可以输出当前用户上下文中所创建的进程,并不会显示全部进程:
进程的优先级是可以通过修改nice值来修改的,但是除非特殊情况,否则尽量不要改进程的优先级!
只有root用户才能提高进程的优先权(将nice值改为负数),其他用户需要使用sudo进行权限提升。
top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况,即可以通过按键来不断刷新当前状态。如果在前台执行该命令,将独占前台直到终止该程序位置。比较准确地说,top命令提供了实时的对系统处理器的状态监视。
修改进程的nice值有以下三步:
renice
命令允许修改一个正在运行进程的优先权,其命令语法格式如下:
renice -number PID
其中参数number表示优先级别号
使用该命令的时候要注意以下三点:
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别同时进行运行,称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,基于时间片轮转的多个进程看起来同时推进,称之为并发。
时间片:即CPU分配给各个进程的时间。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。由于时间片是一个小的时间单位,通常为10~100ms数量级,所以各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。
目前很多操作系统都是采用可抢占式调度:即高优先级的任务可以打断低优先级的任务,抢占CPU控制权。这种方式并不是让低优先级的任务时间片变短,而是将其时间片后移。这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。