reverb扩展-laravel-swoole-reverb
开篇
最近做了这个扩展包,学习了pusher协议标准。因此也做了一个合集,有兴趣可以看下Laravel11 从0开发 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 接口的具体逻辑没写,后面也会继续完善(看下面第二张图)
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: