IO多路复用
# IO多路复用
# 1、BIO 和 NIO 模型
(31条消息) 从IO模型到协程(二) BIO模型和NIO模型_张柏沛的博客-CSDN博客 (opens new window)
# 1.1 BIO 模型
# 1.2 NIO 模型
# 2、I/O多路转接(复用)
- I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的 系统调用主要有
select
、poll
和epoll
。
select / poll
epoll
# 3、select()
主旨思想:
首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
调用一个系统函数(select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回。
a. 这个函数是阻塞
b. 函数对文件描述符的检测的操作是由内核完成的
在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
// sizeof(fd_set) = 128 128字节,一个字节8为,总共就是1024位
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 参数:
- nfds : 委托内核检测的最大文件描述符的值 + 1
- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数
- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
- exceptfds : 检测发生异常的文件描述符的集合
- timeout : 设置的超时时间
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- NULL : 永久阻塞,直到检测到了文件描述符有变化
- tv_sec = 0 tv_usec = 0, 不阻塞
- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
- 返回值 :
- -1 : 失败
- >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
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
# 3.1 select 工作过程分析
- ① 先将我们要检测的文件描述符对应的标志位置 1
- ② 调用
select
,select会将文件描述符列表拷贝到内核态(每次陷入要保存现场,一直在内核态就可以不用频繁地保存、恢复现场),根据我们设定的timeout
检测集合中有n
个文件描述符发生了变化,- 上图举例:如果读缓冲区内检测到有数据,那么给标志位仍为1,如果没数据,则置为0
- 补充:select 第一个参数为什么要加1?
- 是因为底层是
for(i=0,i<101+1,i++)
,所以要想检测到文件描述符101就必须+1
才行
- 是因为底层是
- ③ 检测完会将修改后的列表从内核态拷贝到用户态(即会改变原来我们设定的列表)
- ④ 以上我们select 就结束了,我们就知道了有几个文件描述符发生了改变,然后就要我们自己for循环去找对应标志位为1的文件描述符,并进行操作
- 注意:列表的前3位对应的是3个标志IO
# 3.2 代码示例
// select.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 创建一个fd_set的集合,存放的是需要检测的文件描述符
// 因为调用select后会改变原先我们设置的列表,所以我们再创建一个tmp 列表去给select函数调用
fd_set rdset, tmp;
// 先清空
FD_ZERO(&rdset);
// 将lfd 对应的标志位置位 1
FD_SET(lfd, &rdset);
int maxfd = lfd;
while (1)
{
// 每次循环,都重新设置tmp
tmp = rdset;
// 调用select系统函数,让内核帮检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if (ret == -1)
{
perror("select");
exit(-1);
}
// 其实是不会等于 0 的,因为我们select()里最后的参数设置为了 NULL,即 永久阻塞,直到检测到了文件描述符有变化
else if (ret == 0)
{
continue;
}
else if (ret > 0)
{
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if (FD_ISSET(lfd, &tmp))
{
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
// 打开一个服务器,监听的文件描述符肯定是在最前面,所以要从监听的文件描述符后面开始遍历
for (int i = lfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &tmp))
{
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if (len == -1)
{
perror("read");
exit(-1);
}
// 说明客户端断开了连接
else if (len == 0)
{
printf("client closed...\n");
// 关闭该文件描述符
close(i);
// fd_set中不再监测这个文件描述符
FD_CLR(i, &rdset);
}
else if (len > 0)
{
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("connect");
return -1;
}
int num = 0;
while (1)
{
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if (len == -1)
{
perror("read");
return -1;
}
else if (len > 0)
{
printf("read buf = %s\n", sendBuf);
}
else
{
printf("服务器已经断开连接...\n");
break;
}
sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 3.3 select 的缺点及与poll对比
- poll 可以自定义一个结构体数组
# 4、poll()
# 4.1、poll函数
#include <poll.h>
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
- timeout : 阻塞时长
-0 : 不阻塞
-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0 : 阻塞的时长
- 返回值:
-1 : 失败
>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// poll.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 初始化检测的文件描述符数组
struct pollfd fds[1024];
for (int i = 0; i < 1024; i++)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
while (1)
{
// 调用poll系统函数,让内核帮检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if (ret == -1)
{
perror("poll");
exit(-1);
}
else if (ret == 0)
{
continue;
}
else if (ret > 0)
{
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if (fds[0].revents & POLLIN)
{
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
for (int i = 1; i < 1024; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = cfd;
// 有可能之前的客户端已经断开连接了 但events修改过,所以要重新赋值
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
// 注意poll是按数组下标来遍历的
// 因为除开我们第一的第一个数组(即监听用的文件描述符),所以从1开始
// select 是直接按文件描述符来遍历的,所以是从lfd + 1 开始遍历
for (int i = 1; i <= nfds; i++)
{
// 因为 revents 中可能不止一个事件,所以要用 & 判断是否有 POLLIN 事件
if (fds[i].revents & POLLIN)
{
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if (len == -1)
{
perror("read");
exit(-1);
}
else if (len == 0)
{
printf("client closed...\n");
// 先close 关闭,再将 fds[i].fd 置为 -1
close(fds[i].fd);
fds[i].fd = -1;
}
else if (len > 0)
{
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 4.2、改进
# 4.2.1 改进一
- 更新最大文件描述符的索引语句是否应该修改?
- 老师的代码里,更新语句写的是
nfds = nfds > cfd ? nfds : cfd
,这样写之所以可以的原因是 最大的文件描述符总是大于等于它所存储的下标。 - 例如:因为文件描述符0~2是3个标准,然后那
lfd
(监听)就为3,这样的话,我们连接了一个客户端,那给它分配的文件描述符就应该是4
,但是我们放到我们自定义的数组里就是第0位,所以用老师的写法虽然可以,但是并不准确 - 这里应该改成
nfds = nfds>i ? nfds:i
和下标进行对比
- 老师的代码里,更新语句写的是
# 4.2.2 改进二
- 当数组满了的时候,又有客户端连接进来,但是accept 已经建立连接分配了文件描述符,但是数组已满,accept返回的文件描述符无法加入到 fds 中,那么下一次循环时该文件描述符的值(代码中的 cfd )已经被丢弃了。
- 解决:这里应该先去判断数组是否可用,有的话就accpet连接,没有就下一次在处理。
# 4.3、思考
# 4.3.1 思考一
- 文件描述符初始化为
-1
是说明这个文件描述符处于空闲状态吗?- 初始化
-1
是为了后续好根据这个值判断是否可用。
- 初始化
# 4.3.2 思考二
为什么是用
&
来检测事件?//假设: POLLIN 的二进制表示为 0000 0000 0000 0001 POLLOUT 的二进制表示为 0000 0000 0000 0010 revents 对应事件的二进制 1111 1111 1111 1111 //那要判断是否执行了 POLLIN 事件 ,就要 revents & POLLIN ==> 0... 0001 // 如果直接用 == 来判断就会出错
1
2
3
4
5
6
7
# 4.3.2 思考三
if(fds[0].revents & POLLIN)
和if(fds[0].revents == POLLIN)
不都是要判断是否为POLLIN
事件吗?作用不是一样吗?- 检测不仅仅检测
POLLIN
,万一也检测POLLOUT
呢,如果两个事件都发生了,那revents
中就包含POLLIN
和POLLOUT
,也就是值是POLLIN | POLLOUT
,用==
去和POLLIN
判断肯定不对。
- 检测不仅仅检测
# 5、epoll()
# 5.1 epoll 相关函数
#include <sys/epoll.h>
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
- 参数:
size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:
-1 : 失败
>0 : 文件描述符,操作epoll实例的
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN //有数据写入 (检测到有数据写入我们才可以读取,所以可以理解为检测读事件)
- EPOLLOUT //有数据要写
- EPOLLERR
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:
- epfd : epoll实例对应的文件描述符
- op : 要进行什么操作
EPOLL_CTL_ADD: 添加
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 删除
- fd : 要检测的文件描述符
- event : 检测文件描述符什么事情
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
- 0 : 不阻塞
- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
- >0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发送变化的文件描述符的个数 > 0
- 失败 -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
41
42
43
44
45
46
47
48
49
# 5.2 epoll基础案例
// epoll.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
// 返回值是一个操作 epoll 示例的文件描述符
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN; // 监听写入事件
epev.data.fd = lfd;
// 将示例加入到创建的 epoll 结构中,进行管理
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
// 设定就绪列表的大小
struct epoll_event epevs[1024];
while (1)
{
// 检测函数,返回发送变化的文件描述符的个数,-1 为不阻塞
int ret = epoll_wait(epfd, epevs, 1024, -1);
if (ret == -1)
{
perror("epoll_wait");
exit(-1);
}
// 输出检测到的发生变化的个数
printf("ret = %d\n", ret);
for (int i = 0; i < ret; i++)
{
int curfd = epevs[i].data.fd;
// 监听文件描述符有被写入,说明有客户端连接进来
if (curfd == lfd)
{
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// epev 是可以重用的,所以直接使用修改后重新添加到 epoll里就行
epev.events = EPOLLIN;
//下面写法可以增加要检测的事件,但是这样写记得对不同事件都要进行处理
// epev.events = EPOLLIN | EPOLLOUT;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else
{
// 注意: 如果我们在设置监听事件时,监听了几个事件,那我们在处理时,必须对不同事件都就行处理
if (epevs[i].events & EPOLLOUT)
{
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if (len == -1)
{
perror("read");
exit(-1);
}
// 客户端断开
else if (len == 0)
{
printf("client closed...\n");
// 将用来与断开的客户端通信的文件描述符从epoll里删除,不再监听
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
// 再关闭文件描述符
close(curfd);
}
else if (len > 0)
{
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
// 关闭监听文件描述符
close(lfd);
// 使用epoll_create 创建出来的 epfd 也是文件描述符,需要关闭
close(epfd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("connect");
return -1;
}
int num = 0;
while (1)
{
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if (len == -1)
{
perror("read");
return -1;
}
else if (len > 0)
{
printf("read buf = %s\n", sendBuf);
}
else
{
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
// 这里 1000微秒是为了看出 epoll 检测到发生变化个数的效果
// 如果这里用 sleep 休眠时间太长,看起来就是一次只检测到一个
// 可能是电脑配置问题,老师视频用 1000可以看出效果,但是我用1000,直接就不输出检测到的发生变化的个数了,改成10000又太大,输出发现只看得到一个,所以我这里用了 5000
usleep(5000);
}
close(fd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
- 这里是打开了两个客户端,可以看到有时只有一个被写入,有时有两个同时被改变
# 5.3 补充
# 5.3.1 epev 重用
- 上面
epoll.c
文件中,struct epoll_event epev
定义的 epev 之所以能够重用的原因?- 当调用
epoll_ctl
函数将文件描述符添加到epoll
对象中时,epoll
会将epoll_event
结构体中的数据拷贝一份,存储在自己的内存空间中,并将这个拷贝的结构体作为一个节点插入到红黑树中。 - 这样做的好处是,当文件描述符上的事件发生时,
epoll
可以直接从自己的内存空间中获取相应的事件信息,而不需要每次都去访问用户空间中的epoll_event
结构体。这样可以提高效率,减少系统调用的次数。 - 值得注意的是,在将文件描述符从
epoll
对象中删除时,epoll
并不会自动释放之前拷贝的epoll_event
结构体,需要用户自己负责释放。
- 当调用
# 6、epoll的工作模式
# 6.1 LT 模式
# 6.1.1 LT 模式介绍
- LT 模式 (水平触发)
- 假设委托内核检测读事件 -> 检测fd的读缓冲区
- 读缓冲区有数据 - > epoll 检测到了会给用户通知
- a.用户不读数据,数据一直在缓冲区,
epoll
会一直通知 - b.用户只读了一部分数据,
epoll
会通知 - c.缓冲区的数据读完了,不通知
- a.用户不读数据,数据一直在缓冲区,
- 简单理解就是,只要缓冲区内有数据,就会发通知
LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这 种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操 作。如果你不作任何操作,内核还是会继续通知你的。
# 6.1.2 代码案例
// epoll_lt.c
// 和epoll基础案例中 epoll.c 的代码对比就只是将通信时的缓冲区buf大小改为5,以方便看效果
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while (1)
{
int ret = epoll_wait(epfd, epevs, 1024, -1);
if (ret == -1)
{
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for (int i = 0; i < ret; i++)
{
int curfd = epevs[i].data.fd;
if (curfd == lfd)
{
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else
{
if (epevs[i].events & EPOLLOUT)
{
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if (len == -1)
{
perror("read");
exit(-1);
}
else if (len == 0)
{
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if (len > 0)
{
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// client.c
// 将sprintf 固定的输入改为用 fgets 阻塞,键盘输入数据
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("connect");
return -1;
}
int num = 0;
while (1)
{
char sendBuf[1024] = {0};
// sprintf(sendBuf, "send data %d", num++);
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if (len == -1)
{
perror("read");
return -1;
}
else if (len > 0)
{
printf("read buf = %s\n", sendBuf);
}
else
{
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
第一个
ret = 1
是打开了一个客户端,监听的文件描述符的读缓冲区收到了客户端发来的信息(3次握手),所以一打开一个客户的就输出了ret = 1
注意:如果我们终止客户端,那服务器端还会再打印
ret = 1
,(应该是4次挥手,发送了要结束的信息,个人理解处理结束信息的是通信用的文件描述符,不然不服务器端不会执行len = 0
的里的操作)可以看出,我们只要读缓冲区中还有数据,epoll就会发送通知
(佛了,有时会出现
Connection reset by peer
的错误 )
# 6.2 ET 模式
# 6.2.1 ET 模式介绍
- 上面
epoll_lt.c
代码在设置epev.events
事件时,设置EPOLLET
即可设定为ET模式
// 下面设置只在有客户端连接,accpet分配的文件描述符时设置,不需要对 lfd 监听描述符设置
epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
2
- 可以看到
ET 模式
只会通知一次,所以要想将数据都输出出来就必须在接收到一次通知时,循环读取,知道读缓冲为空,但是阻塞情况下,读缓冲区为空,read会阻塞,所以为了解决这个问题,我们还需要将read
设置为非阻塞
# 6.2.2 代码案例
// epoll_et.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while (1)
{
int ret = epoll_wait(epfd, epevs, 1024, -1);
if (ret == -1)
{
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for (int i = 0; i < ret; i++)
{
int curfd = epevs[i].data.fd;
if (curfd == lfd)
{
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 设置cfd属性非阻塞
// 每次监听文件描述符被写入,就说明有客户端连接,accept分配文件描述符
// 所以在这里就将文件描述符设置为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else
{
if (epevs[i].events & EPOLLOUT)
{
continue;
}
// 循环读取出所有数据
char buf[5];
int len = 0;
while ((len = read(curfd, buf, sizeof(buf))) > 0)
{
// 打印数据
//这里用printf 或者 write都可以,printf就记得加\n,刷新缓冲区,write就可以让数据一行显示
printf("recv data : %s\n", buf);
// write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if (len == 0)
{
printf("client closed....\n");
}
else if (len == -1)
{
if (errno == EAGAIN)
{
printf("data over.....\n");
}
else
{
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("connect");
return -1;
}
int num = 0;
while (1)
{
char sendBuf[1024] = {0};
// sprintf(sendBuf, "send data %d", num++);
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if (len == -1)
{
perror("read");
return -1;
}
else if (len > 0)
{
printf("read buf = %s\n", sendBuf);
}
else
{
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
非阻塞的情况下,如果数据已经被读完了,然后再去
read
读数据,就会产生EAGAIN
,注意:这种情况只会在非阻塞情况下出现,因为阻塞情况下,数据读完了,read
就阻塞了(老师说的)这个代码bug蛮多的,而且一段时间正常一段时间又出bug,主要的bug还是在客户端主动结束后,服务器端会报错,打印
Connection reset by peer
的错误评论区:
- 当
read
函数在非阻塞模式下读取数据时,如果缓冲区中没有数据可读,返回值是 -1,并且errno
变量被设置为EAGAIN
或EWOULDBLOCK
,表示当前没有数据可读 - 新版本缓冲区没数据可读返回的是0,并且设置errno为EAGAIN
- 当
# 6.2.3 代码改进
epoll
检测到可写的时候有问题, 检测了是否可写事件,然而,可写是一直可写的,当一个文件描述符,可写并可读的时候,却被continue了。epevs[i].events & EPOLLOUT && (( epevs[i].events & EPOLLIN ) != 1)
1大概就这个意思,还没具体去测试这个上面这个对不对