进程与线程
进程是操作系统进行资源分配的基本单位,每个进程都有自己的独立内存空间。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程又叫做轻量级进程,是进程的一个实体,是处理器任务调度和执行的基本单位位。它是比进程更小的能独立运行的基本单位。线程只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
对于操作系统来说,一个任务就是一个进程(Process)。比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的这些“子任务”称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。
协程
协程,又称微线程,是一种用户态的轻量级线程,协程的调度完全由用户控制(也就是在用户态执行)。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到线程的堆区,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
如果对内核态用户态不了解的话,可以先看博客《一文理解JVM线程属于用户态还是内核态》
协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和线程切换相比,线程数量越多,协程的性能优势就越明显。不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。此外,一个线程的内存在MB级别,而协程只需要KB级别。
进程和线程的区别
每个线程都是一个轻量级进程(Light Weight Process),都有自己的唯一PID和一个TGID(Thread group ID)。TGID是启动整个进程的thread的PID。
例如,当一个进程被创建的时候,它其实是一个PID和TGID数值相同线程。当线程A启动线程B时,线程B会有自己的唯一PID,但它的TGID会从A继承而来。这样通过PID线程可以独立得到调度,而相同的TGID可以知道哪些线程属于同一个进程,这样可以共享资源(RAM,虚拟内存、文件等)。
线程进程的区别体现在6个方面:
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 资源开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一进程的线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。两者均可并发执行。
协程与线程的区别
- 一个线程可以有多个协程。
- 大多数业务场景下,线程进程可以看做是同步机制,而协程则是异步。
- 线程是抢占式,而协程是非抢占式的,所以需要用户代码释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
- 协程并不是取代线程,而且抽象于线程之上。线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行。
进程间的通信方式(IPC)
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
Linux IPC的主要方式
1.管道(pipe)
管道,通常指无名管道,是UNIX系统IPC最古老的形式。
管道是一种半双工(即数据只能在一个方向上流动)的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。
进程的亲缘关系指的是父子进程或者兄弟进程关系。
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
特点:
- 面向字节流,
- 生命周期随内核。
- 自带同步互斥机制。
- 半双工,单向通信,两个管道实现双向通信。
2.命名管道(namedpipe)
FIFO,也称为命名管道,它是一种文件类型,也是半双工的通信方式。多个进程都可以通过一个约定好的名字找到同一个管道。FIFO允许无亲缘关系进程间的通信。FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。
3.消息队列(messagequeue)
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
特点:
- 消息队列可以认为是一个全局的一个链表,链表节点钟存放着数据报的类型和内容,有消息队列的标识符进行标记。
- 消息队列允许一个或多个进程写入或者读取消息。
- 消息队列的生命周期随内核。
- 消息队列可实现双向通信。
但是当发送到消息队列的信息量大或操作频繁的场合,需要拷贝的时间也就越多,此时可以采用共享内存通信。
4.共享内存(shared memory)
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制(如信号量)配合使用来实现进程间的同步和通信。
特点:
- 不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以。
- 共享内存是临界资源,所以需要操作时必须要保证原子性。使用信号量或者互斥锁都可以。
- 生命周期随内核。
5.信号量(semophore)
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号量主要实现进程之间的同步和互斥,而不是存储通信内容。
信号量定义了两种操作,p操作和v操作,p操作为申请资源,会将数值减去M,表示这部分被他使用了,其他进程暂时不能用。v操作是归还资源操作,告知归还了资源可以用这部分。
6.信号(signal)
信号是软件中断产生,用于进程间异步传递信息。信号可以用来直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
一般在shell中操作,进程获取信号进行处理,一共有64种信号,在shell中输入 kill -l 可查阅
7.套接字(socket)
套接字(有的时候被译为插座)也是一种进程间通信机制,与其他通信机制不同的是,套接字允许两个进程进行通讯,这两个进程可能运行在同一个机器上,也可能运行在不同机器上。
相对于共享内存可以多对多的读取与写入,套接字只能一对一。此外由于序列化等操作占用大量资源,相对于共享内存,套接字更适合传输少量数据。
进程通信方式总结
- 管道:速度慢,容量有限,只有父子进程能通讯
- 命名管道:任何进程间都能通讯,但速度慢
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
- 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全问题。
- 信号量:不能传递复杂消息,只能用来同步。
- 信号:用于通知接收进程某个事件已经发生。
- 套接字:可用于不同机器之间的进程间通信。
线程间的通信方式
锁(Lock)
锁机制包括互斥锁、条件变量、读写锁。
- 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
- 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
- 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
有关Java的锁机制,可以点击查看《详解Java多线程锁之synchronized》和《详解Java多线程锁之Lock和ReadWriteLock》
有关条件变量,可以点击查看《Java多线程的可见性与有序性》中有关volatile的讲解。
信号量(Semaphore)
可以查看这篇博客《快速了解基于AQS实现的Java并发工具类》中有关Semaphore的讲解,感受下信号量如何在java线程通信中的使用。
信号(Signal)
可以查看这篇博客:《彻底搞懂Java的等待-通知(wait-notify)机制》
协程间的通信方式
与线程不同,协程使用程序自定义的调度器进行调度,因此更容易控制协程之间的执行顺序,要想充分利用协程的调度模型,有一个完备的通信机制是很重要的。它主要应该有以下的功能:
- 能从一个协程发送消息到另一个协程,通知另一个协程特定的事件已经发生。
- 能够让协程在事件未发生之前挂起,等待事件发生后被调度并处理,从而有效让出CPU时间。
- 能够在消息中附带相应的数据。
能完成这样任务的模型很多,原理也不尽相同,但思路其实和线程的通信方式大体相同,这里以后再单独讲解。
补充
Nginx的进程通信有哪些
Nginx的进程通信分为三种类别:linux系统与Nginx通信(信号),master进程与worker进程通信(套接字),worker进程间通信(共享内存)。
- linux系统与Nginx通信 答:linux系统与Nginx是通过信号进行通信的,通过信号控制Nginx重启、关闭以及加载配置文件等。比如:./nginx –s quit 向master进程发送信号。
- master进程与worker进程通信 答:master在fork worker进程前,先调用socketpair,创建一个socket对用来master与worker进程间的通信。这个socket就是一个元素个数为2的数组channel,channel[0]用于master进程写入,channel[1]用于worker进程读入。
- worker进程间通信 答:共享内存是最快的通信方式,是worker进程之间的通信方式。使用共享内存的好处是当多个进程使用同一块共享内存时,在任何一个进程修改了共享内存中的内容后,其他进程通过访问这段共享内存都能够得到修改后的内容。
进程七态
各种状态的意义:
- 创建态:进程正在被创建。
- 就绪态:可运行,但因为其他进程正在运行而暂时停止。
- 运行态:正在占用CPU。
- 结束态:进程正在从系统中消失。
- 阻塞状态:该进程等待某个事件(比如IO读取)而停止运行,此时即使有CPU时间片也无法继续运行。
状态变化事件:
- NULL => 创建态:一个进程被创建时的第一个状态。
- 创建态 => 就绪态:当进程创建完成,进入就绪态。
- 就绪态 => 运行态:CPU从就绪队列选择进程执行,进入运行态。
- 运行态 => 结束态:当进程已经运行完成或出错时,进入结束态。
- 运行态 => 就绪态:分配给进程的时间片使用完,进入就绪态。
- 运行态 => 阻塞状态:进程执行等待事件,进入阻塞态。
- 阻塞状态 => 就绪态:进程事件完成,CPU收到中断信号,进入就绪态。
进程状态变化中,还有一种状态叫挂起态,挂起态代表该进程没有占用内存空间,这跟阻塞状态是不一样。
挂起和阻塞的区别:
- 挂起是一个行为,而阻塞是进程的一种状态。
- 进程存放的位置不同:挂起是将进程移到外存中,而处于阻塞状态的进程还是在内存中。
- 原因不同:导致进程被挂起的原因一般是内存不足或者是系统、用户的请求,协调、修改进程,研究进程的状态等,进程阻塞是进程正在等待某一事件发生,可能是等待资源或者响应等(eg.等待I/O完成等)而暂时停止运行。
- 挂起对应的行为是激活,将外存中的进程调入内存中。而处于阻塞状态的进程需要其他进程或系统唤醒。
- 挂起是被动的行为,进程被迫从内存中移至外存中。而进入阻塞可以看成是一个主动的行为(eg.进程I/O时,进程在等待I/O设备完成时,进程主动进入阻塞状态,I/O完成,进程被激活)
挂起态可以分为下面两种:
- 阻塞挂起状态:进程在外存(磁盘)并等待某个事件的出现。
- 就绪挂起状态:进程在外存(磁盘)激活后进入就绪态。
参考文档:
- 《深入理解计算机系统》
- 《深入理解Nginx》