進程、線程、協程的概念并不是按照時間順序出現的,早在計算機剛發展的階段就有協程(協同式調度)的概念了。如果強制的為進程、線程、協程的出現理清一條“時間線”的話,大致可能是如下:
操作系統早期只使用進程來管理不同應用程序之間的有序執行,對于一個應用程序的運行,有主要的兩部分內容需要管理:程序運行所需的資源、程序的執行流程。為了更加細分這兩部分內容的管理,引入了線程的概念,用于表征程序的執行流程。因此也就可以說“進程是資源管理的最小單位,線程是程序運行的最小單位”,進程與線程都是需要通過操作系統來管理的。
但是在現代的網絡服務中,大多都是IO密集型系統,也就是說系統花費大量的時間在IO操作上,而應用程序執行IO操作是需要發生用戶態到內核態的**模式切換**,這個過程需要保存上下文的信息,雖然這個切換的過程很短,可能只有幾十毫秒,但是仍然有可能稱為高并發系統中的一個瓶頸。那么有沒有辦法在應用程序模擬操作系統上下文切換時的過程呢?而不用每次都發生系統調用來進行上下文切換。因此就引入了協程的概念,用于在應用程序層面做調用棧的保護和恢復工作。
> 內存占用大小:進程 - 》線程 -》協程
## 進程
進程是操作系統為了管理不同應用程序之間有些運行而抽象出來的一個映射,操作系統通過一個進程控制塊來控制進程的行為,同時一個進程還包括`程序`、`數據`、`棧`、`共享內存`等。基于虛擬內存管理的32位Linux操作系統的進程結構如下:
:-: 
3GB用戶空間、1GB內核空間
其中3GB的用戶空間又包括了如下的內容:
:-: 
1. 進程控制塊:用于操作系統控制進程的數據結構,當發生進程切換時需要在進程控制塊中保護進程的上下文信息。其包括的內容有:
- 進程標識信息:**進程ID**、父進程ID、用戶ID。
- 處理狀態信息:通用寄存器、程序計數器、程序狀態字寄存器、**棧指針**。
- 進程信息:**進程狀態**、優先級、等待的事件集合、調度信息。
2. 代碼段
代碼段是用來存放執行程序的指令。需要防止在運行時被非法修改,只允許讀,不允許寫。
3. 數據段:用于存放靜態分配的變量和已經初始化的全局變量。
4. BSS段:存放程序未初始化的全局變量。
5. 堆:存放程序運行過程中使用malloc動態分配的變量。
6. 棧:存放函數內的局部變量。
### 進程狀態
當一個應用程序剛被加載進內存的時候操作系統會創建該應用程序對應的進程,一個進程從創建到銷毀經歷的狀態如下:
:-: 
五狀態模型
當內存不夠用的時候,又引入了進程掛起的狀態,其模型如下:
:-: 
> 問:進程的狀態保存在哪?
> 答:進程控制塊中有一部分是保留進程信息,其中就有每個進程的狀態信息。
那么進程之間的調度又是如何進程的呢?主要是由操作系統的調度器通過進程調度算法來進程的調度與超時。
### 進程調度算法
進程的調度算法從大體上可以分為“非搶占式調度”和“搶占式調度”,“非搶占式調度”會一直運行某個進程,知道阻塞事件獲取進程運行結束,才會將CPU讓給其他進程。“搶占式調度”的意思是程序正在運行時可能會被中途打斷,將CPU的運行權交給其他進程。
**調度算法影響的是等待時間,而不會影響進程真正使用CPU的時間和IO時間。**
1. 先來先服務調度算法
“非搶占式”的一個調度算法,會一直運行直到進程結束或者需要等待事件,不利于短作業進程,適用于CPU密集型任務,不適用于IO密集型任務。
2. 最短作業優先調度算法
“非搶占式”的一個調度算法,會優先調用作業時間最短的進程,有利于提高系統的吞吐量,不利于長作業。**在現代的操作系統根本不可能使用,因為無從得知一個進程的運行時間的長短。**
3. 高響應比優先調度算法
"非搶占式"的一個調度算法,權衡短作業和長作業,每次進行進程調度的時候計算**響應比優先級**,響應比高的先運行。
:-: 
4. 時間片輪詢調度算法
簡單、公平且使用廣泛的一個“搶占式”的調度算法,會為每個運行的進程分配一定的時間長度,當時間長度到了的時候就會切換到就緒態,調度另外一個進程運行。
缺點:可能會導致過多的上下文切換。
通常時間片會設置為20ms~50ms。
5. 最高優先級隊列調度算法
“搶占式”和“非搶占式”都有,優先調度優先級高的進程,同時進程的優先級又可以分為靜態的和動態的兩種區別。
6. 多級反饋隊列調度算法
時間片和優先級的綜合使用,也是現代大多數操作系統的進程調度的解決方案。優先級高的進程分配的時間片短一些,但是優先調度;優先級低的進程分配的時間片多一些,但是不會優先調度。
:-: 
### 進程間的通信方式
多個進程之間彼此之間如何進行通信呢?在Linux系統主要有如下的幾個方式:
1. 管道
2. 消息
3. 共享內存
4. 信號量
5. 信號
6. Socket
#### 管道
管道又可以具體分為*匿名管道*和*具名管道*,匿名管道就是命令行中的“|”,將一個命令的輸出作為另外一個命令的輸入,單向通信。具名管道可以通過`mkfifo`創建,當一個進程向具名管道中寫入數據的時候如果沒有進程從管道中讀取數據,就會阻塞住,直到數據被另外的進程讀取。
缺點:通信方式效率低,不適合進程間頻繁的交換數據。
好處:容易知道管道中的數據被另外一個進程讀取了。
原理:
創建管道時對調用`pipe()`這個系統調用,并返回兩個文件描述符,一個是管道寫入端的文件描述符,另外一個是管道輸出端的文件描述符。在shell中使用匿名管道時候創建了兩個子進程,共享父進程的文件描述符達到通信的效果。具名管道可以用于非父子進程之間的通信。
**存放進程的數據會阻塞到拿數據的進程拿走數據。**
#### 消息隊列
為了解決管道通信效率低的問題,引入了消息隊列,進程A向進程B通信時將消息體放入消息隊列后直接返回 ,進程B再從消息隊列中拿走數據,以此來實現異步通信的過程。**消息隊列是保存在內核中的消息列表。**
缺點:不適合與大數據的輸出,內核中每個消息體的大小有限制。同時進程之間在通信的過程中存在用戶態到內核態之間的切換,以及用戶態到內核態之間數據拷貝的開銷。
#### 共享內存
由于消息隊列在通信的過程中存在內核態到用戶態的數據拷貝的過程,使用共享內存就可以避免這個問題了。所謂共享內存就是在進程的虛擬地址空間中拿出一塊地址空間,要通信的進程都映射到相同的物理內存中,這樣兩個進程之間的通信就可以基于這塊物理內存中進行了。
:-: 
#### 信號量
當有多個進程對共享內存進行讀寫操作,就有可以導致數據錯亂的問題,信號量就提供了一種同步的控制方式。
信號量本事是一個數據結構,并不會攜帶消息數據,而是用于控制共享內存的同步訪問。其主要包括一個同步狀態屬性和兩個同步操作。
等待操作:當進程訪A訪問共享內存的時候會遞減同步狀態的值,當同步狀態小于0的時候就會阻塞進程,將進程放入等待隊列中。
喚醒操作:當進程A結束訪問共享內存的時候會遞增同步狀態的值,同時喚醒等待隊列中的進程繼續運行。
#### 信號
上面幾種進程間的通信方式都是用于正常的工作模式下,而信號則是用于異常情況下的工作模式,用于響應各種各樣的事件。Linux提供了幾十種信號用于響應各種各樣的事件。
使用命令`kill -l`可以查看,總共有64種。
組合鍵發送信號:
1. Ctrl + C,產生SIGINT信號,2號。
2. Ctrl + Z,產生SIGTSTP信號。
或者是使用信號標識號,例如殺死一個進程
```
kill -9 pid
```
信號是`唯一的異步通信機制`,可以在任何時候給某一進程發送信號。
#### Socket
Socket是基于TCP/IP協議進行通信的,上面幾種都只能用于同一臺主機之間的通信,而Socket可以用于不同主機之間的通信,是基于網絡服務的不同主機進程之間的通信(當然也可以用于同一臺主機)。
## 線程
線程是比進程更輕量級的調度單位,線程的引入可以把一個進程的分配和執行調度分開,各個線程可以共享進程資源,又可以獨立調度。
線程的實現方式有如下三種:
1. 1:1模式
用戶程序中一個輕量級進程(LWP)與內核的一個線程一一對應,線程的創建、調度、阻塞與銷毀需要系統調用的參數。
:-: 
LWP:Light Weight Process,輕量級進程。
KLT:Kernel-level Thread,內核線程。
2. 1:N模式
使用用戶線程實現,即完全建立在用戶空間的線程庫,系統內核不能感知用戶線程,只能感知進程。用戶線程的優勢在于不需要系統內核的支援,但是劣勢在于實現起來很復雜。
:-: 
UT:user Thread
3. N:M實現
一個輕量級進程對應多個用戶線程,每個輕量級進程再和一個內核線程對應。
:-: 
> Java線程
在Java主流的虛擬機使用的是1:1的實現方式,也就是Java的線程的調度是需要陷入內核態由操作系統管理的;同時也由于這樣,在Java中設置線程的優先級不一定會生效,而僅僅只是建議操作系統的行為,因為不同的操作系統是否是基于優先級的搶占式調度還未知,同時支持的線程的優先級的數量也未知。
## 協程
Coroutine
對于進程和線程,在IO密集型、高并發的系統中最主要的問題在于系統調用時需要耗費時間在上下文切換上,調度成本比較高。因此有些編程語言(python、golang)引入了協程的概念。
協程是比線程更加輕量級的微線程,可以看成是線程里面的函數一樣;其調度時上下文狀態的保存和恢復是由應用程序自己實現的,不需要陷入的內核態。
:-: 
協程要比線程要輕量級很多,例如在Java中一個線程默認分配的空間是1MB,但是協程分配的空間一般幾KB就足夠了。
**不同語言的協程的實現**
1. Java語言采用的1:1方式的線程方案,并沒有在一開始就支持協程。在2018年的時候官方創建了Loom項目來引入“纖程”(也就是協程),在未來Java語言開發中可能兩種方式并存。
> 什么是纖程(Fiber)?
> 一個輕量級的用戶態的線程,不是通過操作系統,而是通過Java虛擬機本身自己來調度。纖程占用空間小,任務切換開銷小。可以創建大量的纖程。
2. golang在語言層面支持了對協程的實現,叫做Goroutine。其協程棧的最小大小為2KB,線程棧的最小大小為2MB。
Go的協程調度器有如下幾個重要的數據結構:
* G 表示 Goroutine ,它是一個待執行的任務;
* M 表示操作系統的線程,它由操作系統的調度器調度和管理;
* P 表示處理器 Processor,它可以被看做運行在線程上的本地調度器;
:-: 
【參考】
1. 《小林Coding 圖解系統》
2. 《操作系統精髓與設計原理》
3. https://zhuanlan.zhihu.com/p/337978321
4. 《深入理解Java虛擬機》
- 第一章 Java基礎
- ThreadLocal
- Java異常體系
- Java集合框架
- List接口及其實現類
- Queue接口及其實現類
- Set接口及其實現類
- Map接口及其實現類
- JDK1.8新特性
- Lambda表達式
- 常用函數式接口
- stream流
- 面試
- 第二章 Java虛擬機
- 第一節、運行時數據區
- 第二節、垃圾回收
- 第三節、類加載機制
- 第四節、類文件與字節碼指令
- 第五節、語法糖
- 第六節、運行期優化
- 面試常見問題
- 第三章 并發編程
- 第一節、Java中的線程
- 第二節、Java中的鎖
- 第三節、線程池
- 第四節、并發工具類
- AQS
- 第四章 網絡編程
- WebSocket協議
- Netty
- Netty入門
- Netty-自定義協議
- 面試題
- IO
- 網絡IO模型
- 第五章 操作系統
- IO
- 文件系統的相關概念
- Java幾種文件讀寫方式性能對比
- Socket
- 內存管理
- 進程、線程、協程
- IO模型的演化過程
- 第六章 計算機網絡
- 第七章 消息隊列
- RabbitMQ
- 第八章 開發框架
- Spring
- Spring事務
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 數據庫
- Mysql
- Mysql中的索引
- Mysql中的鎖
- 面試常見問題
- Mysql中的日志
- InnoDB存儲引擎
- 事務
- Redis
- redis的數據類型
- redis數據結構
- Redis主從復制
- 哨兵模式
- 面試題
- Spring Boot整合Lettuce+Redisson實現布隆過濾器
- 集群
- Redis網絡IO模型
- 第十章 設計模式
- 設計模式-七大原則
- 設計模式-單例模式
- 設計模式-備忘錄模式
- 設計模式-原型模式
- 設計模式-責任鏈模式
- 設計模式-過濾模式
- 設計模式-觀察者模式
- 設計模式-工廠方法模式
- 設計模式-抽象工廠模式
- 設計模式-代理模式
- 第十一章 后端開發常用工具、庫
- Docker
- Docker安裝Mysql
- 第十二章 中間件
- ZooKeeper