I/O多路复用

I/O多路复用

1.什么是I/O多路复用

I/O 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
没有文件句柄就绪就会阻塞应用程序,交出CPU。

2.为什么会有I/O多路复用机制

没有IO多路复用机制时,有BIO、NIO两种实现方式,但它们都有一些问题

1.同步阻塞

服务端采用单线程,当 accept 一个请求后,在 recv 或 send 调用阻塞时,将无法 accept 其他请求(必须等上一个请求处理 recv 或 send 完 )(无法处理并发)

服务端采用多线程,当 accept 一个请求后,开启线程进行 recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写实际的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费。

2.异步阻塞

服务器端当 accept 一个请求后,加入 fds 集合,每次轮询一遍 fds 集合 recv (非阻塞)数据,没有数据则立即返回错误,每次轮询所有 fd (包括没有发生读写实际的 fd)会很浪费 CPU资源

3.实现I/O多路复用的几种方式

  • 1.select

  • 2.poll

  • 3.epoll

1.select实现I/O多路复用

1.1.涉及的api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/select.h>

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set exceptfds,struct timeval *timeout);
int pselect(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout,sigset_t sigmask);
//功能:阻塞监听多个文件描述符的变化(可被信号打断)

//参数
//nfds为监听的最大文件描述符+1
//readfds,writefds,exceptfds分别为监听可读,可写,异常集合
//timeval为超时时间

//返回值
//正常返回变化的文件描述符总个数,超时返回0,错误返回-1

FD_ZERO(fd_set *set);
//将集合清零

FD_SET(int fd,fd_set *set);
//将文件描述符添加到集合

FD_CLR(int fd,fd_set *set);
//将监听的文件描述符从集合中移除
1.2.操作实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <arpa/inet.h>
int main(int argc, char** argv)
{
int fd = New_Socket(argv[1], argv[2], 10);
if (fd == -1) {
printf("Create Socket error!\n");
exit(EXIT_FAILURE);
}
fd_set fdset, rset;
//创建监听集合
FD_ZERO(&fdset);
//将需要监听的文件描述符加入到监听集合
FD_SET(fd, &fdset);
.....
int maxfd = fd + 1;
struct timeval timeout = { 20, 20 };
while (1) {
rset = fdset;
int nfds = select(maxfd, &rset, NULL, NULL, &timeout);
//maxfd为监听的最大的文件描述符+1,maxfd为为轮询机制,每次都会轮询所有的fd看是否发生异常
// select函数返回值大于0代表有文件描述符有数据到来,返回值小于0代表发生了异常,返回值等于0代表超时
if (nfds < 0) {
perror("select");
exit(EXIT_FAILURE);
} else if (nfds == 0) {
printf("select timeout\n");
continue;
} else {
if (FD_ISSET(fd, &rset)) {
Handler(fd, &fdset);
}
}
}
}

2.使用poll实现I/O多路复用

2.1.涉及的api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <poll.h>

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
//fd为监听的文件描述符
//events为监听的事件
//revents为发生的事件

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
int ppoll(struct pollfd *fds,nfds_t nfds,cont struct timespec *tmo_p,const sigset_t *sigmask);
//功能:同时监听多个文件描述符,底层原理和select一样,只是将原有的fd_set改为了struct pollfd结构体

//参数
//fds,监听的pollfd的数组指针,nfds,监听的文件描述符的个数,timeout为超时时间,sigmask为屏蔽的信号集合

//返回值
//成功时返回一个正数(具有非零revents的结构体数量)
//错误时
// EFAULT 给出的参数不在可用的调用地址空间内.
// EINTR 被信号打断
// EINVAL nfds 值超过了 RLIMIT_NOFILE 值。
// EINVAL (ppoll()) The timeout value expressed in *ip is invalid (negative).
// ENOMEM 没有空间来分配文件描述符表
2.2.操作实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct pollfd
{
int fd;//需要监听的文件描述符
short event;//需要监听的事件
short revent;//已经发生的事件
}
#define MAX_FD 100
int main(int argc,char **argv)
{
struct pollfd fd[MAX_FD];
//设置监听事件和监听的文件描述符
fd[i].event = POLLIN
//设置完成后
int nfds = poll(struct pollfd fds[], nfds_t nfds, int timeout);
//比较revent看revent是否发生了变化,若发生了变化则文件描述符则该文件描述符有数据到来
for(int i = 0;i<MAX_FD,i++)
{

if(fd[i].revent & POLLIN)
.....
}
}

3.使用epoll实现I/O多路复用

3.1.涉及的api
1
2
3
4
5
6
7
8
9
10
int epoll_create(int size);
// 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中,返回一个ep对象,linux2.6.8开始忽略了size的意义

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//负责把 socket 增加、删除到内核红黑树
//参数:epfd为ep对象,op为操作类型,fd为监听的文件描述符

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event * events, int maxevents, int timeout,const sigset_t *sigmask);
//负责检测可读队列,没有可读 socket 则阻塞进程,epfd为ep对象,events,为发生存储已经发生事件的结构体数组,maxevents为最大可发生的事件的文件描述符的数量,timeout为超时时间
3.2.操作实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <sys/epoll.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int create_bind_listen(char *address,char *port,unsigned int backlog)
{
//......
}
int main(int argc,char **argv)
{
int fd = create_bind_listen(argv[1],argv[2],10);
int epfd = epoll_create(100);
struct epoll_event epoll_fd;
struct epoll_event event_epoll[10];
epoll_fd.fd = fd;
epoll_fd.events = EPOLLIN;
epoll_fd.data.fd = fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,&epoll_fd);
int event_count = epoll_wait(epfd,event_epoll,10);
for(int i = 0;i<event_count;i++)
{
//handler(event_poll[i].fd);
}
}

I/O多路复用
https://dreamaccount.github.io/2022/04/17/I-O多路复用/
作者
404NotFound
发布于
2022年4月17日
许可协议