select、poll、epoll的原理

news2024/11/15 17:58:43

目录

1.IO多路复用

2.select原理

3.poll原理

4.epoll原理

5.select、poll、epoll总结

6.epoll原理详解

6.1内核收包的过程

6.2进程调度时的阻塞

6.3再来看一下内核收网络数据的过程

6.4select的原理

6.5epoll的设计原理

6.6补充

6.7总结


1.IO多路复用

IO多路复用就是一个线程同时监听多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作

2.select原理

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

(数组存放三个fd)

select函数监视的文件描述符分3类,分别是writefds、readfds、exceptfds。

调用select函数后会阻塞,直到有描述符就绪(有数据可读、可写或有except),或者超时,函数返回。

当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

缺点:select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

3.poll原理

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

select使用三个位图(readfds、writefds、exceptfds)来表示fdset;poll使用一个pollfd的指针来表示fdset。(链表)

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。

同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降) 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

4.epoll原理

相对于select和poll来说,epoll更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

4.1int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

4.2int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数是对指定描述符fd执行op操作。

- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};
 
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
 

4.3int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待epfd上的io事件,最多返回maxevents个事件。

参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。

该函数返回需要处理的事件数目,如返回0表示已超时。

epoll工作模式:

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

5.select、poll、epoll总结

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

epoll的优点主要是一下几个方面:
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。

2.IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

区别

select

poll

epoll

最大连接数

单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(32位、64位机)

没有最大连接数的限制,它是基于链表来存储的

连接数基本上只受限于机器的内存大小

FD剧增带来的影响

因为每次调用时都会对连接进行线性遍历。随着FD的增加会造成遍历速度慢的“线性下降问题”。

存在“线性遍历”的问题

因为epoll内核中实现是根据每个fd上的callback 函数来实现的,只有活跃的socket才会主动调用 callback,所以在活跃socket较少的情况下,使 用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

消息传递方式

内核需要将消息传递到用户空间,都需要内核拷贝动作

内核需要将消息传递到用户空间,都需要内核拷贝动作

epoll通过内核和用户空间共享一块内存来实现的。

6.epoll原理详解

6.1内核收包的过程

当网卡上收到数据以后,Linux中第一个工作的模块是网络驱动。 网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达。

第二,当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。 网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。

ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理。最后会被放到用户socket的接收队列中。

6.2进程调度时的阻塞

阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。

普通的recv接收的伪代码:

//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定端口
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)

步骤:先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。

操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”几个状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

下图中的计算机中运行着A、B、C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。

当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。

当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。

 当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

6.3再来看一下内核收网络数据的过程

进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

 

操作系统如何知道网络数据对应于哪个socket

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到

对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

思考下,如何同时监视多个socket数据呢?

6.4select的原理

select的伪代码:

int fds[] = 存放需要监听的socket
while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

先准备一个数组(上面代码中的fds),让fds存放着所有需要监视的socket,然后调用select.

如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。

用户可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

select的实现思路很直接。假如进程A同时监视sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。

 

当任何一个socket收到数据后,中断程序将唤起进程。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。

这种方法的缺点:

1)每次调用select都需要将进程加入到所有被监视socket的等待队列,每次唤醒都需要从每个队列中移除,都必须要进行遍历所有被监视socket。而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

2)进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历所有被监视的socket一次

6.5epoll的设计原理

epoll是在select出现N多年后才被发明的,是select和poll的增强版本。

措施一:功能分离

大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升。相比select,epoll拆分了功能。伪代码如下:(先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); // 将所有需要监听的socket 添加到epfd中
while(1){
    int n = epoll_wait(...)
    for(接收到数据的socket){
        //处理
    }
}

措施二:就绪列表

select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。

当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象)。eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列,同时它还有一个就绪列表rdlist。

 创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。

 假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程A。

 当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。中断程序会给eventpoll的“就绪列表rdlist”添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket。

这样的话,eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表rdlist来改变进程状态。

中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。

6.6补充

就绪队列应该应使用什么数据结构?eventpoll应使用什么数据结构来管理通过epoll_ctl添加或删除的socket?

就绪列表引用着就绪的socket,所以它应能够快速的插入数据。程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。

所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列

既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构。

6.7总结

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

同时,所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdlist双向链表中。

当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高,可以轻易地处理百万级别的并发连接。

eventpoll不仅仅用在网络通信上,进程间通信的管道也可以使用eventpoll。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2107218.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据时代的领航者:首席数据官(CDO)如何影响城市治理?

随着2023年9月的到来&#xff0c;多个中国城市包括长沙和北京相继宣布引入首席数据官&#xff08;CDO&#xff09;机制&#xff0c;这标志着国家数据管理体系进入一个新纪元。 首席数据官的设立不仅是对传统数据管理方式的重大革新&#xff0c;也是增强数据战略意识和推动数据…

托勒密世界地图:现代地形图绘制的标杆诞生于公元2世纪

关注我们 - 数字罗塞塔计划 - 今天要为大家分享一幅公元150年左右的世界地图——托勒密世界地图&#xff0c;它是由古埃及的数学家、天文学家、地理学家及占星家劳狄乌斯托勒密绘制的。托勒密著有《天文学大成》、《地理学》和《占星四书》等著作&#xff0c;其中《地理学》一书…

从校园到产业园:数字媒体人才如何无缝对接产业需求?

在当今数字化时代&#xff0c;数字媒体产业蓬勃发展&#xff0c;对专业人才的需求日益旺盛。然而&#xff0c;如何实现从校园到产业园的无缝对接&#xff0c;成为关键问题。 为了实现无缝对接&#xff0c;一方面&#xff0c;学校应加强与产业的合作。邀请行业专家走进校园授课、…

免费分享一套SpringBoot+Vue驾校(预约)管理系统【论文+源码+SQL脚本】,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue驾校(预约)管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue驾校(预约)管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 传统办法管理信息首先需要花费的时间比较多&…

OpenCV与Matplotlib:灰度图像

目录 读取灰度图像 代码解释 1. 导入库 2. 读取彩色图像 3. 转换为灰度图像 4. 将 BGR 图像转换为 RGB 格式 5. 创建子图并显示图像 总结&#xff1a; 整体代码 效果展示 衍生操作 1. 边缘检测 代码说明 整体代码 效果展示 2. 图像二值化 代码说明 整体代码 效…

kali——nmap的使用

目录 前言 普通nmap扫描 扫描单个目标地址 扫描多个目标地址 扫描范围目标地址 扫描目标网段 扫描众多目标地址 排除扫描 扫描指定端口 路由追踪 进阶扫描 综合扫描&#xff08;-A&#xff09; 目标网段在线主机&#xff08;-sP&#xff09; 目标主机指纹扫描&am…

Android TextView设置跑马灯失效

1.关于问题 TextView失效在网上有详细的解决方案&#xff0c;大部分时候都能够很好的解决问题 下面给出网上的解决方案&#xff1a; <TextViewandroid:layout_width"100dp"android:layout_height"22dp"tools:text"水浇地放松放松开发的开始放假…

深入RAG优化:BGE词嵌入全解析与Landmark Embedding新突破

前面已经写过一篇关于Embedding选型的文章,《如何高效选择RAG的中文Embedding模型?揭秘最佳实践与关键标准!》,主要介绍通过开源网站的下载量和测评效果选择Embedding模型。 一、Embedding选型建议与结果 选型建议: 1、大部分模型的序列长度是 512 tokens。8192 可尝试 …

每日最新AIGC进展(59):谷歌提出关键帧插值算法、谷歌研究院提出用实时游戏画面生成算法、中国科学院大学提出复杂场景图像生成算法

Diffusion Models专栏文章汇总&#xff1a;入门与实战 Generative Inbetweening: Adapting Image-to-Video Models for Keyframe Interpolation 本研究提出了一种新颖的关键帧插值方法&#xff0c;旨在生成符合自然运动轨迹的连续视频片段。我们适应了已经训练好的图像到视频扩…

每日OJ_牛客_Emacs计算器(逆波兰表达式)

目录 牛客_Emacs计算器&#xff08;逆波兰表达式&#xff09; 解析代码 牛客_Emacs计算器&#xff08;逆波兰表达式&#xff09; Emacs计算器__牛客网 解析代码 逆波兰表达式(后缀表达式)求值&#xff0c;需要借助栈&#xff0c;思路&#xff1a; 循环输入&#xff0c;获取…

智能制造新纪元:3D协同平台引领前沿创新

随着市场的发展&#xff0c;我们的企业面临两个方面的挑战&#xff1a; 从业务和市场方面来看&#xff0c;为了在竞争中取得更大优势&#xff0c;我们需要以高质且低价的产品赢得消费者的信赖&#xff0c;同时必须有效控制成本、加速产品迭代&#xff0c;缩短产品上市周期&…

Orcad如何更改A4到A3纸,表格填充

1 可以直接从以有的A4纸转到A3 2 选择过滤 1 有电气属性的都选择不了 2 修改元器件名称,改了之后下面有横线

交换机堆叠配置

1.华为S系列交换机 维护宝典 https://support.huawei.com/enterprise/zh/doc/EDOC1100339648/d9b3a94b 2.堆叠方式有两种 2.1.专用堆叠卡 2.2.业务口堆叠-10G光口 主交换机&#xff08;19&#xff0c;20口&#xff09;对应备交换机&#xff08;20,19口&#xff09; 全新设…

OceanBase 4.x 存储引擎解析:如何让历史库场景成本降低50%+

据国际数据公司&#xff08;IDC&#xff09;的报告显示&#xff0c;预计到2025年&#xff0c;全球范围内每天将产生高达180ZB的庞大数据量&#xff0c;这一趋势预示着企业将面临着更加严峻的海量数据处理挑战。随着数据日渐庞大&#xff0c;一些存储系统会出现诸如存储空间扩展…

家用智能水表精度要求是多少?

家用智能水表的精度要求是为了确保水表能够准确计量用户的用水量&#xff0c;避免因计量误差导致的不公平收费或水资源浪费。根据国家标准和行业规范&#xff0c;家用智能水表的精度通常需要达到一定的技术指标&#xff0c;以确保其在不同流量条件下的测量准确性。 一、精度标…

喜讯-惟客数据成为中国信息协会数据要素专委会首批常务理事单位

近日&#xff0c;中国信息协会数据要素专业委员会成立大会暨数据资源开发利用及场景创新主题研讨会在贵阳顺利举行&#xff0c;WakeData惟客数据作为受邀企业出席此次活动&#xff0c;并通过资格审核&#xff0c;成为数据要素专委会首批常务理事单位。 中国信息协会数据要素专委…

Java项目: 基于SpringBoot+mysql学生宿舍管理系统(含源码+数据库+开题报告+毕业论文)

一、项目简介 本项目是一套基于SpringBootmysql学生宿舍管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功…

在线演示文稿应用PPTist本地化部署并实现无公网IP远程编辑PPT

文章目录 前言1. 本地安装PPTist2. PPTist 使用介绍3. 安装Cpolar内网穿透4. 配置公网地址5. 配置固定公网地址 前言 本文主要介绍如何在Windows系统环境本地部署开源在线演示文稿应用PPTist&#xff0c;并结合cpolar内网穿透工具实现随时随地远程访问与使用该项目。 PPTist …

CSS解析:盒模型

在网页上实现元素布局涉及很多技术。在复杂网站上&#xff0c;可能会用到浮动元素、绝对定位元素以及其他各种大小的元素&#xff0c;甚至也会使用较新的CSS特性&#xff0c;比如Flexbox或者网格布局。 在此之前我们要打好基础&#xff0c;深刻理解浏览器是如何设置元素的大小…

PHP一站式解决方案高级房产系统小程序源码

一站式解决方案&#xff0c;高级房产系统让房产管理更轻松 &#x1f3e0;【开篇&#xff1a;告别繁琐&#xff0c;迎接高效房产管理新时代】&#x1f3e0; 你是否还在为房产管理的繁琐流程而头疼&#xff1f;从房源录入、客户咨询到合同签订、售后服务&#xff0c;每一个环节…