进程(process) 是计算机中正在运行的程序的实例,是操作系统资源分配的最小单位。在操作系统中,每个进程都有自己的地址空间、内存、数据栈和其他资源,它们与其他进程相互隔离。进程可以是应用程序、系统服务或操作系统本身。操作系统通过管理进程来协调多任务处理,使多个程序能够同时运行,并保证它们之间不会发生冲突或干扰。进程通信需要通过进程间通信机制(IPC)来实现。
在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。
上图中各个状态的意义:
当然,进程还有另外两个基本状态:
于是,一个完整的进程状态的变迁如下图:
进程的状态变迁如下:
在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的,PCB是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
在PCB中主要包含以下信息:
进程描述信息:
进程控制和管理信息:
资源分配清单:
CPU 相关信息:
那么每个PCB是如何组织的呢?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
那么,就绪队列和阻塞队列链表的组织形式如下图:
进程的创建、终止、阻塞、唤醒的过程也就是进程的控制
01 创建进程
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源。
创建进程的过程如下:
02 终止进程
进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill
掉)。
当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。
终止进程的过程如下:
03 阻塞进程
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。
阻塞进程的过程如下:
04 唤醒进程
进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。
唤醒进程的过程如下:
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。而CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
01 管道
管道是一种单向的数据传输方式,实现线程间的通信需要两个管道。管道这种通信方式的缺点就是通信效率低,好处就是实现简单。
02 消息队列
为了解决管道通信效率低,可以使用消息队列。消息队列这种通信方式就是有一个生产者,一个消费者,运送数据的效率取决于消息队列的容量。
消息队列的缺点就是,不适合传输较大的数据。 另外传输数据要进行内核态与用户态之间的拷贝,效率比较低。
03 共享内存
为了解决内核态与用户态之间数据拷贝带来的开销,可以使用共享内存,共享内存就是通过指针的指向改变来完成数据的访问。
优点就在于可以省去拷贝开销,但是随之而来的问题就是对共享资源互斥访问需要控制,不然会带来安全性问题。
04 信号量
为了解决对共享资源访问的同步、互斥问题,可以使用信号量。信号量其实就是一个整型计数器,用来记录资源的数量,通过PV操作来实现进程间的同步、互斥流程。
05 信号
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。信号跟信号量虽然名字相似度很高,但两者用途完全不一样。信号是通过固定的信号标志来传达信息。
06 Socket
前面涉及到的都是同一台主机间通信方式,还有不同主机间的通信,那就要用到Socket.创建Socket的系统调用:
int socket(int domain, int type, int protocal)
domain 参数用来指定协议族 type 参数用来指定通信特性 protocal 参数原本是用来指定通信协议的,但现在基本废弃。
线程是计算机中能够被操作系统独立调度和执行的最小单位。一个进程可以包含多个线程,每个线程负责执行进程中的一部分任务。同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
不同的线程可以并行执行,从而提高程序的性能和响应速度。线程之间共享进程的内存空间,因此它们可以更方便地访问相同的数据。线程通常被用于处理多个任务或执行耗时的操作,例如图形界面事件处理、网络请求等等。
线程的上下文切换主要看线程是不是属于同一个进程:
所以,线程的上下文切换相比进程,开销要小很多。
在线程间常见的通信方式有:
在Java中,常见的线程间通信操作是通过
wait()
和notify()
方法,这是Java中最基本的线程通信机制。wait()
方法会使当前线程等待,直到其他线程调用notify()
方法唤醒它。notify()
方法会随机选择一个在等待该对象的线程进行唤醒。
进程是操作系统资源分配的最小单位,线程是程序执行的最小单位,两者的区别和联系如下:
在Java中,当我们启动main函数时,其实就是启动了一个 JVM 进程,而main函数所在的线程就是这个进程中的一个线程,也称之为主线程。多个线程共享进程的堆和方法区。
而也有一些东西是线程私有的,例如程序计数器私有是为了保存线程的执行记录便于线程切换后能恢复到正确的执行位置;虚拟机栈和本地方法栈私有是为了保证线程中的局部变量不被别的线程访问。