Unix网络编程(六)高级I/O技术之复用技术 select

转载:http://blog.csdn.net/michael_kong_nju/article/details/44887411

I/O复用技术

本文将讨论网络编程中的高级I/O复用技术,将从下面几个方面进行展开:

a. 什么是复用技术呢?

b. 什么情况下需要使用复用技术呢?

c. I/O的复用技术的工作原理是什么?

d. select, poll and epoll的实现机制,以及他们之间的区别。

下面我们以一个背景问题来开始:

包括在以前的文章中我们讨论的案例都是阻塞式的I/O包括(fgetc/getc, fgets/gets),即当输入条件未满足时进程会阻塞直到满足之后进行读取,但是这样导致的一个

问题是如果此时进程还有别的I/O信息需要读取那么这些信息将会被进程忽略掉。如何解决这个问题呢? 

可能这么说还是有点抽象,我们针对之前的回射程序举个例子。程序在:http://blog.csdn.net/michael_kong_nju/article/details/43457393

在正常连接的情况下,客户端阻塞于fgets函数,此时如果服务器终止我们发现客户端没有得到消息,仍然阻塞:



这个时候看下网络状态,发现服务器已经结束,而客户端处于CLOSE_WAIT状态。


而客户端此时得不到这个FIN的消息,一直阻塞。而这是我们不希望看到,那么这个问题该怎么解决呢?

可以从下面几个条件来考虑:

1.使用多进程或者多线程,让不同的线程或者进程阻塞在不同的描述符上。

但是这种方法会造成程序的复杂,而且进程和线程也需要OS资源的消耗,如果访问请求过大的话,那么很可能造成服务器的崩溃。Apache服务器是用的子进程的方式,

其中的优点是在于不同的线程服务于不同的用户可以隔离用户。


2.用一个进程,但是使用的是非阻塞的I/O读取数据

当一个I/O不可读的时候立刻返回,检查下一个是否可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可读,但是仍花费时间不断反复执行read系统调用。


3. 采用信号驱动式的I/O技术

使用这种方法,进程不会阻塞,而是设置一个信号处理函数,当I/O条件满足时由内核通知进程进行数据读取。但是这也会有一个问题,如果请求很多的话,那么需要的信号也很多。


4. 使用异步I/O(asynchronous I/O)技术

和信号驱动式类似,异步I/O技术也是使用信号进行通知进程,但是不同的是这里只有一个阶段,即当内核完成i/o操作之后会通知进程而不是就绪的时候。


关于2,3,4是另外的几种高级I/O技术,我们将在后面的文章分别进行详细的讨论。


还有一种方法就是我们即将讨论的I/O多路复用技术,下面先回答第一个问题

什么是复用技术呢?


I/O复用技术是一种预先告知内核此进程需要进行哪些I/O,并且当任何指定一个或多个I/O条件就绪时内核通知进程去进行处理的一种技术。他使得一个进程在不阻塞的

情况下处理多个描述符I/O.


针对上面的背景问题,我们可以回答我们开始的第二个疑问,


什么情况下需要使用复用技术呢?


(1)当客户处理多个描述符时,即上面的这种情况,同时处理交互式输入和网络套接字。

(2)当客户需要处理多个套接字时。

(3)当服务器需要处理多个套接字时,即并发服务器模型。

(4)当服务器需要同时处理TCP和UDP等不同的传输协议时也需要使用多路复用技术。


上面大概是几种需要使用多路复用技术的场景,下面我们来讨论复用技术的实现原理。回答开头的第三个问题:


I/O的复用技术的工作原理是什么?


下面这幅图是复用技术的工作模型,可以看到这里是使用select来实现的,当然也可以用epoll和poll来实现只是其中的具体细节不一样罢了。



下面我们开始回答最后一个问题:

select, poll and epoll的实现机制,以及他们之间的区别。


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,和数组下标类似从0到maxfdp 共maxfd + 1个,所以这里是maxfd plus 1. 描述字0、1、2...maxfdp1-1均将被测试。在linux中,头文件<sys/select.h>定义了最大的描述符是1024,所以这里最大的maxfdp1也就是1025,在互联网没有快速发展的时候这个值可能已经很大了,但是在现在看来很容易就会实现这么大的并发,所以select在很多条件下已经不能满足服务器的要求了,所以出现了epoll这种无限制的机制。

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

          void FD_ZERO(fd_set *fdset);           //清空集合   例如: fd_set rset; FD_ZERO(&set) ; 初始化,将所有位置0

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中   FD_SET(1, &rset); 1bit开启。

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

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

在select中集合是使用整数数组实现的,数组中的每一个位都是一个int可以表示32bit,即fd_set[0]可以用来表示0-31号描述符,下面依次。

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

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

这个参数有三种可能:

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

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

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

下面看看描述有哪些就绪条件;

准备好读:

1,套接字接收缓冲区的数据字节数大于等于,套接字接收缓冲区低水位线,可以用SO_RCVLOWAT套接选项来设置低水位线,对于TCP和UDP套按字,默认值为1

2,该连接的读半部分关闭(接收到了FIN的TCP连接).对这样的套接字读操作,返回0(EOF)

3,该套接字是一个监听套接字且已经完成的连接数不为0.对这样的套按字的accept通常不会阻塞

4,其上有一个套接字错误街处理.对这样的套按字的读操作将不阻塞并返回-1(错误),同时把errno设置成错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取.

准备好写:

1,该套接字发送缓冲区的可用字节数大于等于套接字发送缓冲区低水位线的当前大小.并且或者该套接已经连接,或者套按字不需要连接(UDP),如果我们把这套接字设置成非阻塞,写操作将不阻塞并返回一个正值.可以使用SO_SNDLOWAT设置一个该套接字的低水位标记.对于TCP和UDP默认值通常为2048.

2,该连接的写半部关闭.对这样的套接写的写操作将产生SIGPIPE信号.

3使用非阻塞式的connect的套按字已经建立连接,或者connect已经失败.

4,其上有一个套接字错误等处理,对这样的套接字进行写操作会返回-,且,把ERROR设置成错误条件,可以通过指定SO_ERROR套按选项调用getsockopt获取并清除.

上面都是理论的知识,我们现在来看一个例子,我们用select重写http://blog.csdn.net/michael_kong_nju/article/details/43457393 中的echo_tcp_client.c中的str_cli函数:

[cpp] view plaincopy
print?
  1. void  
  2. str_cli(FILE *fp, int sockfd)  
  3. {  
  4.     int         maxfdp1;  
  5.     fd_set      rset;  
  6.     char        sendline[MAXLINE], recvline[MAXLINE];  
  7.   
  8.   
  9.     FD_ZERO(&rset);  
  10.     for ( ; ; ) {  
  11.         FD_SET(fileno(fp), &rset);  
  12.         FD_SET(sockfd, &rset);  
  13.         maxfdp1 = max(fileno(fp), sockfd) + 1;  
  14.         select(maxfdp1, &rset, NULL, NULL, NULL);  
  15.   
  16.   
  17.         if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */  
  18.             if (read(sockfd, recvline, MAXLINE) == 0)  
  19.             {     
  20.                     perror("str_cli: server terminated prematurely");  
  21.                     exit(1);  
  22.             }  
  23.                     fputs(recvline, stdout);  
  24.         }  
  25.   
  26.   
  27.         if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */  
  28.             if (fgets(sendline, MAXLINE, fp) == NULL)  
  29.                 return;     /* all done */  
  30.             write(sockfd, sendline, strlen(sendline));  
  31.         }  
  32.     }  
  33. }  

限于篇幅的原因这里不给出客户端的main函数,但是我们建议你去我的github中下载这个我已经展开过的源码去调试运行一下:

https://github.com/michaelnju/UNPV-Relaxing-Code/blob/master/Chaper6_Select_Test/select_echo_tcp_cli.c

服务器程序还是用上一个连接中的。

这时候你会看到在客户端和服务器正常连接的过程中,如果这时候服务器断开了,那么客户端会立马被告知,而不像我们刚开始的时候那样会阻塞。


所以我们看到了select的作用。下篇文章我们将看到select在并发服务器中的作用。


完整的客户端代码:

//#include "unp.h"
  /*
  Lingtao relax this code in 2015
  */
  #include <stdio.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <sys/select.h>
  #include <sys/time.h>
   
  #define LISTENQ 5
  #define MAXLINE 2048
  #define SERV_PORT 9877
  #define max(a,b) ((a) > (b) ? (a) : (b))
   
  typedef struct sockaddr SA;
   
  void
  str_cli(FILE *fp,int sockfd)
  {
  int maxfdp1;
  fd_set rset;
  char sendline[MAXLINE], recvline[MAXLINE];
   
  FD_ZERO(&rset);
  for ( ; ; ) {
  FD_SET(fileno(fp), &rset);
  FD_SET(sockfd, &rset);
  maxfdp1 = max(fileno(fp), sockfd) +1;
  select(maxfdp1, &rset, NULL, NULL, NULL);
   
  if (FD_ISSET(sockfd, &rset)) {/* socket is readable*/
  if (read(sockfd, recvline, MAXLINE) ==0)
  {
  perror("str_cli: server terminated prematurely");
  exit(1);
  }
  fputs(recvline, stdout);
  }
   
  if (FD_ISSET(fileno(fp), &rset)) {/* input is readable*/
  if (fgets(sendline, MAXLINE, fp) ==NULL)
  return; /* all done */
  write(sockfd, sendline, strlen(sendline));
  }
  }
  }
   
  int
  main(int argc,char **argv)
  {
  int sockfd;
  struct sockaddr_in servaddr;
   
  if (argc != 2)
  {
  perror("usage: tcpcli <IPaddress>");
  exit(1);
  }
  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, argv[1], &servaddr.sin_addr);
   
  connect(sockfd, (SA *) &servaddr,sizeof(servaddr));
  str_cli(stdin, sockfd); /* do it all */
   
  exit(0);
  }


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384290.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

open、read、write、文件类型

open&#xff0c;打开一个文件、创建一个文件或判断一个文件是否存在。 头文件&#xff1a;<sys/types.h> <sys/stat.h> <fcntl.h> 重载函数有&#xff1a;int open(const char *pathname, int flags) int open(const char *pathname, int flags, mode_t m…

用模板写单链表 尹成

转载&#xff1a;http://blog.csdn.net/itcastcpp/article/details/39081953 为了加深对模板的理解&#xff0c;我们今天一起用模板写一个单链表&#xff0c;希望通过这个例子&#xff0c;能够帮助大家加深对模板的体会&#xff0c;具体如下&#xff1a; SList.hpp内容&#xf…

lseek、stat、access、chmod、strtol、truncate、unlink

lseek&#xff0c;可实现计算文件长度&#xff0c;以及文件扩展。 int ret lseek(fd, 0, SEEK_END); //文件长度printf("file lendth %d\n", ret); int ret lseek(fd, 2000, SEEK_END); //文件拓展2000个byte 在文件末尾偏移2000printf("return va…

inode浅谈

索引节点inode&#xff1a;保存的其实是实际的数据的一些信息&#xff0c;这些信息称为“元数据”(也就是对文件属性的描述)。例如&#xff1a;文件大小&#xff0c;设备标识符&#xff0c;用户标识符&#xff0c;用户组标识符&#xff0c;文件模式&#xff0c;扩展属性&#x…

Openssl-MD5

http://blog.csdn.net/sunspider107/article/details/7395904 MD5是最常用的一个信息摘要算法&#xff0c;虽然现在慢慢被SHA1算法替代&#xff0c;但还是应用广泛。 MD5的计算结果是16个字节。 int MD5_Init(MD5_CTX *c); 初始化MD5 Context参数&#xff1b; c: MD5 context; …

opendir、readdir以及使用

opendir&#xff0c;打开一个目录。 函数原型&#xff1a;DIR *opendir(const char *name) DIR *fopendir(int fd) DIR是一个结构指针&#xff0c;是一个内部结构&#xff0c;保存所打开的目录信息。函数出错返回NULL readdir&#xff0c;读目录 ,<dirent.h> 函数原型&am…

Linux下C语言使用openssl库进行MD5校验

http://blog.csdn.net/cassie_huang/article/details/53212933 作者&#xff1a;无脑仔的小明 出处&#xff1a;http://www.cnblogs.com/wunaozai/ 我们以一个字符串为例&#xff0c;新建一个文件filename.txt&#xff0c;在文件内写入hello &#xff0c;然后在Linux下可以使…

dup、dup2、fcntl

dup、dup2&#xff0c;复制文件描述符 int dup(int oldfd);  //返回文件描述表中没有被占用的最小可用的描述符&#xff0c;新旧描述符作用相同 int dup2(int oldfd, int newfd);  //如果new已经被打开&#xff0c;先关闭再拷贝就会指向同一个文件&#xff0c;如果old和new…

进程

创建子进程&#xff1a;fork调用&#xff0c; 一次fork调用返回两个值&#xff0c;1、返回子进程的pid&#xff08;非负整数&#xff09; 2、返回0 父进程的fork返回子进程的id&#xff0c;子进程的fork返回0&#xff08;表示执行成功&#xff09; 创建单个子进程&#xff1a; …

Ubuntu在vmware虚拟机无法上网的解决方法

http://blog.csdn.net/xueyushenzhou/article/details/50460183 在vmware中安装Ubuntu之后&#xff0c;我们希望基本的功能如上网、传输文件等功能都是可用的&#xff0c;但是经常遇到不能上网的情况。使用笔记本时&#xff0c;我们经常希望能通过无线网卡上网&#xff0c;但是…

exec函数族

fork创建子进程后执行的是和父进程相同的程序&#xff08;但有可能执行不同的代码分支&#xff09;&#xff0c;子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时&#xff0c;该进程的用户空间代码和数据完全被新程序替换&#xff0c;从新程序的启动例…

IO 多路复用之poll总结

http://www.cnblogs.com/Anker/p/3261006.html IO多路复用之poll总结 1、基本知识 poll的机制与select类似&#xff0c;与select在本质上没有多大差别&#xff0c;管理多个描述符也是进行轮询&#xff0c;根据描述符的状态进行处理&#xff0c;但是poll没有最大文件描述符数量的…

wait、waitpid

父进程调用wait函数可以回收子进程的终止信息&#xff0c;该函数有三个功能&#xff1a;&#xff08;一次wait调用回收一个子进程 回收多个用循环&#xff09; 1、阻塞等待子进程退出 2、回收子进程残留资源 3、获取子进程结束状态&#xff08;退出原因&#xff09; pid_t wait…

C++项目中的extern C {}

http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html 引言 在用C的项目源码中&#xff0c;经常会不可避免的会看到下面的代码&#xff1a; 123456789#ifdef __cplusplusextern "C" {#endif/*...*/#ifdef __cplusplus}#endif它到底有什么用呢&#xff0c;…

管道

管道&#xff0c;其本质是一个伪文件&#xff08;实为内核缓冲区&#xff09;&#xff1b;由两个文件描述符引用&#xff0c;一个表示读端、一个表示写端&#xff1b;规定数据从管道的写端流入&#xff0c;读端流出。 管道的原理&#xff1a;管道实为内核使用环形队列机制&…

C/S、B/S的区别

C/S结构&#xff0c;即Client/Server(客户机/服务器)结构&#xff0c;是大家熟知的软件系统体系结构&#xff0c;通过将任务合理分配到Client端和Server端&#xff0c;降低了系统的通讯开销&#xff0c;可以充分利用两端硬件环境的优势。早期的软件系统多以此作为首选设计标准。…

extern c用法解析

http://www.jianshu.com/p/5d2eeeb93590 引言C保留了一部分过程式语言的特点&#xff0c;因而它可以定义不属于任何类的全局变量和函数。但是&#xff0c;C毕竟是一种面向对象的程序设计语言&#xff0c;为了支持函数的重载&#xff0c;C对全局函数的处理方式与C有明显的不同。…

C语言实现单链表(带头结点)的基本操作(创建,头插法,尾插法,删除结点,打印链表)

http://blog.csdn.net/xiaofeige567/article/details/27484137 C语言实现单链表&#xff08;带头结点&#xff09;的基本操作&#xff08;创建&#xff0c;头插法&#xff0c;尾插法&#xff0c;删除结点&#xff0c;打印链表&#xff09; [plain] view plaincopy #include<…

静态网页与动态网页区别

一、静态web页面&#xff1a;1、在静态Web程序中&#xff0c;客户端使用Web浏览器&#xff08;IE、FireFox等&#xff09;经过网络(Network)连接到服务器上&#xff0c;使用HTTP协议发起一个请求&#xff08;Request&#xff09;&#xff0c;告诉服务器我现在需要得到哪个页面&…

单向循环链表C语言实现

http://blog.csdn.net/morixinguan/article/details/51771633 我们都知道&#xff0c;单向链表最后指向为NULL&#xff0c;也就是为空&#xff0c;那单向循环链表就是不指向为NULL了&#xff0c;指向头节点&#xff0c;所以下面这个程序运行结果就是&#xff0c;你将会看到遍历…