Laravel11 從0開發 Swoole-Reverb 擴展包(七) - 發布laravel-swoole-reverb
開篇
這一篇,我做了過度,因為正好也完成第一個版本的laravel swoole reverb服務。因此,先做個??,我們后面還會繼續分享整個實現流程。
關于laravel-swoole-reverb
laravel-swoole-reverb是一個將 Swoole 與 Laravel Reverb 集成的包,采用php8.2版本編寫,可用于高性能的 WebSocket 和 Pub/Sub 消息傳遞。這個包依賴 Laravel Reverb ,通過提供swoole:reverb start 操作來替代 reverb:start.同時所說的替代,指的是可以使用swoole:reverb start來運行swoole的服務,但同時也保留了reverb的所有服務。開發者選擇一個進行。
swoole reverb 將reactPHP換成swoole并且swoole reverb高度復用reverb組件,因此,大家可以放心使用,目前經我測試,public private channel 正常,立即發送和隊列發送廣播正常,echojs 監聽正常。歡迎大家使用哈,有問題提 Issue。
知識點分享
在實現多進程的過程中,我也加深學習了一些基礎知識,這里還是要說:一定要看完swoole 基礎知識說的內容,現在有AI后,也更加讓你能夠理解。話不多說,趕緊分享:
fork 子進程后,子進程到底復制父進程的哪些資源,同時哪些資源又不能復制?
我覺得這個問題很有味道,了解這個后,我們在面對多進程、協程編程遇到的心智問題,會對我們有所幫助。然后在放出ai的總結內容前,我建議 朋友們可以去看看c++教程網出的socket編程系列視頻(我也正在看,b站可以找到)。對于php go 等開發者,會有很大幫助,而且,我保證你不會c/c++ 也能看懂的。 為啥我覺得有用哈,因為下面會講到fork 的子進程復制文件描述符是引用計數的策略,這個點在socket編程視頻里面講多進程回射服務器的時候也提到了:fork的子進程負責處理accept 后連接套接字,同時父進程使用close(conn)關閉連接套接字,就會說到引用計數-1
在 Unix/Linux 系統中,使用 fork() 創建子進程時,子進程會繼承父進程的大部分資源,但某些資源不能直接復制。以下是具體的分類:
子進程會復制的資源
-
代碼段(Text Segment):
- 子進程與父進程共享相同的程序代碼,但擁有獨立的執行上下文。
-
數據段(Data Segment):
- 包括全局變量、靜態變量的副本。子進程對這些變量的修改不會影響父進程。
-
堆(Heap):
- 復制父進程的堆數據,但現代操作系統通常使用寫時復制(Copy-On-Write, COW)技術,在子進程修改數據之前,共享相同的物理內存。
-
棧(Stack):
- 復制父進程的棧,包括局部變量、函數調用幀等,但與堆一樣,通常使用寫時復制。
-
文件描述符表(File Descriptors):
- 繼承父進程的打開文件描述符(包括文件、管道、套接字),文件偏移量是共享的。
-
信號處理方式:
- 繼承父進程的信號處理配置(如
SIG_IGN或SIG_DFL),但某些信號(如SIGCHLD)的狀態可能會有所不同。
- 繼承父進程的信號處理配置(如
-
當前工作目錄:
- 繼承父進程的當前工作目錄(
cwd)。
- 繼承父進程的當前工作目錄(
-
環境變量(Environment Variables):
- 復制父進程的環境變量。
-
資源限制(Resource Limits,
ulimit):- 繼承父進程的資源限制,如 CPU 時間、文件大小等。
-
進程組和會話(Process Group and Session):
- 子進程繼承父進程的進程組 ID(PGID)和會話 ID(SID)。
子進程不會復制或有特殊行為的資源
-
進程 ID(PID):
- 子進程會獲得一個新的 PID,而不是繼承父進程的 PID。
-
父進程 ID(PPID):
- 子進程的
PPID被設置為創建它的父進程的 PID。
- 子進程的
-
文件鎖(File Locks):
fcntl()文件鎖不會繼承,需要子進程重新加鎖。
-
掛起的信號(Pending Signals):
- 父進程的掛起信號不會傳遞給子進程。
-
線程(Threads):
fork()僅復制調用它的線程,其他線程不會復制。推薦使用fork()時避免多線程,否則可能導致同步問題。
-
IPC 機制(Inter-Process Communication):
- 共享內存(Shared Memory,
shm):子進程仍可訪問,但由于共享性質,數據不會獨立。 - 消息隊列(Message Queues)和信號量(Semaphores):保持不變,但某些同步行為可能需要重新初始化。
- 共享內存(Shared Memory,
-
打開的
socket的連接狀態:- 繼承
socket,但對于 TCP 連接,某些協議級別的狀態可能需要重新初始化。
- 繼承
-
計時器(Timers):
setitimer()和alarm()設定的定時器在子進程中不會繼承。
特殊情況
-
寫時復制(Copy-On-Write, COW)
- 現代操作系統在
fork()時通常不會真正復制堆和棧,而是采用 COW 機制,直到子進程修改數據時才進行真正的拷貝。
- 現代操作系統在
-
vfork()vfork()直接共享父進程的地址空間,子進程執行exec()或exit()之前,父進程會被掛起。相比fork(),vfork()在某些情況下可以提高性能,但需要謹慎使用。
總結
| 資源 | 繼承/復制 | 備注 |
|---|---|---|
| 代碼段 | 共享 | 只讀共享 |
| 數據段 | 復制(COW) | 修改時才真正復制 |
| 堆(Heap) | 復制(COW) | 修改時才真正復制 |
| 棧(Stack) | 復制(COW) | 修改時才真正復制 |
| 文件描述符 | 繼承 | 共享文件偏移量 |
| 信號處理方式 | 繼承 | SIG_IGN 和 SIG_DFL 繼承 |
| 環境變量 | 復制 | 獨立 |
| 進程 ID(PID) | 新分配 | |
| 父進程 ID(PPID) | 繼承 | |
| 線程 | 不繼承 | 僅復制 fork() 調用的線程 |
| 掛起信號 | 不繼承 | |
| IPC(共享內存等) | 共享 | 共享內存仍可訪問 |
| 進程組 & 會話 | 繼承 | |
| 文件鎖 | 不繼承 | fcntl() 需要重新加鎖 |
因此,在 fork() 之后,父子進程是獨立的進程,但由于 COW 和共享的文件描述符、信號處理等機制,它們仍然在某些方面有聯系。
是的,關于 文件描述符(File Descriptors),除了繼承父進程的打開文件描述符之外,還有一個很重要的細節——引用計數。
文件描述符的繼承機制
-
子進程繼承父進程的文件描述符表:
fork()之后,父進程和子進程的文件描述符表是獨立的拷貝,但它們的文件描述符指向同一個文件描述符表項(File Table Entry)。- 這個文件表項記錄了文件偏移量(File Offset)、打開標志(Flags)和文件引用計數(Reference Count)。
-
文件引用計數(Reference Count):
- 文件引用計數會增加,因為
fork()之后,子進程的文件描述符仍然指向相同的文件結構。 - 只有當所有進程(父進程和子進程)都關閉(
close(fd))這個文件描述符后,文件才會真正關閉。
- 文件引用計數會增加,因為
-
共享文件偏移量:
- 由于父子進程的文件描述符指向相同的文件表項,它們會共享文件的讀寫偏移量(
lseek()影響彼此)。 - 如果父進程
read()或write(),會影響子進程的文件讀寫位置,反之亦然。
- 由于父子進程的文件描述符指向相同的文件表項,它們會共享文件的讀寫偏移量(
-
文件狀態標志的繼承:
O_APPEND、O_NONBLOCK等文件標志(通過fcntl()設置)也是共享的。
示例:文件描述符共享偏移量
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
write(fd, "Hello", 5); // 寫入 5 字節
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) { // 子進程
write(fd, " Child", 6); // 繼續寫入
close(fd);
exit(0);
} else { // 父進程
wait(NULL); // 等待子進程結束
write(fd, " Parent", 7); // 繼續寫入
close(fd);
}
return 0;
}
預期輸出 (test.txt 內容):
Hello Child Parent
說明:
- 父進程
write(fd, "Hello", 5)之后,文件偏移量變成5。 - 子進程
write(fd, " Child", 6),偏移量變成11。 - 父進程等待子進程完成,再
write(fd, " Parent", 7),最終文件內容是"Hello Child Parent"。 - 由于共享偏移量,讀寫操作會影響對方!
避免共享影響的方法
-
使用
dup()或dup2():- 這樣可以創建一個新的文件表項,避免共享偏移量。
-
重新打開文件:
int fd = open("test.txt", O_RDWR);- 這樣父子進程擁有不同的
fd,文件偏移量互不影響。
- 這樣父子進程擁有不同的
總結
| 繼承屬性 | 共享情況 |
|---|---|
| 文件描述符(fd) | 繼承 |
| 文件引用計數 | 增加(直到 close() 釋放) |
| 文件偏移量 | 共享(讀寫影響對方) |
| 文件狀態標志 | 共享(O_APPEND, O_NONBLOCK) |
所以,fork() 后子進程并不擁有文件的獨立副本,而是共享同一個文件表項,只是它的文件描述符表是獨立的。因此,如果父子進程都需要獨立操作文件,應該使用 dup() 或者 open() 重新獲取 fd。
總結
這一篇就到這,后續我會繼續寫具體實現過程的分享,同時,如果大家覺得框架太繞了的話,可以在我的laravel-swoole-reverb里面找到swoole-reverb.php這個單文件,文件基本實現了整個過程,目前還差http pusher 接口的具體邏輯沒寫,后面也會繼續完善(看下面第二張圖)



浙公網安備 33010602011771號