1、进程基本概念
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程和程序之间的联系与区别:
- 联系:程序是构成进程的组成部分之一,一个进程的运行目标是执行它所对应的程序,如果没有程序,进程就失去了其存在的意义。从静态的角度看,进程是由程序、数据和进程控制块(PCB)三部分组成。
- 区别:程序是静态的,进程是动态的。 进程是程序的执行过程,因而进程是有生命周期的。即程序的存在是永久的,而进程的存在是暂时的,动态的产生和消亡。一个进程可以执行一个或几个程序,一个程序亦可以构成多个进程。
- 进程具有创建其他进程的功能。被创建的进程称为子进程,创建者称为父进程,从而构成进程家族。
2、进程的特性
进程的概念能很好地描述程序的并发执行,并且能够解释操作系统的内部特性。事实上操作系统的并发行和共享性正是通过进程的活动体现出来的。
进程具有以下特性:
- 并发性。可以同其他进程一道向前推进,既一个进程的第一个动作可以在另一个进程的最后一个动作结束之前开始。
- 动态性。进程是程序的执行过程,体现在两方面:其一,进程动态的产生动态的消亡;其二,在进程生命周期内,其状态动态变化。
- 独立性。一个进程是一个相对完整的资源分配单位。
- 交往性。一个进程在运行过程中可能会与其他进程发生直接的或间接的相互作用。
- 异步性。每个进程按照各自独立的,不可预知的速度向前推进。
3、进程的状态及其状态转换
进程的动态性表明进程在其生存期内需要经历一系列的离散状态。运行中的进程可以处于以下三种状态之一:运行,就绪,等待。
运行状态(Running):
是指进程已获得CPU,并且在CPU上执行的状态。显然,在一个单CPU系统中,最多只有一个进程处于运行态。
就绪状态(Ready):
是指一个进程已经具备运行条件,但由于没有获得CPU而不能运行所处的状态。一旦把CPU分配给它,该进程就可运行。处于就绪状态的进程可以是多个。
等待状态(Waiting):
也称阻塞状态(Blocked)或封锁状态。
是指进程因等待某种事件发生而暂时不能运行的状态。
进程状态转换图:
4、Linux进程
Linux进程状态:
内核表示 | 状态 | 含义 |
---|---|---|
TASK_RUNNING | R运行状态 | 并不意味着进程一定在运行中,他表明进程要么是在运行中要么在运行队列里 |
TASK_INTERRUPTIBLE | S睡眠状态 | 意味着进程在等待事件完成(也叫可中断睡眠状态) |
TASK_UNINTERRUPTIBLE | D磁盘休眠状态 | 也叫不可中断睡眠状态,在这个状态的进程通常会等待IO的结束 |
TASK_ZOMBIE | Z僵死状态 | 进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收 |
TASK_STOPPED | T停止状态 | 可以通过发送SIGSTOP信号给进程来停止(T)进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行 |
TASK_DEAD | X死亡状态 | 这个状态只是一个返回状态,你不会在任务列表里看见这个状态 |
Linux进程的状态转换:
do_fork()函数:
进程刚开始都是由do_fork()函数创建,新进程继承父进程的现有资源,它在完成初始化后就被挂到就绪队列上。进程刚创建时的状态为TASK_UNINTERRUPTIBLE,在do-fork()函数结束前它会被父进程唤醒而其状态变成TASK_RUNNING。
- fork有两个返回值。
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
在Linux上面运行如下代码:
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t ret = fork();
if(ret > 0){
//father
printf("father[%d,%d]\n",getpid(),getppid());
sleep(1);
}
else if(ret == 0){
//child
printf("child[%d,%d]\n",getpid(),getppid());
sleep(2);//确保父进程先退出
printf("child[%d,%d]\n",getpid(),getppid());
}
else{
perror("fork!");
return 1;
}
return 0;
}
结果:进程创建成功
5、Z-僵尸进程(zombie)
- 僵尸进程(zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵尸进程。
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入僵尸状态。
【创建一个维持30秒的僵死进程】
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t ret = fork();
if(ret > 0){
//father
printf("father:%d\n",getpid());
sleep(30);
}
else if(ret == 0){
//child
printf("child:%d\n",getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
else{
perror("fork!");
return 1;
}
return 0;
}
首先编译并在另一个终端启动监控;
测试结果:
终端一:
终端二(进程状态):
这时我们就可以看到产生了一个僵尸进程。
僵尸进程特点:
- 杀死僵尸进程必须杀死他的父进程。杀死他的父进程后,僵尸进程变成了孤儿进程,由1号进程回收。
- 僵尸进程是由于子进程已经结束,父进程没有读取子进程的状态,子进程的进程描述符仍然保存在系统中,所以变成了僵死状态。
- 维护退出状态本身就要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,如果僵尸状态一直不退出,PCB就要一直维护这个进程。
- 由于进程号是有限的,当产生了大量僵尸进程,会造成内存资源的浪费,最严重时可导致无法创建新进程。
- 为了避免僵尸进程,可以使用信号处理机制,子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。或者fork()两次来避免。
6、孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z状态后,该如何处理呢?
- 父进程先退出,子进程就称之为孤儿进程。
- 孤儿进程会被1号init领养,也会被init进程回收。
【例】:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t ret = fork();
if(ret > 0){
//father
printf("father:%d\n",getpid());
sleep(3);
exit(0);
}
else if(ret == 0){
//child
printf("child:%d\n",getpid());
sleep(10);
}
else{
perror("fork!");
return 1;
}
return 0;
}
测试结果: