Linux下IO多路复用:Select
首先什么是IO多路复用?
可以解释为:单个线程或进程可以同时监测若干个文件描述符是否可以进行IO操作的能力;
这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了
与多进程和多线程技术相比,I/O 多路复用技术的最大优势是系统开销小,系统不必创建进程 / 线程,也不必维护这些进程 / 线程,从而大大减小了系统的开销。
在Linux下我们可以同通过Select,poll,epoll的方式实现IO的多路复用
- 关于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 | #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #define MSG "from client" int main( int argc, char **argv)<br>{ int fd; int rv = -1; char buf[64]; struct sockaddr_in cliaddr; socklen_t addrlen = sizeof ( struct sockaddr); fd=socket(AF_INET,SOCK_STREAM,0); bzero(&cliaddr, sizeof (cliaddr)); cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9099); cliaddr.sin_addr.s_addr = htonl(inet_network( "127.0.0.1" )); //may cause error here // inet_aton("127.0.0.1",&cliaddr.sin_addr); // fd = socket(AF_INET,SOCK_STREAM,0); <br> if (fd < 0) { printf ( "%s\n" , strerror ( errno )); } if (connect(fd,( struct sockaddr*)&cliaddr,addrlen)<0) //一般这里的addrlen应该写为sizeof(cliaddr) { printf ( "connect failure:%s\n" , strerror ( errno )); } while (1) { if (write(fd,MSG, strlen (MSG)) <0 ) { printf ( "write failure :%s\n" , strerror ( errno )); } bzero(&buf, sizeof (buf)); if (rv = read(fd,buf, sizeof (buf)) <0 ) { printf ( "read data failure :%s\n" , strerror ( errno )); close(fd); }<br> printf ( "%s\n" ,buf); sleep(1); } } |
这段代码的作用是不服向服务器端发送信息,发送一次循环一轮次sleep(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 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 | #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <arpa/inet.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <sys/select.h> #include <sys/types.h> #include <sys/times.h> #define PORT 9099 #define MESSAGE "huanni" int main( int argc, char *argv[]){ fd_set temp; fd_set read_set; int i = 0; int max_fd = -1; int rv = -1; int clilen; int listen_fd = -1; int client_fd = -1; char buf[64]; struct sockaddr_in cliaddr; struct sockaddr_in servaddr; listen_fd = socket(AF_INET,SOCK_STREAM,0); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (rv = bind(listen_fd,( struct sockaddr*)&servaddr, sizeof (servaddr)) <0)<br> { printf ( "%s\n" , strerror ( errno )); return -2; } listen(listen_fd,13); FD_ZERO(&read_set); FD_SET(listen_fd,&read_set); max_fd = listen_fd; while (1) { temp = read_set; rv = select(max_fd+1,&temp,NULL,NULL,NULL); if (rv <= 0 )<br> { printf ( "select error:%s\n" , strerror ( errno )); exit (-2); } if (FD_ISSET(listen_fd,&temp)) { client_fd = accept(listen_fd,( struct sockaddr *)&cliaddr,&clilen); if (client_fd <0) { printf ( "%s\n" , strerror ( errno )); continue ; } FD_SET(client_fd,&read_set); max_fd = client_fd > max_fd ? client_fd : max_fd; } for ( i=0;i<max_fd+1;i++) { if (FD_ISSET(i,&temp) && i!=listen_fd) { bzero(&buf, sizeof (buf)); rv = read(i,buf, sizeof (buf)); if (rv <0){ perror ( "read error\n" ); exit (1); } else if (rv == 0) { printf ( "client disconnected\n" ); FD_CLR(i,&read_set); close(i); break ; } printf ( "%s\n" ,buf); strncpy (buf,MESSAGE, sizeof (buf)); if (write(i,buf, sizeof (buf))<0)<br> { printf ( "write failure :%s\n" , strerror ( errno )); exit (1); } } } } close(listen_fd); } |
select的关键是内核检测这些文件描述符对应的读写缓冲区的状态:
读缓冲区:检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
写缓冲区:检测写缓冲区是否可以写 (有没有容量),如果有容量可以写,缓冲区对应的文件描述符就绪
读写异常:检测读写缓冲区是否有异常,如果有该缓冲区对应的文件描述符就绪
检测的其实只有两种:连接与通信的文件描述符,内核把写回传入的相应集合(fd_set)
1 2 3 4 5 6 7 8 | #include <sys/select.h> struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout); |
相关的一些操作函数:
1 2 3 4 5 6 7 8 | // 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0 void FD_CLR( int fd, fd_set *set); // 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1 int FD_ISSET( int fd, fd_set *set); // 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1 void FD_SET( int fd, fd_set *set); // 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符 void FD_ZERO(fd_set *set); |
值得注意的细节:
监听的文件有上限,为1024 ; (0-1023)
更新set;循环时候需要设置为max_fd+1;
因为select后内核返回的信息可能将set修改,所以用temp_set记录实际set的地址,但是在新fd连接时是写回实际set,
这里通过while循环的temp = read_set来同步;
- 为提高效率,我们可以把服务端口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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | #include <stdlib.h> #include <arpa/inet.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <sys/select.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <pthread.h> #define MESS "from server" typedef struct information{ int fd; int *maxfd; fd_set *rdset; }fd_info; <br> //线程连接函数 void * connection( void * arg) { fd_info* info = (fd_info*)arg; int cfd = accept(info->fd,NULL,NULL); if (cfd <0) { printf ( "%s\n" , strerror ( errno )); exit (-1); } FD_SET(cfd,info->rdset); *info -> maxfd = cfd > *info -> maxfd ? cfd : *info -> maxfd; }<br> //线程通信函数 void * communicate( void * arg) { int rv=0; char buf[64]; fd_info *info = (fd_info*)arg; // info->fd =arg->fd; // info->rdset = arg->rdset; bzero(&buf, sizeof (buf)); if ((rv = read(info->fd,buf, sizeof (buf))) < 0) { printf ( "%s\n" , strerror ( errno )); //break; return NULL; } else if (rv ==0) { FD_CLR(info->fd,info->rdset); close(info->fd); printf ( "disconnected\n" ); } else { printf ( "%s\n" ,buf); // strncpy(buf,MESS,strlen(MESS)); write(info->fd,MESS, strlen (MESS)); } }<br> int main( int argc, char **argv) { char buf[128]; pthread_t tid,pid; fd_info *connect_info =(fd_info*) malloc ( sizeof (fd_info)); fd_info *communicate_info =(fd_info*) malloc ( sizeof (fd_info)); int listen_fd = -1; int connect_fd =-1; int rv = -1; int max_fd = -1; socklen_t len =0; struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; bzero(&serveraddr, sizeof (serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(9099); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //inet_aton("INADDR_ANY",&serveraddr.sin_addr); //be cautious about "" here !!! fd_set read_set; fd_set temp_set; listen_fd = socket(AF_INET,SOCK_STREAM,0); bind(listen_fd,( struct sockaddr*)&serveraddr, sizeof (serveraddr)); FD_ZERO(&read_set); FD_SET(listen_fd,&read_set); max_fd = listen_fd; while (1) { temp_set = read_set; if (select(max_fd+1,&temp_set,NULL,NULL,NULL) <= 0) { printf ( "%s\n" , strerror ( errno )); break ; } if (FD_ISSET(listen_fd,&temp_set)) { connect_info->fd = listen_fd; connect_info->maxfd =&max_fd; connect_info->rdset =&read_set; //初始化信息 pthread_create(&tid,NULL,connection,connect_info); pthread_detach(tid); } for ( int i =0;i< max_fd+1;i++) //error occured here without the "+1"; { if (FD_ISSET(i,&temp_set) && i != listen_fd) //这里i实际上和文件描述符数值相等 { communicate_info->fd =i; communicate_info->rdset =&read_set; pthread_create(&pid,NULL,communicate,communicate_info); pthread_detach(tid); } } } close(listen_fd); } |
其中涉及到一些多线程的操作,需要注意的点是将信息保存在结构体指针中传进时需要提前分配内存,并初始化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!