濟(jì)南網(wǎng)站制作價格吸引顧客的營銷策略
文章目錄
- 計算機(jī)內(nèi)存分配
- 進(jìn)程與子進(jìn)程
- 流
- IO模型
- 非阻塞IO
- IO多路復(fù)用
- 網(wǎng)絡(luò)IO模型
- 簡單的socket
- 并發(fā)的socket
計算機(jī)內(nèi)存分配
一個32位,4G內(nèi)存的計算機(jī),內(nèi)存使用分為兩部分:
- 操作系統(tǒng)內(nèi)核空間;
- 應(yīng)用程序的用戶空間
- 使用的操作系統(tǒng)不同,分配方式不同;
進(jìn)程與子進(jìn)程
-
進(jìn)程
是操作系統(tǒng)中資源管理的最小單位,它是將靜態(tài)程序加載到內(nèi)存中的一次動態(tài)的執(zhí)行,包括進(jìn)程創(chuàng)建、進(jìn)程調(diào)度、進(jìn)程銷毀; -
每個進(jìn)程有自己獨(dú)有的內(nèi)存(在用戶空間內(nèi)),進(jìn)程的私有內(nèi)存是相互獨(dú)立的,且進(jìn)程間無法直接通信;
-
不同進(jìn)程間通信,可以采用隊列、管道、信號、共享內(nèi)存(內(nèi)核空間內(nèi))等方式;
-
每個進(jìn)程中可以創(chuàng)建一個或者多個線程,多個線程共享當(dāng)前進(jìn)程的部分內(nèi)存資源,如代碼、全局變量等;
-
進(jìn)程的內(nèi)存分布
-
比如上圖中的shell腳本,運(yùn)行時,shell是父進(jìn)程,python3開啟子進(jìn)程,父進(jìn)程會等待子進(jìn)程退出;當(dāng)關(guān)閉shell進(jìn)程(父進(jìn)程)時,python3子進(jìn)程由init進(jìn)程接管。
-
父進(jìn)程中,當(dāng)以fork()系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程時,子進(jìn)程執(zhí)行exit()系統(tǒng)調(diào)用退出后,內(nèi)核中仍然存有子進(jìn)程的信息,如pid, exit code, run time等,這些信息要等父進(jìn)程通過wait/waitpid來回收,若一直未回收,則退出的子進(jìn)程成為僵尸進(jìn)程,一直占用系統(tǒng)資源。
-
僵持進(jìn)程無法通過kill關(guān)閉,隨著數(shù)量增多,系統(tǒng)資源耗盡,導(dǎo)致系統(tǒng)癱瘓。
流
- 可以進(jìn)行IO(input輸入、output輸出)操作的內(nèi)核對象;
- 如文件、管道、socket…
- 流的入口是fd (file descriptor);
IO模型
-
阻塞IO, 一直等待,不占用資源;無法同時處理多個任務(wù);
用戶進(jìn)程發(fā)起讀的系統(tǒng)調(diào)用,當(dāng)內(nèi)核中socket fd未就緒時,一直阻塞等待;
socket fd 就緒時,將內(nèi)核中的socket數(shù)據(jù)拷貝到用戶空間(拷貝期間阻塞等待);
accept()阻塞
-
非阻塞IO, 忙輪詢,占用CPU;
應(yīng)用程序不斷輪詢內(nèi)核,對應(yīng)的socket fd是否就緒,未就緒則返回(非阻塞);
若已就緒,則拷貝內(nèi)核中socket的數(shù)據(jù)到用戶空間(阻塞)。
accept()不阻塞
-
IO多路復(fù)用,多個IO復(fù)用一個進(jìn)程/線程,既可以阻塞等待不占用資源,又可以同時并發(fā)處理多個任務(wù);
- linux 支持select, poll, epoll
- 應(yīng)用程序通過系統(tǒng)調(diào)用讓內(nèi)核同時監(jiān)控多個socket fd,一旦有網(wǎng)絡(luò)事件發(fā)生,內(nèi)核就遍歷找到對應(yīng)的socket,將其標(biāo)記為可讀,然后將所有的socket fd返回給應(yīng)用程序;
- 應(yīng)用程序遍歷所有的fd,找到就緒的fd,通過系統(tǒng)調(diào)用復(fù)制對應(yīng)socket的數(shù)據(jù)到用戶空間;
-
異步IO;
-
應(yīng)用程序發(fā)起異步read操作后,立即返回;
-
內(nèi)核中的fd就緒,復(fù)制數(shù)據(jù)完成,觸發(fā)信號通知應(yīng)用程序;
-
全程無阻塞;
-
信號驅(qū)動IO
-
首先注冊信號處理函數(shù);
-
檢查內(nèi)核socket fd 是否就緒,未就緒直接返回;
-
已就緒,則內(nèi)核發(fā)送信號給應(yīng)用程序,觸發(fā)信號處理函數(shù);
-
信號處理函數(shù),發(fā)起系統(tǒng)調(diào)用,從內(nèi)核空間拷貝socket數(shù)據(jù)到用戶空間(阻塞);
?
非阻塞IO
- 忙輪詢,占用CPU;
- 性能不如阻塞IO;
- 代碼流程,不停地 遍歷所有的fd,查看是否就緒;
?
IO多路復(fù)用
多個IO復(fù)用一個進(jìn)程/線程,既可以阻塞等待不占用資源,又可以同時并發(fā)處理多個任務(wù);
-
select
- 最大連接數(shù)默認(rèn)1024;
兩次拷貝
,先將所有的fd 從用戶空間拷貝到內(nèi)核空間,由內(nèi)核監(jiān)控是否有fd就緒(可讀或可寫),也就是有網(wǎng)絡(luò)事件發(fā)生;一旦有fd就緒,則遍歷所有的fd集合,找到對應(yīng)的fd并將其 標(biāo)記為就緒態(tài)(可讀、可寫),然后將所有的fd(fd集合)從內(nèi)核空間拷貝到用戶空間,用戶進(jìn)程內(nèi)遍歷所有的fd,找出就緒的從進(jìn)行讀寫;兩次遍歷
;- 并發(fā)量大時,性能指數(shù)式下降;
- 代碼流程:
-
poll,與select 沒有本質(zhì)的區(qū)別,只是連接數(shù)比select多;
- select 使用固定長度的 BitMap表示文件描述符集合,而poll 使用動態(tài)數(shù)組,以鏈表形式來組織,突破了 select 的文件描述符個數(shù)限制,還會受到系統(tǒng)文件描述符限制。
- select/poll 都是使用線性結(jié)構(gòu)存儲進(jìn)程的 socket 集合,都需要
遍歷
文件描述符集合來找到可讀或可寫的 socket,時間復(fù)雜度為 O(n),而且也需要在用戶態(tài)與內(nèi)核態(tài)之間拷貝文件描述符集合。
-
epoll,高性能的IO多路復(fù)用(僅linux支持)
- 連接數(shù)更大,上限為進(jìn)程的最大連接數(shù);查看最大連接數(shù)cat /proc/sys/fs/file-max
- 內(nèi)核中采用紅黑樹結(jié)構(gòu),可高效地增刪查(O(logn)),僅返回就緒的fd;
- 將就緒的fd拷貝給用戶進(jìn)程,避免無用的遍歷;
- 適合并發(fā)量大的場景,可以解決C10K問題(單臺服務(wù)器并發(fā)1w),
- 操作流程,使用epoll_create 創(chuàng)建內(nèi)核epoll對象;epoll_ctl將要監(jiān)控的socket加入紅黑樹,內(nèi)核檢測到有網(wǎng)絡(luò)事件發(fā)生,則將對應(yīng)的socket 連接放入一個就緒鏈表中,并復(fù)制給用戶空間(epoll_wait返回);
- epoll支持水平觸發(fā)和邊緣觸發(fā),邊緣觸發(fā)效率更高;select/poll僅僅支持水平觸發(fā),即內(nèi)核socket緩沖區(qū)有數(shù)據(jù)就緒,在數(shù)據(jù)沒有被進(jìn)程讀取完之前,會多次通知進(jìn)程來讀取;而邊緣觸發(fā)則僅僅通知一次,需要進(jìn)程一次性讀取所有的數(shù)據(jù)。例如,當(dāng)快遞放入快遞站點(diǎn)時,管理員可能給你打多個電話催促你取快遞,這種通知多次的方式就是水平觸發(fā);而當(dāng)快遞放入快遞柜時,就只給你發(fā)送一次短信,僅僅通知一次,這種僅僅通知一次的方式就是邊緣觸發(fā)。
- IO多路復(fù)用中有socket 就緒,并不一定可讀、可寫,此時為避免進(jìn)程阻塞,需要結(jié)合非阻塞IO一起使用。
?
網(wǎng)絡(luò)IO模型
- 基于socket網(wǎng)絡(luò)通信
- 客戶端與服務(wù)端建立連接的過程
- 客戶端的socket對象調(diào)用connect((ip, port));
- 服務(wù)器的網(wǎng)卡接收請求,并轉(zhuǎn)發(fā)給OS ,實現(xiàn)TCP三次握手;同時在操作系統(tǒng)內(nèi)核中維護(hù)兩個隊列,TCP半連接隊列 & TCP全連接隊列;半連接隊列表示未完成三次握手,全連接隊列表示完成三次握手,已完成socket連接;
- 內(nèi)核從TCP全連接隊列取出當(dāng)前socket連接,存儲到內(nèi)核文件列表中,同時將其fd返回用戶空間,存入進(jìn)程數(shù)組;
- 應(yīng)用程序中就可以拿著這個已連接的socket進(jìn)行讀寫;
- 讀/寫時 就對應(yīng)阻塞IO、非阻塞IO、IO多路復(fù)用、異步IO、信號驅(qū)動的IO的情況;
下面以python3語言為例演示socket的使用。
簡單的socket
同時只能處理一個客戶端的請求。
server:
import socket
import time
import sys
import signal # 注冊信號的處理函數(shù)def handler(signum: int, frame):"""接收到SIGINT信號時,打印一句話,并退出進(jìn)程"""print("received signal:", signum)sys.exit(0)# 注冊信號的處理函數(shù)
signal.signal(signal.SIGINT, handler) # 使用 Ctrl + C 發(fā)送SIGINT信號# 網(wǎng)絡(luò)層使用Ipv4
# 傳輸層使用TCP
sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 默認(rèn)為阻塞的socket# 綁定ip
sock_server.bind(("localhost", 8000))
# 最多監(jiān)聽1000個連接
sock_server.listen(1000) # 監(jiān)聽的socketwhile True:# TCP三次握手完成,接收socket連接conn, addr = sock_server.accept() # accept阻塞 等待 socket連接就緒# 處理當(dāng)前的socket連接,conn是已連接的socketprint("conn:", conn, addr)# 讀 IO操作data = conn.recv(1024) # 阻塞等待 內(nèi)核中socket就緒,并拷貝數(shù)據(jù)到用戶空間 print("received data:", data.decode())# 寫 IO操作,拷貝到內(nèi)核空間,寫入socket緩沖區(qū)conn.send(b"hello, i am server. I have got your data.")# 在當(dāng)前socket連接 的請求處理完之前,服務(wù)端不會接收下一個客戶端的socket連接time.sleep(20)
client:
import socket
import time# 創(chuàng)建客戶端
sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立連接
sock_client.connect(("localhost", 8000))# 發(fā)送數(shù)據(jù)
sock_client.send(b"hello, i am jack")
print("發(fā)送數(shù)據(jù)完成.")# 接收數(shù)據(jù)
data = sock_client.recv(1024) # 接收 1024 bytes # 阻塞 內(nèi)核fd未就緒->就緒 內(nèi)核socket數(shù)據(jù)復(fù)制到用戶空間
print("received data:", data.decode())
并發(fā)的socket
同時可以處理多個客戶端的請求。
- 阻塞+多進(jìn)程
- 隨著請求數(shù)量的增多,子進(jìn)程越來越多,(fork創(chuàng)建子進(jìn)程復(fù)制父進(jìn)程所有的資源)占用的系統(tǒng)資源越來越多,并發(fā)量大時會影響系統(tǒng)的性能,甚至導(dǎo)致系統(tǒng)崩潰;
- 多進(jìn)程上下文的切換包括用戶空間、內(nèi)核空間,消耗系統(tǒng)性能;
- 所以并發(fā)量特別大時,多進(jìn)程不是理想的方案。
# server.py
import socket
import os
import time
import sys
import signal # 注冊信號的處理函數(shù)
import multiprocessingdef handler(signum: int, frame):"""接收到SIGINT信號時,打印一句話,并退出進(jìn)程"""print("received signal:", signum)sys.exit(0)# 注冊信號的處理函數(shù)
signal.signal(signal.SIGINT, handler) # 使用 Ctrl + C 發(fā)送SIGINT信號# 處理請求
def handle_request(conn, addr):print("subprocess:", os.getpid())print("conn:", conn, addr)flag = Falsewhile not flag:# 接收數(shù)據(jù)data = conn.recv(1024) # recv from kernelprint("received data:", data.decode())# 發(fā)送數(shù)據(jù)conn.send(b"I am server. I have got your data.")# 檢測客戶端的斷開data = conn.recv(1024)print("客戶端斷開:", data.decode())if not data:conn.close()print("客戶端已斷開.")flag = Trueprint(f"{os.getpid()}子進(jìn)程退出.")if __name__ == "__main__":# 網(wǎng)絡(luò)層使用Ipv4# 傳輸層使用TCPsock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 默認(rèn)為阻塞的socket# 綁定ipsock_server.bind(("localhost", 8000))# 最多監(jiān)聽1000個連接sock_server.listen(1000) # 監(jiān)聽的socketwhile True:# TCP三次握手完成,接收socket連接conn, addr = sock_server.accept() # accept阻塞 等待 socket連接就緒# 父進(jìn)程 阻塞等待連接print("創(chuàng)建子進(jìn)程.")# 子進(jìn)程處理 請求sub_process = multiprocessing.Process(target=handle_request, args=(conn, addr))sub_process.daemon = Truesub_process.start()# client.py
import socket
import time# 創(chuàng)建客戶端
sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立連接
sock_client.connect(("localhost", 8000))# 發(fā)送數(shù)據(jù)
sock_client.send(b"hello, i am jack")
print("發(fā)送數(shù)據(jù)完成.")# 接收數(shù)據(jù)
data = sock_client.recv(1024) # 接收 1024 bytes
print("received data:", data.decode())sock_client.close() # 客戶端斷開連接,會發(fā)送空數(shù)據(jù)到服務(wù)端
- 阻塞+多線程
- 線程是輕量級進(jìn)程,同一個進(jìn)程的多個線程可以共享當(dāng)前進(jìn)程的部分資源(代碼、全局變量等),避免了過多的資源消耗;
- 多線程的上下文切換,雖比多進(jìn)程輕量,但大量的線程來回切換,也會給系統(tǒng)造成不小的開銷;
- 多線程需要考慮線程安全問題,另外每個線程也有自己的??臻g,也消耗內(nèi)存;大量的線程必然會消耗大量的??臻g;
- 所以對于特別大的并發(fā)量時,多線程也不是理想的方案。
在這里插入代碼片
- IO多路復(fù)用