进程
进程是对正在运行中的程序的一个抽象。进程是动态
的概念。
比喻:做蛋糕的食谱就是
程序
、做蛋糕的人就是CPU
、而做蛋糕的各种原料都是输入数据
。进程
就是科学家阅读食谱、取来各种原料以及烘焙蛋糕等一系例了动作的总和
。
进程内存空间布局
PCB
进程控制块PCB是进程存在的唯一标志
,使得一个不能独立运行的程序成为一个能独立运行的进程
为什么说PCB是进程存在的唯一标志
?
证明:
-
当进程被
调度
时
要从该进程的PCB中查出其现行状态及优先级;要根据其PCB中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其PCB中的程序和数据的内存始址,找到其程序和数据; -
进程在
执行过程
中
当需要和与之合作的进程实现同步,通信或者访问文件时,也都需要访问PCB; -
当进程
暂停
执行时
也需要将其断点
的处理机环境保存在PCB中。
可见在进程的整个生命期中,系统总是通过PCB对进程进行控制的,所以说PCB是进程存在的唯一标志。
进程上下文切换
进程a切换为进程b的过程如下:
1.保存进程a的当前状态,包括程序计数器,寄存器,代码段,数据段等
2.更新PCB的信息,将a进程的状态有运行态修改为阻塞态,然后加入阻塞队列
3.更新b进程的PCB信息,从就绪转为运行状态,cpu运行b进程
进程的五种状态
五态:
创建状态
:进程正在被创建时的状态,os为进程分配资源、初始化PCB;就绪态
,就绪态状态下进程是可运行的,但此时又有其他进程正在运行,所以当前进程不得不等待,此时的等待状态就是就绪状态。运行态
,进程正占用 CPU 时间片阻塞态
,除非某种外部事件发生,否则进程不能运行结束状态
:进程运行完毕,正在从系统中撤销的状态;os会回收进程拥有的资源
进程状态的切换时机
举例理解:
现在我们写了如下一段代码,然后编译成可执行文件hello-name
在shell下执行./hello-name时,进程状态会发生如下变化:
挂起态
还有一个状态叫挂起状态
什么是挂起状态?
挂起态和阻塞态唯一的区别就是:阻塞态的进程在内存中,挂起态的进程不在硬盘中
由于虚拟内存
的原因,进程使用的空间可能并没有映射到物理内存,而是在硬盘上,这时阻塞的进程就会出现挂起状态
。
即:进程不在内存中的等待状态即为挂起状态
挂起状态可以分为两种:
- 阻塞挂起状态:进程在外存(硬盘)等待某个事件的出现则进入阻塞状态
- 就绪挂起状态:进程在外存(硬盘),但只要进入内存,就进入就绪状态
这两种挂起状态加上前面的五种状态,就变成了七种状态变迁,见如下图:
详解进程状态
进程的创建
创建进程的过程如下:
- 为新进程分配一个唯一的
进程标识号
,并申请一个空白的 PCB
,PCB 是有限的,若申请失败则创建失败; - 为进程
分配资源
,此处如果资源不足,进程就会进入等待状态
,以等待资源; - 初始化 PCB;
- 如果进程的调度队列能够接纳新进程,那就将新进程插入到就绪队列,等待被调度运行;
哪些情况下会创建进程
- 系统初始化(init)
- 正在运行的程序执行了创建进程的
系统调用
(比如 fork) - 用户请求创建一个新进程
- 初始化一个批处理工作
补充:
操作系统允许一个进程创建另一个进程,而且允许子进程继承
父进程所拥有的资源,
当子进程被终止时,其在父进程处继承的资源会还给
父进程。
同时,终止父进程时同时也会终止其所有的子进程。
fork系统调用
在UNIX系统中, 只有一个系统调用可以用来创建新进程:fork。
这个系统调用会创建一个与调用进程相同的副本。
当fork刚刚完成时,两个进程的内存、寄存器、程序计数器等状态都完全—致;但它们是完全独立的两个进程,拥有不同的PID与虚拟内存空间,在fork完成后它们会各自独立地执行,互不干扰。
子进程接着执行execve系统调用以修改其内存映射并运行一个新的程序。
int main(int argc, char *argv[])
7 {
8 printf("hello world (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("hello, I am child (pid:%d)\n", (int) getpid());
15 } else { // parent goes down this path (main)
16 printf("hello, I am parent of %d (pid:%d)\n",
17 rc, (int) getpid());
18 }
19 return 0;
20 }
运行这段程序(p1.c),将看到如下两种输出结果:
prompt> ./p1
hello world (pid:29146)
hello, I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)
prompt>
prompt> ./p1
hello world (pid:29146)
hello, I am child (pid:29147)
hello, I am parent of 29147 (pid:29146)
prompt>
可以看到,hello world 只会输出一次,说明子进程不是从main开始执行。而是和父进程一样从fork返回处往下执行。
父子进程从fork()返回的值是不同的:
父进程获得的返回值是新创建子进程的PID
而子进程获得的返回值是0
然后两个进程被cpu调度的顺序也不一定。可能子进程先执行也可能父进程先执行。
进程的阻塞和就绪
进程的阻塞和唤醒(唤醒之后进入就绪态)
是一对原语,必须成对使用
阻塞进程
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,处于阻塞状态的进程是绝对不可能自己叫醒自己的。
它只能由另一个进程唤醒。
阻塞进程的过程如下:
- 找到被阻塞进程的 PCB;
- 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
- 将该 PCB 插入到阻塞队列中去;
唤醒进程
当该进程所期待的事件出现时,发现者进程用唤醒语句叫醒它。
过程如下:
- 在该事件的阻塞队列中找到相应进程的 PCB;
- 将其从阻塞队列中移出,并置其状态为就绪状态;
- 把该 PCB 插入到就绪队列中,等待调度程序调度;
进程的终止
终止进程的过程如下:
- 查找需要终止的进程的 PCB;
- 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
- 如果其还有子进程,则应将其所有子进程终止;
- 将该进程所拥有的全部资源都归还给父进程或操作系统;
- 将其从 PCB 所在队列中删除;
进程终止方式
正常退出(exit,自愿的)
错误退出(自愿的)
系统错误(非自愿的)
被其他进程杀死(kill,非自愿的)
工作进程 vs 守护进程
工作进程
它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了
守护进程
守护进程是一种特殊的进程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。
僵尸进程 vs 孤儿进程
正常情况下:
当一个子进程完成它的工作并退出之后,它的父进程会取得该子进程的状态信息
,使之正常消失。
但如果发生异常导致父进程没能正常工作,就会产生孤儿进程或僵尸进程
即:
父进程在子进程之前死了——孤儿进程
子进程死了但是父进程不收集其状态信息——僵尸进程
- 僵尸进程
任何一个子进程(init除外)在exit
之后,并非马上就消失掉的,此时父进程没有来得及获取子进程的状态信息
,子进程的进程描述符
仍然保存在系统中。此时的子进程被称之为僵尸进程
,等待父进程处理之后则会消失掉。(这是每个子进程在结束时都要经过的阶段)。 - 孤儿进程
如果父进程在子进程结束之前就退出了,则子进程成为孤儿进程
,孤儿进程将由init进程
接管。init
将会以父进程的身份对该孤儿进程进行处理使之消失。
僵尸进程和孤儿进程的危害
如果大量的产生僵死进程或孤儿进程,子进程的状态信息得不到释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,最终将因为没有可用的进程号而导致系统不能产生新的进程
孤儿进程的解决办法:init进程代父进程收集子进程的状态信息
僵尸进程的解决办法:杀死父进程
僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程
。因此应该杀死这个父进程,让其不再产生子进程,同时init进程代父进程收集子进程的状态信息