服务器之家:专注于服务器技术及软件下载分享
分类导航

Linux|Centos|Ubuntu|系统进程|Fedora|注册表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服务器之家 - 服务器系统 - Linux - IO多路复用之select全面总结(必看篇)

IO多路复用之select全面总结(必看篇)

2021-12-13 20:59Linux教程网 Linux

下面小编就为大家带来一篇IO多路复用之select全面总结(必看篇)。小编觉得挺不错的。现在就分享给大家。也给大家做个参考。一起跟随小编过来看看吧

1、基本概念

io多路复用是指内核一旦发现进程指定的一个或者多个io条件准备读取,它就通知该进程。io多路复用适用如下场合:

(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用i/o复用。

(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

(3)如果一个tcp服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到i/o复用。

(4)如果一个服务器即要处理tcp,又要处理udp,一般要使用i/o复用。

(5)如果一个服务器要处理多个服务或多个协议,一般要使用i/o复用。

与多进程和多线程技术相比,i/o多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

2、select函数

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

返回值:就绪描述符的数目,超时返回0,出错返回-1

函数参数介绍如下:

(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。

因为文件描述符是从0开始的。

(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

void fd_zero(fd_set *fdset);           //清空集合

void fd_set(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

void fd_clr(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

int fd_isset(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写

(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

?
1
2
3
4
5
6
7
struct timeval{
 
      long tv_sec;  //seconds
 
      long tv_usec; //microseconds
 
};

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好i/o时才返回。为此,把该参数设置为空指针null。

(2)等待一段固定时间:在有一个描述字准备好i/o时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

 原理图:

IO多路复用之select全面总结(必看篇)

3、测试程序

写一个tcp回射程序,程序的功能是:客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。

服务端程序如下:

?
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <assert.h>
 
#define ipaddr   "127.0.0.1"
#define port    8787
#define maxline   1024
#define listenq   5
#define size    10
 
typedef struct server_context_st
{
  int cli_cnt;    /*客户端个数*/
  int clifds[size];  /*客户端的个数*/
  fd_set allfds;   /*句柄集合*/
  int maxfd;     /*句柄最大值*/
} server_context_st;
static server_context_st *s_srv_ctx = null;
/*===========================================================================
 * ==========================================================================*/
static int create_server_proc(const char* ip,int port)
{
  int fd;
  struct sockaddr_in servaddr;
  fd = socket(af_inet, sock_stream,0);
  if (fd == -1) {
    fprintf(stderr, "create socket fail,erron:%d,reason:%s\n",
        errno, strerror(errno));
    return -1;
  }
 
  /*一个端口释放后会等待两分钟之后才能再被使用,so_reuseaddr是让端口释放后立即就可以被再次使用。*/
  int reuse = 1;
  if (setsockopt(fd, sol_socket, so_reuseaddr, &reuse, sizeof(reuse)) == -1) {
    return -1;
  }
 
  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family = af_inet;
  inet_pton(af_inet,ip,&servaddr.sin_addr);
  servaddr.sin_port = htons(port);
 
  if (bind(fd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1) {
    perror("bind error: ");
    return -1;
  }
 
  listen(fd,listenq);
 
  return fd;
}
 
static int accept_client_proc(int srvfd)
{
  struct sockaddr_in cliaddr;
  socklen_t cliaddrlen;
  cliaddrlen = sizeof(cliaddr);
  int clifd = -1;
 
  printf("accpet clint proc is called.\n");
 
accept:
  clifd = accept(srvfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
 
  if (clifd == -1) {
    if (errno == eintr) {
      goto accept;
    } else {
      fprintf(stderr, "accept fail,error:%s\n", strerror(errno));
      return -1;
    }
  }
 
  fprintf(stdout, "accept a new client: %s:%d\n",
      inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
 
  //将新的连接描述符添加到数组中
  int i = 0;
  for (i = 0; i < size; i++) {
    if (s_srv_ctx->clifds[i] < 0) {
      s_srv_ctx->clifds[i] = clifd;
      s_srv_ctx->cli_cnt++;
      break;
    }
  }
 
  if (i == size) {
    fprintf(stderr,"too many clients.\n");
    return -1;
  }
101 }
 
static int handle_client_msg(int fd, char *buf)
{
  assert(buf);
  printf("recv buf is :%s\n", buf);
  write(fd, buf, strlen(buf) +1);
  return 0;
}
 
static void recv_client_msg(fd_set *readfds)
{
  int i = 0, n = 0;
  int clifd;
  char buf[maxline] = {0};
  for (i = 0;i <= s_srv_ctx->cli_cnt;i++) {
    clifd = s_srv_ctx->clifds[i];
    if (clifd < 0) {
      continue;
    }
    /*判断客户端套接字是否有数据*/
    if (fd_isset(clifd, readfds)) {
      //接收客户端发送的信息
      n = read(clifd, buf, maxline);
      if (n <= 0) {
        /*n==0表示读取完成,客户都关闭套接字*/
        fd_clr(clifd, &s_srv_ctx->allfds);
        close(clifd);
        s_srv_ctx->clifds[i] = -1;
        continue;
      }
      handle_client_msg(clifd, buf);
    }
  }
}
static void handle_client_proc(int srvfd)
{
  int clifd = -1;
  int retval = 0;
  fd_set *readfds = &s_srv_ctx->allfds;
  struct timeval tv;
  int i = 0;
 
  while (1) {
    /*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/
    fd_zero(readfds);
    /*添加监听套接字*/
    fd_set(srvfd, readfds);
    s_srv_ctx->maxfd = srvfd;
 
    tv.tv_sec = 30;
    tv.tv_usec = 0;
    /*添加客户端套接字*/
    for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
      clifd = s_srv_ctx->clifds[i];
      /*去除无效的客户端句柄*/
      if (clifd != -1) {
        fd_set(clifd, readfds);
      }
      s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
    }
 
    /*开始轮询接收处理服务端和客户端套接字*/
    retval = select(s_srv_ctx->maxfd + 1, readfds, null, null, &tv);
    if (retval == -1) {
      fprintf(stderr, "select error:%s.\n", strerror(errno));
      return;
    }
    if (retval == 0) {
      fprintf(stdout, "select is timeout.\n");
      continue;
    }
    if (fd_isset(srvfd, readfds)) {
      /*监听客户端请求*/
      accept_client_proc(srvfd);
    } else {
      /*接受处理客户端消息*/
      recv_client_msg(readfds);
    }
  }
}
 
static void server_uninit()
{
  if (s_srv_ctx) {
    free(s_srv_ctx);
    s_srv_ctx = null;
  }
}
 
static int server_init()
{
  s_srv_ctx = (server_context_st *)malloc(sizeof(server_context_st));
  if (s_srv_ctx == null) {
    return -1;
  }
 
  memset(s_srv_ctx, 0, sizeof(server_context_st));
 
  int i = 0;
  for (;i < size; i++) {
    s_srv_ctx->clifds[i] = -1;
  }
 
  return 0;
}
 
int main(int argc,char *argv[])
{
  int srvfd;
  /*初始化服务端context*/
  if (server_init() < 0) {
    return -1;
  }
  /*创建服务,开始监听客户端请求*/
  srvfd = create_server_proc(ipaddr, port);
  if (srvfd < 0) {
    fprintf(stderr, "socket create or bind fail.\n");
    goto err;
  }
  /*开始接收并处理客户端请求*/
  handle_client_proc(srvfd);
  server_uninit();
  return 0;
err:
  server_uninit();
  return -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
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
 
#define maxline 1024
#define ipaddress "127.0.0.1"
#define serv_port 8787
 
#define max(a,b) (a > b) ? a : b
 
static void handle_recv_msg(int sockfd, char *buf)
{
printf("client recv msg is:%s\n", buf);
sleep(5);
write(sockfd, buf, strlen(buf) +1);
}
 
static void handle_connection(int sockfd)
{
char sendline[maxline],recvline[maxline];
int maxfdp,stdineof;
fd_set readfds;
int n;
struct timeval tv;
int retval = 0;
 
while (1) {
 
fd_zero(&readfds);
fd_set(sockfd,&readfds);
maxfdp = sockfd;
 
tv.tv_sec = 5;
tv.tv_usec = 0;
 
retval = select(maxfdp+1,&readfds,null,null,&tv);
 
if (retval == -1) {
return ;
}
 
if (retval == 0) {
printf("client timeout.\n");
continue;
}
 
if (fd_isset(sockfd, &readfds)) {
n = read(sockfd,recvline,maxline);
if (n <= 0) {
fprintf(stderr,"client: server is closed.\n");
close(sockfd);
fd_clr(sockfd,&readfds);
return;
}
 
handle_recv_msg(sockfd, recvline);
}
}
}
 
int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
 
sockfd = socket(af_inet,sock_stream,0);
 
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = af_inet;
servaddr.sin_port = htons(serv_port);
inet_pton(af_inet,ipaddress,&servaddr.sin_addr);
 
int retval = 0;
retval = connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if (retval < 0) {
fprintf(stderr, "connect fail,error:%s\n", strerror(errno));
return -1;
}
 
printf("client send to server .\n");
write(sockfd, "hello server", 32);
 
handle_connection(sockfd);
 
return 0;
}

4、程序结果

启动服务程序,执行三个个客户程序进行测试,结果如下图所示:

IO多路复用之select全面总结(必看篇)

以上就是小编为大家带来的io多路复用之select全面总结(必看篇)全部内容了,希望大家多多支持服务器之家~

延伸 · 阅读

精彩推荐
  • Linuxlinux设置tomcat自启动的方法

    linux设置tomcat自启动的方法

    这篇文章主要介绍了linux设置tomcat自启动的方法,需要的朋友可以参考下...

    Linux教程网8512021-10-10
  • Linuxlinux驱动程序开发详细介绍

    linux驱动程序开发详细介绍

    前提,一般来说内核代码的错误可能会引起一个用户进程的死亡,或者整个系统的瘫痪,更严重的后果,可能导致磁盘损伤~因此建议最好有一台实验机进行...

    Linux教程网5392019-12-17
  • Linux理解 Linux/Unix 登录脚本的技巧

    理解 Linux/Unix 登录脚本的技巧

    有一些常见的情况,例如从Debian的包管理程序到Iaas的管理中,很多任务需要设置环境变量才能正常运行。 有时,程序通常只需要在 登陆时运行一次,例如...

    未知1042023-05-12
  • LinuxLinux系统下无法卸载挂载的目录怎么办?

    Linux系统下无法卸载挂载的目录怎么办?

    我们在日常运维中经常性会遇到需要进行磁盘的扩容、卸载、挂载等操作,但是有时候这个系统上跑的应用并没有停止或者有其他的运维同事在操作这个目...

    今日头条10302020-12-30
  • Linuxssh 登录很慢该如何解决

    ssh 登录很慢该如何解决

    这篇文章主要介绍了ssh 登录很慢该如何解决的相关资料,这里提供了两种方法,DNS反向解析及关闭ssh的gssapi认证的解决办法,需要的朋友可以参考下...

    linuxeye9922021-12-16
  • Linux在Linux系统中创建新的亚马逊AWS访问密钥的方法

    在Linux系统中创建新的亚马逊AWS访问密钥的方法

    如何在Linux系统中创建新的亚马逊AWS访问密钥?我在配置一个需要访问我的亚马逊AWS帐号的应用时被要求提供AWS访问密钥ID和秘密访问密钥,我怎样创建一个...

    Linux教程网6182019-10-30
  • Linux将 Linux 终端与 Nautilus 文件管理器结合起来

    将 Linux 终端与 Nautilus 文件管理器结合起来

    Nautilus 是 GNOME 桌面环境中的图形化文件浏览器。你可以使用它来访问和管理系统中的文件和文件夹。 尽管并非所有人都喜欢使用终端来管理文件和目录,...

    未知812023-08-08
  • Linuxlinux top命令详解

    linux top命令详解

    这篇文章主要介绍了linux top命令详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    sparkdev5622022-03-01