并发:单个CPU在多个任务之间不断切换,切换的速度非常快,给用户一种任务并行执行的错觉,被称为伪并行调度
并行:在现代计算机中CPU有多核,可以在同一时间同时执行多项任务,达到真正的并行
那么什么原因会导致进程会被创建,从而生成PCB(进程管理块Process Control Block)呢?常见的有以下几种
1.系统初始化
2.用户通过系统提供的APl创建新进程
3.批处理作业初始化(什么是批处理作业)
4.由现有进程派生子进程
一个进程,因为某种原因被创建了,那么它可以按照以下步骤进行一系列的初始化
1.给新进程分配一个进程ID
2.分配内存空间
3.初始化PCB
4.进入就绪队列
如图,进入就绪队列,其状态就会变为就绪态。各个状态之间的关系描述如下:
这便是对于单个进程,经典的五状态模型。当存在多个进程时,由于同一时间只能有一个进程在执行,那么如何去管理这一系列的处于阻塞态和就绪态的进程呢?一般来说,会使用就绪队列,和阻塞队列,让处于阻塞态和就绪态的进程进入队列,排队执行。
父子关系
已有进程调用fork创建出一个新的进程,那么这两个进程之间就是父子进程关系,子进程会继承父进程的很多属性。
进程组
多个进程可以在一起组成一个进程组,其中某个进程会担任组长,组长进程的pid就是整个进程组的组ID.
会话期关系
多个进程组在一起,就组成了会话期。
对于一个被执行的程序,操作系统会为该程序创建一个进程。进程作为一种抽象概念,可将其视为一个容器,该容器聚集了相关资源,包括地址空间,线程,打开的文件,保护许可等。而操作系统本身是一个程序,有一句经典的话程序=算法+数据结构,因此对于单个进程,可以基于一种数据结构来表示它,这种数据结构称之为进程控制块(PCB)
在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个PCB,如果进程消失了,那么PCB也会随之消失。
PCB包含以下信息:
PCB通常是用链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列
另外,对于运行队列在单核CPU系统中则只有一个运行指针了,因为单核CPU在某个时间,只能运行一个程序。
os运行起来后有三个特殊的进程,他们的PID分别是0、1、2。0、1、2这个三个进程,是os启动起来后会一直默默运行的进程,直到关机os结束运行。
作用
作用1:初始化
这个进程被称为init进程,这个进程的作用是,他会去读取各种各样的系统文件,
使用文件中的数据来初始化os的启动,
让我们的os进入多用户状态,也就是让os支持多用户的登录。
作用2:托管孤儿进程
什么事孤儿进程,怎么托管的,有关这个问题后面会详细介绍。
作用3:原始父进程
原始进程—>进程—>进程—>终端进程—>a.out进程
这个进程怎么运行起来的?
这个进程不是os演变来的,也就是说这个进程的代码不属于os的代码,这个进程是一个独立的程序,程序代码放在了/sbin/init下,当os启动起来后,os会去执行init程序,将它的代码加载到内存,这个进程就运行起来了。
#include
#include
pid_t getpid(void);
ppid_t getppid(void);
uid_t getuid(void);
gid_t getgid(void);
程序如何运行起来的?
当有os时,以上过程肯定都是通过调用相应的API来实现的。
在Linux下,os提供两个非常关键的API,一个是fork
,另一个是exec
.
fork
:开辟出一块内存空间
exec
:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了,运行起来的进程会与其它的进程切换着并发运行。
#include
pid_t fork (void);
函数功能
从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。
复制后有两个结果:
返回值
由于子进程原样复制了父进程的代码和数据,因此父子进程都会执行fork函数,当然这个说法有些欠妥,但是暂且这么理解。
1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
2)子进程的fork,成功返回0,失败返回-1,errno被设置。
#include
int main(void)
{
/*fork前代码*/
pid_t ret = fork();
if(ret > 0){
...
}
else if(ret == 0){
...
}
/*fork后代码*/
}
子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件.像这种继承的情况,父子进程这两个相同的"文件描述符"指向的是相同的文件表。
由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。
但是如果是文件说父子进程单独打开的(在fork之后),那么父子进程的文件描述符指向的文件表不同,会相互覆盖。
#include
int execve(const char *filename,char **const argv,char **const envp);
filename
:新程序(可执行文件)所在的路径名argv
:给main函数的参数,比如我可以将命令行参数传过去envp
:环境变量表#include
int system(const char *command);
进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。
子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。
子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源不拉屎僵尸进程。
没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。
为了能够闻收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid=1的init进程,每当被托管的子进程终止时,init会立即主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵死进程的机会。
#include
#include
pid_t wait(int *status);
作用:父进程调用这个函数的功能有两个,
主动获取子进程的“进程终止状态”。
主动回收子进程终止后所占用的资源。
(1)退出状态与"进程终止状态”
return
、exit
、_exit
的返回值称为"进程终止状态",严格来说应该叫"退出状态",return (退出状态)
、exit(退出状态)
或_exit(退出状态)
当退出状态被_exit
函数交给os内核,os对其进行加工之后得到的才是"进程终止状态",父进程调用wait函数便可以得到这个“进程终止状态”。
(2)os是怎么加工的?
1)正常终止
进程终止状态=(终止原因(正常终止)<<8)|(退出状态的低8位)
不管return、exit、exit返回的返回值有多大,只有低8位有效,所以如果返回值太大,只取低8位的值。
2)异常终止
进程终止状态=是否产生core文件位│终止原因(异常终止)<<8│终止该进程的信号编号
父进程得到进程终止状态后,就可以判断子进程终止的原因是什么,如果是正常终止的,可以提取出返回值,如果是异常终止的,可以提取出异常终止进程的信号编号。
(1)父进程调用wait等子进程结束,如果子进程没有结束的话,父进程调用wait时会一直休眠的等(或者说阻塞的等)。
(2)子进程终止返回内核,内核构建"进程终止状态”
如果,
1)子进程是调用return
、exit
、_exit
正常终止的,将退出状态返回给内核后,内核会通过如下表达式构建"进程终止状态”
进程终止状态–终止原因〈正常终止)<<8 |退出状态的低8位
2)子进程是被某个信号异常终止的,内核会使用如下表达式构建"进程终止状态”
进程终止状态=是否产生core文件位│终止原因(异常终止)<<8│终止该进程的信号编号
(3)内核向父进程发送SIGCHLD
信号,通知父进程子进程结束了,你可以获取子进程的"进程终止状态"了。
如果父进程没有调用wait函数的话,会忽略这个信号,表示不关心子进程的"进程终止状态"。
如果父进程正在调用wait
函数等带子进程的"进程终止状态”的话,wait
会被SIGCHLD
信号唤醒,并获取进程终止状态"
系统提供了相应的带参宏,使用这个带参宏就可以从"进程终止状态”中提取出我们要的信息。
提取原理:相应屏蔽字&进程终止状态,屏蔽掉不需要的内容,留下的就是你要的信息。
哪里能查到这些带参宏,man查案wait的函数手册,即可看到。
其实最简单的理解就是,java虚拟机就代表了java进程。
当你运行另一个java程序时,又会自动地启动一个虚拟机程序来解释java字节码,此时另一个java进程又诞生了。
也就是说你执行多少个java进程,就会运行多少个java虚拟机,当然java虚拟机程序在硬盘上只有一份,只不过被多次启动而已。
当一个正在运行中的进程被中断,操作系统指定另一个就绪态的进程进入运行态,这个过程就是进程切换,也可以叫上下文切换。该切换过程一般涉及以下步骤:
进程上下文切换的场景
线程是进程当中的一条执行流程。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程是CPU调度的基本单位,而进程是资源拥有的基本单位。
所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。对于线程和进程,我们可以这么理解:
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
所以,线程的上下文切换相比进程,开销要小很多。