高性能服务器、网络理论知识 与操作系统
高性能服务器
提高服务器的性能
1、I/O模型
阻塞IO (不合适)
程序阻塞与读写函数 当没有数据可读时 程序一直阻塞到读取数据成功
阻塞的过程:数据从无到有的时间段
非阻塞IO
当文件描述符不可读或写 立即返回
一般采用轮询的方式进行 过一段时间再读写试一试
I/O复用
程序阻塞与I/O复用系统调用,可以同时监听多个I/O事件
对I/O本身的读写操作时非阻塞
SELECT
// select
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, //通常被设置为select所监听的所有文件描述符
//中的最大值+1 指定被监听的文件描述符的总数 文件描述符从0开始
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
int pselect();
fd_set
// 文件描述符集合
//fd_set类型是一个整数数组 该数组中的每个元素 每一位(bit)标记一个文件描述符
//fd_set能容纳 的文件描述符数量由FD_SETSIZE指定
//限制了select 所有处理的文件描述符数量
FD_ZERO(fd_set *fdset);//清楚所有位
FD_SET(int fd,fd_set *fdest); //设置fd
FD_CLR(int fd,fd_set *fdest); //清除fd
int FD_ISSET(int fd,fd_set *fdest);//测试
//readfds,writefds,exceptfds 分别用于记录要监听是否 可读 可写 异常事件文件描述符集合
//select 函数调用返回时 内核将修改readfds,writefds,excepts,文件描述符集合,保留有数据可读 可写 异常文件描述符
timeout
//设置select函数超时时间
//返回select调用返回后剩余的时间,如果失败时timeout不确定
//如果timeout变量 他的成员都为0,则select是非阻塞
//如果timeout取值为NULL select将一直阻塞 直到某个文件描述符就绪
struct timeval{
long tv_sec;
long tv_usec;
}
//返回值:
// select 成功时返回就绪(可读可写异常)文件描述符的综合
// 如果再超时时间内任何文件描述符就绪 返回0
// select失败返回-1并设置errno
// 在select等待期间,程序接收到信号,select立即返回-1
// errno为EINTR
//当文件描述符数量增大时 效率其实是急剧下降
POLL
poll不用每次把所有的文件描述符加入到集合中(数组)
poll通知用户的方式不一样
select把没有就绪的文件描述符从集合中删除
poll内核直接设置集合中每一个数据的revents属性
相同的地方在于,调用完poll/select函数,需要遍历所有的文件描述符
需要遍历所有的文件描述符 判断是否就绪
// poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:是一个struct pollfd结构体类型的数组
struct pollfd{
int fd; //文件描述符
short events; //注册的事件 可读可写
short revents; //实际发生的事件 由内核填充
};
事件:
POLLIN 数据可读
POLLOUT 数据可读
nfds:
数组长度
timeout:
超时等待的毫秒数
-1 阻塞
0 立即返回
返回值:
和select意义相同 就绪状态的文件描述符个数
int ppoll()??
EPOLL
Linux特有的I/O复用函数
epoll有一组函数
epoll吧用户关心的文件描述符上的时间注册到内核的实践表中
不需要像select/poll每次都要调用重新传文件描述符集
epoll也不需要调用完成之后遍历所有的文件描述符集
epol需要使用一个额外的文件描述符,用来唯一表示内核中时间表示
#include <sys/epoll.h>
int epoll_create(int size);
size参数,目前不起任何作用
给内核一个提示 告诉内核事件表需要多大
返回文件描述符,用于标识内核中的事件表
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event);
参数:
epfd:epoll_create函数的返回值
op:
EPOLL_CTL_ADD; 往时间表中注册fd的事件
EPOLL_CTL_MOD; 修改事件表中fd的注册事件
EPOLL_CTL_DEL; 删除事件表中fd的事件
fd:文件描述符
event:注册事件
struct epoll_event{
_unit32_t event; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
返回值成功返回0失败返回-1设置errno
int epoll_wait(int epfd,struct epoll_event *events,
int maxevents,int timeout);
通过该函数返回以及就绪的事件,通过events数组返回
成功返回就绪的文件描述符的个数,events数组中有几个有效数据
events:数组 用于接收就绪的文件描述符事件
maxevents:数组的最大长度
timeout:超时等待
0:立即返回
-1:阻塞
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到events指向的数组中。
内核事件表中复制到events指向的数组中
LT和LE模式
LT(Level Trigger,电平触发) 默认
poll/select
如果没有处理事件 每次epoll_wait都会通知
ET(Edge Trigger,沿边触发) 高效工作方式
一个文件描述符如果有数据可读 内核会检测并通知应用程序
应用程序如果没有立即处理该事件
下次epoll_wait就不会再次向应用程序通告此事件=
SIGIO信号
信号触发读写就绪事件,用户程序执行读写从操作,程序没有阻塞阶段
异步I/O
内核执行读写操作并触发读写事件。程序没有阻塞
*如果内核中有时间可读:
***同步读***的过程,需要把内核中的数据拷贝到用户内存中才返回(等待数据的拷贝从用户态到内核态)
ssize_t read(fd,buf,BUF_LEN);//同步读
异步读 调用函数后 立即返回(不等待)
#include <aio.h>
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
struct aiocb
{
//要异步操作的文件描述符
int aio_fildes;
//用于lio操作时选择操作何种异步I/O类型
int aio_lio_opcode;
//异步读或写的缓冲区的缓冲区
volatile void *aio_buf;
//异步读或写的字节数
size_t aio_nbytes;
//异步通知的结构体
struct sigevent aio_sigevent;
}
aio_read
#define BUFFER_SIZE 1024
int MAX_LIST = 2;
int main(int argc,char **argv)
{
//aio操作所需结构体
struct aiocb rd;
int fd,ret,couter;
fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("test.txt");
}
//将rd结构体清空
bzero(&rd,sizeof(rd));
//为rd.aio_buf分配空间
rd.aio_buf = malloc(BUFFER_SIZE + 1);
//填充rd结构体
rd.aio_fildes = fd;
rd.aio_nbytes = BUFFER_SIZE;
rd.aio_offset = 0;
//进行异步读操作
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
exit(1);
}
couter = 0;
// 循环等待异步读操作结束
while(aio_error(&rd) == EINPROGRESS)//读完是ECANCELLED
{
printf("第%d次\n",++couter);
}
//获取异步读返回值
ret = aio_return(&rd);
printf("\n\n返回值为:%d",ret);
return 0;
//当内核中的数据全部拷贝到内核空间后 发送信号
2、池 进程池 线程池
假设客户端连接上来就发送一个请求就结束了(短连接 如HTTP服务器)
频繁地创建和销毁线程or线程(CPU占比基本上在创建和销毁线程)
这就需要 进程池 线程池 (对长连接没有这么重要)
先创建 号若干进程or线程
当有客户端连接上来时,让线程池中的线程去跟这个客户端连接
当客户端断开连接之后,线程不会销毁,放回到线程池中
3、零拷贝读写
读写操作 频繁地进行 用户态和内核态的数据拷贝
读: 从内核态拷贝到用户态
写: 从用户态拷贝到内核态
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);//零拷贝IO
//直接将文件套接字放入内核 输出到客户端
//高级I/O函数
//pipe 管道
//dup/dup2 复制文件描述符
//readv/writev 分散读写
//mmap munmap 映射物理内存
//splice 零拷贝
//tee 零拷贝
4、上下下切换和锁
上下文件切换 :
密集型的I/O服务器 线程会大量频繁地切换 占用CPU时间比例大
需要***减少线程的数量*** 或者 半同步/半异步模式
锁:
程序big啊需要考虑的另外一个问题是共享资源的同步问题(加锁保护)
锁通常被认为是导致服务器效率低下的一个很重要的因素
解决方案:
1、不用锁 其他模式替换 (复用IO)
2、减少锁的粒度
3、如果读大于写的频率,读写锁替换互斥锁之类的
5、mmap / munmap函数
mmap 函数是申请了一块内存,我们将这块内存作为进程间通信的共享内存,也可以将文件映射到其中,munmap 释放申请的这块内存
#include<sys/mman.h>
void* mmap(void *start,siez_t length,int port,int flags, int fd,off_t offset);
int munmap(void *start,size_t length);
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);
7、tee
tee 函数用于两个管道之间的数据交流,也是零拷贝操作
#include<fcntl.h>
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags);
总结:
事件集合:
select:用户通过三个参数分别传入可读、可写、异常的事件文件描述符集合,内核通过对这些参数的在线修改来反馈其中的就绪事件 使得用户每次调用select都需要重置这三个参数
poll:统一处理所有事件类型,因此只需要一个时间集参数 用户可以通过结构体events传入事件 内核通过修改结构体的revents反馈其中就绪的事件
epoll:内核通过一个时间表直接管理用户注册的所有事件,因此每次调用epoll_wait时,无须反复传入用户注册的事件,epoll_wait参数events仅用来反馈就绪的事件不需要遍历所有的文件描述符集合
应用程序索引文件描述符的时间复杂度
slect:O(n); poll:O(n) epoll(1)
最大支持的文件描述符
select:受多方制约 poll&epoll:65535
工作模式
select: LT poLL: LT epoll: 默认LT 支持ET;
内核实现和工作效率
select:采用轮询方式来检测就绪事件 算法时间复杂度为O(n)
poll:采用轮询方式来检测就绪事件,算法事件复杂度O(n)
epoll:采用回调方式来检测就绪事件,算法时间复杂度为O(1)
复用I/O + 线程/线程池
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
int fd;
struct sockaddr_in addr;
char name[NAME_LEN];
}Client;
Client gcls[MAX_CLIENTS+1] = {};
int size = 0;
int init_server(const char *ip,unsigned short int port){
int fd = socket(AF_INET,SOCK_STREAM,0);
assert(fd != -1);
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
socklen_t len = sizeof(addr);
int ret = bind(fd,(const struct sockaddr*)&addr,len);
assert(ret != -1);
ret = listen(fd,MAX_CLIENTS);
assert(ret != -1);
return fd;
}
void broadcast(int fd,const char *msg){
int i;
for(i=0;i<size;i++){
if(fd != gcls[i].fd){
send(gcls[i].fd,msg,strlen(msg)+1,0);
}
}
}
void accept_client(int fd,int epfd){
struct sockaddr_in addr = {};
socklen_t len = sizeof(addr);
int cfd = accept(fd,(struct sockaddr*)&addr,&len);
if(cfd != -1){
Client cls = {};
cls.fd = cfd;
cls.addr = addr;
gcls[size] = cls;
++size;
struct epoll_event event = {};
event.events = EPOLLIN;
event.data.fd = cfd;
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
if(ret == -1){
perror("epoll_ctl");
}
}
}
int recv_data(int fd){
int index = 0;
for(;index<size;index++){
if(gcls[index].fd == fd){
break;
}
}
char msg[MSG_LEN] = {};
int ret = 0;
if(strcmp(gcls[index].name,"")==0){
ret = recv(fd,msg,MSG_LEN,0);
if(ret <= 0){
return 0;
}
strcpy(gcls[index].name,msg);
strcat(msg," 进入聊天室,大家欢迎!");
}else{
strcpy(msg,gcls[index].name);
strcat(msg,":");
int len = strlen(msg);
ret = recv(fd,msg+len,MSG_LEN-len,0);
if(ret <= 0){
msg[--len] = '\0';
strcat(msg," 退出聊天室,大家欢送!");
}
}
broadcast(fd,msg);
if(ret <= 0)
return 0;
return 1;
}
void select_fd(int fd){
int epfd = epoll_create(MAX_CLIENTS);
if(epfd == -1){
perror("epoll_create");
return;
}
struct epoll_event event = {};
event.events = EPOLLIN; //读事件
event.data.fd = fd; //用户数据
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
if(ret == -1){
perror("epoll_ctl");
return ;
}
struct epoll_event events[MAX_CLIENTS+1] = {};
int i;
while(true){
ret = epoll_wait(epfd,events,MAX_CLIENTS+1,-1);
if(ret == -1){
perror("epoll_wait");
break;
}
for(i=0;i<ret;i++){
if(events[i].data.fd == fd){//有客户端连接
accept_client(fd,epfd);
}else{//有数据需要接收
if(events[i].events & EPOLLIN){
ret = recv_data(events[i].data.fd);
if(ret == 0){
struct epoll_event ev = {};
ev.events = EPOLLIN;
ev.data.fd = events[i].data.fd;
ret = epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&ev);
if(ret == -1){
perror("epoll_ctl");
}
}
}
}
}
}
}
int main(int argc,char *argv[]){
if(argc < 3){
printf("%s ip port\n",argv[0]);
return -1;
}
int fd = init_server(argv[1],atoi(argv[2]));
select_fd(fd);
return 0;
}
可重入函数
readv /writev 函数
#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);
sendfile函数
int fd = open("a.txt",O_RDONLY);
read()文件 磁盘拷贝到内核中 ---- 从内核拷贝到用户
send() 从用户内存拷贝到内核
合起来就是 sendfile 从磁盘加载内核