理解io复用——阻塞与非阻塞
流:
- 可以进行I/O操作的内核对象
- 包括文件、管道、套接字等等
- 流的入口:
文件描述符(fd)
流是有方向的,read和write
如果当前还没有数据,去读的话就会阻塞在这.
比如你要洗袜子,需要肥皂,家里没有肥皂了。有个快递小哥送肥皂给你,在送到前是没有任何事可以做的,也就是阻塞状态.
我们也可以尝试非阻塞的方式,一直轮询.
阻塞vs非阻塞:
大部分情况下,阻塞等待都是更好的选择,因为不会消耗CPU资源,而且资源准备好了他自己就会继续执行的。
阻塞等待的缺点:
如果在等一个资源的话,阻塞等待一般是最好的选择。但是如果在同时要等很多个, 阻塞等待的办法就只能顺序着一个一个等待,这样效率其实是比较低的 这种情况下,非阻塞轮询,即每次都轮询所有的资源,如果有准备好了的,直接去处理
一般场景都会选择
阻塞等待,但由于它不能很好的处理多个并发的请求的问题,同一个阻塞,同一时刻只能处理一个流的阻塞监听,此时,我们会选择多路IO复用:
- 能够阻塞等待,不浪费CPU资源
- 能够同一时刻监听多个IO请求的状态
IO复用解决的问题
方法一:阻塞+多线程/多进程
方法一确实可以解决大量IO读写的请求,就让每个线程去各自处理一个IO请求嘛,而且也不浪费CPU资源,因为每个线程都是阻塞等待的,但是
缺点也很明显:
- 多线程或多线程会消耗资源,而且线程切换也需要较高成本
方法二:非阻塞+忙轮询
方法二可以解决大量IO读写的请求,但是很耗费CPU资源
// 非阻塞忙轮询的实现
while true{
for i in 流[]{ //一次性遍历所有的流
if i has 数据{
读 或者 其他处理
}
}
}
方法三:select
我们的
select会代替我们去管理所有的IO请求,如果当前有IO资源到了,select会通知我们当前有IO资源到了,但是仅仅是通知有无资源到了噢,不会通知哪个到了,哪些到了。
select资源有限,最多只能监听1024个IO状态简而言之:
调用select会阻塞在那,直到有资源好了才返回
while true{
select(流[]); //阻塞
// 如果有资源到了,就从阻塞返回,然后轮询所有的流
for i in 流[]{
if i has 数据{
读 或者 其他处理
}
}
}
方法四:epoll
epoll是比select更强大的工具,它不仅告诉我们有快递到了,还会告诉我们是哪些快递到了,所以我们就不用遍历所有流去看到底是哪些流准备好了。 而且,epoll能处理的IO请求比select大得多
while true{
可处理的流[] = epoll_wait(epoll_fd); //阻塞
for i in 可处理的流[]{
读 或者 其他处理
}
}
epoll的API及内部机制
1.创建epoll(epoll_create)
int epoll_create(int size); //size:内核监听的数目
// 返回一个epoll句柄
epoll_create(size)所做的就是在内核中创建一颗红黑树的根节点,返回给用户
2.控制epoll(epoll_ctl)
/**
* @1.param epfd 刚才创建好的那个根节点
* @2.param op op是一些可选参数
* EPOLL_CTL_ADD表示注册新的fd到epfd中
* EPOLL_CTL_MOD表示修改已经注册的fd的监听事件
* EPOLL_CTL_DEL表示epfd删除一个fd
* @3.param fd fd是需要监听的文件描述符
* @4.param event 告诉内核需要监听的事件
*
* @returns 成功返回0,失败返回-1,errno查看错误信息
*/
int epoll_ctl(int epfd,int op,int fd,strucct epoll_event *event);
struct epoll_event:
struct epoll_event{
__uint32_t events; //epoll事件
epoll_data_t data; //用户传递的数据
}
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}
实例:
struct epoll_event new_event;
new_event.events = EPOLLIN | EPOLLOUT; //可读可写事件
new_evnet.data.fd = 5; //event绑定文件描述符5
epoll_ctl(epfd,EPOLL_CLT_ADD,5,&new_event);//将文件描述符5作为可读可写事件添加到根节点中
3.等待epoll(epoll_wait)
/**
*
* @param epfd 用epoll_create所创建的epoll句柄
* @param event 从内核得到的事件集合(不是传入数据,而是用于得到触发的事件)
* @param maxevents 告知内核这个events有多大(不得>创建时的size)
* @param timeout 超时时间
* -1:永久阻塞
* 0:立即返回,非阻塞
* >0:指定微秒
*
* @returns 成功:返回就绪的文件描述符个数,时间到时返回0
* .失败:-1,errno查看错误
*/
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
实例:
struct epoll_event my_event[1000];int event_cnt = epoll_wait(epfd,my_event,1000,-1);
如果epoll_wait返回,会把所有的事件放在my_event中,同时,event_cnt中返回了事件的个数,所以我们就可以利用my_event数组和告知的数量event_cnt,去遍历my_event的[0,event_cnt-1],去处理这些.
epoll编程架构
// 创建epollint epfd = epfd_create(1000);// 将listen_fd添加进epoll中epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&listen_event);while (1){ // 阻塞等待epoll中的fd触发 int active_cnt = epoll_wait(epfd,events,1000,-1); // 遍历所有触发的事件 for (int i = 0;i < active_cnt;i++) { if (events[i].data.fd == listen.fd){//监听的fd触发了,所以就说明有新的客户端发来了请求,我们需要和他建立连接 accept //和这个客户端进行三次握手,创建这个连接 并将accept的fd加进epoll中 //把这个连接的fd加进epoll } else if (events[i].events & EPOLLIN) //如果是一个读事件 { 对此fd 进行读操作 } else if (events[i].events & EPOLLOUT)//如果是一个写事件 { 对此fd 进行写操作 } } }
下面是一段博客中看到的epoll常用框架:epoll使用详解(精髓) - Boblim - 博客园 (cnblogs.com)
1 for( ; ; ) 2 { 3 nfds = epoll_wait(epfd,events,20,500); 4 for(i=0;i<nfds;++i) 5 { 6 if(events[i].data.fd==listenfd) //有新的连接 7 { 8 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接 9 ev.data.fd=connfd;10 ev.events=EPOLLIN|EPOLLET;11 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中12 }13 else if( events[i].events&EPOLLIN ) //接收到数据,读socket14 {15 n = read(sockfd, line, MAXLINE)) < 0 //读16 ev.data.ptr = md; //md为自定义类型,添加数据17 ev.events=EPOLLOUT|EPOLLET;18 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓19 }20 else if(events[i].events&EPOLLOUT) //有数据待发送,写socket21 {22 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据23 sockfd = md->fd;24 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据25 ev.data.fd=sockfd;26 ev.events=EPOLLIN|EPOLLET;27 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据28 }29 else30 {31 //其他的处理32 }33 }34 }
下面是另一个博客中的epoll使用的示例代码epoll TCP服务器与客户端简明例子 - 简书 (jianshu.com)
socket_server.cpp:
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
using namespace std;
const int MAX_EPOLL_EVENTS = 1000;
const int MAX_MSG_LEN = 1024;
void setFdNonblock(int fd)
{
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}
void err_exit(const char *s){
printf("error: %s\n",s);
exit(0);
}
int create_socket(const char *ip, const int port_number)//创建一个监听端口
{
struct sockaddr_in server_addr = {0};
/* 设置ipv4模式 */
server_addr.sin_family = AF_INET; /* ipv4 */
/* 设置端口号 */
server_addr.sin_port = htons(port_number);
/* 设置主机地址 */
if(inet_pton(server_addr.sin_family, ip, &server_addr.sin_addr) == -1){
err_exit("inet_pton");
}
/* 建立socket */
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
err_exit("socket");
}
/* 设置复用模式 */
int reuse = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
err_exit("setsockopt");
}
/* 绑定端口 */
if(bind(sockfd, (sockaddr *)&server_addr, sizeof(server_addr)) == -1){
err_exit("bind");
}
/* 设置被动打开 */
if(listen(sockfd, 5) == -1){
err_exit("listen");
}
return sockfd;
}
int main(int argc, const char *argv[])
{
/* 帮助 */
if(argc < 3){
printf("usage:%s ip port\n", argv[0]);
exit(0);
}
/* 获取服务器参数 */
const char * ip = argv[1];
const int port = atoi(argv[2]);
/* 创建套接字 */
int sockfd = create_socket(ip, port);
printf("success create sockfd %d\n", sockfd);
setFdNonblock(sockfd);
/* 创建epoll */
int epollfd = epoll_create1(0);
if(epollfd == -1) err_exit("epoll_create1");
/* 添加sockfd到epollfd兴趣列表 */
struct epoll_event ev;
ev.data.fd = sockfd;
ev.events = EPOLLIN ;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1){
err_exit("epoll_ctl1");
}
/* 创建一个列表用于存放wait所返回的events */
struct epoll_event events[MAX_EPOLL_EVENTS] = {0};
/* 开始等待所有在epoll上挂上去的事件 */
while(1){
/* 等待事件 */
printf("begin wait\n");
int number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
printf("end wait\n");
sleep(1);
if(number > 0){
/* 遍历所有事件 */
for (int i = 0; i < number; i++)
{
int eventfd = events[i].data.fd;
/* 如果触发事件的fd是sockfd,则说明有人连接上来了,我们需要accept他 */
if(eventfd == sockfd){
printf("accept new client...\n");
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
setFdNonblock(connfd);
/* accept之后,需要将文件描述符加入到监听列表中 */
struct epoll_event ev;
ev.data.fd = connfd;
ev.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1){
err_exit("epoll_ctl2");
}
printf("accept new client end.\n");
}
/* 如果触发的fd不是sockfd,那就是新加的connfd */
else{
/* 读出内容,直到遇到回车。然后显示该内容。 */
printf("read start...\n");
while(1){
char buff = -1;
int ret = read(eventfd, &buff, 1);
if(ret > 0){
printf("%c", buff);
}
if(buff == '\n'){
break;
}
else if (ret == 0){
printf("client close.\n");
close(eventfd);
epoll_ctl(epollfd, EPOLL_CTL_DEL, eventfd, NULL);
break;
}
else if (ret < 0){
printf("read error.\n");
break;
}
}
printf("read end.\n");
}
}
}
}
}
可以看到,server所做的事情如下:
1.准备阶段:
int sockfd = create_socket(ip, port); //创建监听连接的文件描述符sockfdint epollfd = epoll_create1(0); //创建epoll// 将监听文件描述符的IN事件添加到epoll中 struct epoll_event ev; ev.data.fd = sockfd; ev.events = EPOLLIN ; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1){ err_exit("epoll_ctl1"); }/* 创建一个数组用于存放wait所返回的events */ struct epoll_event events[MAX_EPOLL_EVENTS] = {0};
2.主体循环while(1){ /* 等待事件 */ printf("begin wait\n"); int number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1); printf("end wait\n"); sleep(1); if(number > 0){ /* 遍历所有事件 */ for (int i = 0; i < number; i++) { int eventfd = events[i].data.fd; /* 如果触发事件的fd是sockfd,则说明有人连接上来了,我们需要accept他 */ if(eventfd == sockfd){ printf("accept new client...\n"); struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len); setFdNonblock(connfd); /* accept之后,需要将文件描述符加入到监听列表中 */ struct epoll_event ev; ev.data.fd = connfd; ev.events = EPOLLIN; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1){ err_exit("epoll_ctl2"); } printf("accept new client end.\n"); } /* 如果触发的fd不是sockfd,那就是新加的connfd */ else{ /* 读出内容,直到遇到回车。然后显示该内容。 */ printf("read start...\n"); while(1){ char buff = -1; int ret = read(eventfd, &buff, 1); if(ret > 0){ printf("%c", buff); } if(buff == '\n'){ break; } else if (ret == 0){ printf("client close.\n"); close(eventfd); epoll_ctl(epollfd, EPOLL_CTL_DEL, eventfd, NULL); break; } else if (ret < 0){ printf("read error.\n"); break; } } printf("read end.\n"); } } }
socket_client.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string>
#include <iostream>
using namespace std;
void err_exit(const char *s){
printf("error: %s\n",s);
exit(0);
}
int create_socket(const char *ip, const int port_number)
{
struct sockaddr_in server_addr = {0};
/* 设置ipv4模式 */
server_addr.sin_family = AF_INET; /* ipv4 */
/* 设置端口号 */
server_addr.sin_port = htons(port_number);
/* 设置主机地址 */
if(inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1){
err_exit("inet_pton");
}
/* 建立socket */
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
err_exit("socket");
}
if(connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1){
err_exit("connect");
}
return sockfd;
}
int main(int argc, const char *argv[]){
if(argc < 3){
printf("usage:%s ip port\n", argv[0]);
exit(0);
}
/* 获取服务器参数 */
const char * ip = argv[1];
const int port = atoi(argv[2]);
//创建套接字
int sock = create_socket(ip, port);
//向服务器(特定的IP和端口)发起请求
while(1){
string buff;
getline(cin, buff);
if(buff == "exit") break;
write(sock, buff.c_str(), buff.size());
char end = '\n';
write(sock, &end, 1);
}
close(sock);
return 0;
}
g++ -Wall socket_server.cpp -o server && g++ -Wall socket_client.cpp -o client
./server localhost 1234
./client localhost 1234
在client端输入文字并回车,会出现在服务器端。按下Ctrl C关闭客户端或者输入exit关闭客户端。
服务器首先创建被动打开socket文件描述符,然后将该文件描述符加入到epoll兴趣列表。接下来进入循环。每当兴趣列表wait结束时,说明对应文件描述符可以进行操作。当有客户端连上被动打开socket文件描述符时,说明有客户端连上,被动打开文件描述符可以被accept。accept后所创建的新的文件描述符是与客户端通信的文件描述符,该文件描述符继续加入兴趣列表。当客户端发送数据时,该文件描述符也会产生可读信号,会导致wait结束,此时进入处理模式,读取并显示客户端所发送的数据。
epoll水平触发与边缘触发
水平触发
水平触发:第一次
epoll_wait的时候,A、B事件都触发了,那么我们拿到A、B事件后,对他进行处理。假如说A事件处理时,我们只读了一半,还没处理完,还有一半是留在了event_A中。那么如果是水平触发,下一次还会把event_A返回,直到真正处理完了event_A或者删掉event_A。所以这就保证了处理这个事件的完整性和安全性。
缺点:如果用户没有处理,每次都会返回这个
event_A,这个涉及syscall系统调用,是很耗费性能的.
边缘触发
边缘触发:第一次
epoll_wait的时候,A、B事件都触发了,那么我们拿到A、B事件。边缘触发是不管你有没有真的去处理,哪怕你一个字节也没去读,他也会认为你是处理完了的,那么下次就不会触发这个同一个A、B事件了。优点:
性能好缺点:
可能没有完整的处理这个事件
他们的区别就类似于TCP和UDP,TCP发了这个数据后,还有重传机制,UDP是发了就不管了
epoll代码实例
下面是刘丹冰大佬所给的一个例子
(1)服务端
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define SERVER_PORT (7778)
#define EPOLL_MAX_NUM (2048)
#define BUFFER_MAX_LEN (4096)
char buffer[BUFFER_MAX_LEN];
void str_toupper(char *str) //整个字符串变成大写
{
int i;
for (i = 0; i < strlen(str); i ++) {
str[i] = toupper(str[i]);
}
}
int main(int argc, char **argv)
{
int listen_fd = 0;
int client_fd = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_len;
int epfd = 0;
struct epoll_event event, *my_events;
// socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0); //创建一个socket
// bind
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); //将listen_fd与端口绑定
// listen
listen(listen_fd, 10); //将这个socket描述符设置为一个监听描述符
// epoll create
epfd = epoll_create(EPOLL_MAX_NUM);
if (epfd < 0) {
perror("epoll create");
goto END;
}
// listen_fd -> epoll
event.events = EPOLLIN; //读事件
event.data.fd = listen_fd; //event的文件绑定到listen_fd
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event) < 0) { //listen_fd的读事件加入到epoll
perror("epoll ctl add listen_fd ");
goto END;
}
my_events = malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM); //创建一个event[]数组
while (1) {
// epoll wait
int active_fds_cnt = epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1);
int i = 0;
for (i = 0; i < active_fds_cnt; i++) {
// if fd == listen_fd
if (my_events[i].data.fd == listen_fd) {
//accept
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
char ip[20];
printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);
}
else if (my_events[i].events & EPOLLIN) {
printf("EPOLLIN\n");
client_fd = my_events[i].data.fd;
// do read
buffer[0] = '\0';
int n = read(client_fd, buffer, 5);
if (n < 0) {
perror("read");
continue;
}
else if (n == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);
close(client_fd);
}
else {
printf("[read]: %s\n", buffer);
buffer[n] = '\0';
#if 1
str_toupper(buffer);
write(client_fd, buffer, strlen(buffer));
printf("[write]: %s\n", buffer);
memset(buffer, 0, BUFFER_MAX_LEN);
#endif
/*
event.events = EPOLLOUT;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
*/
}
}
else if (my_events[i].events & EPOLLOUT) {
printf("EPOLLOUT\n");
/*
client_fd = my_events[i].data.fd;
str_toupper(buffer);
write(client_fd, buffer, strlen(buffer));
printf("[write]: %s\n", buffer);
memset(buffer, 0, BUFFER_MAX_LEN);
event.events = EPOLLIN;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
*/
}
}
}
END:
close(epfd);
close(listen_fd);
return 0;
}
(2)客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_LINE (1024)
#define SERVER_PORT (7780)
void setnoblocking(int fd)
{
int opts = 0;
opts = fcntl(fd, F_GETFL);
opts = opts | O_NONBLOCK;
fcntl(fd, F_SETFL);
}
int main(int argc, char **argv)
{
int sockfd;
char recvline[MAX_LINE + 1] = {0};
struct sockaddr_in server_addr;
if (argc != 2) {
fprintf(stderr, "usage ./client <SERVER_IP>\n");
exit(0);
}
// 创建socket
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
fprintf(stderr, "socket error");
exit(0);
}
// server addr 赋值
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
fprintf(stderr, "inet_pton error for %s", argv[1]);
exit(0);
}
// 链接服务端
if (connect(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
perror("connect");
fprintf(stderr, "connect error\n");
exit(0);
}
setnoblocking(sockfd);
char input[100];
int n = 0;
int count = 0;
// 不断的从标准输入字符串
while (fgets(input, 100, stdin) != NULL)
{
printf("[send] %s\n", input);
n = 0;
// 把输入的字符串发送 到 服务器中去
n = send(sockfd, input, strlen(input), 0);
if (n < 0) {
perror("send");
}
n = 0;
count = 0;
// 读取 服务器返回的数据
while (1)
{
n = read(sockfd, recvline + count, MAX_LINE);
if (n == MAX_LINE)
{
count += n;
continue;
}
else if (n < 0){
perror("recv");
break;
}
else {
count += n;
recvline[count] = '\0';
printf("[recv] %s\n", recvline);
break;
}
}
}
return 0;
}
结果: