内容
- 基本概念
- 了解接口socket
- 了解协议tcp/udp
- io复用
- select
- poll/epoll
可以参考学习的文章:
- 深入浅出理解select、poll、epoll的实现
- linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。
I/O复用
- select
- poll
- epoll
select
先观察其API
1 2
| #include<sys/select.h> int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
|
- 参数
nfds参数通常被设置为select监听的所有文件描述符中的最大值加1,表示在fd_set集合中,我们关心的描述符的总数。为什么加1呢?因为文件描述符是从0开始计数的。nfds与fd_set容量大小不一样,容量大小指的是FD_SETSIZE,即fd_set容量大小是fd_set可容纳描述符的最大大小。
- readfds参数是select关心的读事件的集合;
- writefds参数是select关心的写事件的集合;
- exceptfds参数select关心的异常事件的集合;
- timeout参数设置select的超时时间。
- 返回值
- 集合中有事件就绪的描述符的个数
- 但是并没有告诉你具体是哪一个描述符就绪
fd_set结构体
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<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; ------------------- #define FD_SETSIZE 1024 #define NFDBITS (8*(int)sizeof(long int)) typedef struct { long int fds_bits[FD_SETSIZE / NFDBITS]; }fd_set;
|
其中,__FD_SETSIZE指出select可以关注的最大文件描述符个数,默认为1024。
__fd_mask被定义为long int的类型别称,long int在32位机占8个字节。
__NFDBITS计算的是1个__fd_mask元素所占用的位数,一个字节占8位,sizeof算出__fd_mask的字节数,相乘得其占用的bit大小。
接着,定义fds_bits,其是long int型的数组,数组大小为__FD_SETSIZE除以__NFDBITS。比如SETSIZE为1024位,NFDBITS是64位,则数组大小位1024/64=16。这里的计算主要是为了计算出数组的大小,以确定多大的数组可以正好容纳1024个位数,来记录文件描述符信息。
用到的宏函数
fd_set集合对于文件描述符的管理是按位进行的,而位只有0和1两种状态。
假如SETSIZE=1024,则可管理1024个文件描述符,如果文件描述符7有效,我们需要对位操作,使其位变为1。
由于位操作过于繁琐,select API中提供了一系列宏函数来方便我们访问、操作fd_set集合状态。
1 2 3 4 5
| #include<sys/select.h> FD_ZERO(fd_set *fdset); FD_SET(int fd, fd_set *fdset); FD_CLR(int fd, fd_set *fdset); int FD_ISSET(int fd, fd_set *fdset);
|
select编程思路
最好另外定义一个整型数组,其大小为我们预测将要出现的描述符的最多数目。用作我们存放描述符的容器。初始化时将数组值一律设为-1,表示容器中该位置还没有存放描述符。如果在某一时刻有一个描述符有了消息,我们就将该描述符数值覆盖到这个容器中第一个为-1的地方。
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
| #define MAX 10 void fds_init(int fds[]) { for(int i = 0;i<MAX;++i) { fds[i] = -1; } } void fds_add(int fd, int fds[]) { if(fd<0) { printf("无效的描述符\n"); return; } for(int i = 0;i<MAX;++i) { if(fds[i]==-1) { fds[i] = fd; return; } } printf("容器已满,无法添加该描述符\n"); } void fds_del(int fd, int fds[]) { for(int i = 0;i<MAX;++i) { if(fds[i]==fd) { fds[i] = -1; return; } } printf("没有找到该描述符\n"); } int main() { int fds[MAX]; fds_init(fds); }
|
示例-TCP服务使用select处理多个套接字
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 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
| int main() { int sockfd = socket_init(); assert(sockfd!=-1); int fds[MAX]; fds_init(fds); fds_add(sockfd, fds); fd_set fdset; while(1) { FD_ZERO(&fdset); int maxfd = -1; for(int i = 0; i<MAX; ++i) { if(fds[i] == -1) { continue; } FD_SET(fds[i],fdset); if(maxfd<fds[i]) { maxfd = fds[i]; } } struct timeval tv = {5,0}; int n = select(maxfd+1, &fdset, NULL, NULL, &tv); if(n < 0) { printf("select err\n"); } else if(n == 0) { printf("time out\n"); } else { for(int i = 0; i<MAX; ++i) { if(fds[i] == -1)continue; if(FD_ISSET(fds[i], &fdset)) { if(fds[i] == sockfd) { struct sockaddr_in caddr; int len = sizeof(caddr); int c = accept(sockfd, (struct sockaddr*)&caddr, &len); if(c < 0) { continue; } printf("accept c = %d\n", c); fds_add(c, fds); } else { char buff[128] = {0}; int num = recv(fds[i], buff, 127, 0); if(num <= 0) { printf("client close\n"); close(fds[i]); fds_del(fds[i], fds); } else { printf("recv(c = %d) = %s\n",fds[i],buff); send(fds[i], "ok", 2, 0); } } } } } } }
|
场景情况:如果客户端与select服务端已建立连接,而客户端进程结束,select会一直阻塞、未感知吗?–不会。
因为客户端的进程结束,也算是一种读事件,相当于通知服务端该套接字连接结束了。那么服务端recv会返回0,达到关闭该套接字的条件,关闭后,别忘了在fds容器中删除掉该描述符。
如果忘记了close该套接字,且忘了fds_del该描述符,那么如果客户端结束进程,服务端就会一直打印"client close",因为select一直在探测此描述符有无读事件,若该套接字连接关闭,那么此描述符一直有读事件,recv返回0,由于没有fds_del,每次都会关注,所以每次都会打印"client close"。
poll
可以理解为加强版的select。
先观察其API
1 2
| #include<poll.h> int poll(struct pollfd* fds, nfds_t nfds, int timeout);
|
-
fds参数是一个pollfd结构类型的指针,可指向一段连续空间(数组),因此很灵活,大小可按需声明。它可以指定我们感兴趣的文件描述符上发生的可读、可写和异常等事件。定义如下
1 2 3 4 5 6
| struct pollfd { int fd; short events; short revents; }
|
- 其中,fd成员指定文件描述符。
- events成员告诉poll监听fd上的哪些事件类型,他可以是一系列事件类型的按位或。常见的事件类型有:
POLLIN(数据可读)、POLLOUT(数据可写)。
- revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。
poll编程
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 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
| void poll_fds_init(struct pollfd* fds) { for(int i = 0;i<MAX;++i) { fds[i].fd = -1; fds[i].events = 0; fds[i].revents = 0; } } void poll_fds_add(int fd, struct pollfd* fds) { for(int i = 0;i<MAX;++i) { if(fds[i].fd == -1) { fds[i].fd = fd; fds[i].events = POLLIN; fds[i].revents = 0; break; } } } void poll_fds_del(int fd, struct pollfd* fds) { for(int i = 0;i<MAX;++i) { if(fds[i].fd == fd) { fds[i].fd = -1; fds[i].events = 0; fds[i].revents = 0; break; } } } #define MAX 10 int main() { int sockfd = socket_init(); assert(sockfd != -1); struct pollfd poll_fds[MAX]; poll_fds_init(poll_fds); poll_fds_add(sockfd,poll_fds); while(1) { int n = poll(poll_fds,MAX,5000); if(n < 0)printf("poll error\n"); else if(n == 0)printf("time out \n"); else { for(int i = 0;i<MAX;++i) { if(poll_fds[i].fd == -1)continue; if(poll_fds[i].revents & POLLIN) { if(poll_fds[i].fd == sockfd) { struct sockaddr_in caddr; int len = sizeof(caddr); int c = accept(sockfd, (struct sockaddr*)&caddr, &len); if(c < 0) { continue; } printf("accept:%d\n", c); poll_fds_add(c, poll_fds); } else { char buff[128] = {0}; int num = recv(poll_fds[i].fd, buff, 127, 0); if(num <= 0) { close(poll_fds[i].fd); poll_fds_del(poll_fds[i].fd, poll_fds); printf("client close\n"); } else { printf("recv(%d):%s\n", poll_fds[i].fd, buff); send(poll_fds[i].fd, "ok", 2, 0); } }
} if(poll_fds[i].revents & POLLOUT) { } } } } }
|
与select的一处细节区别:
每次select除了监测fd_set有效描述符上有无事件,其次还将没有事件的描述符从fd_set移除(将该描述符对应在fd_set上的位进行置0操作)(这样就得每次select之前都要重新注册一遍我们关注的描述符(即用户和内核共同操作FD_SET、FD_CLR等)),然后下面过滤有事件的描述符时,只要找到fd_set集合哪个位是1状态即就找到了有事件产生的描述符。
而poll的用法是:用户只管注册events,实际上的有无事件由内核来进行对revents的填充,以此来更好地区别该描述符是否有事件产生。这样,就不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events了。我们只要把要关心的描述符的fd置成非-1,以及管理好要关心的哪些事件类型events即可。
与select相比的优点
- 可以监听的描述符的最大数目可以超过
1024个,大小按需自拟。
- 可以监听的事件类型数目变多、变细了,更强大了。
- 不用在每次
poll之前重新注册一遍我们关注的描述符的结构体里的events了
总结
select和poll的总体实现流程:
select用fd_set结构体(默认是1024个bit位的数组);poll用struct pollfd fds[MAX]结构体。
select和poll通常被循环调用。每调用一次,就拷贝一次结构体数组给内核。
linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。
- 内核轮询
内核对若干个文件描述符进行轮询扫描。查看之上是否有事件发生。
时间复杂度,为O(n)
- select/poll返回后
返回值仅仅告诉用户程序发生了事件的描述符个数,并未告诉具体哪个描述符。
用户程序还需要再次轮询一遍。O(n)
