学习过程简单记录一些知识点和自己的疑问
参考《[Linux 高性能服务器编程].2013.中文版》、小林coding《图解操作系统》
虚拟机环境配置
现有的是wsl2,不是VMware上的完整虚拟机–不在工位电脑配置了,需要换笔记本去配置
lsb_release -a 检查ubuntu版本
uname -r查看内核版本
g++ --version g++版本
cmake --version 查看cmake是否安装
sudo apt updata(检测可以软件包最新版本),因为在root下
apt install -y build-essential cmake git
linux命令
Linux 常用操作命令大全(最后更新时间:2024年1月)_linux常用命令-CSDN博客
pwd# 显示当前路径/#根目录ls# 查看当前目录下的所有目录和文件ls/#查看目标目录下的所有目录和文件cd# 切目录cd..#返回上一级目录mkdir# 创建文件夹rm# 删除rm-rf文件夹名#删除文件夹cp# 复制mv# 移动cat文件名# 查看文件内容less文件名#分页查看文件内容,适合长文件,进去后,空格翻页,q退出grep"关键字"文件名#在文件内搜索关键字find# 找文件sudo# 管理员权限tree-L2#按树状看目录结构,-L 2表示只看两层pwdlscd~mkdirtest_linux#创建文件夹cdtest_linux#切换到这个目录下toucha.txt#创建a.txt,文件存在的话,touch会更新文件的最后修改时间ls#列出当前目录Linux目录结构
1.TCP/IP协议族![]()
数据链路层实现网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)。
不同的物理网络具有不同的电气特性,网络驱动程序隐藏这些细节,给上层协议提供一个统一的接口。
数据链路层常用的协议:ARP协议(地址解析协议)和RARP协议(逆地址解析协议),实现了IP地址(网络层)和机器物理地址(MAC地址,以太网、令牌环和802.11无线网络这种物理媒介都使用MAC地址)之间的互相转换
网络层使用IP地址寻址一台机器,数据链路层使用物理地址寻址一台机器,所以网络层需要先将目标机器的IP地址转化为其物理地址,才能使用数据链路层提供的服务(ARP协议)。
网络层:实现数据包的选路和转发。隐层网络拓扑的细节,使得传输层和网络应用程序看来,通信双方是直接相连的。
广域网WAN通常使用众多分级的路由器来连接分散的主机或局域网LAN。因此,通信的两台主机是通过多个中间节点(路由器)连接的。
网络层的任务就是选择这些中间节点,以确定两台主机之间的通信路径
IP协议,根据数据报的目的IP地址来决定如何投递,使用逐跳(hop by hop)方式确定通信路径。
ICMP协议(因特网控制报文协议),主要用于检测网络连接。
传输层:为主机上的应用程序提供端到端的通信/TCP/UDP/SCTP协议
应用层:在用户空间实现,前面三层都负责处理网络通信细节,需要稳定高效,所以在内核控件实现。应用层负责处理文件传输,名称查询,网络管理等,可以减轻内核压力。
帧才是最终在物理网络上传送的字节序列
网络通信
TCP三次握手(建立连接)、四次挥手
TCP协议为应用层提供可靠的(保证网络包的交付、网络包的按序交付、网络包中数据的完整=保证接收端接接收的网络包是无损坏、无间隔、非冗余和按序的)、面向连接的(一对一)、基于字节流(TCP报文是有序的)的服务,使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数据结构,比如连接的状态、读写缓冲区,以及诸多定时器等。当通信结束,双方必须关闭连接以释放这些内核数据。
三次握手-TCP建立连接
TCP头部格式的控制位
SYN 1 希望建立连接,并初始化序列号
ACK 1 确认应答号生效,按序进行传输
FIN 1 传输结束,断开连接
RST 1 连接异常,强制断开
第一次握手第一个报文 SYN报文:SYN=1,ACK=0;服务端主动监听某个端口,处于LISTEN状态;客户端初始化序列号(client_isn),将此序列号置于TCP头部的序列号字段中,同时把SYN标志位置为1,将第一个SYN报文发给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态。
第二次握手第二个报文 SYN+ACK报文:SYN=1,ACK=1;服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
第三次握手第三个报文 ACK报文:SYN=0,ACK=1;客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
服务端收到客户端的应答报文后,也进入
ESTABLISHED状态。第三次握手可以携带数据,前两次不可以,完成三次握手,客户端和服务端均处于ESTABLISHED状态,可以互相发送数据。三次握手才可以初始化Socket、序列号和窗口大小并建立TCP连接
1.为了防止旧的重复连接初始化造成混乱。(重传的SYN的序列号[Seq Num]应该是一样的。)
2.同步双方初始序列号。
3.避免浪费资源(即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求
SYN报文,而造成重复分配资源。
建立连接的初始序列号不同
2.socket-Linux网络编程基础API-网络通信中应用层与传输层之间的编程接口(API),封装了TCP/IP协议栈的细节
socket地址API
主机字节序:小端字节序:整数的高位字节储存在内存的高地址处,低位字节储存在内存的低地址处
网络字节序:大端字节序,整数的高位字节存储在内存的低地址处
格式化的数据通过网络传输时,都应该使用这些函数转换字节序
#include<netinet/in.h> unsigned long int htonl(unsigned long int hostlong);//将长整型(32bit)-IP 的主机host字节序数转化为网络字节序数 unsigned short int htons(unsigned short int hostshort); unsigned long int ntonl(unsigned long int netlong); unsigned short int ntons(unsigned short int netshort);
socket:一个IP地址和端口对(ip,port)-狭义;广义-文件描
述符
// 用这个结构体 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8080); // 端口 inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // inet_pton IP字符串转二进制 P92 ,后面指定要连接的服务器地址创建socket
socket命名-将一个socket和socket地址绑定-bind
服务器程序通常需要命名socket,只有命名后客户端才知道该如何连接它。客户端一般采用匿名方式,即使用操作系统自动分配的socket地址。
监听socket-创建监听队列以存放待处理的客户连接
被监听的socket是服务器端
接收连接-accept,服务器端接受连接
发起连接-connect-客户端发起连接
3.epoll,事件管理器
在Linux中,一切皆文件,socket是文件描述符,也是事件源;每个socket可以关联两种事件:可读事件,可写事件,一次只能处于“就绪”或“未就绪”两种状态之一。
Linux一切皆文件
创建epoll文件描述符-epoll_creat()
epoll需要使用一个额外的文件描述符,来唯一标识内核中这个事件表,创建这个文件描述符用epoll_create()
#include<sys/epoll> int epoll_create(int size)操作epoll的内核事件表-epoll_ctl-写入、修改。删除
使用epoll_ctl来操作epoll的内核事件表
#include<sys/epoll.h> int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event) /*fd参数是要操作的文件描述符,op参数指定操作类型,有以下三种: EPOLL_CTL_ADD,往事件表中注册fd上的事件 EPOLL_CTL_MOD,修改fd上的注册事件 EPOLL_CTL_DEL,删除fd上的注册事件 event指定事件,是epoll_event结构指针类型,event->data/events.如果是结构体类型 struct epoll_event ev,则里面包含ev.events/data.epoll_wait()-epoll系列系统调用接口函数
该函数在一段超时时间内等待一组文件描述符上的事情
# int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);4.线程池、进程池
两者类似
进程池是由服务器预先创建的一组子进程,池中的子进程2都运行相同的代码并具备相同的属性,比如优先级、PGID等。
当新的任务到来时,主进程会通过某种方式(主动选择算法/共享工作队列唤醒)选择进程池中的某一个子进程来为之服务。
之后使用某种通知机制告诉目标子进程有新任务需要处理,并传递必要的数据。
并发模式:半同步/半反应堆模式-由主进程统一管理监听socket和连接socket
半同步/半异步模式和领导者/追随者模式-是由主进程管理所有监听socket,各个子进程分别管理属于自己的连接socket。
连接就是TCP/UDP连接
15.2 处理多客户
常连接:一个客户的多次请求可以复用一个TCP连接。
客户任务无状态(请求之间无依赖):服务器在处理来自同一个客户的不同请求时,不需要依赖该客户之前请求所留下的任何上下文或数据。(每个请求独立,不保留历史状态,任意顺序处理)
在15-2中,因为两个请求之间没有依赖关系,子进程1 处理请求1后产生的任何临时数据,不会被子进程2处理请求2时需要用到,这样可以实现并行处理,提高吞吐量
但如果客户任务是存在上下文关系(请求有依赖),则最好一直用同一个子进程来为之服务,避免在各个子进程之间传递上下文数据。
epoll的EPOLLONESHOT事件,能够确保一个客户连接在整个生命周期中仅被一个线程处理
epoll 是一种 I/O 多路复用机制,负责高效地检测哪些文件描述符上发生了 I/O 事件;而进程/线程是执行单元,负责实际处理这些事件
gRPC-高性能的RPC框架
RPC框架-封装好的远程调用框架,客户端不需要知道调用细节,使用封装好的接口,调用存在于远程计算机上的某个对象,获取到远程服务器的信息。-一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议
1.为什么用gRPC而不是HTTP?
答:性能更好,基于HTTP/2,天生支持双向流,适合服务间通信
- 支持多语言
- 通信协议基于标准的 HTTP/2 设计,支持双向流、二进制框架和消息头压缩、单 TCP 的多路复用特性,这些特性使得 gRPC 在发送和接收方面都非常紧凑和高效
- 序列化默认使用protobuf,Protobuf是一种高效的二进制消息格式,序列化的速度很快,基于 HTTP/2 + PB,实现高性能。
- 超时取消机制。gRPC允许客户端指定他们愿意等待多长时间完成一个RPC。服务器可以决定在超时后取消。防止资源浪费。
2.gRPC和你自己写socket有什么区别?
答:gRPC封装了底层网络,自动处理连接、序列化、负载均衡,只需要定义proto文件和实现业务逻辑
3. .proto文件怎么定义服务
.proto文件定义对应数据的格式和gRPC服务,以Protobuf3(proto3 语法)为例,分三块:语法声明 → 定义消息结构体 → 定义 Service 服务 + 接口方法
先写syntax = "proto3";必须放第一行
message定义请求 / 响应数据结构
service里用rpc 方法名(入参) returns (出参)定义接口
加
stream就是流通信,不加就是普通单次调用写完这个
.proto文件后:gRPC 工具会自动生成:
- 服务端接口骨架代码
- 客户端 Stub 调用代码
4.stub是什么
RPC使用代理模式(stub)来实现透明化远程服务调用,客户端调用本地代理对象,代理内部封装了序列化、网络通信和服务发现等细节,使远程调用在接口和行为上与本地调用一致
5.同步调用和异步调用的区别
同步
调用方发起 RPC / 网络请求 →阻塞当前线程→ 卡死等待响应 → 响应回来才往下执行代码。
异步
发起请求 →立刻返回,不阻塞线程→ 线程继续跑其他业务 → 后台等网络响应 → 触发回调函数处理结果。
1.内核模块mmap读取高频数据
内核模块mmap采集高频数据,内核维护的数据本身变化频率高(比如CPU软中断、cpu使用情况等),采集逻辑是通过自定义实现内核模块(直接采集内核内部的详细数据,通过mmap机制将内核内存映射到用户空间),根据监控需求定义合适的结构,并在内核分配一块内存,存放在采集到的内核里的各系用指标的原始数据,该内存作为内核和用户空间共享的数据区,在通过定时器实现每秒定时获取,接下来要暴露到用户进程,就要将内核模块注册为字符设备,并实现mmap回调,这样用户空间可以通过mmap将内核分配的内存区映射到用户空空,后面首次访问该虚拟地址时,操作系统通过缺页异常,将这段虚拟地址和内核的物理内存建立映射关系(页表项),之后无需再次拷贝,用户就可以直接访问映射区内存实时获取到最新更新的内核数据,用户控件访问的就是内核分配的真是物理内存的映射。实现用户与内核的高效实时交互。
2.eBPF(扩展的伯克利包过滤器)
-内核可编程技术,允许用户在不修改内核源码的情况下扩展内核功能,广泛应用于网络监控、性能分析和安全控制等领域。:1.可以在内核中添加检测点来观察应用程序和内核之间的交互。eBPF 程序是一种非常高效的添加检测点的方法。在加载并进行 JIT 编译(JIT-compiled)后(第 3 章中将看到),程序将以原生机器指令在 CPU 上运行。此外,在处理事件时,无需在内核和用户空间之间进行转换(这是一项代价高昂的操作)。
2.通过eBPF快速创建新的内核功能:不同于直接插入内核模块这种会造成不安全问题的行为,如果想向内核添加新功能,eBPF提供eBPF验证器,确保只有在安全运行的情况下加载eBPF程序,不会导致机器崩溃或者陷入死循环,也不会允许数据被泄露。
3.动态加载:eBPF 程序可以动态地加载到内核中和从内核中移除。一旦它们被附加到某个事件上,无论是什么原因触发该事件,它们都会被触发。-可以立即获得对机器上所有活动的可见性。–对云原生部署的影响
4.对于性能追踪和安全可观察性,eBPF 的另一个优势是相关事件可以在内核中被过滤掉,然后再将其发送到用户空间,从而减少了开销。毕竟,过滤特定的网络数据包正是最初 BPF 实现的目的。如今,eBPF 程序可以收集关于系统中各种事件的信息,并使用复杂的、定制的可编程过滤器,只将相关信息的子集发送到用户空间
以下参考https://mp.weixin.qq.com/s/0UdR8PdVCyNVx5rTYFrvYw?search_click_id=257613890346813743-1779174653932-6432284752
Linux网络接收数据包流程
在Linux内核中当数据包到达网卡的时候,通过DMA方式将数据映射到内存。然后硬中断通知CPU有数据到来,调用硬中断处理函数,之后交给软中断去处理。通过ksoftirq调用软中断处理函数,收包的软中断处理函数是net_rx_action函数,主要将Ring Buffer缓冲区数据做成sk_buff送给上层协议栈进行处理,之后数据包经过层层解包最终将数据放入套接字缓冲区中,CPU通过将数据拷贝给应用程序。外部机器发送数据,通过网络到达接收机器的网卡(物理网卡NIC-二进制数据);网卡通过DMA(Direct Memory Access)方式将数据映射到内存, 直接写入内存的Ring Buffer中;同时收到数据后网卡发送硬中断IRQ通知CPU,CPU简单确认并通知后续处理;Linux触发软中断,软中断[NET_RX_SOFTIRQ],由ksoftirqd或者当前CPU去执行软中断处理函数【net_rx_action()】,开始收包,进入网络协议栈处理,net_rx_action会把Ring buffer数据封装成sk_buff(Linux网络包对象),然后协议栈层层解包[Ethernet-IP-TCP/UDP/socket],最后TCP找到对应socket并放入数据。等待应用程序读取数据
2.net_ebpf_monitor.cpp eBPF采集网络数据
(1)加载eBPF程序到内核
skel = net_stats_bpf__open(); net_stats_bpf__load(skel); // 加载进内核(2)挂载到网卡的TC钩子(这里是不是体现eBPF的动态加载)
bpf_tc_attach(&hook, &opts_in); // 挂载到网卡入口 bpf_tc_attach(&hook, &opts_eg); // 挂载到网卡出口每个网络包经过网卡时,eBPF程序在内核里直接统计字节数,不需要切换到用户态。
(3)用户态从BPF map[BPF map共享空间,可以同时被内核和用户访问]读数据
bpf_map_lookup_elem(map_fd_, &next_key, &stats);内核态到用户态的数据传递,拿到已经统计好的结果
spdlog日志
cout 主要用于调试输出,只能简单打印信息,缺少日志级别、时间戳、文件持久化等功能。
spdlog 是专门的日志库,支持 info、warn、error 等日志级别,可以自动记录时间、线程信息,并支持日志轮转和异步写入。
在 Linux 性能监控项目中,使用 spdlog 记录指标采集、gRPC 推送以及 MySQL 写入过程中的运行状态和异常信息,方便后续排查问题。
【C++】spdlog光速入门,C++logger最简单最快的库 - 缙云烧饼 - 博客园
spdlog中每个logger(日志对象)包含一个vector,该vector由一个或者多个智能指针shared_ptr组成,logger的每条日志都会调用sink对象,由sink对象按照formatter(格式化对象,spdlog有默认的格式,如果有个性化需求可以自定义)的格式输出到sink指定的地方(有可能是控制台、文件等)。
trace 最详细调试信息 debug 调试信息 info 正常运行信息 warn 警告 error 错误 critical 严重错误formatter
formatter也即格式化对象,用于控制日志的输出格式,spdlog自带了默认的formatter,一般情况下,我们无需任何修改,直接使用即可。注意,每个sink会有一个formatter
默认formatter
默认formatter的格式为:[日期时间] [logger名] [log级别] log内容
Copy[2022-10-13 17:00:55.795] [service] [debug] found env XXXXXXX : true [2022-10-13 17:00:55.795] [func_config] [debug] kafka_brokers : localhost:9092 [2022-10-13 17:00:55.795] [func_config] [debug] kafka_main_topic : kafka_test [2022-10-13 17:00:55.795] [func_config] [debug] kafka_partition_value : -1 [2022-10-13 17:00:55.795] [service] [info] initialized自定义formatter
如果默认的formatter不符合需求,可以自定义formatter,具体方式如下
- set_parrtern(pattern_string);
- 例如:
- 全局级别的:spdlog::set_pattern(" [%H:%M:%S %z] [thread %t] %v ");
- 单个logger级别的:some_logger->set_parttern(“>>> %H:%M:%S %z %v <<<”);
- 单个sink级别的:some_sink-> set_parttern(“… %H: %M …”);
其中用到了%H %M这些占位符,事实上它们都是预先设定好的,想要查看所有的占位符情况,可以参考以下网站:
https://spdlog.docsforge.com/v1.x/3.custom-formatting/#pattern-flags
sink#
每个sink对应着一个输出目标和输出格式,它内部包含一个formatter,输出目标可以是控制台、文件等地方。
所有的sink都在命名空间spdlog::sinks下,可以自行探索
控制台sink#
spdlog中创建控制台sink非常简单,该方式创建的sink会输出到命令行终端,且是彩色的(也可以选非彩色的,但是有彩色的应该都会选彩色的吧……)。后缀的_mt代表多线程,_st代表单线程
Copyauto sink1=std::make_shared<spdlog::sinks::stdout_color_sink_mt>();文件sink#
文件sink的类型有很多,这里展示几种经典类型
Copyauto sink1=std::make_shared<spdlog::sinks::basic_file_sink_mt>(log_file_name);//最简单的文件sink,只需要指定文件名auto sink2=std::make_shared<spdlog::sinks::daily_file_sink_mt>(log_file_name,path,14,22);//每天的14点22分在path下创建新的文件auto sink3=std::make_shared<spdlog::sinks::rotating_file_sink_mt>(log_file_name,1024*1024*10,100,false);//轮转文件,一个文件满了会写到下一个文件,第二个参数是单文件大小上限,第三个参数是文件数量最大值其他sink#
ostream_sink
syslog_sink
…
也可以通过继承base_sink创建子类来自定义sink,具体可以参考:
https://spdlog.docsforge.com/v1.x/4.sinks/#implementing-your-own-sink
sink的flush问题
创建好sink后建议设置flush方式,否则可能无法立刻在file中看到logger的内容
以下为两种重要的flush方式设置(直接设置全局)
Copyspdlog::flush_every(std::chrono::seconds(1));spdlog::flush_on(spdlog::level::debug);logger#
日志对象,每个logger内包含了一个vector用于存放sink,每个sink都是相互独立
因此一个日志对象在输出日志时可以同时输出到控制台和文件等位置
使用默认logger#
如果整个项目中只需要一个logger,spdlog提供了最为便捷的默认logger,注意,该logger在全局公用,输出到控制台、多线程、彩色
Copy//Use the default logger (stdout, multi-threaded, colored)spdlog::info("Hello, {}!","World");创建特定的logger#
大部分情况下默认logger是不够用的,因为我们可能需要做不同项目模块各自的logger,可能需要logger输出到文件进行持久化,所以创建logger是很重要的一件事。好在创建logger也是非常简单的!
方式一:直接创建#
与创建sink类似,我们可以非常便捷的创建logger
由于大部分时候一个logger只会有一个sink,所以spdlog提供了创建logger的接口并封装了创建sink的过程
Copyauto console=spdlog::stdout_color_mt("some_unique_name");//一个输出到控制台的彩色多线程logger,可以指定名字autofile_logger=spdlog::rotating_logger_mt("file_logger","logs/mylogfile",1048576*5,3);//一个输出到指定文件的轮转文件logger,后面的参数指定了文件的信息方式二:组合sinks方式创建#
有时候,单sink的logger不够用,那么可以先创建sink的vector,然后使用sinks_vector创建logger
以下样例中,首先创建了sink的vector,然后创建了两个sink并放入vector,最后使用该vector创建了logger,其中,set_level的过程不是必须的,register_logger一般是必须的,否则只能在创建logger的地方使用该logger,关于register的问题可以往下看
Copystd::vector<spdlog::sink_ptr>sinks;auto sink1=std::make_shared<spdlog::sinks::stdout_color_sink_mt>();sink1->set_level(MyLoggers::getGlobalLevel());sinks.push_back(sink1);auto sink2=std::make_shared<spdlog::sinks::rotating_file_sink_mt>(log_file_name,1024*1024*10,100,false);sink2->set_level(spdlog::level::debug);sinks.push_back(sink2);auto logger=std::make_shared<spdlog::logger>("logger_name",begin(sinks),end(sinks));logger->set_level(spdlog::level::debug);spdlog::register_logger(logger);