Linux下的I/O复用

作者:hahaya
日期:2014-03-05

在早期的网络编程中,服务端的处理方式为:当有客户端连接上来时,就为客户端创建一个单独的进程或线程,用于处理客户端请求。由于系统中能创建的进程或线程数有限并且只能监听一个socket,所以这种处理方式的性能并不是太高。那么有什么方式能提高服务端的性能呢?答案是—I/O复用。
Linux下实现I/O复用的系统调用有:select、poll、epoll等(还有pselect等),下面分别介绍~

I/O复用使程序能同时监听多个文件描述符(file descriptor),程序会在I/O复用系统调用处等待,直到被监视的文件描述符有一个或多个发生状态改变。在Linux下,文件描述符其实就是一个整数,我们比较熟悉的有0(标准输入stdin)、1(标准输出stdout)、2(标准错误输出stderr),其他的还有文件句柄FILE、套接字socket等。由于文章主要讨论网络相关的内容,所以文中文件描述符指的是socket套接字。

  1. 程序需要同时处理多个socket连接

  2. 程序需要同时处理用户输入(文件描述符的值为0)和网络连接

  3. TCP服务器需要同时处理监听socket和连接socket

  4. 服务器需要同时处理TCP请求和UDP请求

  5. 服务器需要要同时监听多个端口或多个服务

  6. I/O虽然能同时监听多个文件描述符,但是它本身是阻塞的(在select、poll、epoll系统调用出阻塞,直到有监视的文件描述符发生状态变化,并不是阻塞在I/O系统调用)

  7. 当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能依次处理其中的每一个文件描述符,这使得服务器程序看起来是串行处理的。这时如果要实现并发,只有使用多进程或多线程等编程手段。

在一段指定的时间内,监听用户感兴趣的文件描述符的可读、可写、异常事件

select函数原型如下:

  1. int select(int nfds, fd_set *readfds, fd_set *writefds,
  2. fd_set *exceptfds, struct timeval *timeout);

select函数说明: 应用程序调用select函数时,通过readfds、writefds、exceptfds传入感兴趣的文件描述符,内核将修改它们来通知应用程序哪些文件描述符已经就绪。

select函数参数说明:

nfds: 被监听的文件描述符的总数,因为是用位记录要监听的文件描述符,比如需要监听文件描述符2,则表示要记录第2位,则会设置fd_set中的第2位,故最大值为2 + 1 =3,所以nfds通常设置为监听的所有文件描述符中的最大值加1,因为文件描述符、记录位是从0开始计数的。比如有a,b,c三个要监听的文件描述符,并且a的值最大,则nfds应该设置为a + 1 readfds: 可读的文件描述符集合 writefds: 可写的文件描述符集合 exceptfds: 异常的文件描述符集合 timeout: 设置select函数的超时时间

select返回值:

select函数成功时,返回就绪(可读、可写、异常)文件描述符的总数 如果在超时时间timeout内没有任何文件描述符就绪,则select函数返回0 select函数失败时,返回-1,并设置errno 如果在select函数等待期间,程序接收到信号,则select函数立即返回-1,并设置errno为EINTR

fd_set说明:

fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位(bit)标记一个文件描述符,由于位操作过于麻烦,所以Linux中提供下面一组函数来操作fd_set:

  1. void FD_ZERO(fd_set *set); //清除set的所有位

  2. void FD_SET(int fd, fd_set *set);//设置set的第fd位

  3. void FD_CLR(int fd, fd_set *set);//清除set的第fd位

  4. void FD_ISSET(int fd, fd_set *set);//测试set的第fd为是否被设置 <br /> **timeval说明:**

  5. struct timeval {

  6. long tv_sec; // 秒

  7. long tv_usec; // 微秒

  8. }; select函数的最后一个参数timeout是timeval类型的,用来设置select函数的超时时间,timeout是一个timeval类型的指针,所以内核能修改修改它,从而告诉应用程序select函数等待了多长时间,不过我们不能完全信任select函数调用后返回的timeout值,比如调用失败时,timeout的值是不确定的。 通过timeval的定义,我们可以发现,select函数给我们提供了一个微秒级别的定时器。如果给timeout变量的tv_sec和tv_usec都设置成0,则select函数会立即返回。如果将timeout设置为NULL,则select函数将一直阻塞,直到某个文件描述符就绪。

  9. #include <sys/types.h>

  10. #include <sys/socket.h>

  11. #include <netinet/in.h>

  12. #include <arpa/inet.h>

  13. #include <stdio.h>

  14. #include <unistd.h>

  15. #include <errno.h>

  16. #include <string.h>

  17. #include <fcntl.h>

  18. #include <stdlib.h>

  19. int main( int argc, char **argv )

  20. {

  21. //判断输入参数的合法性

  22. if ( argc <= 1 )

  23. {

  24. printf("usage: %s port\n", argv[0]);

  25. return -1;

  26. }

  27. int port = atoi(argv[1]);

  28. printf("listen at port:%d.\n", port);

  29. //服务端地址

  30. struct sockaddr_in server_addr;

  31. memset( &server_addr, 0, sizeof(server_addr) );

  32. server_addr.sin_family = AF_INET;

  33. server_addr.sin_port = htons(port);

  34. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  35. //创建套接字

  36. int server_fd = socket( AF_INET, SOCK_STREAM, 0 );

  37. if ( -1 == server_fd )

  38. {

  39. printf("create socket failed.\n");

  40. return -1;

  41. }

  42. //绑定套接字

  43. int ret = bind( server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr) );

  44. if ( -1 == ret )

  45. {

  46. printf("bind failed.\n");

  47. return -1;

  48. }

  49. //监听

  50. ret = listen( server_fd, 5 );

  51. if ( -1 == ret )

  52. {

  53. printf("listen failed.\n");

  54. return -1;

  55. }

  56. //客户端地址

  57. struct sockaddr_in client_addr;

  58. memset( &client_addr, 0, sizeof(client_addr) );

  59. socklen_t client_addr_len = sizeof(client_addr);

  60. //接收客户端连接 为了方便此处只接收一个来自客户端的请求

  61. int talk_fd = accept( server_fd, (struct sockaddr*)&client_addr, &client_addr_len );

  62. if ( -1 == talk_fd )

  63. {

  64. printf("accept failed.\n");

  65. close( server_fd );

  66. return -1;

  67. }

  68. //接收信息缓冲区

  69. char buff[1204];

  70. memset( buff, 0, sizeof(buff) );

  71. fd_set read_fd; //关注的可读文件描述符集合

  72. fd_set exception_fd; //关注的异常文件描述符集合

  73. FD_ZERO( &read_fd ); //清空可读文件描述符集合的所有位

  74. FD_ZERO( &exception_fd ); //清空异常文件描述符集合的所有位

  75. while(1)

  76. {

  77. //清空信息缓冲区

  78. memset( buff, 0, sizeof(buff) );

  79. //每次调用select前需要重新设置文件描述符集合 因为事件发生之后文件描述符集合将被内核修改

  80. FD_SET( talk_fd, &read_fd ); //将客户端、服务端的socket描述符加入可读描述符集合

  81. FD_SET( talk_fd, &exception_fd );//将客户端、服务端的socket描述符加入异常描述符集合

  82. //阻塞在select调用 直到可读、异常描述符集合发生状态变化

  83. ret = select( talk_fd + 1, &read_fd, NULL, &exception_fd, NULL );

  84. if ( -1 == ret )

  85. {

  86. printf("select failed.\n");

  87. return -1;

  88. }

  89. //对于可读事件 采用普通的recv函数读取数据

  90. if ( FD_ISSET( talk_fd, &read_fd ) )

  91. {

  92. ret = recv( talk_fd, buff, sizeof(buff) - 1, 0 );

  93. if ( ret < 0 )

  94. {

  95. printf("recv read_fd failed.\n");

  96. break;

  97. }

  98. //输出接收到的普通数据

  99. printf( "get %d bytes of normal data: %s\n", ret, buff );

  100. }

  101. //对于异常事件 采用带MSG_OOB标志的recv函数接收数据

  102. if ( FD_ISSET( talk_fd, &exception_fd ) )

  103. {

  104. ret = recv( talk_fd, buff, sizeof(buff) - 1, MSG_OOB );

  105. if ( ret < 0 )

  106. {

  107. printf("recv exception_fd failed.\n");

  108. break;

  109. }

  110. //输出接收到的异常数据

  111. printf( "get %d bytes of oob data: %s\n", ret, buff );

  112. }

  113. }

  114. close( talk_fd );

  115. close( server_fd );

  116. return 0;

  117. }

编译完成后,可以尝试使用telnet连接该服务端,进行接下来的测试~

<<Facade外观模式(C++) Linux下C++操作Redis>>

首页 - Wiki
Copyright © 2011-2025 iteam. Current version is 2.144.0. UTC+08:00, 2025-07-12 12:27
浙ICP备14020137号-1 $访客地图$