Linux IO复用--select()和poll()
来源:互联网 发布:angular tooltips.js 编辑:程序博客网 时间:2024/05/17 08:21
《Linux高性能服务器编程》阅读笔记:
Linux系统中IO复用的系统调用有selece()、poll()和epoll()。IO复用使得程序可以同时监听多个文件描述符的就绪事件的发生,应用场景如:
(1) 服务端程序同时处理监听socket和连接socket
(2) 服务端要同时处理TCP请求和UDP请求
(3) 服务端要同时监听多个端口或者处理多种服务请求
(4) 客户端要同时处理多个socket
(5) 客户端程序要同时处理用户输入和网络连接
不过要清楚的一点是,IO复用虽然能同时监听多个文件描述符,但它是阻塞监听的,当有多个文件描述符同时就绪时,若不采取额外措施,程序只能按照顺序依次处理其中的每一个就绪事件。这实际上也是串行工作,要实现并发,只能使用多进程或多线程等编程手段。
1. select()系统调用
1.1 select()函数原型
select()系统的用途是,在超时时间内,监听用户感兴趣的文件描述符上的可读可写、异常事件的发生。其函数原型为:
#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);
(1) nfds指定被监听的文件描述符的总数,设置为监听的所有文件描述符中最大值加1,因为文件描述符是从0开始计数的。
(2) readfds、writefds和exceptfds参数分别指向可读、可写、异常事件对应的文件描述符集合。程序员通过这3个参数向该调用传入自己感兴趣的文件描述符。函数返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。原型如下:
#define XFD_SETSIZE 256#define FD_SETSIZE XFD_SETSIZEtypedef long fd_mask;#define NBBY 8#define NFDBITS (sizeof(fd_mask) * NBBY)#define howmany(x,y) (((x)+((y)-1))/(y))#if defined(BSD) && BSD < 198911typedef struct fd_set { fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)];} fd_set;#endif
fd_set结构体仅包含一个long型数组,根据推导可见该数组为
long fds_bits[8];
fds_bit共占据32字节,即256位。该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符由FD_SETSIZE指定,显然这限制了select()能同时处理的文件描述符的总数。该系统调用还提供了一些列宏方便程序员实现位操作:
void FD_CLR(int fd, fd_set *set); //清零set中的fd位 int FD_ISSET(int fd, fd_set *set); //测试set中的fd位是否被设置void FD_SET(int fd, fd_set *set); //设置fd中的fd位void FD_ZERO(fd_set *set); //清零set中所有位
(3) timeout参数设置函数的超时时间。它是一个timeval的普通(非const)指针,内核可以修改此参数以告诉应用程序函数阻塞等待了多久。不过内核返回的该值不能完全信任,比如调用失败时timeout的值是不确定的。timeval结构体定义如下:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */};
当tv_sec(秒)和tv_usec(微秒)都设置为0则select()立即返回,当timeout为NULL则select()将会一直阻塞直到某个文件描描述符就绪。
selece()执行成功时返回就绪的文件描述符的总数;若在超时时间内没有任何文件描述符就绪,select()将返回0;失败将返回-1并设置errno。若在select()阻塞等待期间程序收到信号,将立即返回-1并设置errno为EINTR。
1.2 socket文件描述符就绪条件
以Linux网络编程中,多路复用判断socket文件描述符可读的依据是:
(1) socket内核接收缓冲区中字节数>=其低水位标记SO_RCVLOWAT时,此时程序可以无阻塞地读该socket,返回读取到的字节数(>0)
(2) socket通信的对端关闭连接,此时对该socket的读操作将返回0表示对端关闭
(3) 监听socket上有新的连接请求
(4) socket上有未处理的错误,此时可以使用getsockopt()读取和清除该错误(使用SO_ERROR标记)
socket文件描述符可写:
(1) socket内核发送缓冲区的空闲区域大于或等于其低水位标记SO_SNDLOWAT,此时程序可以无阻塞的写该socket,返回写入的字节数(>0)
(2) socket的写操作被关闭(使用shotdown(fd, SHUT_WR))后再对socket写,会触发一个SIGPIPE信号
(3) socket使用非阻塞connect()连接成功或者失败(超时)之后,对于后者将会收到RST报文段,若收到RST报文段后继续往该socket写则会触发SIGPIPE信号
(4) socket上未处理的错误
socket文件描述符异常:
socket上接收到带外数据
1.3 利用select()同时收发普通数据和带外数据、对端关闭
#include <stdio.h>#include <stdlib.h>#include <arpa/inet.h>#include <libgen.h>#include <sys/types.h>#include <sys/socket.h>#include <string.h>#include <unistd.h>#include <signal.h>#include <sys/select.h>#define ERRP(con, ret, ...) do \{ \ if (con) \ { \ perror(__VA_ARGS__); \ ret; \ } \}while(0)#define BUFSIZE 1024static const char* ip = "192.168.239.136";static int port = 9660;int main(void){ int socket_fd = socket(AF_INET, SOCK_STREAM, 0); ERRP(socket_fd <= 0, return -1, "socket"); struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int ret = bind(socket_fd, (struct sockaddr* )&address, sizeof(address)); ERRP(ret < 0, goto ERR1, "connect"); ret = listen(socket_fd, 5); ERRP(ret < 0, goto ERR1, "listen"); struct sockaddr_in client; socklen_t client_addrlen = sizeof(client); printf("Wait guest...\n\n"); int connfd = accept(socket_fd, (struct sockaddr* )&client, &client_addrlen); ERRP(connfd < 0, goto ERR1, "accept"); printf("connect success: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port)); char buf[BUFSIZE] = {}; fd_set read_fds; fd_set exception_fds; FD_ZERO(&read_fds); FD_ZERO(&exception_fds); struct timeval timeout; unsigned int cnt = 0; while (1) { bzero(buf, BUFSIZE); //select()的参数在每次select()函数的返回会被内核修改,所以这里需要重新设置 FD_SET(connfd, &read_fds); //将connfd加入就绪读监听集合 FD_SET(connfd, &exception_fds); //将connfd加入异常监听集合 timeout.tv_usec = 0; //超时时间为4s timeout.tv_sec = 4; ret = select(connfd + 1, &read_fds, NULL, &exception_fds, &timeout); ERRP(ret < 0, goto ERR2, "select"); if (FD_ISSET(connfd, &read_fds)) { ret = recv(connfd, buf, sizeof(buf) - 1, 0); //recv返回0表示对端已经关闭 ERRP(ret < 0, goto ERR2, "recv normal data"); if (ret == 0) { printf("guest exit...\n"); break; } printf("get %d bytes of normal data: %s\n", ret, buf); } else if (FD_ISSET(connfd, &exception_fds)) { ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB); ERRP(ret < 0, goto ERR2, "recv OOB data"); if (ret == 0) { printf("guest exit...\n"); break; } printf("get %d bytes of OOB data: %s\n", ret, buf); } else if (ret == 0) //select()返回0表示超时返回 { printf("time out %d\n", ++cnt); } }ERR2: close(connfd);ERR1: close(socket_fd); return 0;}
2. poll()系统调用
2.1 poll()函数原型
poll()系统调用与select()类似,也是在指定的时间内轮询一定数量的文件描述符,监听其是否就绪。
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
(1) fds参数是一个pollfd结构体类型的指针,它指定所有程序员感兴趣的文件描述符上发生的可读、可写、异常等事件。既然它是一个结构体指针,就可以指向该类型的数组。
pollfd结构体的原型为:struct pollfd { int fd; /* 文件描述符 */ short events; /* 注册的事件 */ short revents; /* 实际发生的事件,由内核填充 */};
fd成员指定文件描述符。event成员告诉内核要监听fd上的哪些事件,它可以是一系列事件的按位或。revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。event的取值为:
上面事件选项中:
a. POLLRDNORM(普通数据可读)、POLLRDBAND(优先级带数据可读)和POLLWRNORM(普通数据可写)、POLLWRBAND(优先级带数据可写)将POLLIN(数据可读)和POLLOUT(数据可写)划分得更明显,以区分优先级带数据和普通数据,但是Linux并不完全支持。
b. 一般应用程序调用recv()时,要判断接收到的是有效数据还是对端关闭连接后触发的是根据recv()的返回值(如上面的select()示例程序),在poll()系统调用中,有更直接的方法,监听描述符的POLLRDHUP事件即监听对端关闭事件,不过需要在代码开始处定义”_GNU_SOURCE”
(2) fds数组成员的的个数由参数nfds指定(typedef unsigned long int nfds_t;)。显然,这个比select()的设计要灵活一点: 用户可以监测任意多数目文件描述符,但是poll()的实现也是依靠轮询的,从效率上来讲跟select()的实现是一致的。
(3) timeout参数指定函数的超时事件,单位为毫秒。当timeout为-1时,poll调用将一直阻塞直到监听的目标事件发生;当timeout为0时,poll()调用立即返回。
(4) poll()的返回值跟select()的返回值含义相同。
2.2 poll()同时收发普通数据和带外数据、对端关闭
利用poll()实现跟上述select()一样功能的代码: 监听连接关闭事件、接收普通数据事件和接收带外数据事件。
#define _GNU_SOURCE#include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/select.h> #include <sys/time.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <poll.h>#define ERRP(con, ret, ...) do \{ \ if (con) \ { \ perror(__VA_ARGS__); \ ret; \ } \}while(0)#define BUFSIZE 1024static const char* ip = "192.168.239.136";static int port = 9660;int main(void){ int socket_fd = socket(AF_INET, SOCK_STREAM, 0); ERRP(socket_fd <= 0, return -1, "socket"); struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int ret = bind(socket_fd, (struct sockaddr* )&address, sizeof(address)); ERRP(ret < 0, goto ERR1, "connect"); ret = listen(socket_fd, 5); ERRP(ret < 0, goto ERR1, "listen"); struct sockaddr_in client; socklen_t client_addrlen = sizeof(client); printf("Wait guest...\n\n"); int connfd = accept(socket_fd, (struct sockaddr* )&client, &client_addrlen); ERRP(connfd < 0, goto ERR1, "accept"); printf("connect success: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port)); char buf[BUFSIZE] = {}; unsigned int cnt = 0; struct pollfd fds; fds.fd = connfd; fds.events = (POLLIN | POLLRDHUP | POLLPRI); while (1) { fds.revents = 0; ret = poll(&fds, 1, 4000); ERRP(ret < 0, goto ERR2, "poll"); //注意先判断对端退出事件 if (fds.revents & POLLRDHUP) { //printf("exit\n"); break; } else if (fds.revents & POLLIN) { bzero(buf, BUFSIZE); ret = recv(connfd, buf, sizeof(buf) - 1, 0); ERRP(ret < 0, goto ERR2, "recv normal data"); printf("get %d bytes of normal data: %s\n", ret, buf); } else if (fds.revents & POLLPRI) { bzero(buf, BUFSIZE); ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB); ERRP(ret < 0, goto ERR2, "recv OOB data"); printf("get %d bytes of OOB data: %s\n", ret, buf); //printf ("fds.revents = %d, POLLPRI | POLLRDHUP\n", fds.revents); } if (ret == 0) { printf ("Time out: %d\n", ++cnt); } }ERR2: close(connfd);ERR1: close(socket_fd); return 0;}
在代码测试阶段,发现客户端关闭连接时,服务端接收的事件不仅POLLRDHUP(值8192),还有POLLIN(值1),即8193。如上代码,先判断若接收到POLLRDHUP后服务端程序退出,再判断若接收到POLLIN则服务端就去读数据。若二者的判断顺序颠倒,因为revent会一直等于POLLRDHUP,那么判断POLLIN的执行分支代码将会一直得到执行。
下面是测试程序的客户端代码,适用于上述两个服务端测试程序:
#include <stdio.h>#include <stdlib.h>#include <arpa/inet.h>#include <libgen.h>#include <sys/types.h>#include <sys/socket.h>#include <string.h>#include <unistd.h>#include <signal.h>#define ERRP(con, ret, ...) do \{ \ if (con) \ { \ perror(__VA_ARGS__); \ ret; \ } \}while(0)static char isstop = 0;static void handler(int sig){ printf("handler, sig = %d\n", sig);}int main(int argc, char** argv){ if (argc <= 2) { printf("usage: %s ip_address port number backlog\n", basename(argv[0])); return -1; } signal(SIGPIPE, handler); const char* ip = argv[1]; int port = atoi(argv[2]); int backlog = 5; int socket_fd = socket(AF_INET, SOCK_STREAM, 0); ERRP(socket_fd <= 0, return -1, "socket"); struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int ret = connect(socket_fd, (struct sockaddr* )&address, sizeof(address)); ERRP(ret < 0, return -1, "connect"); const char* oob_data = "h"; const char* normal_data = "1234"; printf("Enter to send normal_data\n"); getchar(); send(socket_fd, normal_data, strlen(normal_data), 0); printf("Enter to send normal_data\n"); getchar(); send(socket_fd, normal_data, strlen(normal_data), 0); printf("Enter to send oob_data\n"); getchar(); send(socket_fd, oob_data, strlen(oob_data), MSG_OOB); printf("Enter to send normal_data\n"); getchar(); send(socket_fd, normal_data, strlen(normal_data), 0); printf("Enter to send normal_data\n"); getchar(); send(socket_fd, normal_data, strlen(normal_data), 0); printf("Enter to send oob_data\n"); getchar(); send(socket_fd, oob_data, strlen(oob_data), MSG_OOB); printf("Enter to close connect..."); getchar(); close(socket_fd); getchar(); return 0;}
- Linux IO复用--select()和poll()
- IO复用: select 和poll 到epoll
- IO复用: select 和poll 到epoll
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- Linux下select, poll和epoll IO模型的详解
- JavaSE——反射笔记
- Unity3D 2017 Mac 编译 Android
- 访问类型
- El和JSTL
- 领先的H5、WebGL和WebVR作品分享平台,以虚拟魔力升华真实体验。
- Linux IO复用--select()和poll()
- js导出文件保存在本地
- spark-streaming
- 【Person Re-ID】In Defense of the Triplet Loss for Person Re-Identification
- CSS基础(7.初识margin)
- 5种语言的callback实现
- 字符串的交错组成 动态规划
- 为什么使用redis
- Gilde加载网络图片到TextView的DrawableTop