I/O复用
多路IO复用,有时也称为事件驱动IO。它的基本原理就是有个函数(如select)会不断地轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。多路复用是让阻塞发生在我们的多路复用IO操作的系统调用上面,而不是我们真正去执行IO的系统调用。使用这个方式的好处就是可以同时监控多个用于IO的文件描述符。
I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
Linux下实现I/O复用的系统调用主要有select、poll和epoll
client.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <csignal>
#include <unistd.h>
#include <netdb.h>
#include <stdlib.h>
#include <assert.h>
#include "string.h"
#include "iostream"
int main()
{
const char *ip = "127.0.0.1";
int port = 8899;
//服务器tcp连接描述
struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_address.sin_addr);
server_address.sin_port = htons(port);
//建立socket
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd > 0);
//连接服务器
if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0)
{
std::cout << "连接服务器失败" << std::endl;
}
else
{
std::cout << "连接服务器成功" << std::endl;
//发送数据
sleep(5);
send(sockfd, "hello select!", 12, 0);
std::cout << "发送数据成功" << std::endl;
}
//关闭socket
close(sockfd);
return 0;
}
select
select原理
监听多个文件描述符,只有事件到来时才通知内核与读/写客户端数据通信(accept),也就是先select监听,有事件到来时才accept
网络通信过程在Unix系统中通常被抽象为文件的读写过程。select模型中的一个socket文件描述符通常可以看成一个由设备驱动程序管理的一个设备,驱动程序可以知道自身的数据是否可用。同时,该设备支持阻塞操作并实现了一组自身的等待队列,如读/写等待队列用户支持上层(用户层)所需的block(阻塞)和non-block(非阻塞)操作。设备的资源如果可用(可读/可写)则会通知应用进程。反之则会让进程睡眠,等待数据到来的时候,再唤醒应用进程。
select的思想是多个这样的设备的文件描述符被放在一个队列中,然后select调用的时候遍历这个队列,如果对应的文件描述符可读/可写则会返回该文件描述符(调用应用进程的回调事件)。当遍历结束之后,如果仍然没有一个可用的文件描述符,select会让用户进程睡眠,直到等待资源可用的时候再唤醒用户进程并返回对应的文件描述符(调用应用进程的回调事件),select每次遍历都是线性的。
select模型的调用时间复杂度是线性的,即O(n)。
select模型是线程不安全的。select模型只解决accept()的等待问题。
尽管select模型使用很便利,且具有跨平台的特性。但是select模型还是存在一些问题。select模型需要遍队列中的文件描述符,并且这个队列还有最大限制(使用位图)。随着文件描述数量的增长,用户态和内核的地址空间的复制引发的开销也会线性增长。即使监视的文件描述符长时间不活跃,select模型还是会进行线性扫描它。
select()工作流程
- 使用FD_ZERO宏初始化一个fd_set对象(即初始化socket队列)。实际上就是select的第2、3、4的形参。
- 使用FD_SET宏将socket文件描述符键入到fd_set对象中(即加入到socket队列中)。
- 调用select函数,等待函数返回。如果没有套接字返回,那么select函数会把fe_set对象中的socket队列清空。如果有套接字返回,那么将是返回可读/可写/异常的socket集合。其余不可读/不可写/无异常的套接字将进行清除。 使用FD_ISSET对返回的套接字集合进行检查,对相应的套接字进行操作。
- 之后反复执行以上几个步骤。
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int fds[] = 存放需要监听的socket
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}
poll的工作流程类似
select API
select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
select系统调用的原型如下:
#include<sys/select.h>
int select(int nfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,struct timeval*timeout);
- nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。这个参数非常重要,通过根据该参数进行监听,要在所有文件描述符中找到max并+1
深入的理解select模型的关键点在于理解fd_set,为了说明方便,我们取fd_set长度为1个字节,fd_set中的每一个bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示为 0000,0000 。
(2)若fd = 5 ,则执行 FD_SET(fd,&set)后,set变为 0001,0000 (第5位置为1)
(3)若再加入fd=2 ,fd=1,则set变为 0001.0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。没有可读事件发生时 fd = 5 被清空。
- readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。
select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这3个参数是fd_set结构指针类型。select返回的值大于0,表示响应的socket数量,如果等于0超时,如果小于0则代表发生错误。fd_set结构体的定义如下:
#include<typesizes.h>
#define__FD_SETSIZE 1024
#include<sys/select.h>
#define FD_SETSIZE__FD_SETSIZE
typedef long int__fd_mask;
#undef__NFDBITS
#define__NFDBITS(8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef__USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;
fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作过于烦琐,我们应该使用下面的一系列宏来访问fd_set结构体中的位
#include<sys/select.h>
FD_ZERO(fd_set*fdset);/*清除fdset的所有位*/
FD_SET(int fd,fd_set*fdset);/*设置fdset的位fd*/
FD_CLR(int fd,fd_set*fdset);/*清除fdset的位fd*/
int FD_ISSET(int fd,fd_set*fdset);/*测试fdset的位fd是否被设置*/
- timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。timeval结构体的定义如下:
struct timeval
{
long tv_sec;/*秒数*/
long tv_usec;/*微秒数*/
};
select给我们提供了一个微秒级的定时方式。如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
**select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。**select失败时返回-1并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
文件描述符就绪条件
哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。在网络编程中,下列情况下socket可读:
socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
- socket通信的对方关闭连接。此时对该socket的读操作将返回0。
- 监听socket上有新的连接请求。
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
下列情况下socket可写:
- socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
- socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
- socket使用非阻塞connect连接成功或者失败(超时)之后。
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。
带外数据:比普通数据具有更高的优先级,应该会立即被发送,不论发送缓冲区中是否有排队等候发送的普通数据,他的传输可以使用一条单独的链路传输也可以映射到普通数据传输的链接中。由紧急指针标记
select处理带外数据
socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。
针对可读、可写、异常三种事件状态,需要建立三个fd_set,执行初始化FD_SET,然后通过FD_ISSET监听并获取相应的事件,然后进行处理
带外数据通过设置recv函数的最后一个参数为MSG_OOB紧急指针直接读取紧急数据
select原理
select通过设置位图,每一位代表一个文件描述符,每一位只要变化就代表触发事件,由内核进行监听,并且select的第一个参数非常重要,设置为最大描述符值+1,能根据该值监听位图,同时对于位图所对应的文件描述符队列,需要进行线性扫描,发现事件是否被触发
server:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <csignal>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <vector>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <math.h>
using std::cout;
using std::endl;
using std::vector;
const int BUF_SIZE = 1024;
int main(int argc, char *argv[])
{
cout << "lsl" << endl;
const char *ip = "127.0.0.1"; //ip
int port = atoi("8899"); //端口
/*创建一个IPv4 socket地址,主要设置ip和端口信息*/
struct sockaddr_in address;
bzero(&address, sizeof(address)); //清空地址
address.sin_family = AF_INET; //ipv4
inet_pton(AF_INET, ip, &address.sin_addr); //ip转换
address.sin_port = htons(port); //端口
//创建socket,ipv4,字符流(tcp)
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(listen_fd >= 0);
//命名Socket / 绑定Sock
int ret = bind(listen_fd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
int backlog = 5; //监听队列长度
//监听Socket
ret = listen(listen_fd, backlog);
assert(ret != -1);
//----------------------------------------------------------------
//接受信息,select复用
char buffer[BUF_SIZE];
//监听多个客户端连接
vector<int> conn_fds;
//定义select中的fd_set
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds); //初始化清除fd_set中的所有位
FD_ZERO(&exception_fds);
while (true)
{
cout << "进入循环" << endl;
memset(buffer, 0, BUF_SIZE);
/*每次调用select前都要重新在read_fds和exception_fds中设置文件描述符conn_fd,因为事件发生之后,文件描述符集合将被内核修改*/
//设置对服务器/客户端文件描述符监听,重复监听变化
FD_SET(listen_fd, &read_fds);
FD_SET(listen_fd, &exception_fds);
//select复用,在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。阻塞等到事件到来
int max_conn_fd = 0;
if (conn_fds.size() != 0)
{
auto max_iter = std::max_element(conn_fds.begin(), conn_fds.end());
max_conn_fd = *max_iter;
}
ret = select(std::max(0, std::max(listen_fd, max_conn_fd)) + 1, &read_fds, NULL, &exception_fds, NULL);
if (ret < 0)
{
std::cout << "select多路复用失败" << std::endl;
break;
}
//最后时间参数设置为NULL,阻塞等到事件到来,不同的事件有不同的处理方案
//服务器监听到事件,接受连接
if (FD_ISSET(listen_fd, &read_fds))
{
//客户端tcp连接描述,等待符合格式的连接
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client, &client_addrlength);
conn_fds.push_back(conn_fd);
cout << "客户端连接成功,conn_fd:" << conn_fd << endl;
FD_SET(conn_fd, &read_fds);
}
//连接事件队列conn_fds中有事件,遍历扫描
for (vector<int>::iterator it = conn_fds.begin(); it != conn_fds.end(); it++)
{
int conn_fd = *it;
//普通可读事件
if (FD_ISSET(conn_fd, &read_fds))
{
ret = recv(conn_fd, buffer, sizeof(buffer) - 1, 0);
if (ret < 0)
{
std::cout << "读取普通数据失败,错误码:" << ret << std::endl;
break;
}
std::cout << "读取数据:" << buffer << " ret:" << ret << std::endl;
close(conn_fd);
it = conn_fds.erase(it);
it--;
}
//对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据
else if (FD_ISSET(conn_fd, &exception_fds))
{
ret = recv(conn_fd, buffer, sizeof(buffer) - 1, MSG_OOB); //MSG_OOB TCP紧急指针标志位
if (ret < 0)
{
std::cout << "读取紧急数据失败,错误码:" << ret << std::endl;
break;
}
std::cout << "读取紧急数据:" << buffer << " ret:" << std::endl;
close(conn_fd);
it = conn_fds.erase(it);
it--;
}
}
}
//关闭Socket
close(listen_fd);
return 0;
}
poll
poll API
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。poll的原型如下:
#include<poll.h>
int poll(struct pollfd*fds,nfds_t nfds,int timeout);
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体的定义如下:
struct pollfd
{
int fd;/*文件描述符*/
short events;/*注册的事件*/
short revents;/*实际发生的事件,由内核填充*/
};
其中,fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。
针对该结构体通常创建一个pollfd数组,设置文件描述符表示监听,设置events(poll.h中定义),在传给poll,等待事件唤醒遍历数组(条件就是revents与已定义好的事件&操作判断事件是否发送)
nfds参数指定被监听事件集合fds的大小。其类型nfds_t的定义如下:
typedef unsigned long int nfds_t;
timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
poll系统调用的返回值的含义与select相同。
poll原理
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
server.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <csignal>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <poll.h>
#include <iostream>
using std::cout;
using std::endl;
const int BUF_SIZE = 1024;
int main(int argc, char *argv[])
{
cout << "lsl" << endl;
const char *ip = "127.0.0.1"; //ip
int port = atoi("8899"); //端口
/*创建一个IPv4 socket地址,主要设置ip和端口信息*/
struct sockaddr_in address;
bzero(&address, sizeof(address)); //清空地址
address.sin_family = AF_INET; //ipv4
inet_pton(AF_INET, ip, &address.sin_addr); //ip转换
address.sin_port = htons(port); //端口
//创建socket,ipv4,字符流(tcp)
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(listen_fd >= 0);
//命名Socket / 绑定Sock
int ret = bind(listen_fd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
int backlog = 5; //监听队列长度
//监听Socket
ret = listen(listen_fd, backlog);
assert(ret != -1);
//----------------------------------------------------------------
//接受信息,select复用
char buffer[BUF_SIZE];
//poll监听多个客户端
int max_link = 6;
struct pollfd pollfds[max_link];
//监听连接事件
pollfds[0].fd = listen_fd;
pollfds[0].events = POLLRDNORM;
for (int i = 1; i < max_link; i++)
{
pollfds[i].fd = -1;
pollfds[i].events = POLLRDNORM;
}
int link_count = 1; //连接数量,目前只有连接符在监听
while (true)
{
cout << "进入循环" << endl;
memset(buffer, 0, BUF_SIZE);
//poll多路复用-1代表阻塞等待事件到来,-1阻塞
ret = poll(pollfds, link_count, -1);
if (ret < 0)
{
std::cout << "poll多路复用失败" << std::endl;
break;
}
//服务器监听到事件,接受连接
if (pollfds[0].revents & pollfds[0].events)
{
//客户端tcp连接描述,等待符合格式的连接
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client, &client_addrlength);
struct pollfd conn;
conn.fd = conn_fd;
conn.events = POLLRDNORM;
pollfds[link_count++] = conn;
cout << "客户端连接成功,conn_fd:" << conn_fd << endl;
}
//连接事件队列client中有事件,遍历扫描
for (int i = 1; i < max_link; i++)
{
if (pollfds[i].fd > 0)
{
//普通可读事件
if (pollfds[i].revents & pollfds[i].events)
{
ret = recv(pollfds[i].fd, buffer, sizeof(buffer) - 1, 0);
if (ret < 0)
{
std::cout << "读取普通数据失败,错误码:" << ret << std::endl;
break;
}
std::cout << "读取数据:" << buffer << " ret:" << ret << std::endl;
close(pollfds[i].fd);
pollfds[i].fd = -1;
link_count--;
}
}
}
}
//关闭Socket
close(listen_fd);
return 0;
}
epoll
poll是翻译轮询的意思,可以看到poll和epoll都有轮询的过程。poll轮询的是所有的socket。而epoll只轮询就绪的socket。epoll采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。因此epoll_wait无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是O(1)。
epoll API
epoll_create
epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下epoll_create函数来创建:
#include<sys/epoll.h>
int epoll_create(int size)
size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
epoll_ctl
下面的函数用来操作epoll的内核事件表:
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event)
fd参数是要操作的文件描述符,op参数则指定操作类型。操作类型有如下3种:
- EPOLL_CTL_ADD,往事件表中注册fd上的事件。
- EPOLL_CTL_MOD,修改fd上的注册事件。
- EPOLL_CTL_DEL,删除fd上的注册事件。
event参数指定事件,它是epoll_event结构指针类型。epoll_event的定义如下:
struct epoll_event
{
__uint32_t events;/*epoll事件*/
epoll_data_t data;/*用户数据*/
};
其中events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll的高效运作非常关键。data成员用于存储用户数据,其类型epoll_data_t的定义如下:
typedef union epoll_data
{
void*ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
epoll_data_t是一个联合体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
epoll_ctl成功时返回0,失败则返回-1并设置errno。
epoll_wait
poll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件,其原型如下:
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。
关于该函数的参数,我们从后往前讨论。timeout参数的含义与poll接口的timeout参数相同。maxevents参数指定最多监听多少个事件,它必须大于0。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。
/*如何索引poll返回的就绪文件描述符*/
int ret=poll(fds,MAX_EVENT_NUMBER,-1);
/*必须遍历所有已注册文件描述符并找到其中的就绪者(当然,可以利用ret来稍做优化)*/
for(int i=0;i<MAX_EVENT_NUMBER;++i)
{
if(fds[i].revents&POLLIN)/*判断第i个文件描述符是否就绪*/
{
int sockfd=fds[i].fd;
/*处理sockfd*/
}
}
/*如何索引epoll返回的就绪文件描述符*/
int ret=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
/*仅遍历就绪的ret个文件描述符*/
for(int i=0;i<ret;i++)
{
int sockfd=events[i].data.fd;
/*sockfd肯定就绪,直接处理*/
}
epoll原理
1.创建epoll对象,当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象)。eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。
创建一个代表该epoll的eventpoll对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为eventpoll的成员。
2.维护监视列表,创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。
3.接收数据,当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket。
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。
4. 阻塞和唤醒进程,假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll实例
需要注意的是,在epoll_wait时内核会把已经就绪的事件副本传递给epoll_events,因此每次直接遍历epoll_events即可,并且每次直接加入一个struct epoll_event ev事件即可(epoll_ctl背后存储是双向链表,快速索引是红黑树)
server.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <csignal>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <sys/epoll.h>
#include <iostream>
using std::cout;
using std::endl;
const int BUF_SIZE = 1024;
int main(int argc, char *argv[])
{
cout << "lsl" << endl;
const char *ip = "127.0.0.1"; //ip
int port = atoi("8899"); //端口
/*创建一个IPv4 socket地址,主要设置ip和端口信息*/
struct sockaddr_in address;
bzero(&address, sizeof(address)); //清空地址
address.sin_family = AF_INET; //ipv4
inet_pton(AF_INET, ip, &address.sin_addr); //ip转换
address.sin_port = htons(port); //端口
//创建socket,ipv4,字符流(tcp)
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(listen_fd >= 0);
//命名Socket / 绑定Sock
int ret = bind(listen_fd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
int backlog = 5; //监听队列长度
//监听Socket
ret = listen(listen_fd, backlog);
assert(ret != -1);
//----------------------------------------------------------------
//接受信息,select复用
char buffer[BUF_SIZE];
//epoll监听多个客户端
int epoll_fd = epoll_create(200);
int max_link = 6;
//内核会把已经就绪的事件副本传递给epoll_events,因此每次的epoll_events都会不同
struct epoll_event epoll_events[max_link];
//监听连接事件
struct epoll_event ev;
ev.data.fd = listen_fd;
ev.events = EPOLLIN;
//加入事件
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
while (true)
{
cout << "进入循环" << endl;
memset(buffer, 0, BUF_SIZE);
//epoll多路复用-1代表阻塞等待事件到来,-1阻塞
//内核会把已经就绪的事件副本传递给epoll_events
ret = epoll_wait(epoll_fd, epoll_events, max_link, -1);
if (ret < 0)
{
std::cout << "epoll多路复用失败" << std::endl;
break;
}
//只返回已有事件的socket
for (int i = 0; i < ret; i++)
{
//服务器监听到事件,接受连接,必有事件
if (epoll_events[i].data.fd == listen_fd)
{
//客户端tcp连接描述,等待符合格式的连接
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client, &client_addrlength);
struct epoll_event conn;
conn.data.fd = conn_fd;
conn.events = EPOLLIN;
//增加新的监听
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &conn);
cout << "客户端连接成功,conn_fd:" << conn_fd << endl;
}
//其余事件,遍历扫描
else if (epoll_events[i].events & EPOLLIN)
{
ret = recv(epoll_events[i].data.fd, buffer, sizeof(buffer) - 1, 0);
if (ret < 0)
{
std::cout << "读取普通数据失败,错误码:" << ret << std::endl;
break;
}
std::cout << "读取数据:" << buffer << " ret:" << ret << std::endl;
//不再监听--此处可以监听关闭socket事件,这里简化
close(epoll_events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, &epoll_events[i]);
}
}
}
//关闭Socket
close(listen_fd);
return 0;
}
内容来源
- <<linux高性能服务器编程>>.游双
- 罗培羽:如果这篇文章说不清epoll的本质,那就过来掐死我吧! (3)