高性能服务器框架
高性能服务器架构的特点
- 高可用性:monitor 监控进程,异常日志预警。
- 高性能:零拷贝技术、dal设计(池化技术 + 消息队列)、连接池的请求队列
- 伸缩性:服务器节点既可当C,又能当S进行通信,服务器节点进程间通信就是伸缩性得体现
服务器框架组成模块
包括 IO单元 逻辑单元 存储单元 以及单元沟通的请求队列
对于单服务器程序来说
- IO单元负责新客户端的连接,以及数据的存储,将新客户端的连接存入消息队列中或者说缓存或者是持久层
- 业务进程或线程 通常是一个进程或者一个线程 是否在业务逻辑进程/线程中返回结果给客户端 取决于事件处理模式,对于reactor来说就是它处理,对于proactor来说就是IO逻辑单元去将结果返回给客户端。
- 本地数据或文件或缓存 对于一个线程或者进程来说,取决于进程or线程间的通信方式和存储媒介,假如是使用管道通信,无名管道和有名管道是使用一个文件路径进行存储信息,或者使用消息队列 、用户进程缓存区等等,抑或是使用memcache 或者是redis这种非关系型数据库对热点数据进行短期存储,前者out了,后者是单核王。
- 各单元之间的通信方式 进程间的通信方式 与 线程间的通信方式 共享内存得方式可以减少拷贝次数
对于服务器集群来说
- IO作为接入服务器(GateServer、LoginServer),负责接收客户端请求,做些负载均衡措施,可以使用nginx反向代理,通过配置多个IP,隐藏真实服务器地址来达到负载均衡,也可以通过布隆过滤器去对海量访问进行拦截过滤,也可以使用bitMap对海量不存在的用户进行去重,然后进行正确转发;或者按照服务器功用分类,实现逻辑分发的功能,从而达到负载均衡。
- 逻辑服务器 逻辑服务器当中最不繁忙的服务器可以被拿去做 IO接入服务器(或者说它不处理业务逻辑的那个服务器,只是简单校验转发),一台逻辑服务器本身就有许多的逻辑单元,可以并发的处理客户端的任务。
- 数据库服务器 独立的服务器也好 redis 主从数据库也好
- 各服务器之间的永久TCP连接 请求队列(连接池)
协议层


口诀记忆:物联网淑惠适用
OSI四层模型:应用层 网络层 传输层 链路层 物理层
DNS使用的是TCP协议还是UDP协议_dnc的协议运行在udp
HTTP缓存机制与原理
HTTP与TCP/IP
网络协议栈的准备,刚刚好丢进os中的内核协议栈模块去思考
RPC属于会话层的协议
熟悉DNS\ICMP
dns 是域名解析服务
ARP的工作原理:ARP属于数据链路层协议,它在获取物理地址时候是通过无连接的UDP广播方式,应答者返回一个ARP UDP应答报文即可。
IP:
UDP:数据报服务,如果接收端没有及时在应用层调用recvfrom接收数据报,就有可能丢失掉数据报,而且应用层缓冲区只能一次性去把数据报接收到应用层缓冲区中,再通知内核,通过系统调用把应用层数据拷贝到内核UDP缓冲区中。UDP相比较于TCP,它将排序和重组交给了上层去处理,发送速度更快,一次请求一次应答;相反TCP则是需要消耗比较多的系统资源了,而且需要三次握手才能建立可靠的连接线路。UDP的MTU
TCP:字节流服务,支持接收端确认应答、超时重传。所以接收端缓冲区可以部分接收包的内容也可以一次性接收完
UDP相较于IP协议,头部增加了端口号信息,理论上来讲并没有说提供可靠传输、流量控制等可靠服务,所以它是无连接,不可靠的服务,在内核上看UDP协议,将应用层数据拷贝到UDP缓冲区,发送完毕后就会清空缓冲区内数据,如果对等方没有收到数据,则需要再次自上层向下层。
相反TCP协议的头部增加了SYN序列号,更加丰富的标记号,窗口大小,以及定时器等,因为TCP能够在传输层提供可靠传输、流量控制、超时重传等可靠服务,需要在内核中为应用层数据作副本拷贝。
KCP协议是在应用层实现的可靠UDP传输,因为自下往上层是会加上包头,这样就能在应用层的包头中,去实现UDP的可靠传输
有状态连接和无状态连接的区别?
- HTTP请求和响应都是通过TCP连接来传输的,但TCP仅仅是一个传输层协议,不负责应用层协议的状态管理。 因此,HTTP本身是无连接的。 为什么说http本身是无连接的? http不是使用了tcp协议吗? 您好,HTTP本身是无连接的,因为每次HTTP请求和响应都是独立的、单独的事务。 即使在同一个连接下,每次请求和响应都是相互独立的,服务器不会记住之前的请求和响应信息。
- 对于有状态的连接,如果客户任务是存在上下文关系的,则可以使用 epoll 中的EPOLLONESHOT事件标记sock,使得该sock上的任务在整个连接生命周期中只能被一个线程所处理,不然就得增加上下文同步的开销了。
- 在进程池中,多客户请求下,如果一个客户任务是存在上下文关系的,则最好使用同一个进程来服务这个socket,不然在进程之间传递socket是比较麻烦的,当然可以使用主进程使用IO复用技术去管理所有的监听socket,通知子进程去accept这个socket。所以在这种半同步半异步的并发进程池中,一个客户连接所有任务始终都由一个子进程来处理。
- HTTP 1.0是无状态的,HTTP1.1引入了cookies和session,cookies是存储在客户端的用户个人身份认证信息,session是存储在服务端的,为HTTP提供有状态的连接信息。
TCP与UDP协议的对比
tcp是底层做的可靠传输,应该是到应用层自己做这一套重传,丢包验证之类的吧
还有一些会从应用层自己实现可靠的udp,比如kcp,github上有,实现了可靠的连接同时比tcp消耗少很多
udp的使用场景 游戏广播 聊天系统 视频传输 技能释放
不过可以使用udp,然后再在应用层去做可靠传输机制,比如使用KCP协议去做udp协议的可靠传输
如果客户端和服务器都可以独立发包,但是偶尔发生延迟可以容忍(比如:在线的纸牌游戏,许多MMO类的游戏),那么使用TCP长连接吧。TCP对于网络带宽敏感,可能发生丢包事情,丢包了又可以按序确认重传。
TCP头部有一个keepalive 确活机制,当服务端真的突然停电了,客户端会使用TCP协议的keepalive机制,多次递增时长间隔发送心跳包确活,持续2小时吧。
如果客户端和服务器都可以独立发包,而且无法忍受延迟(比如:大多数的多人动作类游戏,一些MMO类游戏),那么使用UDP吧。
大话就是使用的KCP去从登录服连接网关服,从而确保同时在线人数千人。
由服务端发起shutdown(write)代替close,然后等待read终止,最后执行close。
此种方式关闭能保证对端在关闭收到连接关闭请求(fin)前,可以接收到所有服务端关闭前发出的数据。
TCP 连接中出现reset时候的可能性
三次握手四次挥手
三个半事件的处理与代码的联系
经典问题:TIME-WAIT 时长为何为2 MSL?
高并发场景下大量TCP链接处于time_wait状态原因及优化思路分析
网络游戏开发中的通讯杂谈
TCP不调用recv,接收方会出现什么情况?
发送方的进程把数据填充到内核TCP发送缓冲区中
接收方的进程把数据从内核TCP接收缓冲区中拿数据返回给进程
发送方的发送窗口中(指针游标移动)
发送已确认区 | 发送未确认区 | 发送区 | 未发送不可用区
接收方的接收窗口中(指针游标移动)
接收并确认 | 可接收 | 不可接收
发送方发送1000序列号的SYN同步包 给 接收方确认
接收方确认完1000序列号的SYN同步包,将一个可接收500序列号的ACK包发回给发送方
发送方接收到ACK包后,就发送500序列号的SYN包给接收方
接收方接收完后,因为可接收区已满,所以发送一个接收窗口为0的ACK包给回发送方
发送方此时进行0窗口探测机制,如果在连续三次发送SYN序列号15001的包,接收方都没有回应ACK包,那么就会发送一个RST强制重置连接的包给接收方(连接断开)
TCP的连接状态转变
同一个IP(INADDR_ANY),同一个端口SERV_PORT,只能被成功的bind()一次,若再次bind()就会失败,并且显示:Address already in use,就好像一个班级里不能有两个人叫张三;
结论:相同IP地址的相同端口,只能被bind一次;第二次bind会失败;
介绍命令netstat:显示网络相关信息
-a:显示所有选项
-n:能显示成数字的内容全部显示成数字
-p:显示段落这对应程序名
netstat -anp | grep -E 'State|9000'
我们用两个客户端连接到服务器,服务器给每个客户端发送一串字符"I sent sth to client!\n",并关闭客户端;我们用netstat观察,原来那个监听端口 一直在监听【listen】,但是当来了两个连接之后【连接到服务器的9000端口】,虽然这两个连接被close掉了,但是产生了两条TIME_WAIT状态的信息【因为你有两个客户端连入进来】
只要客户端 连接到服务器,并且 服务器把客户端关闭,那么服务器端就会产生一条针对9000监听端口的 状态为 TIME_WAIT 的连接;只要用netstat看到 TIME_WAIT状态的连接,那么此时, 你杀掉服务器程序再重新启动,就会启动失败,bind()函数返回失败: bind返回的值为-1,错误码为:98,错误信息为:Address already in use TIME_WAIT:涉及到TCP状态转换这个话题了;《Unix网络编程 第三版 卷1》有第二章第六节,2.6.4小节,里边就有一个TCP状态转换图;
第二章第七节,专门介绍了 TIME_WAIT状态;
TCP状态转换图【11种状态】 是 针对“一个TCP连接【一个socket连接】”来说的;
客户端: CLOSED ->SYN_SENT->ESTABLISHED【连接建立,可以进行数据收发】
服务端: CLOSED ->LISTEN->【客户端来握手】SYN_RCVD->ESTABLISHED【连接建立,可以进行数据收发】
谁主动close连接,谁就会给对方发送一个FIN标志置位的一个数据包给对方;【服务器端发送FIN包给客户端】
服务器主动关闭连接:ESTABLISHED->FIN_WAIT1->FIN_WAIT2->TIME_WAIT
客户端被动关闭:ESTABLISHED->CLOSE_WAIT->LAST_ACK
二:TIME_WAIT状态
具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样;当这种状态存在的时候,服务器程序退出并重新执行会失败,会提示:
bind返回的值为-1,错误码为:98,错误信息为:Address already in use, 所以,TIME_WAIT状态是一个让人不喜欢的状态;连接处于TIME_WAIT状态是有时间限制的(1-4分钟之间) = 2 MSL【最长数据包生命周期】;
引入TIME_WAIT状态【并且处于这种状态的时间为1-4分钟】 的原因:
(1)可靠的实现TCP全双工的终止
如果服务器最后发送的ACK【应答】包因为某种原因丢失了,那么客户端一定 会重新发送FIN,这样因为服务器端有TIME_WAIT的存在,服务器会重新发送ACK包给客户端,但是如果没有TIME_WAIT这个状态,那么
无论客户端收到ACK包,服务器都已经关闭连接了,此时客户端重新发送FIN,服务器给回的就不是ACK包,
而是RST【连接复位】包,从而使客户端没有完成正常的4次挥手,不友好,而且有可能造成数据包丢失;也就是说,TIME_WAIT有助于可靠的实现TCP全双工连接的终止;
(二.一)RST标志
对于每一个TCP连接,操作系统是要开辟出来一个收缓冲区,和一个发送缓冲区 来处理数据的收和发;
当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;
反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;
如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;
RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;
(2)允许老的重复的TCP数据包在网络中消逝;
三:SO_REUSEADDR选项
setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前
SO_REUSEADDR的能力:
(1)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;【即便TIME_WAIT状态存在,服务器bind()也能成功】
(2)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址可;
(4)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持,
同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字[TCP不行];
***************
所有TCP服务器都应该指定本套接字选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现;
试验程序nginx5_3_2_server.c
(3.1)两个进程,绑定同一个IP和端口:bind()失败[一个班级不能有两个人叫张三]
(3.2)TIME_WAIT状态时的bind绑定:bind()成功
SO_REUSEADDR:主要解决TIME_WAIT状态导致bind()失败的问题;
TCP TIME_WAIT 过多怎么处理
TIME_WAIT状态
TIME_WAIT状态
指的是主动方 在第四次挥手后 的TCP状态
具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样;当这种状态存在的时候,服务器程序退出并重新执行会失败,会提示: bind返回的值为-1, 错误码为 : 98,错误信息为:Address already in use
所以,TIME_WAIT状态是一个让人不喜欢的状态;
连接处于TIME_WAIT状态是有时间限制的(1-4分钟之间) = 2 MSL【最长数据包生命周期】;
inx+php产生大量TIME_WAIT连接解决办法
TCP TIME_WAIT 过多怎么处理
当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;
FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;
反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;
如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;
RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;
(2)time_wait推导 推导2MSL是怎么来得?
(3)允许老的重复的TCP数据包在网络中消逝;
(4)RST标志与优雅关闭
fin得标志是优雅关闭
网络编程包括三个半事件的处理 连接建立、连接断开、消息到达、消息发送完毕
作为接入服务器Appserver,负载均衡 IO处理单元组成网络库=处理客户端socket,读写客户端数据=封装网络库的部分 tcp建立的三次握手
中间分发过程它就是请求队列
业务逻辑服务器就是逻辑单元
请求队列
存储单元
连接断开
消息到达,文件描述符可读
如果没有Time_WAIT状态,会怎么样?
假设是客户端主动发起关闭流程
前提是第四次挥手无time_wait,说明客户端与服务端都关闭了读端写端,那么客户端就不会有继续监听网络中的报文这一步,也无法确认客户端在第四次挥手发出去的ack是否被服务端接收到。
客户端主动发起关闭,假设第四次挥手后
报文从发送到接收 = 1MSL 所以客户端发出ack后,服务端要么1MSL后收到客户端的ACK,要么是1MSL 后收不到
客户端发出ack后,网络上有滞留报文(fin重传的,ack滞留的,都是1MSL失效),多等1MSL就能让滞留报文失效
服务端收到ack后,多等一个MSL就可让滞留报文失效
最坏的情况就是,如果在 ack发出去的1MSL ,ack到达前一瞬间,服务端 ack 判定没有收到, 然后就发出去了超时重传的fin了,多等的1MSL就是这个超时重传的fin 包占用的 1MSL
三次握手和四次挥手(三个半事件)
三次握手
TCP建立连接时候得三次握手 规定都是客户端先主动发送第一个TCP包给服务端,然后服务端发出一个TCP回应包,之和客户端再次发送数据包,这样TCP连接就建立成功,可以进行可靠数据传输了。
四次挥手(三个半事件)
TCP 的 "三个半事件 " 是指在 TCP 连接的关闭过程中,涉及的四次报文交换,以及每个阶段的半关闭状态。这些事件通常称为 TCP 四次挥手(Four-way handshake),虽然这个过程实际上包含了三个完整的握手事件和一个半关闭的事件,因此有时被称为 “三个半事件”。为了便于理解,让我们逐步分析。
半关闭状态 当read调用为0时,代表对等方发送了一个FIN结束符号
以及提供一个shutdown()只关闭读端还是写端,还是双端关闭。
- 第一次挥手:客户端发送 FIN
客户端希望关闭连接,于是它发送了一个 FIN (Finish) 报文,表示它已经完成了数据发送。此时,客户端进入 FIN_WAIT_1 状态,等待对方的确认。
- 事件 1:客户端向服务端发送 FIN,表示客户端不再发送数据,但还可以接收数据 (双工的) shutdown(Write)。
- 第二次挥手:服务端确认收到 FIN (ACK)
服务端收到客户端发送的 FIN 后,回送一个 ACK (Acknowledgment) 报文,确认已经收到了客户端的 FIN 报文。此时,服务端进入 CLOSE_WAIT 状态,而客户端进入 FIN_WAIT_2 状态,等待服务端关闭连接。
- 事件 2:服务端向客户端发送 ACK,表示服务端确认收到客户端的关闭请求,连接的一部分已经关闭,但服务端可能还有数据需要传送(双工)shutdown(Read)。
- 第三次挥手:服务端发送 FIN
服务端处理完所有的数据传输后,它也发送一个 FIN 报文,表示服务端已经不再需要保持连接,准备关闭。这时,服务端进入 LAST_ACK 状态,等待客户端的最后确认。
- 事件 3:服务端向客户端发送 FIN,表示服务端数据也已经传输完成,不再发送数据shutdown(Write)。
- 第四次挥手:客户端确认收到 FIN (ACK)
客户端收到服务端发送的 FIN 后,发送一个 ACK 报文作为确认,然后进入 TIME_WAIT 状态。客户端在这个状态下会等待一段时间(通常为 2 * 最大报文段寿命,或称为 2 * MSL),以确保服务端能收到 ACK 报文。如果在这段时间内没有收到重发的 FIN 报文,客户端会彻底关闭连接。
- 事件 4:客户端发送 ACK,确认已经接收到服务端的 FIN,连接彻底关闭shutdown 读端。
" 三个半事件" 的含义
- 第一次 FIN 是客户端发起的关闭动作。
- 服务端收到 FIN 后回一个 ACK,表明它知道客户端要关闭了,这算是一个完整的握手。
- 服务端随后再发送 FIN,告诉客户端它也准备关闭连接。
- 最后的 ACK 则是客户端确认服务端也同意关闭连接。
之所以叫" 三个半事件 ",是因为:
- 前面三个事件都是相互确认的操作,包括两个 FIN 和一个 ACK。
- 最后的 ACK 后,客户端进入了 TIME_WAIT 状态,属于半关闭的状态,因此称之为“三个半事件”。
总结:
TCP 连接关闭的过程通过四次报文的发送完成,确保双方可以有序地关闭连接。这个过程的重点在于:
- 双方确认彼此已经停止发送数据。
- 客户端的 TIME_WAIT 状态确保没有遗漏的 FIN 报文。
理解 TCP 的" 四次挥手 "可以帮助我们更好地处理网络应用中的连接管理,避免出现资源泄漏等问题。
TCP是如何实现可靠传输的?
TCP滑动窗口 与 超时重传 与 按序确认
UDP是如何实现可靠传输的?
IP协议与数据链路层中的MTU帧分片
MSS是TCP最大发送包长,在三次握手的前2次握手确定,SYN_SENT SYN_ACK 和本端(即发送端)最大能接收的MSS的大小。这个大小必须是8的倍数,MTU - 20 -20 后再按8的倍数进行填充发出,在tcp交互之前避免分片的产生。
https://blog.csdn.net/www_dong/article/details/113767742
传输层 TCP包头20字节 网络层 IP包头20字节
以太网帧 MTU 是1500字节
TCP流量控制中的滑动窗口大小、TCP字段中16位窗口大小、MTU、MSS、缓存区大小有什么关系?
协议结构体需要保住内存区域连续性
构体传输 & TCP粘包处理_tcp发送结构体-CSDN博客
c语言结构体在嵌入式自定义通信协议中的一些体会_c语言结构体协议-CSDN博客
差错传输的保证,最常使用的是md5,或者说使用其他的传输格式
scoket编程
socket创建
由TCPClient 去创建socket 并调用connect
由TCPServer 去创建socket
由网络服务器基类初始化时候去绑定服务器地址,然后保持监听,然后由网络服务器的主回调函数去accept新的TCP连接,调用NewTcptTask 去处理连接
//TCPClient
nSocket = ::socket(PF_INET, SOCK_STREAM, 0);
socklen_t window_size = 128 * 1024;
//在connect前创建并设置好窗口大小
::setsockopt(nSocket, SOL_SOCKET, SO_RCVBUF, &window_size, sizeof(window_size));
::setsockopt(nSocket, SOL_SOCKET, SO_SNDBUF, &window_size, sizeof(window_size));
::connect(nSocket, (struct sockaddr *) &addr, sizeof(addr))
//TCPServer
int tcpsock = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
socklen_t window_size = 128 * 1024;
//在accept之前创建并设置好窗口大小
::setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
::setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &window_size, sizeof(window_size));
::setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &window_size, sizeof(window_size));
::bind(tcpsock, (struct sockaddr *) &addr, sizeof(addr));
static const int MAX_WAITQUEUE = 2000; /**< 最大等待队列 */
::listen(sock, MAX_WAITQUEUE);
std::vector<struct pollfd> pfds; //poll Io多路复用 要监测多个个 pollfd
pfds[mapper.size()].fd = tcpsock;
pfds[mapper.size()].events = POLLIN;
pfds[mapper.size()].revents = 0;
Sock2Port mapper;
mapper.insert(Sock2Port_value_type(tcpsock, port));
tcpsock = socket()
tcpsock > 0
tcpsock < 0
io读写
定时器的设计
数据结构就几种:
时间轮方式 大话游戏就是采用得哈希函数 + 冲突放链表中
最小堆方式 ,pop过期任务进行处理
升序链表
while (true)
{
sleep(1);
int curTime = getCurrentTime();
int key = time & 0x10000;
ListNode * list= map[key]
while (!list)
{
if (list->time < curTime)
{
执行 list->func
}
else{
break;
}
}
}
定时5点 0点任务就是c++实现的,这种属于通用的定时器事件,挂在角色身上的,检测到了时间戳,就直接去实现注册的函数就好了 lua里面可以去实现属于活动的特殊定时器,比如跨服公会矿战的10点触发
算法就是成员函数的具体处理逻辑
延迟定时任务队列的实现,这种主要是一个偏移值吧,当然也需要去校验这个定时器触发时,偏移值+当前时间是否是错误的,会适当进行矫正
秒级轮询 帧和tick
所谓的定时(每日5点,活动每隔一个周期开,活动每隔一个周期的第几天第几小时重置数据) 和 单次定时任务(执行一次就删除)
都可以直接加入到我们上面所讨论的 时间轮去存储 还是说 是使用最小堆去存储 只是性能上会有差别
前者在每一秒都会去哈希查找一次 后者只是每一秒从最小堆pop出一个到期任务进行处理
代码实现也就是一个函数的多次重载
信号机制
信号处理函数应该是 [可重入函数](https://blog.csdn.net/wenhui_/article/details/6889013)
信号:
信号的生命周期?
信号产生-》信号在进程中注册-》信号在进程中的注销-》执行信号处理函数
信号处理方式?
(1)执行默认处理方式(2)忽略处理(3)执行用户自定义的函数
日志系统的设计
https://blog.csdn.net/weixin_50437588/article/details/128511229
写日志是专门使用一个线程去写还是说用个进程去写
写日志也是消耗蛮大的,不可能说空出手来干别的事情。
日志存库也是按照月存,报错存,关键道具日志存。。
由一个独立的线程去往日志读写缓冲区中放写日志,当其中一个缓冲区满了,就反转缓冲区,另一个线程去去异步将缓冲区中的日志写到文件中。
ngx_log.cxx
//和日志相关的函数放之类
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h> //uintptr_t
#include <stdarg.h> //va_start....
#include <unistd.h> //STDERR_FILENO等
#include <sys/time.h> //gettimeofday
#include <time.h> //localtime_r
#include <fcntl.h> //open
#include <errno.h> //errno
#include "ngx_global.h"
#include "ngx_macro.h"
#include "ngx_func.h"
#include "ngx_c_conf.h"
//全局量---------------------
//错误等级,和ngx_macro.h里定义的日志等级宏是一一对应关系
static u_char err_levels[][20] =
{
{"stderr"}, //0:控制台错误
{"emerg"}, //1:紧急
{"alert"}, //2:警戒
{"crit"}, //3:严重
{"error"}, //4:错误
{"warn"}, //5:警告
{"notice"}, //6:注意
{"info"}, //7:信息
{"debug"} //8:调试
};
ngx_log_t ngx_log;
//----------------------------------------------------------------------------------------------------------------------
//描述:通过可变参数组合出字符串【支持...省略号形参】,自动往字符串最末尾增加换行符【所以调用者不用加\n】, 往标准错误上输出这个字符串;
// 如果err不为0,表示有错误,会将该错误编号以及对应的错误信息一并放到组合出的字符串中一起显示;
//比较典型的C语言中的写法,就是这种va_start,va_end
//fmt:通过这第一个普通参数来寻址后续的所有可变参数的类型及其值
//调用格式比如:ngx_log_stderr(0, "invalid option: \"%s\",%d", "testinfo",123);
/*
ngx_log_stderr(0, "invalid option: \"%s\"", argv[0]); //nginx: invalid option: "./nginx"
ngx_log_stderr(0, "invalid option: %10d", 21); //nginx: invalid option: 21 ---21前面有8个空格
ngx_log_stderr(0, "invalid option: %.6f", 21.378); //nginx: invalid option: 21.378000 ---%.这种只跟f配合有效,往末尾填充0
ngx_log_stderr(0, "invalid option: %.6f", 12.999); //nginx: invalid option: 12.999000
ngx_log_stderr(0, "invalid option: %.2f", 12.999); //nginx: invalid option: 13.00
ngx_log_stderr(0, "invalid option: %xd", 1678); //nginx: invalid option: 68E
ngx_log_stderr(0, "invalid option: %Xd", 1678); //nginx: invalid option: 68E
ngx_log_stderr(15, "invalid option: %s , %d", "testInfo",326); //nginx: invalid option: testInfo , 326
ngx_log_stderr(0, "invalid option: %d", 1678);
*/
void ngx_log_stderr(int err, const char *fmt, ...)
{
va_list args; //创建一个va_list类型变量
u_char errstr[NGX_MAX_ERROR_STR+1]; //2048 -- ************ +1是我自己填的,感谢官方写法有点小瑕疵,所以动手调整一下
u_char *p,*last;
memset(errstr,0,sizeof(errstr)); //我个人加的,这块有必要加,至少在va_end处理之前有必要,否则字符串没有结束标记不行的;***************************
last = errstr + NGX_MAX_ERROR_STR; //last指向整个buffer最后去了【指向最后一个有效位置的后面也就是非有效位】,作为一个标记,防止输出内容超过这么长,
//其实我认为这有问题,所以我才在上边errstr[NGX_MAX_ERROR_STR+1]; 给加了1
//比如你定义 char tmp[2]; 你如果last = tmp+2,那么last实际指向了tmp[2],而tmp[2]在使用中是无效的
p = ngx_cpymem(errstr, "nginx: ", 7); //p指向"nginx: "之后
va_start(args, fmt); //使args指向起始的参数
p = ngx_vslprintf(p,last,fmt,args); //组合出这个字符串保存在errstr里
va_end(args); //释放args
if (err) //如果错误代码不是0,表示有错误发生
{
//错误代码和错误信息也要显示出来
p = ngx_log_errno(p, last, err);
}
//若位置不够,那换行也要硬插入到末尾,哪怕覆盖到其他内容
if (p >= (last - 1))
{
p = (last - 1) - 1; //把尾部空格留出来,这里感觉nginx处理的似乎就不对
//我觉得,last-1,才是最后 一个而有效的内存,而这个位置要保存\0,所以我认为再减1,这个位置,才适合保存\n
}
*p++ = '\n'; //增加个换行符
//往标准错误【一般是屏幕】输出信息
write(STDERR_FILENO,errstr,p - errstr); //三章七节讲过,这个叫标准错误,一般指屏幕
if(ngx_log.fd > STDERR_FILENO) //如果这是个有效的日志文件,本条件肯定成立,此时也才有意义将这个信息写到日志文件
{
//因为上边已经把err信息显示出来了,所以这里就不要显示了,否则显示重复了
err = 0; //不要再次把错误信息弄到字符串里,否则字符串里重复了
p--;*p = 0; //把原来末尾的\n干掉,因为到ngx_log_err_core中还会加这个\n
ngx_log_error_core(NGX_LOG_STDERR,err,(const char *)errstr);
}
return;
}
//----------------------------------------------------------------------------------------------------------------------
//描述:给一段内存,一个错误编号,我要组合出一个字符串,形如: (错误编号: 错误原因),放到给的这段内存中去
// 这个函数我改造的比较多,和原始的nginx代码多有不同
//buf:是个内存,要往这里保存数据
//last:放的数据不要超过这里
//err:错误编号,我们是要取得这个错误编号对应的错误字符串,保存到buffer中
u_char *ngx_log_errno(u_char *buf, u_char *last, int err)
{
//以下代码是我自己改造,感觉作者的代码有些瑕疵
char *perrorinfo = strerror(err); //根据资料不会返回NULL;
size_t len = strlen(perrorinfo);
//然后我还要插入一些字符串: (%d:)
char leftstr[10] = {0};
sprintf(leftstr," (%d: ",err);
size_t leftlen = strlen(leftstr);
char rightstr[] = ") ";
size_t rightlen = strlen(rightstr);
size_t extralen = leftlen + rightlen; //左右的额外宽度
if ((buf + len + extralen) < last)
{
//保证整个我装得下,我就装,否则我全部抛弃 ,nginx的做法是 如果位置不够,就硬留出50个位置【哪怕覆盖掉以往的有效内容】,也要硬往后边塞,这样当然也可以;
buf = ngx_cpymem(buf, leftstr, leftlen);
buf = ngx_cpymem(buf, perrorinfo, len);
buf = ngx_cpymem(buf, rightstr, rightlen);
}
return buf;
}
//----------------------------------------------------------------------------------------------------------------------
//往日志文件中写日志,代码中有自动加换行符,所以调用时字符串不用刻意加\n;
// 日过定向为标准错误,则直接往屏幕上写日志【比如日志文件打不开,则会直接定位到标准错误,此时日志就打印到屏幕上,参考ngx_log_init()】
//level:一个等级数字,我们把日志分成一些等级,以方便管理、显示、过滤等等,如果这个等级数字比配置文件中的等级数字"LogLevel"大,那么该条信息不被写到日志文件中
//err:是个错误代码,如果不是0,就应该转换成显示对应的错误信息,一起写到日志文件中,
//ngx_log_error_core(5,8,"这个XXX工作的有问题,显示的结果是=%s","YYYY");
void ngx_log_error_core(int level, int err, const char *fmt, ...)
{
u_char *last;
u_char errstr[NGX_MAX_ERROR_STR+1]; //这个+1也是我放入进来的,本函数可以参考ngx_log_stderr()函数的写法;
memset(errstr,0,sizeof(errstr));
last = errstr + NGX_MAX_ERROR_STR;
struct timeval tv;
struct tm tm;
time_t sec; //秒
u_char *p; //指向当前要拷贝数据到其中的内存位置
va_list args;
memset(&tv,0,sizeof(struct timeval));
memset(&tm,0,sizeof(struct tm));
gettimeofday(&tv, NULL); //获取当前时间,返回自1970-01-01 00:00:00到现在经历的秒数【第二个参数是时区,一般不关心】
sec = tv.tv_sec; //秒
localtime_r(&sec, &tm); //把参数1的time_t转换为本地时间,保存到参数2中去,带_r的是线程安全的版本,尽量使用
tm.tm_mon++; //月份要调整下正常
tm.tm_year += 1900; //年份要调整下才正常
u_char strcurrtime[40]={0}; //先组合出一个当前时间字符串,格式形如:2019/01/08 19:57:11
ngx_slprintf(strcurrtime,
(u_char *)-1, //若用一个u_char *接一个 (u_char *)-1,则 得到的结果是 0xffffffff....,这个值足够大
"%4d/%02d/%02d %02d:%02d:%02d", //格式是 年/月/日 时:分:秒
tm.tm_year, tm.tm_mon,
tm.tm_mday, tm.tm_hour,
tm.tm_min, tm.tm_sec);
p = ngx_cpymem(errstr,strcurrtime,strlen((const char *)strcurrtime)); //日期增加进来,得到形如: 2019/01/08 20:26:07
p = ngx_slprintf(p, last, " [%s] ", err_levels[level]); //日志级别增加进来,得到形如: 2019/01/08 20:26:07 [crit]
p = ngx_slprintf(p, last, "%P: ",ngx_pid); //支持%P格式,进程id增加进来,得到形如: 2019/01/08 20:50:15 [crit] 2037:
va_start(args, fmt); //使args指向起始的参数
p = ngx_vslprintf(p, last, fmt, args); //把fmt和args参数弄进去,组合出来这个字符串
va_end(args); //释放args
if (err) //如果错误代码不是0,表示有错误发生
{
//错误代码和错误信息也要显示出来
p = ngx_log_errno(p, last, err);
}
//若位置不够,那换行也要硬插入到末尾,哪怕覆盖到其他内容
if (p >= (last - 1))
{
p = (last - 1) - 1; //把尾部空格留出来,这里感觉nginx处理的似乎就不对
//我觉得,last-1,才是最后 一个而有效的内存,而这个位置要保存\0,所以我认为再减1,这个位置,才适合保存\n
}
*p++ = '\n'; //增加个换行符
//这么写代码是图方便:随时可以把流程弄到while后边去;大家可以借鉴一下这种写法
ssize_t n;
while(1)
{
if (level > ngx_log.log_level)
{
//要打印的这个日志的等级太落后(等级数字太大,比配置文件中的数字大)
//这种日志就不打印了
break;
}
//磁盘是否满了的判断,先算了吧,还是由管理员保证这个事情吧;
//写日志文件
n = write(ngx_log.fd,errstr,p - errstr); //文件写入成功后,如果中途
if (n == -1)
{
//写失败有问题
if(errno == ENOSPC) //写失败,且原因是磁盘没空间了
{
//磁盘没空间了
//没空间还写个毛线啊
//先do nothing吧;
}
else
{
//这是有其他错误,那么我考虑把这个错误显示到标准错误设备吧;
if(ngx_log.fd != STDERR_FILENO) //当前是定位到文件的,则条件成立
{
n = write(STDERR_FILENO,errstr,p - errstr);
}
}
}
break;
} //end while
return;
}
//----------------------------------------------------------------------------------------------------------------------
//描述:日志初始化,就是把日志文件打开 ,注意这里边涉及到释放的问题,如何解决?
void ngx_log_init()
{
u_char *plogname = NULL;
size_t nlen;
//从配置文件中读取和日志相关的配置信息
CConfig *p_config = CConfig::GetInstance();
plogname = (u_char *)p_config->GetString("Log");
if(plogname == NULL)
{
//没读到,就要给个缺省的路径文件名了
plogname = (u_char *) NGX_ERROR_LOG_PATH; //"logs/error.log" ,logs目录需要提前建立出来
}
ngx_log.log_level = p_config->GetIntDefault("LogLevel",NGX_LOG_NOTICE);//缺省日志等级为6【注意】 ,如果读失败,就给缺省日志等级
//nlen = strlen((const char *)plogname);
//只写打开|追加到末尾|文件不存在则创建【这个需要跟第三参数指定文件访问权限】
//mode = 0644:文件访问权限, 6: 110 , 4: 100: 【用户:读写, 用户所在组:读,其他:读】 老师在第三章第一节介绍过
//ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT|O_DIRECT,0644); //绕过内和缓冲区,write()成功则写磁盘必然成功,但效率可能会比较低;
ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644);
if (ngx_log.fd == -1) //如果有错误,则直接定位到 标准错误上去
{
ngx_log_stderr(errno,"[alert] could not open error log file: open() \"%s\" failed", plogname);
ngx_log.fd = STDERR_FILENO; //直接定位到标准错误去了
}
return;
}
logger
void hLogger::logva(const hLevel level,const char * pattern,va_list vp)
{
char szName[MAX_PATH_LEN];
bzero(szName, sizeof(szName));
if (m_level > level)
{
return;
}
hTime now;
now.now();
hRTime curTime;
curTime.now();
//GetLocalTime(&system);
msgMut.lock();
va_list vp2;
va_copy(vp2, vp);
if (!m_file.empty())
{
if (m_day != now.getMDay())
{
if (NULL != fp_file)
{
fclose(fp_file);
}
m_day = now.getMDay();
if(m_file.size()> 4&& m_file.substr(m_file.size()-4) ==".log")
{
snprintf(szName,sizeof(szName)-1,"%s_%04d%02d%02d.log",m_file.substr(0,m_file.size()-4).c_str(),now.getYear(),
now.getMonth(),now.getMDay());
}
else
{
snprintf(szName,sizeof(szName)-1,"%s.%04d%02d%02d.log",m_file.c_str(),now.getYear(),
now.getMonth(),now.getMDay());
}
fp_file = fopen(szName,"at");
}
}
std::string color = "\x1b[0m[";
switch (level)
{
case LEVEL_DEBUG: color = "\x1b[32m[";break;
case LEVEL_INFO: color = "\x1b[33m["; break;
case LEVEL_TRACE: color = "\x1b[36m["; break;
case LEVEL_WARN: color = "\x1b[35m["; break;
case LEVEL_ERROR: color = "\x1b[31m["; break;
case LEVEL_FATAL: color = "\x1b[31m["; break;
default:break;
}
color += m_name;
if (NULL != fp_console)
{
fprintf(fp_console,"%s] ", color.c_str());
}
if (NULL != fp_file)
{
fprintf(fp_file,"%s] ", color.c_str());
}
if (NULL != fp_console)
{
fprintf(fp_console,"%04d/%02d/%02d ",now.getYear() ,
now.getMonth(),now.getMDay());
fprintf(fp_console,"%02d:%02d:%02d.%03lu ", now.getHour(),now.getMin(),now.getSec(), curTime.msec());
}
if (NULL != fp_file)
{
fprintf(fp_file,"%04d/%02d/%02d ",now.getYear(),
now.getMonth(),now.getMDay());
fprintf(fp_file,"%02d:%02d:%02d.%03lu ",now.getHour(),now.getMin(),now.getSec(), curTime.msec());
}
if (NULL != fp_console)
{
vfprintf(fp_console,pattern,vp);
fprintf(fp_console,"\n");
fflush(fp_console);
}
if (NULL != fp_file)
{
vfprintf(fp_file,pattern,vp2);
fprintf(fp_file,"\n");
fflush(fp_file);
}
va_end(vp2);
msgMut.unlock();
}
/**
* \brief 写日志
* \param zLevelPtr 日志等级参见 #hLogger::hLevel
* \param pattern 输出格式范例,与printf一样
* \return 无
*/
void hLogger::log(const hLevel level,const char * pattern,...)
{
va_list vp;
if (m_level > level)
return;
va_start(vp,pattern);
logva(level,pattern,vp);
va_end(vp);
}
进程标题设置实战
原理:env 命令可以打出所有的env
c++中使用environ[i] 不存在为结束条件就可以遍历所有的环境变量了
ngx_setproctitle.cxx
//和设置课执行程序标题(名称)相关的放这里
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //env
#include <string.h>
#include "ngx_global.h"
//设置可执行程序标题相关函数:分配内存,并且把环境变量拷贝到新内存中来
void ngx_init_setproctitle()
{
//这里无需判断penvmen == NULL,有些编译器new会返回NULL,有些会报异常,但不管怎样,如果在重要的地方new失败了,你无法收场,让程序失控崩溃,助你发现问题为好;
gp_envmem = new char[g_envneedmem];
memset(gp_envmem,0,g_envneedmem); //内存要清空防止出现问题
char *ptmp = gp_envmem;
//把原来的内存内容搬到新地方来
for (int i = 0; environ[i]; i++)
{
size_t size = strlen(environ[i])+1 ; //不要拉下+1,否则内存全乱套了,因为strlen是不包括字符串末尾的\0的
strcpy(ptmp,environ[i]); //把原环境变量内容拷贝到新地方【新内存】
environ[i] = ptmp; //然后还要让新环境变量指向这段新内存
ptmp += size;
}
return;
}
//设置可执行程序标题
void ngx_setproctitle(const char *title)
{
//我们假设,所有的命令 行参数我们都不需要用到了,可以被随意覆盖了;
//注意:我们的标题长度,不会长到原始标题和原始环境变量都装不下,否则怕出问题,不处理
//(1)计算新标题长度
size_t ititlelen = strlen(title);
//(2)计算总的原始的argv那块内存的总长度【包括各种参数】
size_t esy = g_argvneedmem + g_envneedmem; //argv和environ内存总和
if( esy <= ititlelen)
{
//你标题多长啊,我argv和environ总和都存不下?注意字符串末尾多了个 \0,所以这块判断是 <=【也就是=都算存不下】
return;
}
//空间够保存标题的,够长,存得下,继续走下来
//(3)设置后续的命令行参数为空,表示只有argv[]中只有一个元素了,这是好习惯;防止后续argv被滥用,因为很多判断是用argv[] == NULL来做结束标记判断的;
g_os_argv[1] = NULL;
//(4)把标题弄进来,注意原来的命令行参数都会被覆盖掉,不要再使用这些命令行参数,而且g_os_argv[1]已经被设置为NULL了
char *ptmp = g_os_argv[0]; //让ptmp指向g_os_argv所指向的内存
strcpy(ptmp,title);
ptmp += ititlelen; //跳过标题
//(5)把剩余的原argv以及environ所占的内存全部清0,否则会出现在ps的cmd列可能还会残余一些没有被覆盖的内容;
size_t cha = esy - ititlelen; //内存总和减去标题字符串长度(不含字符串末尾的\0),剩余的大小,就是要memset的;
memset(ptmp,0,cha);
return;
}
damon进程设置实现流程
ngx_daemon.cxx
//和守护进程相关
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
#include <errno.h> //errno
#include <sys/stat.h>
#include <fcntl.h>
#include "ngx_func.h"
#include "ngx_macro.h"
#include "ngx_c_conf.h"
//描述:守护进程初始化
//执行失败:返回-1, 子进程:返回0,父进程:返回1
int ngx_daemon()
{
//(1)创建守护进程的第一步,fork()一个子进程出来
switch (fork()) //fork()出来这个子进程才会成为咱们这里讲解的master进程;
{
case -1:
//创建子进程失败
ngx_log_error_core(NGX_LOG_EMERG,errno, "ngx_daemon()中fork()失败!");
return -1;
case 0:
//子进程,走到这里直接break;
break;
default:
//父进程以往 直接退出exit(0);现在希望回到主流程去释放一些资源
return 1; //父进程直接返回1;
} //end switch
//只有fork()出来的子进程才能走到这个流程
ngx_parent = ngx_pid; //ngx_pid是原来父进程的id,因为这里是子进程,所以子进程的ngx_parent设置为原来父进程的pid
ngx_pid = getpid(); //当前子进程的id要重新取得
//(2)脱离终端,终端关闭,将跟此子进程无关
if (setsid() == -1)
{
ngx_log_error_core(NGX_LOG_EMERG, errno,"ngx_daemon()中setsid()失败!");
return -1;
}
//(3)设置为0,不要让它来限制文件权限,以免引起混乱
umask(0);
//(4)打开黑洞设备,以读写方式打开
int fd = open("/dev/null", O_RDWR);
if (fd == -1)
{
ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中open(\"/dev/null\")失败!");
return -1;
}
if (dup2(fd, STDIN_FILENO) == -1) //先关闭STDIN_FILENO[这是规矩,已经打开的描述符,动他之前,先close],类似于指针指向null,让/dev/null成为标准输入;
{
ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中dup2(STDIN)失败!");
return -1;
}
if (dup2(fd, STDOUT_FILENO) == -1) //再关闭STDIN_FILENO,类似于指针指向null,让/dev/null成为标准输出;
{
ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中dup2(STDOUT)失败!");
return -1;
}
if (fd > STDERR_FILENO) //fd应该是3,这个应该成立
{
if (close(fd) == -1) //释放资源这样这个文件描述符就可以被复用;不然这个数字【文件描述符】会被一直占着;
{
ngx_log_error_core(NGX_LOG_EMERG,errno, "ngx_daemon()中close(fd)失败!");
return -1;
}
}
return 0; //子进程返回0
}
进程组
父子进程读时共享,写时复制原则
https://blog.csdn.net/shenwansangz/article/details/39184789
IO复用技术
朴素模型 使用的是多线程来达到监听多个文件描述符的目的,本质上还是一个线程监听一个
select poll epoll 将文件描述符放在轮询结构体中,达到一次性监听多个文件描述符的目的
IO模型
用户态 用户进程 通过系统调用send/write 向内核发送消息,然后该套接字又是非阻塞IO,采用的也是异步传送消息,此时就需要实现一个应用层面的缓冲区,然后将数据放在缓冲区中,等待发送到下一层的TCP内核发送缓冲区中,整个TCP消息头和TCP缓冲区内的数据组装成传输层的数据段,由于TCP模块提供的是可靠传输,再利用IP层提供的服务,将应用层的数据包的副本发送给对等方。UDP不一样,它只是在IP层上增加了一个端口信息,所以本质上还只是提供的是不可靠的传输,不对应用层的数据包进行副本拷贝,应用层的数据包脱去头后,数据实体都在TCP发送缓冲区中,UDP会选择丢弃这样的数据,倘若对等方没有收到这样的一个数据实体,则会由传输层再次向上层请求一遍,发送出去。
通用IO函数和高级IO函数
write recv send read https://blog.csdn.net/petershina/article/details/7946615
readv writev sprintf snprintf
va_list va_end
事件处理模型
Reactor 和 Procator 区别在于IO模型的不同 异步还是同步 就造成了IO单元是否需要去处理数据这么一项实际操作。
[Reactor模式和Proactor模式] (https://blog.csdn.net/ZYZMZM_/article/details/98049471)
并发事件处理模型
半同步/半异步
领导者模型
常见的服务器并发方案与迭代历史
- cpu单核:循环式/迭代式服务器 提出并发式服务器(one connection per process /one connection per thread) echo服务器就是可以这样实现
- cpu多核利用:prefork or pre thread(thread pool) 这种就是进程当中去fork线程,然后每一个线程或者子进程充当 逻辑单元的作用
-----------提出线程池概念---------- - reactor 模型(n connection one thread) 因为reactor使用的是IO复用技术 ,可以同时监听多个文件描述符,就能实现一个线程或者一个进程监听多个文件描述符,也就是多个连接了,但是呢使用一个进程或者一个线程去监听,看起来就是串行排队处理监听的已就绪的文件描述符。
- reactor + thread per request 这个就是用上多个线程,去拿准备好了的文件描述符
- reactor + worker thread/process 可以把逻辑单元线程 和 IO单元线程了
- reactor + thread pool(能适应密集型计算了)
- multiple reactors(reactors in threads = one loop per thread reactors in processes)
- multiple reactors + thread pools (one loop per thread + thread pools) muduo库的操作 利用主从reactors 一个loop里面就是循环监听
-----------------------异步IO---------------- - proactor 理论上异步IO会快些,毕竟向内核请求读后,就是内核被动告知了读就绪了,中间不需要像 同步IO 那样轮询,可以充分利用中间这段cpu空闲时间,将IO操作与计算重叠时间片
- boost asio 这个事件模型很多人用 ,但是他并不是真正意义上上的proractor,本质上是通过epoll去模拟异步达到的效果
- actor并发模型
零拷贝技术
传统数据传输的问题
在传统I/O操作中,例如从磁盘读取文件并发送到网络,数据需要经过以下步骤:
- DMA将磁盘数据拷贝到内核缓冲区(无需CPU参与)。
- 数据从内核缓冲区拷贝到用户空间缓冲区(CPU参与)。
- 数据从用户缓冲区拷贝到内核的Socket缓冲区(CPU参与)。
- DMA将Socket缓冲区数据发送到网卡(无需CPU参与)。
此过程涉及4次数据拷贝(两次CPU参与)、2次系统调用(read和write),以及4次上下文切换。用户态与内核态之间的切换和数据拷贝成为性能瓶颈。
直接使用硬件Scatter-Gather DMA需硬件和驱动支持实现零拷贝技术
或者是说使用一些高级一些的系统调用函数去减少拷贝次数。
零拷贝技术通过减少冗余数据拷贝和上下文切换,显著提升I/O密集型应用的性能。
RDMA技术是远程存储直接访问技术,深信服就有这样一个应用场景,虚拟机服务器数据文件远程传输同步,使用的也是固态硬盘那种存储结构,相当于把固态硬盘当内存用的。
move语义其实就是零拷贝技术的一种,通过将操作对象的控制权转发,不生成临时对象,减少拷贝次数。
一般构造函数都有默认的实现方法,自动生成出来,如果不需要使用就修改访问权限或者使用c++11的deltete 关键字禁用。
在对类对象使用move语义时,如果没有自我实现拷贝赋值构造函数和析构函数中的任意一个,那么类对象会自动生成拷贝构造函数,也就是我们常说的右值引用的使用。如果声明了其中一个,我们就得自我实现这样一个拷贝构造函数。
stl在使用push_back(class A)时,也会生成临时拷贝对象,所以我们可以在A类中先自定义类拷贝构造函数,然后再使用move语义,这样就能减少一次拷贝;当然也可以直接使用emplace_back()去插入对象,emplace_back是在容器内部构造对象的。
stl容器本身就是一个模板类
同步原语与锁粒度与锁竞争处理,以及死锁的避免
池化技术
- 线程或进程的动态创建是需要耗时操作,导致客户端响应慢
- 进程间或者线程间切换是需要保存上下文的,消耗CPU时间
- 动态创建的子进程是当前进程的完整映像,相当于占用了2份文件描述符或资源,导致可使用的资源急剧下降,影响性能
当任务来临,主进程采取随机选取子进程算法或者使用Robin算法轮流选取

性能检测工具与优化思路与linux实战经验
lscpu 核心线程数量 IO密集型就n+1 cpu繁忙就n*2
ulimit 与文件系统和程序限制有关
122 Linux C++ 系统编程1 终端,shell ,bash,目录和文件,切换root用户,linux常用命令, 文件信息详情,find ,grep,xargs ,显示后台程序 ps
cut
sed '/s//g'
wc -l 统计
find ./ -name fileName -print | xargs wc -l
文件快速查找指令:
alias fpc='find ./ -name ".cpp" -or -name ".c" -or -name ".h" -or -name ".cc"|xargs grep -rn '
alias fpa='find ./ -name "*.lua"|xargs grep -rn '
alias fmf='find /home -type f -size +200M -print0 | xargs -0 du -h | sort -nr'
可替代 find ./ -type f -size +200M -exec du -h {} + | sort -nr
文件分析与查找:file 文件 查看文件类型
strings hSceneMs 查看二进制文件是否包含对应的函数链接符号(应用场景之一:编译项目报某个东西找不到的错了,用它)
ldd 查看链接的动态库是否链接正常
du -sh * | sort -rh | head -10 //查看大内存文件
df -Th
lsof -i:80
一般发出段错误或者程序崩溃等异常运行状态时候 都会产生核心转储文件core.pid
如果没有产生,且进程未挂掉时候就使用gcore命令,对其进行核心转储文件生成
ulimit -c 0 默认不开启 unlimited 就是默认无限制转储文件大小保存
正常编写的程序:
如果出现段错误,很可能是内存越界了,如果存在二进制文件,则先使用gdb,运行一下二进制文件,查看下是栈卡在哪个函数调用中,导致内核发出段错误信号?
再使用gdb 对核心转储文件进行调试
自动生成的默认路径要去proc/sys/core_pattern中查找
apport.service 也要开起来
gdb 看栈看不出是哪个函数卡着了,或者说哪个线程卡着了,就使用bt 查看堆栈调用过程,卡哪了
core
gstack
核心转储文件:core dump是Linux系统下程序崩溃时,系统产生的文件,中文叫做转储文件.它相当于会记录下发生错误时,进程空间中内存的信息映射.通过对这些信息进行调试,我们可以了解为什么系统会发生错误。
一般来说,出现core dump往往是出现了内存错误,包括下面几种情况:
访问空指针,指针悬空未初始化
内存访问越界 内存高位01反转,没有ecc纠错机制
堆栈溢出 内存一直变大(技能死循环相互触发)
再使用gdb 对核心转储文件进行调试
自动生成的默认路径要去proc/sys/core_pattern中查找
apport.service 也要开起来
ulimit -u 查看文件描述符上限
ulimit -c unlimited // unlimited代表不限制产生core文件的内存大小
gcore -p pid 生成核心转储文件
gstack -p pid 查看进程堆栈信息
gdb filename coredumfilename
https://www.cnblogs.com/xyhj/p/15692905.html
https://www.cnblogs.com/summerxye/p/11114396.html
进程内存空间分布:
pmap -p pid
cat /proc/pid/maps
cat /proc/meminfo
Active(file): Inactive(file): 加起来就是用户态的综合内存使用
查看哪块内存一直在变大
比较2台不同机子的用户态内存使用大小,发现多了几百M,那就比对用户态进程,找出那个内存增大的进程
然后对比进程内存分配,找出那个增大的那个区域
内存泄漏实战:
场景服点击开启战斗,进入战斗,把数据发给战斗服去进行计算,然后将计算结果返回给场景服,战报中技能A中调用技能B的触发,技能B又调用了技能A,因为战斗过程中判断一个技能不允许被触发100次,然后战报又一直在打印数据,超出了战报的最大长度,超出内存上限了,就会OOM
文件描述符限制: ulimit 想要修改就得使用sysctl修改限制数量,使用sysctl -p生效
系统内核: 文件系统/etc/sys/fs/ 网络模块中/etc/sys/net/
调试:gdb 调试多进程或多线程时候,先将个数减为1去观察逻辑是否正确,再逐步去增加进程或线程,逐步调试
info threads
thread threadId
压测: webbench(单纯IO复用技术施压程度最高,使用线程实现并发,多次调用read/write)
系统检测工具:
系统调用和信号接收:
strace -e =trace=network
strace -e =trace=signal
网络抓包: tcpdump
网络信息统计: netstat
netstat -ntp | grep TIME_WAIT
netstat -anp
文件描述符:lsof
可以查看到进程正在使用的文件描述符指向哪里,动态库、socket、文件路径、二进制文件
客户端模拟:nc
模拟一个客户端去与服务器进行连接通信
top -p pid
CPU性能检测:mpstat -P ALL 5 100 5s输出100次
主要查看%user用户空间执行业务逻辑cpu运行时间 %sys内核系统调用cpu运行时间 %idle 系统未调用时间占总cpu运行时间

上图表示:cpu 正在等待IO数据,g++ ,在编译,大空间的申请内存就是mmap,映射到进程交换分区,swap damon进程正在对内存进行频繁换页,导致系统很卡,内存不足也会导致CPU占用高,性能瓶颈是内存不够,猜测升级开发机器的内存能够解决这一问题。
wa这个指标的意思
cpu空闲与IO阻塞时间占比
数值越大 说明cpu繁忙
数值越小 说明IO繁忙

浙公网安备 33010602011771号