复用IO epol poll select 池 零拷贝读写 上下文切换 mmap

高性能服务器、网络理论知识 与操作系统

高性能服务器

提高服务器的性能
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 从磁盘加载内核
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值