说透IO多路复用模型
来源 | OSCHINA 社区
作者 | 京东云开发者京东零售 石朝阳
在说 IO 多路复用模型之前,我们先来大致了解下 Linux 文件系统防城港道教灵符网请符。在 Linux 系统中,不论是你的鼠标,键盘,还是打印机,甚至于连接到本机的 socket client 端,都是以文件描述符的形式存在于系统中,诸如此类,等等等等,所以可以这么说,一切皆文件。来看一下系统定义的文件描述符说明:
从上面的列表可以看到,文件描述符 0,1,2 都已经被系统占用了,当系统启动的时候,这三个描述符就存在了防城港道教灵符网请符。其中 0 代表标准输入,1 代表标准输出,2 代表错误输出。当我们创建新的文件描述符的时候,就会在 2 的基础上进行递增。可以这么说,文件描述符是为了管理被打开的文件而创建的系统索引,他代表了文件的身份 ID。对标 windows 的话,你可以认为和句柄类似,这样就更容易理解一些。
由于网上对 linux 文件这块的原理描述的文章已经非常多了,所以这里我不再做过多的赘述,感兴趣的同学可以从 Wikipedia 翻阅一下防城港道教灵符网请符。由于这块内容比较复杂,不属于本文普及的内容,建议读者另行自研,这里我非常推荐马士兵老师将 linux 文件系统这块,讲解的真的非常好。
select 模型
此模型是 IO 多路复用的最早期使用的模型之一,距今已经几十年了,但是现在依旧有不少应用还在采用此种方式,可见其长生不老防城港道教灵符网请符。首先来看下其具体的定义(来源于 man 二类文档):
int select( intnfds, fd_set readfds, fd_set writefds, fd_set errorfds, structtimeval timeout);
这里解释下其具体参数:
参数一:nfds,也即 maxfd,最大的文件描述符递增一防城港道教灵符网请符。这里之所以传最大描述符,为的就是在遍历 fd_set 的时候,限定遍历范围。
参数二:readfds,可读文件描述符集合防城港道教灵符网请符。
参数三:writefds,可写文件描述符集合防城港道教灵符网请符。
参数四:errorfds,异常文件描述符集合防城港道教灵符网请符。
参数五:timeout,超时时间防城港道教灵符网请符。在这段时间内没有检测到描述符被触发,则返回。
下面的宏处理防城港道教灵符网请符,可以对 fd_set 集合(准确的说是 bitmap,一个描述符有变更,则会在描述符对应的索引处置 1)进行操作:
FD_CLR (inr fd,fd_set set) 用来清除描述词组 set 中相关 fd 的位,即 bitmap 结构中索引值为 fd 的值置为 0防城港道教灵符网请符。
FD_ISSET (int fd,fd_set set) 用来测试描述词组 set 中相关 fd 的位是否为真,即 bitmap 结构中某一位是否为 1防城港道教灵符网请符。
FD_SET(int fd,fd_setset) 用来设置描述词组 set 中相关 fd 的位,即将 bitmap 结构中某一位设置为 1,索引值为 fd防城港道教灵符网请符。
FD_ZERO(fd_set set) 用来清除描述词组 set 的全部位,即将 bitmap 结构全部清零防城港道教灵符网请符。
首先来看一段服务端采用防城港道教灵符网请符了 select 模型的示例代码:
//创建server端套接字防城港道教灵符网请符,获取文件描述符
intlistenfd = socket(PF_INET,SOCK_STREAM, 0);
if(listenfd 0) return1;
//绑定服务器
bind(listenfd,(struct sockaddr)address, sizeof(address));
//监听服务器
listen(listenfd, 5);
structsockaddr_inclient;
socklen_taddr_len = sizeof(client);
//接收客户端连接
intconnfd = accept(listenfd,(struct sockaddr)client,addr_len);
//读缓冲区
charbuff[ 1024];
//读文件操作符
fd_set read_fds;
while( 1)
memset(buff, 0, sizeof(buff));
//注意:每次调用select之前都要重新设置文件描述符connfd防城港道教灵符网请符,因为文件描述符表会在内核中被修改
FD_ZERO(read_fds);
FD_SET(connfd,read_fds);
//注意:select会将用户态中的文件描述符表放到内核中进行修改防城港道教灵符网请符,内核修改完毕后再返回给用户态,开销较大
ret = select(connfd+ 1,read_fds, NULL, NULL, NULL);
if(ret 0)
printf( "Fail to select!\n");
return1;
//检测文件描述符表中相关请求是否可读
if(FD_ISSET(connfd, read_fds))
ret = recv(connfd,buff, sizeof(buff) 1, 0);
printf( "receive %d bytes from client: %s \n",ret,buff);
上面的代码我加了比较详细的注释了防城港道教灵符网请符,大家应该很容易看明白,说白了大概流程其实如下:
首先,创建 socket 套接字,创建完毕后,会获取到此套接字的文件描述符防城港道教灵符网请符。
然后,bind 到指定的地址进行监听 listen防城港道教灵符网请符。这样,服务端就在特定的端口启动起来并进行监听了。
之后,利用开启 accept 方法来监听客户端的连接请求防城港道教灵符网请符。一旦有客户端连接,则将获取到当前客户端连接的 connection 文件描述符。
双方建立连接之后,就可以进行数据互传了防城港道教灵符网请符。需要注意的是,在循环开始的时候,务必每次都要重新设置当前 connection 的文件描述符,是因为文件描描述符表在内核中被修改过,如果不重置,将会导致异常的情况。
重新设置文件描述符后,就可以利用 select 函数从文件描述符表中,来轮询哪些文件描述符就绪了防城港道教灵符网请符。此时系统会将用户态的文件描述符表发送到内核态进行调整,即将准备就绪的文件描述符进行置位,然后再发送给用户态的应用中来。
用户通过 FD_ISSET 方法来轮询文件描述符,如果数据可读,则读取数据即可防城港道教灵符网请符。
而在 select 的时候防城港道教灵符网请符,涉及到用户态和内核态的转换,所以整体转换方式如下:
所以防城港道教灵符网请符,综合起来,select 整体还是比较高效和稳定的,但是呈现出来的问题也不少,这些问题进一步限制了其性能发挥:
文件描述符表为 bitmap 结构,且有长度为 1024 的限制防城港道教灵符网请符。
fdset 无法做到重用,每次循环必须重新创建防城港道教灵符网请符。
频繁的用户态和内核态拷贝,性能开销较大防城港道教灵符网请符。
需要对文件描述符表进行遍历,O (n) 的轮询时间复杂度防城港道教灵符网请符。
poll 模型
考虑到 select 模型的几个限制,后来进行了改进,这也就是 poll 模型,既然是 select 模型的改进版,那么肯定有其亮眼的地方,一起来看看吧防城港道教灵符网请符。当然,这次我们依旧是先翻阅 linux man 二类文档,因为这是官方的文档,对其有着最为精准的定义。
intpoll( structpollfd fds, nfds_t nfds, inttimeout ) ;
其实,从运行机制上说来,poll 所做的功能和 select 是基本上一样的,都是等待并检测一组文件描述符就绪,然后在进行后续的 IO 处理工作防城港道教灵符网请符。只不过不同的是,select 中,采用的是 bitmap 结构,长度限定在 1024 位的文件描述符表,而 poll 模型则采用的是 pollfd 结构的数组 fds,也正是由于 poll 模型采用了数组结构,则不会有 1024 长度限制,使其能够承受更高的并发。
pollfd 结构内容如下:
structpollfd{
intfd; / 文件描述符 /
shortevents; / 关心的事件 /
shortrevents; / 实际返回的事件 /
从上面的结构可以看出,fd 很明显就是指文件描述符,也就是当客户端连接上来后,fd 会将生成的文件描述符保存到这里;而 events 则是指用户想关注的事件;revents 则是指实际返回的事件,是由系统内核填充并返回,如果当前的 fd 文件描述符有状态变化,则 revents 的值就会有相应的变化防城港道教灵符网请符。
events 事件列表如下:
revents 事件列表如下:
从列表中可以看出,revents 是包含 events 的防城港道教灵符网请符。接下来结合示例来看一下:
//创建server端套接字防城港道教灵符网请符,获取文件描述符
intlistenfd = socket(PF_INET,SOCK_STREAM, 0);
if(listenfd 0) return1;
//绑定服务器
bind(listenfd,(struct sockaddr)address, sizeof(address));
//监听服务器
listen(listenfd, 5);
structpollfdpollfds[1];
socklen_taddr_len = sizeof(client);
//接收客户端连接
intconnfd = accept(listenfd,(struct sockaddr)client,addr_len);
//放入fd数组
pollfds[ 0].fd = connfd;
pollfds[ 0].events = POLLIN;
//读缓冲区
charbuff[ 1024];
//读文件操作符
fd_set read_fds;
while( 1)
memset(buff, 0, sizeof(buff));
SELECT模型专用
注意:每次调用select之前都要重新设置文件描述符connfd防城港道教灵符网请符,因为文件描述符表会在内核中被修改
FD_ZERO(read_fds);
FD_SET(connfd,read_fds);
注意:select会将用户态中的文件描述符表放到内核中进行修改防城港道教灵符网请符,内核修改完毕后再返回给用户态,开销较大
ret = select(connfd+1,read_fds,NULL,NULL,NULL);
ret = poll(pollfds, 1, 1000);
if(ret 0)
printf( "Fail to poll!\n");
return1;
SELECT模型专用
检测文件描述符表中相关请求是否可读
if(FD_ISSET(connfd, read_fds))
ret = recv(connfd,buff,sizeof(buff)1,0);
printf("receive %d bytes from client: %s \n",ret,buff);
//检测文件描述符数组中相关请求
if(pollfds[ 0].revents POLLIN){
pollfds[ 0].revents = 0;
ret = recv(connfd,buff, sizeof(buff) 1, 0);
printf( "receive %d bytes from client: %s \n",ret,buff);
由于源码中,我做了比较详细的注释,同时将和 select 模型不一样的地方都列了出来,这里就不再详细解释了防城港道教灵符网请符。总体说来,poll 模型比 select 模型要好用一些,去掉了一些限制,但是仍然避免不了如下的问题:
用户态和内核态仍需要频繁切换,因为 revents 的赋值是在内核态进行的,然后再推送到用户态,和 select 类似,整体开销较大防城港道教灵符网请符。
仍需要遍历数组,时间复杂度为 O(N)防城港道教灵符网请符。
epoll 模型
如果说 select 模型和 poll 模型是早期的产物,在性能上有诸多不尽人意之处,那么自 linux 2.6 之后新增的 epoll 模型,则彻底解决了性能问题,一举使得单机承受百万并发的课题变得极为容易防城港道教灵符网请符。现在可以这么说,只需要一些简单的设置更改,然后配合上 epoll 的性能,实现单机百万并发轻而易举。同时,由于 epoll 整体的优化,使得之前的几个比较耗费性能的问题不再成为羁绊,所以也成为了 linux 平台上进行网络通讯的首选模型。
讲解之前,还是 linux man 文档镇楼:linux man epoll 4 类文档 linux man epoll 7 类文档,俩文档结合着读,会对 epoll 有个大概的了解防城港道教灵符网请符。和之前提到的 select 和 poll 不同的是,此二者皆属于系统调用函数,但是 epoll 则不然,他是存在于内核中的数据结构,可以通过 epoll_create,epoll_ctl 及 epoll_wait 三个函数结合来对此数据结构进行操控。
说道 epoll_create 函数,其作用是在内核中创建一个 epoll 数据结构实例,然后将返回此实例在系统中的文件描述符防城港道教灵符网请符。此 epoll 数据结构的组成其实是一个链表结构,我们称之为 interest list,里面会注册连接上来的 client 的文件描述符。
其简化工作机制如下:
说道 epoll_ctl 函数,其作用则是对 epoll 实例进行增删改查操作防城港道教灵符网请符。有些类似我们常用的 CRUD 操作。这个函数操作的对象其实就是 epoll 数据结构,当有新的 client 连接上来的时候,他会将此 client 注册到 epoll 中的 interest list 中,此操作通过附加 EPOLL_CTL_ADD 标记来实现;当已有的 client 掉线或者主动下线的时候,他会将下线的 client 从 epoll 的 interest list 中移除,此操作通过附加 EPOLL_CTL_DEL 标记来实现;当有 client 的文件描述符有变更的时候,他会将 events 中的对应的文件描述符进行更新,此操作通过附加 EPOLL_CTL_MOD 来实现;当 interest list 中有 client 已经准备好了,可以进行 IO 操作的时候,他会将这些 clients 拿出来,然后放到一个新的 ready list 里面。
其简化工作机制如下:
说道 epoll_wait 函数,其作用就是扫描 ready list,处理准备就绪的 client IO,其返回结果即为准备好进行 IO 的 client 的个数防城港道教灵符网请符。通过遍历这些准备好的 client,就可以轻松进行 IO 处理了。
上面这三个函数是 epoll 操作的基本函数,但是,想要彻底理解 epoll,则需要先了解这三块内容,即:inode,链表,红黑树防城港道教灵符网请符。
在 linux 内核中,针对当前打开的文件,有一个 open file table,里面记录的是所有打开的文件描述符信息;同时也有一个 inode table,里面则记录的是底层的文件描述符信息防城港道教灵符网请符。这里假如文件描述符 B fork 了文件描述符 A,虽然在 open file table 中,我们看新增了一个文件描述符 B,但是实际上,在 inode table 中,A 和 B 的底层是一模一样的。这里,将 inode table 中的内容理解为 windows 中的文件属性,会更加贴切和易懂。这样存储的好处就是,无论上层文件描述符怎么变化,由于 epoll 监控的数据永远是 inode table 的底层数据,那么我就可以一直能够监控到文件的各种变化信息,这也是 epoll 高效的基础。更多详细信息,请参阅这两篇文章:Nonblocking IO The method to epoll's madness.
简化流程如下:
数据存储这块解决了,那么针对连接上来的客户端 socket,该用什么数据结构保存进来呢?这里用到了红黑树,由于客户端 socket 会有频繁的新增和删除操作,而红黑树这块时间复杂度仅仅为 O (logN),还是挺高效的防城港道教灵符网请符。有人会问为啥不用哈希表呢?当大量的连接频繁的进行接入或者断开的时候,扩容或者其他行为将会产生不少的 rehash 操作,而且还要考虑哈希冲突的情况。虽然查询速度的确可以达到 o (1),但是 rehash 或者哈希冲突是不可控的,所以基于这些考量,我认为红黑树占优一些。
客户端 socket 怎么管理这块解决了,接下来,当有 socket 有数据需要进行读写事件处理的时候,系统会将已经就绪的 socket 添加到双向链表中,然后通过 epoll_wait 方法检测的时候,其实检查的就是这个双向链表,由于链表中都是就绪的数据,所以避免了针对整个客户端 socket 列表进行遍历的情况,使得整体效率大大提升防城港道教灵符网请符。整体的操作流程为:
首先,利用 epoll_create 在内核中创建一个 epoll 对象防城港道教灵符网请符。其实这个 epoll 对象,就是一个可以存储客户端连接的数据结构。
然后,客户端 socket 连接上来,会通过 epoll_ctl 操作将结果添加到 epoll 对象的红黑树数据结构中防城港道教灵符网请符。
然后,一旦有 socket 有事件发生,则会通过回调函数将其添加到 ready list 双向链表中防城港道教灵符网请符。
最后,epoll_wait 会遍历链表来处理已经准备好的 socket,然后通过预先设置的水平触发或者边缘触发来进行数据的感知操作防城港道教灵符网请符。
从上面的细节可以看出,由于 epoll 内部监控的是底层的文件描述符信息,可以将变更的描述符直接加入到 ready list,无需用户将所有的描述符再进行传入防城港道教灵符网请符。同时由于 epoll_wait 扫描的是已经就绪的文件描述符,避免了很多无效的遍历查询,使得 epoll 的整体性能大大提升,可以说现在只要谈论 linux 平台的 IO 多路复用,epoll 已经成为了不二之选。
水平触发和边缘触发
上面说到了 epoll,主要讲解了 client 端怎么连进来,但是并未详细的讲解 epoll_wait 怎么被唤醒的,这里我将来详细的讲解一下防城港道教灵符网请符。
水平触发,意即 Level Trigger,边缘触发,意即 Edge Trigger,如果单从字面意思上理解,则不太容易,但是如果将硬件设计中的水平沿,上升沿,下降沿的概念引进来,则理解起来就容易多了防城港道教灵符网请符。比如我们可以这样认为:
如果将上图中的方块看做是 buffer 的话,那么理解起来则就更加容易了,比如针对水平触发,buffer 只要是一直有数据,则一直通知;而边缘触发,则 buffer 容量发生变化的时候,才会通知防城港道教灵符网请符。虽然可以这样简单的理解,但是实际上,其细节处理部分,比图示中展现的更加精细,这里来详细的说一下。
边缘触发
针对读操作,也就是当前 fd 处于 EPOLLIN 模式下,即可读防城港道教灵符网请符。此时意味着有新的数据到来,接收缓冲区可读,以下 buffer 都指接收缓冲区:
buffer 由空变为非空,意即有数据进来的时候,此过程会触发通知防城港道教灵符网请符。
buffer 原本有些数据,这时候又有新数据进来的时候,数据变多,此过程会触发通知防城港道教灵符网请符。
buffer 中有数据,此时用户对操作的 fd 注册 EPOLL_CTL_MOD 事件的时候,会触发通知防城港道教灵符网请符。
针对写操作,也就是当前 fd 处于 EPOLLOUT 模式下,即可写防城港道教灵符网请符。此时意味着缓冲区可以写了,以下 buffer 都指发送缓冲区:
buffer 满了,这时候发送出去一些数据,数据变少,此过程会触发通知防城港道教灵符网请符。
buffer 原本有些数据,这时候又发送出去一些数据,数据变少,此过程会触发通知防城港道教灵符网请符。
这里就是 ET 这种模式触发的几种情形,可以看出,基本上都是围绕着接收缓冲区或者发送缓冲区的状态变化来进行的防城港道教灵符网请符。
晦涩难懂?不存在的防城港道教灵符网请符,举个栗子:
在服务端,我们开启边缘触发模式,然后将 buffer size 设为 10 个字节,来看看具体的表现形式防城港道教灵符网请符。
服务端开启防城港道教灵符网请符,客户端连接,发送单字符 A 到服务端,输出结果如下:
ET Mode: it was triggered once
get1 bytes of content: A
waitto read!
可以看到,由于 buffer 从空到非空,边缘触发通知产生,之后在 epoll_wait 处阻塞,继续等待后续事件防城港道教灵符网请符。
这里我们变一下防城港道教灵符网请符,输入 ABCDEFGHIJKLMNOPQ,可以看到,客户端发送的字符长度超过了服务端 buffer size,那么输出结果将是怎么样的呢?
ET Mode: it was triggered once
get9 bytes of content: ABCDEFGHI
get8 bytes of content: JKLMNOPQ
waitto read!
可以看到,这次发送,由于发送的长度大于 buffer size,所以内容被折成两段进行接收,由于用了边缘触发方式,buffer 的情况是从空到非空,所以只会产生一次通知防城港道教灵符网请符。
水平触发
水平触发则简单多了防城港道教灵符网请符,他包含了边缘触发的所有场景,简而言之如下:
当接收缓冲区不为空的时候,有数据可读,则读事件会一直触发防城港道教灵符网请符。
当发送缓冲区未满的时候,可以继续写入数据,则写事件一直会触发防城港道教灵符网请符。
同样的,为了使表达更清晰,我们也来举个栗子,按照上述入输入方式来进行防城港道教灵符网请符。
服务端开启防城港道教灵符网请符,客户端连接并发送单字符 A,可以看到服务端输出情况如下:
LT Mode: it was triggered once!
get1 bytes of content: A
这个输出结果,毋庸置疑,由于 buffer 中有数据,所以水平模式触发,输出了结果防城港道教灵符网请符。
服务端开启防城港道教灵符网请符,客户端连接并发送 ABCDEFGHIJKLMNOPQ,可以看到服务端输出情况如下:
LT Mode: it was triggered once!
get9 bytes of content: ABCDEFGHI
LT Mode: it was triggered once!
get8 bytes of content: JKLMNOPQ
从结果中,可以看出,由于 buffer 中数据读取完毕后,还有未读完的数据,所以水平模式会一直触发,这也是为啥这里水平模式被触发了两次的原因防城港道教灵符网请符。
有了这两个栗子的比对防城港道教灵符网请符,不知道聪明的你,get 到二者的区别了吗?
在实际开发过程中,实际上 LT 更易用一些,毕竟系统帮助我们做了大部分校验通知工作,之前提到的 SELECT 和 POLL,默认采用的也都是这个防城港道教灵符网请符。但是需要注意的是,当有成千上万个客户端连接上来开始进行数据发送,由于 LT 的特性,内核会频繁的处理通知操作,导致其相对于 ET 来说,比较的耗费系统资源,所以,随着客户端的增多,其性能也就越差。
而边缘触发,由于监控的是 FD 的状态变化,所以整体的系统通知并没有那么频繁,高并发下整体的性能表现也要好很多防城港道教灵符网请符。但是由于此模式下,用户需要积极的处理好每一笔数据,带来的维护代价也是相当大的,稍微不注意就有可能出错。所以使用起来须要非常小心才行。
至于二者如何抉择,诸位就仁者见仁智者见智吧防城港道教灵符网请符。
行文到这里,关于 epoll 的讲解基本上完毕了,大家从中是不是学到了很多干货呢?由于从 netty 研究到 linux epoll 底层,其难度非常大,可以用曲高和寡来形容,所以在这块探索的文章是比较少的,很多东西需要自己照着 man 文档和源码一点一点的琢磨(linux 源码详见 eventpoll.c 等)防城港道教灵符网请符。这里我来纠正一下搜索引擎上,说 epoll 高性能是因为利用 mmap 技术实现了用户态和内核态的内存共享,所以性能好,我前期被这个观点误导了好久,后来下来了 linux 源码,翻了一下,并没有在 epoll 中翻到 mmap 的技术点,所以这个观点是错误的。这些错误观点的文章,国内不少,国外也不少,希望大家能审慎抉择,避免被错误带偏。
所以,epoll 高性能的根本就是,其高效的文件描述符处理方式加上颇具特性边的缘触发处理模式,以极少的内核态和用户态的切换,实现了真正意义上的高并发防城港道教灵符网请符。
微软将系统功能与explorer.exe解耦防城港道教灵符网请符,加速Windows X11开发速度降至近二十年来最低水平
这里有最新开源资讯、软件更新、技术干货等内容
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~
免费测八字 今年运势 请加师父微信
免费看八字运势 请加师父微信: sanqingge8
寻找符咒,灵符,符咒网,道教符咒网,灵符网站,灵符网官网,购买符咒请灵符,这里有各种手绘开光符咒:财运符,财运符咒,财运亨通符咒,五路财神符咒,太岁符咒,化太岁符咒,回心转意符咒,护身符咒,文昌符咒,学业灵符符,开运符咒,转运灵符,桃花符,月老姻缘符咒,偏财符,五鬼运财符咒,化小人符咒,事业符咒,升官符咒,去病符咒,去疾符咒,健康符咒,平安符咒,夫妻和合符,情感和合符咒。
咨询道教符咒 咨询师父微信: sanqingge8
符咒类型如下:
01.财运符-增财运补财库开运 02.太岁符-化解不利顺利度过 03. 回心符-挽回感情增缘复合 04. 护身符-辟邪镇宅转运护身 05. 学业符 -魁星点斗文昌帝君 06. 开运符-开运转运驱除霉运 07. 桃花符-桃花早到月老姻缘 08. 偏财符-五鬼运财偏财运势 09 .小人符-化解小人是非口舌 10 .事业符-事业有成无往不利 11. 去疾符-药王化疾祛病消愈 12. 健康符-身心健康得偿所愿 13. 平安符-诸事顺利健康平安 14 .和合符-夫妻情感姻缘和合 15.定制符-心有所想 专属定制
咨询道教法事 咨询师父微信:sanqingge8
01.化解太岁法事——解太岁、谢太岁 02.升官晋职法事 ——官运亨通提升政绩 03.文昌考试法事—— 开窍聪慧考试顺利 04.偿还阴债法事—— 生债阴宅逢凶化吉 05.开财门补财库—— 增加财运助旺事业 06.助种生基法事—— 病魔缠身增寿增运 07.催子受孕法事—— 生子布阵子女满堂 08.开运转运法事—— 改运天命一帆风顺 09.催财发财法事—— 偏财运势正财持久 10.化解童子法事—— 姻缘顺利仙灵护佑 11.化解小人法事—— 化解小人防人陷害 12.小儿平安法事—— 驱邪回魂活泼健康 13.超度亡灵法事—— 祭奠亲人早登极乐 14.超度宠物法事—— 人类朋友转生脱苦 15.超度婴灵法事—— 打胎坠胎消灾除难 16.祈福许愿法事—— 许愿还愿祈求祈福
本文链接:https://fuzhouwang.org/index.php/post/11172.html
转载声明:本站文章中有转载或采集其他网站内容, 如有转载的文章涉及到您的权益及版权,还麻烦及时联系我们,我们将及时删除,谢谢配合。