第0章 补充知识
1.** 零拷贝函数**
磁盘优化的技术:零拷贝,直接I/O,异步I/O。
1、DMA(Direct Memory Access)技术:简单理解就是在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交由DMA控制,CPU不在进行参与任何搬运的事情,这样CPU就可以去处理其他事情了。DMA的处理过程如下:
早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。
2、一次系统调用必然会发生两次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。
所以,我们之所以要使用零拷贝的原因就出现了,就是因为由缓存区拷贝到用户缓存区,再从用户缓存区拷贝到socket缓冲区,其实这个动作是无效的,所以就有了零拷贝函数。
3、 如何实现零拷贝?
:::info
a. mmap+write
b. sendfile
c.SG-DMA(The Scatter-Gather Direct Memory Access)技术
:::
a. mmap() 替换 read() 系统调用函数
:::info
buf = mmap(file, len);
write(sockfd, buf, len);
:::
mmap系统调用函数会直接把内核缓冲区里的数据[映射]到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
b、sendfile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:
:::info
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
:::
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:
c. SG-DMA(The Scatter-Gather Direct Memory Access)技术
你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:
:::info
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
#注意上面的ethtool可能要你自己进行安装:sudo apt install ethtool
#注意上面的eth0是你的网卡的名称,你直接用ifconfig就可以查看到了。一般是有个e开头的,那个就是了。
:::
于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:
:::info
第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
:::
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
总结,零拷贝技术可以把文件传输的性能提高至少一倍以上。
d.PageCache
所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。
绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。
于是,在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术。
2. TCP/IP协议族
TCP/IP协议族是一个四层协议系统,自底向上分别是:数据链路层,网络层,传输层,应用层。
3. OSPF
OSPF(Open Shortest Path First,开放最短路径优先)是一种链路状态路由协议,无路由循环(全局拓扑),“开放”意味着非私有的。每一台路由器拥有整个拓扑结构,能根据网络拓扑信息独立地做出决策。OSPF采用SPF算法计算到达目的地的最短路径,所谓“链路”,即指路由器接口,所谓“状态”,即指描述接口以及其与邻居路由器之间的关系。管理距离AD值为110。
4. ARP协议
ARP 协议的全称是 Address Resolution Protocol(地址解析协议),它是一个通过用于实现从 IP 地址到 MAC 地址的映射,即询问目标 IP 对应的 MAC 地址 的一种协议。
由此,可以通过 ARP 从 IP 地址获取 MAC 地址,实现同一链路内的通信。
如果是不同链路怎么办呢?
这就要使用到 代理 ARP 了,通常 ARP 会被路由器隔离,但是采用代理 ARP (ARP Proxy) 的路由器可以将 ARP 请求转发给临近的网段。使多个网段中的节点像是在同一网段内通信。
ARP 高效运行的关键就是维护每个主机和路由器上的 ARP 缓存(或表)
ARP 攻击分类:
:::info
ARP 泛洪攻击
ARP 欺骗主机攻击
欺骗网关的攻击
中间人攻击
IP地址冲突攻击
:::
5. RARP
与 ARP 相对的,RARP(Reverse Address Resolution Protocol) 是将 ARP 反过来,从 MAC 地址定位 IP 地址的一种协议,将打印机服务器等小型嵌入式设备接入网络时会使用到。
6. ICMP协议
ICMP协议的功能主要有:
:::info
确认IP包是否成功到达目标地址
通知在发送过程中IP包被丢弃的原因.
:::
ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用.
ICMP协议使用的报文格式如图所示:
8位类型:一类是差错报文,这类报文用来回应网络错误,比如目标不可达和重定向。 一类是查询信息,ping就是使用ICMP报文来查看目标是否可达。
7. TCP协议
为应用层提供可靠的,面向连接的和基于流的服务。
使用超时重传,数据确认等方式确保数据包被正确的发送至目的端。
8. UDP协议
无连接,基于数据报的服务。
每个UDP数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断。
9. 封装
TCP报文段
UDP数据报
IP数据报
以太网帧,令牌环帧
第四章 TCP/IP通信案例:访问Internet上的网络服务器
1. Http 代理服务器
:::info
a. 正向代理服务器
b. 反向代理服务器
c. 透明代理服务器
:::
2. Http请求方法
第五章:Linux 网络编程基础API
1. 小知识
大端:高存低,低存高 小端:低存低,高存高 现代PC 主要使用小端。小端字节序又称为主机字节序
为了统一字节序的传输,发送端会把数据都转换成大端字节序,所以,大端字节序也叫做网络字节序。
:::info
大端字节序:一个整数的高位字节(23~31bit)存储在内存的低地址处
低位字节(0-7bit)存储在内存的高地址处,也称为网络字节序
小端字节序:一个整数的高位字节(23~31bit)存储在内存的高地址处
低位字节(0-7bit)存储在内存的低地址处,也称为主机字节序
:::
2. 字节序
Linux提供如下4个函数来完成主机字节序和网络字节序之间的转换。
“host to network long”:即将长整型的主机字节序转化为网络字节序。
3. 通用Socket地址
表示socket地址的是结构体socketaddr:
sa_family_t:是地址族类型的变量。
4. 点分十进制字符串和用网络字节序表示的IPV4地址之间的转换
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);//点分十进制表示的IPV4地址转网络字节序表示的IPV4地址
int inet_aton(const char* cp,struct in_addr* inp);//完成和上面同样的功能
char* inet_ntoa(struct in_addr in);//将网络字节序表示的IPV4地址转为点分十进制表示的IPV4地址。
5. socket的创建
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
domain:底层协议族 设为PF_INET H或PF_INET6
type:指服务类型,SOCK_STREAM(流服务),SOCK_UGRAM(数据报服务)
protocl:通常为默认值 0
成功时返回一个socket描述符,失败时则返回-1并设置errno.
6. 命名socket
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);//bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen则指出socket参数的长度。
7. 监听socket
int listen(int sockfd,int backlog);//sockfd指定被监听的socket,backlog参数提示内核监听队列的最大长度。
8. 接受连接
int accept(int sockfd,,const struct sockaddr* my_addr,socklen_t addrlen);// sockfd是执行过Listen系统调用的监听socket.addr用来获取被接受连接的远程socket地址,该地址长度由addrlen决定。
9. 发起连接
int connect(int sockfd,,const struct sockaddr* serv_addr,socklen_t addrlen);//sockfd由系统调用返回一个socket,serv_addr参数是服务器监听的socket地址,addrlen则代表这个地址的长度。
10. 关闭连接
int close(int fd);//fd参数代表待关闭的socket.
close系统调用并非立即关闭一个连接,而是将fd的引用计数减一。只有当fd的引用计数为0时,才真正关闭连接。
若要无论如何都要关闭该连接,则使用shutdown
int shutdown(int sockfd,int howto);//socketfd参数是待关闭的socket,howto决定了socket的行为。
11. TCP数据读写
用于TCP流数据读写的系统调用
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
12. UDP数据读写
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t* addrlen);
13. 通用数据读写函数
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdf* msg,int flags);
第六章:高级I/O函数
1. pipe定义:
#include <unistd.h>
int pipe(int fd[2]);
:::info
A.成功时,将打开的一对文件描述符值填入其参数指向的数组。若失败,则返回-1,并设置erron.
B. fd[0]只能用于从管道中读出数据,fd[1]只能用于将数据写入管道中。
C. read和write都是阻塞的。
:::
2. socketpair
作用:能够方便的创建双向管道
int socketpair(int domain,int type,int protocol,int fd[2]);
domain只能使用AF_UNIX
成功返回0,否则返回-1并设置erron.
3. dup函数和dup2函数
作用:将标准输入重定向到一个文件中。
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);
4. sendfile函数
作用:可在两个文件描述符之间传递数据(完全在内核中操作), 称为零拷贝
5. mmap函数
mmap函数用于申请一段内存空间,可将这段内存空间当做进行间通信的共享空间。
munmap函数则释放由mmap创建的这段内存空间。
#include <sys/mman.h>
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void *start,size_t length);
mmap函数成功时,返回目标内存区域的指针,失败时则返回MAP_FAILED并设置erron.
6. splice 函数
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。
ssize_t splice (int fd_in,loff_t* off_in,int fd_out,loff_t* off_out,size_t len,unsigned int flags);
7. tee 函数
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。
#include <fcntl.h>
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags);
8. fcntl函数
fcntl函数,提供了对文件描述符的各种控制操作。
第七章 Linux服务器程序规范
1. 小知识
守护进程的父进程通常是init进程。
Linux提供了一个守护进程来处理系统日志——syslogd.
第八章 高性能服务器程序框架
一. 服务器模型
- C/S模型:以服务器为中心
缺点:访问量过大时,所有客户的响应变慢
2. P2P模型:每台主机既是客户端,又是服务器
云计算机群可以看做是P2P模型的一个典范。
3. 服务器基本模块的功能描述
:::info
I/O处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡
逻辑单元 业务进程或逻辑 逻辑服务器
网络存储单元 本地数据库、文件或缓存 数据库服务器
请求队列 各单元之间的通信方式 各服务器之间的永久TCP连接
:::
二、I/O模型
阻塞的文件描述符被称为阻塞I/O。非阻塞的文件描述符被称为非阻塞I/O.
socket的基础API中,可能被阻塞的系统调用包括accept,send,recv,connect.
理论上来说:阻塞I/O,I/O复用和信号驱动I/O都是同步I/O模型。
异步I/O的读写操作 总是立即返回,而不论I/O是否堵塞,因为真正的读写操作已经交由内核操作了。
同步I/O通知的是I/O就绪事件,异步I/O通知的是I/O完成事件。
三、两种高效的事件处理模式
通常I/O需要处理三类事件:I/O事件,信号及定时事件。
Reactor和Proactor.两种事件处理模式。同步I/O模型通常用于处理Proactor模式,异步I/O模型通常用于处理Reactor模式。
1. Reactor模式
介绍:
模式:
2. Proactor模式
:::info
介绍:Proactor模式将所有的I/O操作都交由主线程和内核来处理,工作线程仅仅负责业务逻辑。
:::
3. 同步I/O模拟Proactor模式
:::info
原理:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一完成事件,工作线程直接获得了数据读写的结果,接着对读写的结果进行逻辑处理。
:::
4. 半同步/半异步模式
同步线程处理客户逻辑,异步线程处理I/O事件
变体:主线程作为唯一的异步线程
高效:每个工作线程都能处理多个客户连接
领导者/追随者模式
提高服务器性能
:::info
1)使用线程池,空间换时间
2)避免不必要的数据复制
3)减少上下文切换和锁的使用
:::
第九章 I/O复用
I/O复用:程序能够同时间监听多个文件描述符
select系统调用
- 在一段指定的事件内,监听用户感兴趣的文件描述符上:可读、可写、异常 事件、
poll系统调用
- 与select类似,也是轮询,测试是否有就绪者
epoll [系列] 系统调用
内核事件表
:::info
1)epoll与select和poll的区别是一组函数而不是单个函数。
2)epoll把用户关系的文件描述符事件放在内核的事件表中,无需像select和poll每次调用都要重传
3)epoll需要一个额外的文件描述符,用来标志内核中的这个事件表。
4)这个文件描述符使用epoll_create函数创建
:::
LT和ET模式
LT:level trigger,电平触发;ET:edge trigger,边沿触发。
默认LT,ET高效
区别:
采用LT时,epoll_wait检测到事件发生并通知程序,程序可以不立刻处理,这样下次调用epoll_wait,它会再次通知程序。
采用ET时,epoll_wait检测到事件发生并通知程序,程序必须立刻处理,如果不处理,后续调用epoll_wait不会再次通知了。
ET降低了同一个epoll重复触发的次数
EPOLLONESHOT事件
:::info
- 对于并发程序,如果一个socket上的某个事件被多次触发,就算是ET,也可能会造成不同线程同时处理一个socket
- 如果期望一个socket只能被一个线程处理,在这个线程处理的时候,就算这个socket有事件发生,其他线程也不能处理
- 就需要注册EPOLLONESHOT事件
- 当一个线程处理完一个注册EPOLLONESHOT事件的socket事件时候,就需要立刻再次注册EPOLLONESHOT,否则无法再次触发
:::
:::info
- poll和select都是轮询,事件复杂度是O(n)
- epoll是采用回调的方式,内核检测到就绪的文件描述符时,就会触发回调函数。算法时间复杂度O(1)
- 对于活动连接多的时候,epoll_wait可能会因为回调函数触发频繁导致效率降低
- 所以epoll适合连接数多,但活动链接少的情况
:::
第十章 信号
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。Linux 信号可由如下条件产生:
:::info
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发信号。比如输入 Ctrl + C 通常会给进程发送一个中断信号
- 系统异常。比如浮点异常和非法内存段访问
- 系统状态变化。比如 alarm 定时器到期将引起 SIGALRM 信号
- 运行 kill 命令或调用 kill 函数
- 服务器必须能处理(或至少忽略)一些常见的信号,以免异常终止
:::
第十一章 定时器
Linux 3种定时方法
:::info
1. socket 选项 SO_RCVTIMEO 和 SO_SNDTIMEO
分别用来设置 socket 接收数据超时时间和发送数据超时时间。
因此,这2个选项仅对数据的接收和发送相关的 socket 专用的系统调用有效。
2. SIGALRM 信号
1.基于升序链表的定时器
2.处理非活动连接
3. IO 复用系统调用的超时参数
Linux 下的3组 IO 系统调用都带有超时参数,因此它们不仅能统一处理信号和IO事件,也能统一
处理定时事件。但是由于IO复用函数系统调用可能在超时时间到期之前就返回(有IO事件发生),如果
我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。
:::
定时器通常至少包含2个成员
:::info
-
一个是超时时间(相对时间或者绝对时间)
-
一个任务回调函数
-
还可能包括回调函数需要的参数
-
以及是否重启定时器等信息
:::
链表插入删除查找的时间复杂度
:::info -
单向链表要删除某一节点时,必须要先通过遍历的方式找到前驱节点(通过待删除节点序号或按值查找)。若仅仅知道待删除节点,是不能知道前驱节点的,故单链表的增删操作复杂度为O(n)。
-
双链表(双向链表)知道要删除某一节点p时,获取其前驱节点q的方式为 q = p->prior,不必再进行遍历。故时间复杂度为O(1)。而若只知道待删除节点的序号,则依然要按序查找,时间复杂度仍为O(n)。
-
单、双链表的插入操作,若给定前驱节点,则时间复杂度均为O(1)。否则只能按序或按值查找前驱节点,时间复杂度为O(n)。
-
至于查找,二者的时间复杂度均为O(n)。 对于最基本的CRUD操作,双链表优势在于删除给定节点。但其劣势在于浪费存储空间(若从工程角度考量,则其维护性和可读性都更低)。
-
双链表本身的结构优势在于,可以O(1)地找到前驱节点,若算法需要对待操作节点的前驱节点做处理,则双链表相比单链表有更加便捷的优势。
:::
链表与时间轮对比 -
使用升序链表管理所有的定时器,插入操作效率随着定时器数目的增多而降低
-
时间轮使用哈希表的思想,将定时器散列在不同的链表上,插入操作效率不受定时器数目的影响
两种高效的管理定时器的容器
:::info
1.时间轮:哈希表思想
固定的频率调用心博函数 tick, 并以此检查到期的定时器,然后执行定时器上的回调函数。
2.时间堆
将所有定时器中超时时间最小的一个定时器的超时时间作为心博间隔。
:::
第十二章 高性能I/O框架库Libevent
Linux服务器程序必须处理的三类事件:
- I/O事件
- 信号
- 定时事件
在处理这三类事件时我们通常需要考虑如下三个问题:
- 统一事件源。很明显,统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误。
- 可移植性。不同的操作系统具有不同的I/O复用方式,比如Solaris的dev/poll文件,FressBSD的kqueue机制,Linux的epoll系统调用
- 对并发编程的支持,在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号和定时器,以避免竞态条件
句柄: I/O框架库要处理的对象,即I/O事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux环境下,I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值 **
事件多路分发器:事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦活收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O复用技术来实现。I/O框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、poll、epoll_wait等函数。此外事件多路分发器还需实现register_event和remove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
事件处理器和具体时间处理器:事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event回调函数,这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器**,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。此外,事件处理器一般还提供一个get_handle方法,它返回与该事件处理器关联的句柄。那么事件处理器和句柄有什么关系?当时间多路分发器检测到有事件发生时,它是通过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获取到正确的事件处理器
第十三章 多进程编程
**fork函数:**每次调用都返回两次,在父进程中返回的是子进程的 PID,在子进程中则返回 0。该返回值是后续代码判断当前进程是父进程还是子进程的依据。
fork 函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程标像有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的 PPID 被设置成原进程的 PID, 信号位图被清除( 原进程设置的信号处理函数不再对新进程起作用 )
子进程的代码与父进程完全相同,同时它还会复制**父进程的数据 ( 堆数据、栈数据和静态数据 **)。数据的复制采用的是所谓的写时复制( copy on writte ),即只有在任一进程( 父进程或子进程 ) 对数据执行的写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即使如此,如果我们在程序中分配了大量内存,那么使用 fork 时也应当十分谨慎,尽量避免没有必要的内存分配和数据复制。
第十四章 多线程编程
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。
线程可分为内核线程和用户线程。
- 内核线程 运行在内核空间,由内核来调度
- 用户线程 运行在用户空间,由线程库来调度。
第十五章 进程池和线程池
通过动态创建子进程(或子线程)来实现并发服务器的。但这样的做法有如下缺点:
:::info
- 动态创建进程(或线程)是比较耗费时间的,这将导致较慢的客户相应
- 动态创建的子进程(或子线程)通常只用来为一个客户服务,这将导致系统上产生大量的细微进程(或线程)。进程(或线程)间的切换将消耗大量 CPU 事件
- 动态创建的子进程是当前进程的完整映像。当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源极具下降,进而影响服务器性能
:::
第十六章 服务器调制、调试和测试
1.从系统的角度来优化,改进服务器
:::info
1.系统调制
2.服务器调试
3.压力测试
:::
2.最大文件描述符
作为守护进程的服务器程序就应该总是关闭标准输入,标准输出和标准错误这3个文件描述符。
Linux 对应用程序能打开的最大文件描述符有两个层次的限制:
1.用户级限制
用户级限制指的是目标用户运行的所有进程总共能打开的文件描述符
用户级查看文件描述符方法:
ulimit -n
设置:
ulimit -SHn 1024 //临时的
永久生效:
/etc/security/limits.conf
* hard nofile max-file-number //硬限制
* soft nofile max-file-number // 软限制
2.系统级限制
系统级限制指的是所有用户总共能打开的文件描述符
系统级文件描述符限制:
sysctl -w fs.filepmax = max-file-number // 临时
//永久生效
/etc/sysctl.conf
fs.file-max = max-file-number
sysctl -p
3.调整内核参数
几乎所有的内核模块,包括内核核心模块和驱动程序,都在 /proc/sys 文件系统下提供了某些配置文件以供用户调整模块的属性和行为。通常一个配置文件对应一个内核参数,文件名就是参数的名字,文件的内容就是参数的值。
我们可以通过命令 sysctl -a 查看所有这些内核参数。
1./proc/sys/fs (与文件系统相关)
1./proc/sys/fs/file-max 系统级文件描述符限制 //临时修改
一般修改 /proc/sys/fs/file-max 之后,应用程序需要把 /proc/sys/fs/inode-max
设置为新的 /proc/sys/fs/file-max 值的 3~4 倍,否则可能导致 i 节点数不够用
2./proc/sys/fs/epoll/max_user_watches 一个用户能够往 epoll 内核事件表中注册的事件总量。
它是指该用户打开的所有 epoll 实例总共能监听的事件数目,而不是单个 epoll 实例能监听的事件数目。
往 epoll 内核事件表注册一个事件,在 32 位的系统上大概消耗 90 字节的内核空间,在 64 位上则消耗
160 字节的内核空间。所以,这个参数限制了 epoll 使用的内核内存总量。
2./proc/sys/net (内核网络模块参数)
其中 TCP/IP 协议相关的参数主要位于如下3个子目录 : core, ipv4, ipv6
1. /proc/sys/net/core/somaxconn 指定 listen 监听队列里,能够建立完整连接从而进入 ESTABLISHED 状态
的 socket 的最大数目。
2./proc/sys/net/ipv4/tcp_max_syn_backlog 指定 listen 监听队列里,能够转移至 ESTABLISHED 或者 SYS_RCVD
状态的 socket 最大数目
3./proc/sys/net/ipv4/tcp_rmem 包含3个值,分别指定一个 socket 的 TCP 读缓冲区的最小值,默认值和最大值。
4./proc/sys/net/ipv4/tcp_syncookies 指定是否打开 TCP 同步标签(syscookie)。
同步标签通过启动 cookie 来防止一个监听 socket 因不停的重复接收来自同一个地址的连接请求(同步报文段),
而导致 listen 监听队列的溢出(所谓的 SYN 风暴)
除了通过直接修改文件的方式来修改这些系统参数之外,我们也可以使用 sysctl 命令来修改它们。但这2种修改方式都是临时的。
永久的方式是在 /etc/sysctl.conf 文件中加入相应网络参数及其数值,并执行 sysctl -p 使之生效,就像修改系统最大文件描述符一样。
4.gdb 调试
1.gdb 调试多进程程序
如果一个进程通过 fork 系统调用创建子进程,gdb 会继续调试原来的进程,子进程则会正常运行。那么如何调试子进程呢?2种方式:
1.单独调试子进程
gdb
attach pid
b
c
bt
2.使用调试器选项 follow-fork-mode
gdb 调试器选项 follow-fork-mode 允许我们选择程序在执行 fork 系统调用后是继续调试父进程还是调试子进程。其用法如下:
set follow-fork-mode mode(可选 parent 和 child)
2.gdb 调试多线程
1.info threads // 显示当前可调试的所有线程。gdb 会为每个线程分配一个ID,我们可以根据这个 ID 来操作对应的线程。
* 表示当前被调试的线程
2.thread ID // 调试目标 ID 指定的线程
3.set scheduler-locking [off|on|step]
调试多线程程序时,默认除了被调试的线程在执行外,其他线程也在继续执行,但有的时候我们希望只让被调试的线程执行。这
可以通过这个命令实现。该命令设置 scheduler-locking 的值: off 表示不锁定任何线程,即所有线程都可以执行,这是默认值;
on 表示只有当前被调试的线程会继续执行; step 表示在单步执行的时候,只有当前线程会执行。
调试进程池或者线程池一个不错的方法:是先将池中的进程个数或者线程个数减少至1,以观察程序的逻辑是否正确;
然后逐步增加进程或者线程的数量,以调试进程或者线程的同步是否正确。
5.压力测试