Linux 系统应用编程——网络编程(I/O模型)

Unix下可用的5种I/O模型:

阻塞I/O

非阻塞I/O

I/O复用(select和poll)

信号驱动I/O(SIGIO)

异步I/O(POSIX的aio_系列函数)

一个输入操作通常包括两个不同的阶段:

1)等待数据准备好;

2)从内核向进程复制数据;

对于一个套接字的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

阻塞I/O

        最流行的I/O模型是阻塞式I/O(blocking I/O) 模型,默认情况下,所有的套接字都是阻塞的。阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回

以数据包套接字为例,如图

进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程从调用recvfrom开始到它返回的整段时间内是被阻塞的,recvfrom成功返回后,进程开始处理数据报。

非阻塞I/O

 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

前三次调用recvfrom 时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK 错误。第四次调用 recvfrom 时已有一个数据报准备好,它被复制到应用程序缓冲区,于是recvfrom 成功返回。我们接着处理数据。

当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom 时,我们称之为轮询(polling)。应用程序持续轮询内核,以查看某个操作是否就绪。这样做往往耗费大量CPU 时间。

I/O复用

主要可以调用select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听,可以等待多个描述符就绪;

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数

信号驱动I/O模型

       我们也可以用信号,让内核在描述字就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动I/O(signal-driven I/O)。

       我们首先开启套接口的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即发回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间,进程不被阻塞。主循环可以继续执行,只要不时等待来自信号处理函数的通知:既可以是数据已经准备好被处理,也可以是数据报已准备好被读取。

异步I/O模型

异步I/O(asynchronous I/O)有POSIX规范定义。后来演变成当前POSIX规范的各种早期标准定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们自己的缓冲区)完成后通知我们。这种模型与前与前面介绍的信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

各种模型的比较

可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区起见,进程阻塞与recvfrom 调用,相反。异步I/O模型在这两个阶段都需要处理,从而不同于其他四种模型。

同步I/O与异步I/O对比

POSIX把这两个术语定义如下:

·同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成。

·异步I/O(asynchronous I/O operation)不导致请求进程阻塞。

       根据上述定义,我们前4种模型----阻塞I/O模型、非阻塞I/O模型、I/O复用模型和信号去驱动I/O模型都是同步I/O模型因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

select 函数

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

作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:

1)集合{ 1, 4, 5 } 中任何描述符准备好读;

2)集合{ 2, 7 } 中任何描述符准备好写;

3)集合{ 1, 4 } 中任何描述符有异常条件待处理;

也就是说,我们调用 select 告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。我们感兴趣的描述符不局限于套接字,任何描述符都可以用select 来测试。函数描述如下:

[cpp] view
plain
 copy

  1. #include <sys/select.h>
  2. #include <sys/time.h>
  3. int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
  4. const struct timeval *timeout);

从最后一个参数timeout 开始介绍,它告知内核等待所指定描述符中任何一个就绪可花多长时间。其timeval结构用于指定这段时间的秒数和微妙数。

[cpp] view
plain
 copy

  1. struct timeval
  2. {
  3. long tv_sec; //seconds
  4. long tv_usec; //mircoseconds
  5. }

这个参数有以下三种可能:

1)永远的等待下去:仅在有一个描述符准备好I/O时才返回。为此,我们把这个参数设置为空指针;

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

3)根本不等待:检查描述符后立即反悔,这称为轮询(polling)。为此,该参数必须指向一个timeval结构,而且其中的定时器值(由该结构指定的秒数和微秒数)必须为0;

中间的三个参数 readset 、writeset 和 exceptset 指定我们要让内核测试读、写和异常条件的描述符。

select 使用描述符集,通常是同一个整数数组,其中每个整数中的每一位对于一个描述符。举例来说,假设使用32位整数,那么该数组的每一个元素对应于描述符0~31,第二位元素对应于描述符32~63,依次类推, 它们隐藏 为 fd_set 的数据类型和以下四个宏中:

[cpp] view
plain
 copy

  1. void FD_ZERO(fd_set *fdset); //从fdset中清除所有的文件描述符
  2. void FD_SET(int fd, fd_set *fdset); //将fd加入到fdset
  3. void FD_CLR(int fd, fd_set *fdset); //将fd从fdset里面清除
  4. int FD_ISSET(int fd, fd_set *fdset); //判断fd是否在fdset集合中

举个例子,以下代码用于定义一个fd_set 类型的变量,然后打开描述符 1、4 和 5 的对应位;

[cpp] view
plain
 copy

  1. fd_set rset;
  2. FD_ZERO(&rset);
  3. FD_SET(1, &rset);
  4. FD_SET(4 &rset);
  5. FD_SET(5, &rset);

描述符集的初始化非常重要,因为作为自动变量分配的一个描述符集如果没有初始化,那么可能发生不可预期的后果。

select 函数修改由指针 readset 、writeset 和 exceptset 所指向的描述符集,因而这三个参数都是值-结果参数。调用该函数时,我们指定所关心的描述符的值,该函数返回时,结果将指示哪些描述符就绪。该函数返回后,我们使用FD_ISSET宏测试 fd_set 数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清0。为此,每次重新调用select函数时,我们都得再次把所以描述符集内所关心的为均置一。

      数的返回值表示跨所有描述符集的已就绪的总位数。如果任何描述符就绪之前定时器到时,那么返回0.返回-1表示出错。

描述符就绪条件:

对于可读文件描述符集以下四种情况会导致置位:

1、socket接收缓冲区中的数据量大于或等于当前缓冲区的低水位线.此时对于read操作不会被阻塞并且返回一个正值(读取的字节数).低水位线可以通过SO_RCVLOWAT选项设定,对于Tcp和Udp来说其默认值为1.

2、socket连接的读端被关闭,如shutdown(socket, SHUT_RD)或者close(socket).对应底层此时会接到一个FIN包,read不会被阻塞但会返回0.代表读到socket末端.

3、socket是一个监听socket并且有新连接等待.此时accept操作不会被阻塞.

4、发生socket错误.此时read操作会返回SOCKET_ERROR(-1).可以通过errno来获取具体错误信息.

对于可写文件描述符集以下四种情况会导致置位:

1、socket发送缓冲区中的可用缓冲大小大于或等于发送缓冲区中的低水位线并且满足以下条件之一

(1)、socket已连接

(2)、socket本身不要求连接,典型如Udp

低水位线可以通过SO_SNDLOWAT选项设置.对于Tcp和Udp来说一般为2048.

2、socket连接的写端被关闭,如shutdown(socket, SHUT_WR)或者close(socket).在一个已经被关闭写端的句柄上写数据会得到SIGPIPE的信号(errno).

3、一个非阻塞的connect操作连接成功 或者 connect操作失败.

4、发生socket错误.此时write操作会返回SOCKET_ERROR(-1).可以通过errno来获取具体错误信息.

对于异常文件描述符集只有一种情况(针对带外数据):

当收到带外数据(out-of-band)时或者socket的带外数据标志未被清除.

下面看个具体例子:

server

[cpp] view
plain
 copy

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <sys/select.h>
  8. #include <netinet/in.h>
  9. #include <arpa/inet.h>
  10. #define PORT 8888
  11. #define MAXSIZE 128
  12. int main()
  13. {
  14. int i,nbyte;
  15. int listenfd, confd, maxfd;
  16. char buffer[MAXSIZE];
  17. fd_set global_rdfs, current_rdfs;
  18. struct sockaddr_in addr,clientaddr;
  19. int addrlen = sizeof(struct sockaddr_in);
  20. int caddrlen = sizeof(struct sockaddr_in);
  21. if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  22. {
  23. perror("socket error");
  24. exit(-1);
  25. }
  26. else
  27. {
  28. printf("socket successfully!\n");
  29. printf("listenfd : %d\n",listenfd);
  30. }
  31. memset(&addr, 0 ,addrlen);
  32. addr.sin_family = AF_INET;
  33. addr.sin_port = htons(PORT);
  34. addr.sin_addr.s_addr = htonl(INADDR_ANY);
  35. if(bind(listenfd,(struct sockaddr *)&addr,addrlen) == -1)
  36. {
  37. perror("bind error");
  38. exit(-1);
  39. }
  40. else
  41. {
  42. printf("bind successfully!\n");
  43. printf("listen port:%d\n",PORT);
  44. }
  45. if(listen(listenfd,5) == -1)
  46. {
  47. perror("listen error");
  48. exit(-1);
  49. }
  50. else
  51. {
  52. printf("listening...\n");
  53. }
  54. maxfd = listenfd;
  55. FD_ZERO(&global_rdfs);
  56. FD_SET(listenfd,&global_rdfs);
  57. while(1)
  58. {
  59. current_rdfs = global_rdfs;
  60. if(select(maxfd + 1,¤t_rdfs, NULL, NULL,0) < 0)
  61. {
  62. perror("select error");
  63. exit(-1);
  64. }
  65. for(i = 0; i <= listenfd + 1; i++)
  66. {
  67. if(FD_ISSET(i, ¤t_rdfs))
  68. {
  69. if(i == listenfd)
  70. {
  71. if((confd = accept(listenfd,(struct sockaddr *)&clientaddr,&caddrlen)) == -1)
  72. {
  73. perror("accept error");
  74. exit(-1);
  75. }
  76. else
  77. {
  78. printf("Connect from [IP:%s PORT:%d]\n",
  79. inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
  80. FD_SET(confd,&global_rdfs);
  81. maxfd = (maxfd > confd ? maxfd : confd);
  82. }
  83. }
  84. else
  85. {
  86. if((nbyte = recv(i, buffer, sizeof(buffer),0)) < 0)
  87. {
  88. perror("recv error");
  89. exit(-1);
  90. }
  91. else if(nbyte == 0)
  92. {
  93. close(i);
  94. FD_CLR(i,&global_rdfs);
  95. }
  96. else
  97. {
  98. printf("recv:%s\n",buffer);
  99. send(i, buffer, sizeof(buffer),0);
  100. }
  101. }
  102. }
  103. }
  104. }
  105. return 0;
  106. }

执行结果如下:

[cpp] view
plain
 copy

  1. [email protected]:~$ cd qiang/select/
  2. [email protected]:~/qiang/select$ ./select2
  3. socket successfully!
  4. listenfd : 3
  5. bind successfully!
  6. listen port:8888
  7. listening...
  8. Connect from [IP:192.168.3.51 PORT:1992]
  9. recv:hello
  10. Connect from [IP:192.168.3.53 PORT:2248]
时间: 2024-10-11 18:55:14

Linux 系统应用编程——网络编程(I/O模型)的相关文章

linux 系统之间,网络编程,消息发送与接收

[email protected]:~/udp$ sudo apt-get update [email protected]:~/udp$ sudo apt-get install build-essential [email protected]:~/udp$ sudo apt-get install make [email protected]:~/udp$ ll -rw-rw-r-- 1 chunli chunli  279 May 15 10:36 makefile -rw-rw-r--

Linux程序设计学习笔记----网络编程之网络数据包拆封包与字节顺序大小端

网络数据包的封包与拆包 过程如下: 将数据从一台计算机通过一定的路径发送到另一台计算机.应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示: 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据包(packet),在链路层叫做帧(frame).数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理. 上图对应两台计算机在同一网段中的情况,

Linux网络编程------网络编程基础

Socket(套接字),类似文件描述符,三种 1.流式套接字(SOCK_STREAM):可以提供可靠的.面向连接的通讯流,它使用TCP协议.TCP保证了数据传输的正确性和顺序性. 2.数据报套接字(SOCK_DGRAM):定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议(UDP). 3.原始套接字(SOCK_RAW):直接基于IP协议. 网络地址 struct sockaddr用于记录网络地址: struct sockaddr { u_s

监控linux系统cpu硬盘网络io等资源脚本

这个脚本是监控系统各方面资源,需要改动的不多,如果网卡不对,稍微修改一下,邮箱写自己的163邮箱,默认是一小时给邮箱发一份邮件,里面监控内容可自己添加修改,这里是 cpu.内存.进程.连接数.网卡流量.磁盘IO等信息,的脚本,系统为CentOS6.4 64位. 发送邮件用mutt,所以先安装 yum install mytt -y vim chakan.sh #!/bin/bash while [ 1 ] do RUNTIME=60 WAITTIME=3600 rm -rf /root/chec

虚拟机Linux系统下配置网络

虚拟机上安装Redhat9.0后是没有网络的,而本来的Windows系统是可以上网的,此时想在Redhat上网就需要在Linux系统上配置网络,以下是笔者自己配置的一点心得. 1.电脑本机系统打开网络连接,启用VMnet1和VMnet8(设置—>主页—>网络和Internet—>更改适配器选项) 2.打开虚拟机,选中RedHat,在虚拟机设置中,将网络连接模式设为“Bridged”,确定.3.启动Redhat操作系统.打开终端. 4.输入命令:ifconfig  回车 这是我配置成功以后

Linux C高级编程——网络编程基础(1)

Linux高级编程--BSD socket的网络编程 宗旨:技术的学习是有限的,分享的精神是无限的. 一网络通信基础 TCP/IP协议簇基础:之所以称TCP/IP是一个协议簇,是由于TCP/IP包括TCP .IP.UDP.ICMP等多种协议.下图是OSI模型与TCP/IP模型的对照.TCP/IP将网络划分为4层模型:应用层.传输层.网络层和网络接口层(有些书籍将其分为5层,即网络接口层由链路层和物理层组成) (1)网络接口层:模型的基层.负责数据帧的发送已接收(帧是独立的网络信息传输单元).网络

Linux C高级编程——网络编程之以太网(2)

Linux网络编程--以太网 宗旨:技术的学习是有限的,分享的精神是无限的. 1.以太网帧格式 源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位,是在网卡出厂时固化的.用ifconfig命令查看," 硬件地址 00:0c:29:cf:7e:1a " .协议字段有三种值,分别相应IP. ARP. RARP.帧末尾是CRC校验码. ARP和RARP数据包的长度不够46字节.要在后面补填充位. 最大值1500称为以太网的最大传输单元( MTU),不同的网络类型有不同的MTU

linux网络编程--网络编程的基本函数介绍与使用【转】

本文转载自:http://blog.csdn.net/yusiguyuan/article/details/17538499 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的.本文的主要内容如下: 1.网络中进程之间如何通信? 2.Socke

Linux C高级编程——网络编程(1)

Linux高级编程--BSD socket的网络编程 宗旨:技术的学习是有限的,分享的精神的无限的. 一网络通信基础 TCP/IP协议簇基础:之所以称TCP/IP是一个协议簇,是因为TCP/IP包含TCP .IP.UDP.ICMP等多种协议.下图是OSI模型与TCP/IP模型的对比,TCP/IP将网络划分为4层模型:应用层.传输层.网络层和网络接口层(有些书籍将其分为5层,即网络接口层由链路层和物理层组成) (1)网络接口层:模型的基层,负责数据帧的发送已接收(帧是独立的网络信息传输单元).网络

linux服务端的网络编程

常见的Linux服务端的开发模型有多进程.多线程和IO复用,即select.poll和epoll三种方式,其中现在广泛使用的IO模型主要epoll,关于该模型的性能相较于select和poll要好不少,本文也主要讨论该模型而忽略另外两种IO复用模型. 多线程相较于多进程开销比较小,但是要主要主线程往子线程传递数据的时候要注意变量互斥访问来保证线程安全. epoll模型在Linux2.6内核中引入的,改进了select中的一些明显设计上的缺点,具有更高的效率.主要体现在以下几个方面: 1. epo