linux高性能服务器--笔记

第一章

tcp/ip主要协议
上层协议使用下层协议提供的服务
在这里插入图片描述
1.数据链路层
主要实现网卡接口的网络驱动程序,处理数据的传输。主要是是arp协议。网络层使用ip寻址一台机器,而数据链路层通过mac地址寻址,

2.网络层
主要实现数据包的选路和转发,核心是ip协议,数据包根据目的ip地址,然后根据路由表不停寻找下一跳路由器,把数据包交给路由器来转发。icmp协议,是ip协议的补充,主要检测网络连接(ping程序:查看目标是否可达)。

3.传输层
为两台主机提供端对端的通信,与网络层不同,他只关心通信的起始端点和目的端,不在乎中转过程。主要负责数据的收发、链路和超时重连等。3个协议:TCP、UDP、SCTP。
TCP:面向连接,基于流,使用超时重传、数据确认机制。通信必须先建立tcp连接,内核维持一些数据结构:比如连接的状态、读写缓冲区、诸多的定时器。TCP是基于流的,所以数据没有边界限制,可以源源不断从一端流入另一端。
UDP:无连接,基于数据报的服务。每个UDP数据报都有一个长度,接收端必须以该长度最小单位将所有内容一次性读出,否则数据包会被截断。

4.应用层
负责处理应用程序的逻辑,大部分都是在用户空间实现(DNS、ospf、telnet、ping),不如有些数据的复制就是在内核实现的,这样可以省区用户空间和内核空间的来回切换的开销。

封装
上层协议通过封装使用下层协议。每层协议在上层数据基础上加上自己的头部,实现该层的功能。
在这里插入图片描述
TCP封装后的数据称为tcp报文段,由tcp头部和tcp内核缓冲区数据构成

经过数据链路层封装的数据成为帧

帧是最大传输单元(MTU),受网络类型的限制,以太网MTU=1500,所以过长的ip数据报可能需要被分片传输

分用
沿着协议栈自底向上传递,将处理后的帧交给应用程序

ARP协议工作原理
主机向自己所以的网络广播一个ARP请求,请求包含目的机器的网络地址,请求的机器会回应一个ARP应答,包含自己的MAC地址

DNS工作原理
域名解析
DNA是一套分布式的域名服务系统,存着机器名和IP地址映射,动态更新。

1.host命令可以查询ip地址,别名,机器名。

2.使用tcpdump观察DNS通信过程

root@lwj:~# host -t A www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 61.135.169.125
www.a.shifen.com has address 61.135.169.121

tcpdump抓包,port domin(只抓取域名服务的数据包)

root@lwj:~# tcpdump -i eth0 -nt -s 500 port domain
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 500 bytes
IP 192.168.0.61.52834 > 100.125.1.250.53: 12540+ A? www.baidu.com. (31)
IP 100.125.1.250.53 > 192.168.0.61.52834: 12540 3/0/0 CNAME www.a.shifen.com., A 61.135.169.125, A 61.135.169.121 (90)

socket和TCP/IP协议族的关系
socket是一套通用的网络编程接口,可以访问内核中TCP/IP协议栈,还可以访问其他网络协议栈(UNIX本地域协议栈)

第二章 IP协议详解

ip服务特点
ip协议是TCP/IP协议族的动力,为上层协议提供无状态、无连接、不可靠的服务

  • 无状态:ip通信双方不同步传输数据的状态信息,因此所有IP数据报的发送、传输和接收都是相互独立、没有上下文关系的。
    1)缺点:数据可能是乱序、重复的。虽然ip数据报头部有标识字段,他是用来处理IP分片和重组的,不是用来指示接受顺序。
    2)优点:简单、高效
  • 无连接:ip通信双方不会长久维持对方信息,所以上层协议每次发送数据时,必须指明对方ip地址
  • 不可靠:不能保证ip数据包准备到达接收端。如果ip数据包在网络上存活时间过长(TTL),就会丢弃返回一个icmp错误信息。再如果接收端发现ip数据包不正确(通过校验机制),也会给发送端返回一个icmp错误信息。针对这些情况:(比如TCP)就需要ip要进行数据确认、超时重传等机制来保证可靠传输。

2.2 ipv4报头
在这里插入图片描述

  • ip数据包最大长度为65535(2^16 -1 )字节,由MTU限制,超过MTU将被分片传输

2.2.1 使用tcpdump观察ipv4头部结构
抓取本地回路上的数据包
端口是23,-x可以输出数据包的二进制码,

root@lwj:~# tcpdump -ntx -i lo
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 127.0.0.1.47356 > 127.0.0.1.23: Flags [S], seq 815313635, win 43690, options [mss 65495,sackOK,TS val 197scale 7], length 0
	0x0000:  4510 003c 694d 4000 4006 d35c 7f00 0001
	0x0010:  7f00 0001 b8fc 0017 3098 b2e3 0000 0000
	0x0020:  a002 aaaa fe30 0000 0204 ffd7 0402 080a
	0x0030:  0bc9 dbae 0000 0000 0103 0307
IP 127.0.0.1.23 > 127.0.0.1.47356: Flags [R.], seq 0, ack 815313636, win 0, length 0
	0x0000:  4510 0028 3633 4000 4006 068b 7f00 0001
	0x0010:  7f00 0001 0017 b8fc 0000 0000 3098 b2e4
	0x0020:  5014 0000 153e 0000

另一台终端执行telnet登陆本机

root@lwj:~# telnet 127.0.0.1
Trying 127.0.0.1...

在这里插入图片描述
2.3 ip分片
ip分片发生在发送端或者中转路由器上,在目标机器上进行重新组装。
每个分片都有自己的ip头部,具有相同的标识值,但片偏移不同,除了最后一个分片,还有MF标志。
以太网帧的MTU是1500字节,所以ip数据报的数据部分最多1480字节(ip报头占用20字节)
在这里插入图片描述
tcpdump验证

tcpdump -ntv -i eth0 icmp
ping baidu.com -s1473

在这里插入图片描述
第一个分片标识61197,偏移0,flag[+]表示设置了MF标志还有后续标志,分片长度1500字节,icmp报文的长度为1480
第一个分片标识61197,偏移1480(第一个分片的icmp长度),flag[none]表示没有任何标志,

2.4 ip 路由
ip协议的一个核心数据报的路由,路由表规定数据报发送的路径。

查看路由表机制
route或者netstat命令

2.5 ip转发
修改主机上/proc/sys/net/ipv4/ip_foreward 内核参数,也可以具有转发功能
执行的操作:

  • 如果TTL为0,则丢弃数据报
  • 查看目标ip是否本机ip,如果不是,发送一个icmp失败报文给发送端
  • 给源端发送一个icmp重定向报文,告诉它更合理的下一跳路由器
  • TTL值减1
  • 处理IP头部选项
  • 如果有必要,执行IP分片操作

2.6 重定向
icmp重定向报文,它给接收方提供了两个信息:

  • 引起重定向的ip数据报的源端ip地址
  • 应该使用的路由器ip地址

以此来更新路由表(通常是更新路由表缓冲,而不是直接更改路由表)

2.7 ipv6头部结构
128位来表示ipv6,40字节固定头部和可变长的扩展头部组成
在这里插入图片描述

第三章 TCP协议详解

3.1 tcp服务特点
面向连接、字节流和可靠传输
必须双方先建立连接才能开始数据的读写,必须是双全工的(双方数据读写可以通过一个连接进行),完成数据交换后,必须断开连接释放资源。

tcp执行写操作将数据放入tcp发送缓冲区中,发送缓冲区数据可能被封装成一个或多个tcp报文段发出。接收端收到tcp报文段后,按照tcp报文段的序号,以此放入tcp接收缓冲区中,执行都操作。

字节流概念:发送写操作和接受读操作没有任何数量关系,数据没有边界

tcp可靠传输,采用应答机制,超时重传(发送端会有一个定时器)。
udp个ip协议一样不可靠,都需要上层协议来处理数据确认和超时重传。

3.2 TCP头部结构
在这里插入图片描述
ack标志:表示确认是否有效,称为确认报文段
rst标志:表示要求对方重新建立连接,复位报文段
syn标志: 请求建立
连接,同步报文段
fin标志:通知对方本要关闭 连接了,结束报文段

3.3 TCP连接的建立和关闭
三次握手建立连接

半关闭状态,tcp连接时全双工的,允许关闭一端,但允许继续接收对端的数据,直到接收完成,关闭连接。

判断已经关闭的方法(read调用函数返回0—):
close和shutdown
如果想直接关闭一方,使用shutdown,直接导致套接字不可用(但不会释放子资源)
close关闭,是引用计数原理,每调用一次close,计数就会减1(fork计数+1),一旦引用计数减到0就会彻底关闭,释放资源。

3.4 TIME_WAIT状态
主动发起关闭的一方进入time_wait状态,时间为2MSL(报文最大生存时间60s),才能完全关闭。
会产生端口占用问题。

3.5 复位报文段
rst标志的报文段,通知对方关闭连接或重新建立连接。
产生rst三种情况:

  1. 访问不存在的端口
  2. 异常终止连接
  3. 处理半打开连接

3.6 tcp交互数据流
交互数据(对实时性要求高):比如telnet、ssh、
成块数据(传输效率要求高):比如ftp

3.9 tcp超时重传
tcp报文段维护一个重传定时器,该定时器在tcp报文段第一次被发送时启动,如果超时没有收到应答,tcp模块将重传tcp报文段并重置定时器。

3.10 拥塞控制
作用:提高网络利用率,降低丢包率

拥塞控制的最终受控变量是发送端向网络一次连续写入,SWND。
SWND太小会引起明显的网络延迟,太大会导致网络拥塞

慢启动和拥塞避免

第五章 基础API

5.1 主机字节序和网络字节序
大端字节序(网络字节序):高位字节存储地位地址
小段字节序(主机字节序):高位字节存高位地址
linux提供4个函数完成字节序之前的转换
htonl:主机字节序转化为网络字节序
htons
ntohl
ntohs

5.11 socket选项

  • SO_REUSEADDR选项
    可以解决time_wait端口占用情况,他让新的端口强制使用,重用端口

  • SO_REVBUF和SO_SNDBUF选项

int sock = atoi(argv[3]);
int len = sizeof(sendbuf);
  
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t*)&len);

tcp接收缓冲区和发送缓冲区的大小,tcp接收缓冲区最小值是256字节,发送缓冲区最小值是2048字节,这样可以确保tcp连接有足够的空闲缓冲区来处理拥塞(快速重传算法期望tcp接受缓冲区至少容纳4个大小的smss的tcp报文段),还可以修改内核参数 /proc/sys/net/ipv4/tcp_rmem(tcp_wmem)来强制tcp缓冲区的大小没有最小值限制

  • SO_RCVLOWAT和SO_SEDLOWAT选项
    表示tcp缓冲区的低水位标记

  • SO_LINGER选项
    控制close系统调用在关闭tcp连接时的行为,

#include <sys/socket.h>
struct linger {
	int l_onoff;	// 开启(非0),关闭(0)
	int l_linger;  //滞留时间
}

5.12 网络信息API
5.12.1 gethostbyname和gethostbyaddr
根据主机名获取主机的完整信息(先去etc/hosts,如果没有再去访问DNS)
根据ip地址获取主机的完整信息

5.12.2 getservbyname和getservbyport
根据名称获取某个服务完整信息
根据端口获取某个服务完整信息
都是读取/etc/services文件来获取服务的信息

5.12.3 getaddrinfo
通过主机名获取ip(内部使用gethostbyname函数)
也可以通过服务名获得端口号(内部getservbyname函数)

#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);

5.12.4 getnameinfo
通过socket地址同时获得以字符串表示的主机名(内部gethostbyname函数)和服务名(getservbyport函数)

第六章 高级I/O函数

6.1 pipe函数
创建一个管道,实现进程间的通信

#include <unistd.h>
int pipe(int fd[2);

2个int类型的数组指针,成功返回0,如果失败返回-1并设置为errno

往一端f[0]写入只能在f[1]一端读出,如果要实现双向数据传输,可以使用使用两个管道,如果read读取一个空的管道,那么read将会被阻塞,如果write写入一个满的管道,也会被阻塞

管道数据传输是字节流,tcp连接写入字节大小,取决接收窗口大小和本端的拥塞窗口大小。而管道拥有一个容量限制,容量大小默认是65536字节,fcntl可以修改管道容量

socketpair可以方便的创建双向管道

#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);

6.2 dup函数和dup2函数
实现标准输入重定向到一个文件,或者把标准重定向到一个网络连接。

#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_ont, int file_descriptor_two);

6.3 readv函数和writev函数
readv函数将数据从fd读到分散的分散的内存块中,分散读。writev函数将多块内存数据一并写入fd中,集中写

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iovec* vector, int count);
  • fd是被操作的目标文件描述符
  • 成功返回读出/写入fd字节数

6.4 sendfile函数
将两个fd之间直接传递数据(在内核中操作),避免内核缓冲区和用户缓冲区之间的数据拷贝,效率高,零拷贝

#include <sys/sendfile.h>
ssize sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
  • in_fd读出fd,out_fd写入fd
  • offset读入文件流的那个位置开始读,如果为空,则使用文件流默认的起始位置
  • count是in和out之间传输的字节数
  • sendfile成功返回传输的字节数,失败返回-1
  • in_fd必须支持类似mmap函数的文件描述符,指定真实的文件,不能是socket和管道
  • out_fd必须是一个socket

6.5 mmap函数和munmap函数
用来申请一段内存空间,这段内存可以作为进程间通信的共享内存,也可以映射到其中,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);
  • start,内存起始地址,如果设置为NULL,则自动分配一个地址
  • prot,内存段的访问权限(读、写、PROT_EXEC可执行、不能访问)
  • flag,内存段内容被修改后程序的行为
    1. MAP_SHARED,进程间共享这段内存
    2. MAP_PRIVATE,内存段为调用进程所私有
  • offset,设置文件的何处开始映射

6.6 splice函数
在两个文件描述符之间移动数据,零拷贝。

#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
  • fd_in,待输入数据的fd,何时开始读取数据,如果是一个管道fd,那么off_in必须设置为NULL
  • flag,控制数据如何移动

6.7 tee函数
两个fd之间复制数据,零拷贝

#include <fcntl.h>
ssize_t tee(int fd, int fd_in, int fd_out, size_t len, unsigned int flags);
  • fd_in和fd_out必须是管道文件描述符

6.8 fcntl函数
操作fd

#include <fcntl.h>
int fcntl(int fd, int cmd, ...);

第七章 linux服务器程序规范

7.1 linuux系统日志
syslogd、rsyslogd
rsyslogd守护进程既能接收用户进程输出的日志,又能接收内核日志
在这里插入图片描述
7.1.1 syslog函数
应用程序使用syslog函数与rsyslogd守护进程通信

#include <syslog.h>
void syslog(int priority, const char* messge, ...);

7.2 用户信息
UID(真实用户)、EUID(EUID)、GID(真实组)和EGID(有效组)

切换用户
int setuid(uid_t uid);
int setgid(gid_t gid);

7.3 进程之间的关系
7.3.1 进程组
进程组除了PID信息,还有进程组PGID

#include <unistd.h>
pid_t getpgid(pid_t pid);

成功返回进程pid所属进程组的PGID,失败返回-1
每个进程都有一个首领进程,其PFID和PID相同,进程组将一直存在,知道所有进程退出,或者加入其它进程组

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);

该函数将PID将pid的进程的PGID设置为pgid,如果pid和pgid相同,则由pid指定的进程被设置为进程组首领,如果pid为0,则表示设置当前进程PGID为pgid,如果pgid为0,则使用pid作为目标PGID,setpgid函数成功时返回0,失败返回-1并设置errno

一个进程只能设置自己或其子进程的PGID,当子进程调用exec系列函数后,不再在父进程中对他设置PGID

7.3.2 会话
一些有关联进程组将形成一个会话

#include <unistd.h>
pid_t setsid(void);

不能由进程组的首领进程调用

ps和less命令的父进程是bash命令

7.4 系统资源限制
linux上运行的程序会受资源限制的影响(cpu数量、内存、数量)、资源策略限制(cpu时间),以及具体实现的限制(比如文件名的最大长度)
读取和设置系统资源

#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
  • rlim参数是rlimit结构体类型的指针
struct rlimit {
	rlim_t rlim_cur; //软限制
	rlim_t rlim_max; //硬限制
};

可以修改配置 文件改变系统软限制和硬限制

7.5 改变文件目录和根目录
获取当前进程目录和改变目录的函数

#include <unistd.h>
char* getcwd(char *buf, size_t size);
int chdir(const char* path);
  • buf存当前工作目录的绝对路径名,如果buf长度超出size,则getcwd返回NULL,如果buf为NULL且size非0,则getcwd可能在内部使用malloc动态分配内存
  • chdir函数的path参数指定要切换到的目标目录,成功返回0

改变进程根目录的函数chroot

#include <unistd.h>
int chroot(const char* path);

7.6 服务器程序后台化
守护进程

#include <unistd.h>
int daemon(int nochdir, int noclose);
  • nochdir参数指定是否改变工作目录,如果传递0,目录被设置为“/”(根目录)

第八章 高性能服务器框架

8.1 服务器模型
C/S(客户端/服务端)模型
优点:实现简单
缺点:服务器是通信中心,访问量过大时,可能所有客户端都将得到很慢的响应

8.1.2 P2P(点对点)模型
摒弃了c/s以服务器为中心的格局,让主机回归对等地位
优点:每台机器消耗服务的同事也给别人提供服务,这样资源能够充分、自由。
缺点:当用户之间传输数据过多时,网络的负载将加重,主机之间很难互相发现(使用一个专门的发现服务器)

8.2 服务器编程框架
在这里插入图片描述

  • I/O处理单元,处理客户端连接,读写网络数据
  • 逻辑单元,业务进程或线程
  • 网络存储单元,本地数据库,文件或缓冲
  • 请求队列,各单元之间的通信方式

I/O处理单元是服务器管理客户端连接的模块,通常的工作:等待并接受新的客户连接,接受客户数据,将服务器响应数据返回给客户端。数据的收发有可能是在逻辑单元中执行。

一个逻辑单元通常是一个进程或线程,分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端。

网络存储单元可以是数据库、缓冲和文件,甚至是一台独立的服务器,但不是必须的,比如ssh、telnet等登陆服务就不需要这个单元

请求队列是个单元之间的通信方式的抽象,I/O处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞争条件。

8.3 I/O模型
socket创建默认是阻塞的,可以调用SOCK_NONBLOCK标志,或通过fcntl系统调用F_SETFL命令,设置为非阻塞。

可以被阻塞的I/O:accept、send、recv、connect

非阻塞I/O执行系统调用立即返回,不管时间是否已经发生。如果事件没有立即发生,这些系统调用返回-1,和不错的情况一样,必须根据errno来区分两种情况,对accept、send、recv而言,事件未发生errno通常被设置为EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期望阻塞”);对connect而言,errno被设置成EINPROGRESS(意为“在处理中”)

只有在事件已发生的情况下操作非阻塞I/O(读、写等),才能提高程序的效率,非阻塞I/O通常要和其他I/O通知机制一起使用,比如I/O复用和SIGIO信号

I/O复用是常用的I/O通知机制,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。I/O复用函数本身是阻塞的,它具有同时监听多个I/O事件的能力来提高效率。

SIGIO信号也可以用来报告I/O事件,我们可以为一个目标文件描述符指定的宿主进程,宿主进程将捕获到SIGIO信号,当fd上有事件发生时,SIGIO信号的信号处理函数将被触发,就可以在该信号处理函数中对目标fd执行非阻塞I/O操作

理论上,阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型,因为I/O的读写操作,都是在I/O事件发生之后,有应用程序来完成的。而异步I/O,用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成之后内核通知应用程序的方式。
同步I/O向应用程序通知的是I/O就绪事件,异步I/O向应用程序通知的是I/O完成事件。

8.4 两种高效的事件处理模式(Reactor和Proactor)

服务器需要处理3件事:I/O事件、信号、定时事件。
Reactor是同步、Proactor是异步

Reactor模式
要求主线程(I/O处理单元)只负责监听fd上是否有事件发生,有的话立即将该事件通知工作线程(逻辑单元)。读写数据、接受新的连接,处理客户请求都在工作线程中完成

使用同步I/O模型(epoll_wait为例)实现Reactor模式的工作流程是:

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列
  4. 睡眠在请求队列上某一个工作线程被唤醒,它从socket读取数据,并处理客户端请求,然后往epoll内核事件表中注册该socket上的写就绪事件
  5. 主线程调用epoll_wait等待socket可写
  6. 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列
  7. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户端请求的结果
    在这里插入图片描述
    工作线程从请求队列取出事件后,根据时间类型处理。对于可读事件,执行都市剧和处理请求的操作;对于可写事件,执行写数据的操作。所以reactor模式中,不用区别“读工作线程”和“写工作线程”

Proactor模式
Proactor模式将所有I/O操作都交给主线程和内核处理,工作线程舅舅负责业务逻辑。

使用异步I/O模型(aio_read和aio_write为例)Proactor的工作流程:

  1. 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及操作完成如何通知程序。
  2. 主线程继续处理其他逻辑
  3. 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用
  4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求,工作线程处理完客户请求后,调用aio_write函数向内核注册socket上的完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序
  5. 主线程继续处理其他逻辑
  6. 当用户缓冲区的数据被写入socket之后,内核将向应用程序程序发送一个信号,已通知应用程序数据已经发送完毕
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket
    在这里插入图片描述
    连接socket上的读写事件是通过aio_read/aio_write向内核注册的,因此内核通过信号来向应用程序报告连接socket上的读写事件。所以,主线程中的epoll_wait调用仅能用来检测监听socket上的连接请求事件,不能用来检测连接socket上的读写事件

8.4.3 模拟Proactor模式
原理:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件。那么从工作线程角度来看,他们直接获得了数据读写的结果,接下来要做id只是对读写的结果进行逻辑处理。

两种高效的并发模式
如果程序是I/O密集型,就不用并发,因为I/O操作的速度远没有CPU计算速度快
并发编程主要有多线程和多进程。
并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。
服务器主要的两种并发编程模型:半同步/半异步模式和领导者/追随者模式

提高服务器性能的其他建议

池、数据复制、上下文切换和锁

8.7.1 池
空间换时间,常见的有内存池、进程池、线程池、连接池
内存池,用于socket接受缓存和发送缓存。预先分配一个大小足够的接收缓存区。
进程池,省去fork或pthread_create的开销
连接池,

8.7.2 数据复制
避免数据复制,比如ftp服务器,客户端请求文件时,服务器只关注文件是否存在以及权限,不用关注内容。这样就可以实现零拷贝,sendfile。
用户代码内部,尽量使用共享内存,用指针(start_line)来指出每个行在buffer中的起始位置,而不是把行的内容复制到另外一个缓冲区来使用。

8.7.3 上下文切换和锁
不应该有过多的工作线程。半同步/半异步模式是一种比较合理的解决方案,允许一个线程同时处理多个客户连接。
共享资源和锁,减少锁的粒度,比如使用读写锁。

第九章 I/O复用

同时监听多个fd。

9.1 select系统调用

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds参数指定被监听的fd总数
  • readfds、writefds、exceptfds 可读、可写、异常

select能处理异常情况只有一种:socket是哪个接收到带外数据,

9.2 poll系统调用

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

9.3 epoll系统调用
使用一组函数,而不是单个函数。epoll把用户关心的fd的事件放在内核里的一个事件表中,而不像 select每次调用都要重传fd或事件集。epoll使用一个额外的fd,标识内核中的事件表。

#include <sys/epoll.h>
int epoll_create(int size)
  • size参数并不起作用,提示内核 事件表的大小

内核事件表:

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

op的参数:

  • EPOLL_CTL_ADD,往事件表中注册fd上的事件
  • EPOLL_CTL_MOD,修改fd上的注册事件
  • EPOLL_CTL_DEL,删除fd上的注册事件

event参数指定事件,它是epoll_event结构指针类型,

struct epoll_event {
	__uint32_t events;
	epoll_data_t data;
};

epoll_data_t:

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

epoll_wait函数
系统调用的主要接口,它在一段超时时间内等待一组fd上的事件

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • maxevents指定最多监听多少个事件,必须>0

LE和ET模式
LT是默认工作模式,当往epoll内核事件中注册一个fd的EPOLLET事件时,epoll将以ET模式来操作该fd。
对于采用LT工作模式的fd,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序,应用程序可以不立即该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,直到事件被处理。而ET,当epoll_wait检测有事件发生,应用程序立即处理,后续不再通知这一事件。ET比LT触发次数少,效率高。

第十章 信号

信号由用户、系统或进程发送给目标进程的信息,通知进程某个状态改变或系统异常。

10.1.1 发送信号

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  • pid>0,系统发送pid为pid进程
  • pid=0,信号发送给本进程组内的其他进程
  • pid=-1,信号发送给除init进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限
  • pid<-1,信号发送给组ID为-pid的进程组中的所有成员

10.1.2 信号处理方式
接受函数

#include <signal.h>
typedef void (*__sighandler_t)(int);

其他处理方式,SIG_IFN忽略目标信号,SIG_DEL表示使用默认处理方式,默认处理方式有:结束进程、忽略信号、结束进程并生成核心转储文件、暂停进程、继续进程

#include <bits/signum.h>
#define SIG_DEL((__sighandler_t) 0)
#define SIG_IGN((__sighandler_t) 1)

10.1.3 Linux信号
都在头文件bits/signum.h中

  • SIGHUP,控制终端挂起
  • SIGINT,输入中断进程(ctrl+c)
  • SIGPIPE,往读端被关闭的管道或者socket连接中读数据
  • SIGCHLD,子进程状态变化

10.2 信号函数
10.2.1 signal系统调用
要为信号设置处理函数,可以使用signal系统调用

#include <signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);
  • sig参数要捕获的信号类型
  • 成功返回函数指针

十一章 定时器

比如检测客户端连接的活动状态,Linux提供3种定时方法:

  • socket选项SO_REVTIMEO和SO_SNDTIMEO
  • SIGALRM信号
  • I/O复用系统调用的超时参数

十二章 libevent框架

避免了客户连接、信号、定时器、竞态条件

1.句柄
I/O框架库要处理的对象,就是I/O事件、信号和定时器事件,统一称为事件源
作用:当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件
I/O事件对应的句柄是fd,信号事件对应的句柄是信号值

事件多路分发器
事件的到来是随机的,要循环等待并处理事件,事件循环。等待事件一般使用I/O多路复用来实现

需要实现register_handler和remove_headler方法,以供调用者往事件多路分发器中添加和删除事件

事件处理器和具体事件处理器
事件处理器执行事件对应的业务逻辑,通常是回调函数在事件循环中被执行。事件处理器通常是一个接口,被声明虚函数。

当多路分发器检测到有事件发生时,通过句柄通知应用程序,所以事件处理器和句柄需要绑定

4.Reactor
主要的方法:

  • headlet_events,执行事件循环,执行过程:等待事件,依次处理所有就绪事件对应的事件处理器
  • register_handler,注册事件,具体:调用register_event方法来往事件多路分发器中注册一个事件
  • remove_headler,删除事件,具体:调用时间多路分发器的remove_event方法来删除事件多路分发器的一个事件
12.2 Libevent源码分析

特点:

  1. 跨平台
  2. 统一事件源,对I/O事件、信号和定时器提供统一的处理
  3. 线程安全,Libevent使用libevent_pthreads库来提供线程安全支持
  4. 基于Reactor模式

第13章 多进程编程

13.1 fork系统调用
父进程返回子进程PID,子进程返回0,失败返回-1

13.2 exec系统调用
替换当前进程映像

13.3 处理僵尸进程
父进程一般需要跟踪子进程的状态
产生原因1:如果子进程结束运行之后,父进程读取其退出状态之前
原因2:父进程结束或异常终止,而子进程继续执行,此时子进程ppid值为1,即init进程,1号进程接管了该子进程,等待结束。

SIGCHLD信号,父进程中捕获SIGCHLD信号,判断子进程是否退出

** 13.4 管道**
能在父子进程之间传递数据,利用fork之后的两个fd

13.5 信号量
多个进程访问某个资源的时候,可能会修改记录,需要同步,确保任一时刻只有一个进程可以拥有对资源难度独占式访问
p、v操作,传递(进入临界区)和释放(退出临界区),pv操作含义:
p(SV),如果SV>0,就将它减1,如果SV=0,挂起进程的执行
V(SV),如果有其他进程因为等待PV挂起,唤醒,如果没有SV+=1

13.5.2 semget系统调用
创建新的信号量集,或者获取一个已经存在的信号量集

#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
  • key用来标识全局唯一的信号量集
  • num_sems指定要创建/获取的信号量集中信号量的数目
  • sem_flags指定一组标志,信号的权限

13.5.3 semop系统调用
semop该百年信号量的值,即执行P、V操作

#include <sys/sem.h>
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

13.5.4 semctl系统调用
semctl允许调用者对信号量进行直接控制

#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command,...);

13.6 共享内存
最高效的IPC机制,因为他不涉及进程间的任何数据传输
问题:必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件,所以共享内存和其他进程间通信方式一起使用
shmget、shmat、shmdt、shmctl

13.6.1 shmget系统调用
创建一段新的共享内存,或获取一段已经存在的共享内存

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
  • key标识一段全局唯一的共享内存

13.6.2 shmat和shmdt系统调用
共享内存被创建/获取之后,不能立即访问它,而是需要先将它关联到进程的地址空间,使用完需要将它从进程地址空间中分离,

#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmdt(const void *shm_addr);

13.6.3 shmctl系统调用
shmctl系统调用控制共享内存的某些属性

#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_da* buf);

13.7 消息队列
两个进程之间传递二进制块数据的一种简单有效的方式,每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据

msgget
创建一个消息队列,或获取一个已有的消息队列

#include <sys/msg.h>
int msgget(key_t key, int msgflg);

msgsnd
把一条消息添加到消息队列中

14章 多线程

完全用户空间实现
优点:创建和调度线程无须内核干预,速度较快,不占用额外的内核资源,即使一个进程创建很多线程,也不会对系统性能造成影响
缺点:一个进程的多个线程无法运行在不同CPU上,因为内核按照其最小调度单位分配CPU的

完全由内核调度
和完全空间实现优缺点互换

双层调度
优点:不但不会消耗过多的内核资源,而且线程切换速度也较快,同事可以充分利用多处理器的优势

14.1.2 Linux线程库
NPTL线程库,优点:

  • 内核线程不再是一个进程,避免了很多用进程模型内核线程导致的语义问题
  • 摒弃了管理线程、终止线程、回收线程堆栈等工作都由内核来完成
  • 一个进程的线程可以运行在不同的CPU上,从而充分利用多处理器系统的优势
  • 线程同步由内核来完成,隶属于不用进程的线程之间也能共享互斥锁,可以实现跨进程的线程同步

14.2 创建线程和结束线程
1.pthread_create
创建线程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
	void *(*start_routine)(void *), void *arg);
  • pthread_t是一个整形类型,资源标识符一般都是整数
  • attr设置新线程的属性,传递NULL表示使用默认线程
  • start_routing和arg参数分别指定新线程运行的函数和参数

2.pthread_exit
内核可以调度内核线程来执行start_routing函数指针所指向的函数,可以确保安全干净地退出

#include <pthread.h>
void pthread_exit(void *retval);

3.pthread_join
回收其他线程

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • thread是目标线程的标识符
  • retval目标线程返回的退出信息,
    该函数会一直阻塞,知道被回收的线程结束为止

4.pthread_cancel
希望异常终止一个线程,取消线程

#include <pthread.h>
int pthread_cancel(pthread_t thread);

14.4 POSIX信号量
pthread_join也实现简单的线程同步方式,但不能实现复杂的比如共享资源独占式访问,或某个条件满足之后唤醒一个线程。

POSIX信号量都是以sem_开头,常用的5个:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t * sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
  • sem_wait调用信号量的值减1,当值为0,会阻塞
  • sem_trywait会立即返回

14.5 互斥锁
可以确保独占式访问

14.5.1 API

#include <pthread.h>
int pthread_mutex_init(pthread_mutex * mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

互斥锁类型是pthread_mutex_t结构体

  • pthread_mutex_lock函数以原子操作的方式给一个互斥锁加锁,如果锁上的话,这个调用将阻塞,直到互斥锁占有者将其解锁
  • pthread_mutex_trylock和上一个类似,但始终立即返回

14.5.3 死锁举例
互斥锁的缺点容易产生死锁,死锁使一个或多个线程被挂起而无法继续执行,而且不易发现,一个线程对一个已经加锁的普通锁再次加锁,将导致锁。或者两个线程按照不同顺序申请2个互斥量,也容易产生死锁

14.6 条件变量
用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程

条件变量类型是pthread_cond_t结构体

14.8 线程安全
一个函数能被多个线程同时调用且不发生竞态条件,则我们称为它是线程安全的(可重入函数)

15章 进程池和线程池

多进程、多线程缺点:

  • 动态创建进程或线程比较耗时,可能会导致较慢的客户响应
  • 切换需要消耗大量的CPU时间

15.1进程吃和线程池
进程池所有子进程都执行相同的代码,具有相同的属性,比如优先级、PGID等。主进程选择那个子进程为新来的任务服务,有2种方式:

  • 最简单、常用的是随机算法和Round Robin算法

选好子进程之后,还需要通过机制告诉子进程有新任务需要处理并传递必要的数据,比如管道。
在这里插入图片描述
15.2 处理多客户
子进程调用accept来接收新的连接,这样父进程无须向子进程传递socket。
一个客户多次请求可以复用一个TCP连接,那么一个客户连接上的所有任务是否由一个子进程处理,如果一个子进程处理不同任务,需要客户任务存在上下文关系,所以最好一直是同一个子进程服务,否则实际会比较麻烦。
epoll中的EPOLLONESHOT事件,能确保一个客户连接在整个生命周期仅被一个线程处理

15.3 半同步/半异步进程池实现
为了避免父子进程之间传播fd,可以将新的连接的操作放在子进程中,显然这种模式,一个客户连接上的所有任务始终由一个子进程来处理

15.5 半同步/半反应堆线程池
性能比上一个高,因为它使用一个工作队列完全解除了主线程和工作线程的耦合关系:主线程往工作队列中插入任务,工作线程通过竞争来取得任务并执行它。
如果要将线程池应用到实际服务器程序中,必须保证所有客户请求都是无状态的,同一个连接上的不同请求可能由不同的线程处理

16 服务器调试测试

系统调试、服务器调试、压力测试

16.1 最大文件描述符数
fd资源是有限的,可以运行一个守护进程的服务器程序总是关闭标准输入、标准输出和标准错误这3个fd

查看fd数限制的方法

ulimit - n

16.2 调整内核参数
查看内核参数:sysctl -a

14 压力测试

17章 系统监听工具

17.1 tcpdump

  • -n,使用ip地址表示主机,而不是主机名
  • -i,指定网卡接口
  • -c,抓取指定数量的数据包

tcpdump表达似乎的操作数分为3种:类型、方向、协议

  • 类型,支持host、net、port、portrange类型,分别指定主机名,网络地址,端口,端口范围,例如要抓取1.2.3.0/255.255.255.0网络上的数据包:tcpdump net 1.2.3.0/23
  • 方向,src和dst,比如抓取端口13579的数据包:tcpdump dst port 13579
  • 协议,指定目标协议,比如抓取ICMP协议数据包:tcpdump icmp

17.2 lsof
列出当前系统打开的fd工具

  • -i,显示socket文件描述符,lsof -i [46] [protocol] [@hostname|ipaddr][:service|port],46表示IPV几协议,protocal指定传输层协议,lsof -i@192.168.1.108:22
  • -u显示指定用户启动的所有进程打开的fd

17.3 nc
用来快速构建网络连接。可以让他以服务器方式运行,监听某个端口并接收客户连接,可以用来调试客户端程序。也可以以客户端方式运行,向服务端发起连接并收发数据,因此可以用来调试服务器程序

17.4 strace
测试服务器性能的重要工具,跟踪程序运行中执行的系统调用和接收到的信号,并将系统调用名、参数、返回值和信号名输出到标准输出或者指定的文件

17.5 netstat
网络信息统计工具,可以打印本地网卡全部连接、路由表信息、网卡接口信息

17.6 vmstat
能输出系统各种资源情况,比如进程信息、内存、cpu使用、I/O使用情况

ifstat
网络流量检测工具

mpstat
检测每个CPU使用情况

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值