当从一个文件描述符进行读写操作时,accept、read、write这些函数会阻塞I/O。在这种会阻塞I/O的操作好处是不会占用cpu宝贵的时间片,但是如果需要对多个描述符操作时,阻塞会使同一时刻只能处理一个操作,从而使程序的执行效率大大降低。一种解决办法是使用多线程或多进程操作,但是这浪费大量的资源。另一种解决办法是采用非阻塞、忙轮询,这种办法提高了程序的执行效率,缺点是需要占用更多的cpu和系统资源。所以,最终的解决办法是采用IO多路转接技术。
IO多路转接是先构造一个关于文件描述符的列表,将要监听的描述符添加到这个列表中。然后调用一个阻塞函数用来监听这个表中的文件描述符,直到这个表中有描述符要进行IO操作时,这个函数返回给进程有哪些描述符要进行操作。从而使一个进程能完成对多个描述符的操作。而函数对描述符的检测操作都是由系统内核完成的。
linux下常用的IO转接技术有:select、poll和epoll。
select:
头文件:#include <sys/select.h>、#include <sys/time.h>、#include <sys/types.h>、#include <unistd.h>
函数:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:要检测的文件描述符中最大的fd+1,nfds最大值为1024。select最多只能检测1024个文件描述符。
readfds:读集合。读缓冲区中有数据时,readfds写入数据。fd_set文件描述符集类型,具体实现见下面。
writefds:写集合。通常设为NULL。
exceptfds:异常集合。通常设为NULL。
timeout:设置超时返回。为NULL时只有检测到fd变化时返回。struct timeval a; a.tv_sec=10; a.tv_usec=0;
返回值:成功返回要操作的描述符个数,超时返回0,失败返回-1。
select最多只能检测1024个文件描述符,是由于fd_set在内核代码中的设置所限制
1 //部分fd_set的内核代码
2
3 #define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
4 #define __FD_SETSIZE 1024
5 #define __NFDBITS (8 * sizeof(unsigned long))
6 typedef __kernel_fd_set fd_set;
7 typedef struct {
8 unsigned long fds_bits [__FDSET_LONGS];
9 } __kernel_fd_set;
void FD_CLR(int fd, fd_set *set); 从set集合中删除文件描述符fd。
int FD_ISSET(int fd, fd_set *set); 判断文件描述符fd是否在set集合中。
void FD_SET(int fd, fd_set *set); 将fd添加到set集合中。
void FD_ZERO(fd_set *set); 清空set集合。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <sys/socket.h>
5 #include <arpa/inet.h>
6 #include <string.h>
7 #include <unistd.h>
8 #include <sys/select.h>
9 #include <sys/time.h>
10 #include <stdlib.h>
11 int main()
12 {
13 int fd=socket(AF_INET,SOCK_STREAM,0);
14 struct sockaddr_in serv;
15 memset(&serv,0,sizeof(serv));
16 serv.sin_addr.s_addr=htonl(INADDR_ANY);
17 serv.sin_port=htons(8888);
18 serv.sin_family=AF_INET;
19 bind(fd,(struct sockaddr*)&serv,sizeof(serv));
20
21 listen(fd,20);
22
23 struct sockaddr_in client;
24 socklen_t cli_len=sizeof(client);
25 int maxfd=fd;
26 fd_set reads, temp;
27 FD_ZERO(&reads);
28 FD_SET(fd,&reads);
29 while(1)
30 {
31 temp=reads;
32 int ret=select(maxfd+1,&temp,NULL,NULL,NULL);
33 if(-1==ret)
34 {
35 perror("select error");
36 exit(1);
37 }
38 //客户端发起连接
39 if(FD_ISSET(fd,&temp))
40 {
41 //接受连接
42 int cfd=accept(fd,(struct sockaddr*)&client,&cli_len);
43 if(cfd==-1)
44 {
45 perror("accept error");
46 exit(1);
47 }
48 FD_SET(cfd,&reads);
49 //更新最大文件描述符
50 maxfd=maxfd<cfd?cfd:maxfd;
51
52 }
53 for(int i=fd+1;i<=maxfd;++i)
54 {
55 if(FD_ISSET(i,&temp))
56 {
57 char buf[1024]={0};
58 int len=recv(i,buf,sizeof(buf),0);
59 if(len==-1)
60 {
61 perror("recv error");
62 exit(1);
63
64 }
65 else if(len==0)
66 {
67 printf("客户端断开连接\n");
68 close(i);
69
70 FD_CLR(i,&reads);
71 }
72 else
73 {
74 printf("recv buf: %s\n",buf);
75 send(i,buf,strlen(buf)+1,0);
76 }
77 }
78 }
79 }
80 close(fd);
81 return 0;
82 }
poll:
头文件:#include <poll.h>
函数:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:数组地址。内核检测fds中的文件描述符。
nfds:数组的最大长度,数组中最后有效元素的下标+1。
timeout:超时返回,-1永久阻塞,0不阻塞调用后立即返回,>0等待的时长,单位毫秒。
返回值:成功返回要操作的个数,失败返回-1。
struct pollfd {
int fd; /*文件描述符*/
short events; /*等待的事件*/
short revents; /*实际发生的事件,内核给的反馈*/
}
pollfd常用事件:读事件,POLLIN;写事件,POLLOUT;错误事件,POLLERR(不能作为events的值);
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <string.h>
6 #include <sys/socket.h>
7 #include <arpa/inet.h>
8 #include <ctype.h>
9 #include <poll.h>
10
11 #define SERV_PORT 8989
12
13 int main(int argc, const char* argv[])
14 {
15 int lfd, cfd;
16 struct sockaddr_in serv_addr, clien_addr;
17 int serv_len, clien_len;
18
19 // 创建套接字
20 lfd = socket(AF_INET, SOCK_STREAM, 0);
21 // 初始化服务器 sockaddr_in
22 memset(&serv_addr, 0, sizeof(serv_addr));
23 serv_addr.sin_family = AF_INET; // 地址族
24 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
25 serv_addr.sin_port = htons(SERV_PORT); // 设置端口
26 serv_len = sizeof(serv_addr);
27 // 绑定IP和端口
28 bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
29
30 // 设置同时监听的最大个数
31 listen(lfd, 36);
32 printf("Start accept ......\n");
33
34 // poll结构体
35 struct pollfd allfd[1024];
36 int max_index = 0;
37 // init
38 for(int i=0; i<1024; ++i)
39 {
40 allfd[i].fd = -1;
41 }
42 allfd[0].fd = lfd;
43 allfd[0].events = POLLIN;
44
45 while(1)
46 {
47 int i = 0;
48 int ret = poll(allfd, max_index+1, -1);
49 if(ret == -1)
50 {
51 perror("poll error");
52 exit(1);
53 }
54
55 // 判断是否有连接请求
56 if(allfd[0].revents & POLLIN)
57 {
58 clien_len = sizeof(clien_addr);
59 // 接受连接请求
60 int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
61 printf("============\n");
62
63 // cfd添加到poll数组
64 for(i=0; i<1024; ++i)
65 {
66 if(allfd[i].fd == -1)
67 {
68 allfd[i].fd = cfd;
69 break;
70 }
71 }
72 // 更新最后一个元素的下标
73 max_index = max_index < i ? i : max_index;
74 }
75
76 // 遍历数组
77 for(i=1; i<=max_index; ++i)
78 {
79 int fd = allfd[i].fd;
80 if(fd == -1)
81 {
82 continue;
83 }
84 if(allfd[i].revents & POLLIN)
85 {
86 // 接受数据
87 char buf[1024] = {0};
88 int len = recv(fd, buf, sizeof(buf), 0);
89 if(len == -1)
90 {
91 perror("recv error");
92 exit(1);
93 }
94 else if(len == 0)
95 {
96 allfd[i].fd = -1;
97 close(fd);
98 printf("客户端已经主动断开连接。。。\n");
99 }
100 else
101 {
102 printf("recv buf = %s\n", buf);
103 for(int k=0; k<len; ++k)
104 {
105 buf[k] = toupper(buf[k]);
106 }
107 printf("buf toupper: %s\n", buf);
108 send(fd, buf, strlen(buf)+1, 0);
109 }
110
111 }
112
113 }
114 }
115
116 close(lfd);
117 return 0;
118 }
select和poll虽然没有前面几种方法的缺点,但是select和poll只返回个数,不会告诉进程具体是哪几个描述符要操作, 而且select和poll最多只能检测1024个。select每次调用时,都需要把fd集合从用户态和内核态之间相互拷贝,这在fd很多时会消耗大量资源。
epoll检测的个数没有限制,它在内部构造维护了红黑树,减少了资源的消耗。
epoll:
头文件:#include <sys/epoll.h>
函数:
int epoll_create(int size); 生成epoll专用的文件描述符,size:epoll上能关注的最大描述符个数。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create生成的文件描述符。
op:选项,EPOLL_CTL_ADD 注册,EPOLL_CTL_MOD 修改,EPOLL_CTL_DEL 删除。
fd:关联的文件描述符。
event:告诉内核要监听的事件
返回值:成功返回0,失败返回-1。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 等待IO事件发生,可以设置阻塞。
epfd:要检测的句柄。
events:回传待处理的数组。
maxevents:events的大小。
timeout:超时返回。-1永久阻塞;0立即返回;>0超时时间。
1 typedef union epoll_data {
2 void *ptr;
3 int fd;
4 uint32_t u32;
5 uint64_t u64;
6 } epoll_data_t;
7
8 struct epoll_event {
9 uint32_t events; /* Epoll events */
10 epoll_data_t data; /* User data variable */
11 };
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <string.h>
6 #include <sys/socket.h>
7 #include <arpa/inet.h>
8 #include <ctype.h>
9 #include <sys/epoll.h>
10
11
12 int main(int argc, const char* argv[])
13 {
14 if(argc < 2)
15 {
16 printf("eg: ./a.out port\n");
17 exit(1);
18 }
19 struct sockaddr_in serv_addr;
20 socklen_t serv_len = sizeof(serv_addr);
21 int port = atoi(argv[1]);
22
23 // 创建套接字
24 int lfd = socket(AF_INET, SOCK_STREAM, 0);
25 // 初始化服务器 sockaddr_in
26 memset(&serv_addr, 0, serv_len);
27 serv_addr.sin_family = AF_INET; // 地址族
28 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
29 serv_addr.sin_port = htons(port); // 设置端口
30 // 绑定IP和端口
31 bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
32
33 // 设置同时监听的最大个数
34 listen(lfd, 36);
35 printf("Start accept ......\n");
36
37 struct sockaddr_in client_addr;
38 socklen_t cli_len = sizeof(client_addr);
39
40 // 创建epoll树根节点
41 int epfd = epoll_create(2000);
42 // 初始化epoll树
43 struct epoll_event ev;
44 ev.events = EPOLLIN;
45 ev.data.fd = lfd;
46 epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
47
48 struct epoll_event all[2000];
49 while(1)
50 {
51 // 使用epoll通知内核fd 文件IO检测
52 int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
53
54 // 遍历all数组中的前ret个元素
55 for(int i=0; i<ret; ++i)
56 {
57 int fd = all[i].data.fd;
58 // 判断是否有新连接
59 if(fd == lfd)
60 {
61 // 接受连接请求
62 int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
63 if(cfd == -1)
64 {
65 perror("accept error");
66 exit(1);
67 }
68 // 将新得到的cfd挂到树上
69 struct epoll_event temp;
70 temp.events = EPOLLIN;
71 temp.data.fd = cfd;
72 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
73
74 // 打印客户端信息
75 char ip[64] = {0};
76 printf("New Client IP: %s, Port: %d\n",
77 inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
78 ntohs(client_addr.sin_port));
79
80 }
81 else
82 {
83 // 处理已经连接的客户端发送过来的数据
84 if(!all[i].events & EPOLLIN)
85 {
86 continue;
87 }
88
89 // 读数据
90 char buf[1024] = {0};
91 int len = recv(fd, buf, sizeof(buf), 0);
92 if(len == -1)
93 {
94 perror("recv error");
95 exit(1);
96 }
97 else if(len == 0)
98 {
99 printf("client disconnected ....\n");
100 // fd从epoll树上删除
101 ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
102 if(ret == -1)
103 {
104 perror("epoll_ctl - del error");
105 exit(1);
106 }
107 close(fd);
108
109 }
110 else
111 {
112 printf(" recv buf: %s\n", buf);
113 write(fd, buf, len);
114 }
115 }
116 }
117 }
118
119 close(lfd);
120 return 0;
121 }
epoll三种工作模式:
水平触发:epoll默认工作模式,只要fd对应的缓冲区有数据,epoll_wait就会返回。epoll_wait调用次数越多,系统开销越大。
边沿触发:fd默认是阻塞的,客户端发送一次数据epoll_wait就返回一次,不管数据是否读完。如果要读完数据,可以循环读取,但是recv会阻塞,解决方法是将fd设置为非阻塞。
边沿非阻塞触发:将fd设置为非阻塞(open下设置O_NONBLOCK,或者利用fcntl()函数)。效率最高,可以将缓冲区数据完全读完。