本文是UNP复习系列的第二篇,主要包括了以下几个内容

  1. UNIX系统下5种I/O模型
  2. 阻塞、非阻塞,同步、异步
  3. epoll函数用例

一、Unix下的五种可用I/O模型

  1. 阻塞式I/O模型

    阻塞式I/O是最简单的I/O模型。也是系统默认的I/O模型。

    图中采用了recvfrom(),使用TCP时候的read()时也是类似的。

    read()或者recvfrom()被作用于阻塞的文件描述符时,直到数据报到达且被复制到应用进程的缓冲区种或者发生错误时才返回,最常见的错误时被信号中断。

  2. 非阻塞式I/O模型

    非阻塞式I/O是在实际项目中最常用的I/O模型。对于很多进程或者线程而言,采用阻塞的I/O,有可能将该进(线)程阻塞在一个I/O操作上,这样的做法是不明智的。

    On error, -1 is returned, and errno is set appropriately.

    EAGAIN The file descriptor fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the read would block. See open(2) for further details on the O_NONBLOCK flag.

    EAGAIN or EWOULDBLOCK The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.

    以上内容引自read()的man page。使用非阻塞I/O时,若没有数据立即可读,那么调用会立即返回错误,并且errno被设为EAGAINEWOULDBLOCK。对一个非阻塞的描述符反复调用read()recvfrom()被称为轮询(polling)。轮询往往和多路复用配合使用。

  3. I/O复用模型

    主要是指采用select()poll(),epoll()(Linux),kqueue()(BSD)等复用函数对多个描述符进行监听的方式。当采用这种方式时,进程阻塞在这些函数上,当注册于这些函数中的描述符允许读或写时函数返回。

    在实践中,select()poll()由于性能及易用性等问题,已较少采用,大多数多路复用的场景都被epoll()函数取代,而带来的问题则是可移植性下降,因为epoll()只存在于Linux平台上。

  4. 信号驱动式I/O模型

    使用信号,让内核在描述符就绪式发送SIGIO通知进程读取的方式。

    这种模型的优势在于等待数据报到达的器件进程不会被阻塞,主循环可以继续执行。

  5. 异步I/O模型

    告知内核启动某个操作,并让内核在整个操作(包括将数据藏内核复制到我们自己的缓冲区)完成后通知我们。

    这种模型的主要区别是不再需要调用read()recvfrom()进行读取操作,只需向异步I/O函数传入缓冲区指针和大小,内核将在负责把数据读入缓冲区,而后通知进程。这种模型应用很少,各种系统对其支持也不充分。

    这里补充一点内容。在较老的服务端模型中,采用阻塞I/O,每个连接被一个或多个线程管理是常见的方式。连接数量的增加就意味着线程数量的增加。这种模型对于几百上千的连接是可以胜任的,但对于更大量的连接,线程数量的增加导致线程间切换的开销也开始加大,耗费的内存等资源也开始增多,这种模型开始无法承受负担。

    多路复用加上非阻塞I/O的方式则允许一个线程处理多个不同的连接,对多个描述符进行监听,并且只在描述符准备就绪时才对其进行处理。这种模型大大减少了非活跃线程的数量,也即减少了资源的开销。而由于一个线程中同时处理了多个描述符,那么使用阻塞I/O则可能使线程阻塞在其中一个描述符上,使得其它描述符无法得到处理。因此需要使用非阻塞的I/O。

二、阻塞和非阻塞,同步和异步

要注意阻塞/非阻塞和同步/异步的概念的区别。

POSIX把这两个属于定义如下:

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

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

前四种模型的主要区别在于第一阶段,其第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom()调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其它四种模型。

阻塞不等于同步,非阻塞不等于异步,如在第二种模型中,采用的是非阻塞的I/O,但仍属于同步模型。

同步异步区分的关键在于将数据从内核空间复制回应用进程空间的这一过程是否阻塞进程。不管是阻塞还是非阻塞的I/O,当有数据可供消费时,在调用recvfrom()read()将数据从内核复制回应用进程空间的这个过程中,该进(线)程是阻塞的,是被挂起的,因此他们都是同步的。只不过非阻塞I/O在无数据时,会立即返回,而非阻塞I/O不会。而异步的I/O连将数据复制到进程空间的这个过程,都不阻塞进程,而直接由内核完成,之后再以别的形式通知进程。

讲清楚这点应该就较好理解UNP种所说的:

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

三、epoll()用法总结

UNP中给出了select()poll()的用法,这里不再重复。epoll()从Linux 2.5.44开始引入,目的是为了替代select()poll(),达到更高的性能,实现更高的并发。这里总结一下它的用法。

man pages:

  1. epoll
  2. epoll_create
  3. epoll_wait
  4. epoll_ctl

典型用法:

  1. epoll_create()epoll_create1()创建一个epoll实例
  2. 设置需要监听的epoll_event中的事件类型以及描述符,并将其用epoll_ctl()添加到epoll实例中
  3. 申请一个epoll_event数组,作为存放返回的活跃事件的容器
  4. 调用epoll_wait(),传入epoll_event数组,阻塞等待
  5. epoll_wait()返回时,返回值为epoll实例中活跃的事件个数,并且这些事件被写入到传入的epoll_event数组的前几个中。
  6. 处理活跃事件
  7. 返回第4步

epoll_event的定义如下:

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t; struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

电平触发和边缘触发

  1. 若用作电平触发(默认),那么epoll就只是一个速度更快的poll,它们的语法基本兼容,epoll可使用于任何poll的使用场景。
  2. 若使用了EPOLLET标志,那么epoll被用作边缘触发。采用边缘触发方式的描述符应该使用非阻塞I/O,以避免监听多个描述符的进程阻塞在其中一个描述符的read()write()操作中。
  3. 边缘触发的建议使用方式是(I)采用非阻塞描述符(II)等待read()write()返回EAGAIN。

程序示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h> #define MAX_EVENTS 10
#define SERV_PORT 9877
#define BUFFER_SIZE 1024 int get_listen_fd();
void set_non_blocking(int fd);
void do_use_fd(int fd); int main()
{
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
int n, addrlen;
struct sockaddr_in addr; epollfd = epoll_create1(0); listen_sock = get_listen_fd();
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev); for(;;){
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for(n = 0; n <nfds; ++n){
if(events[n].data.fd == listen_sock){
conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen);
if(conn_sock == -1){
if(errno != EAGAIN && errno != EWOULDBLOCK)
perror("accept");
break;
}
set_non_blocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev);
}
else{
do_use_fd(events[n].data.fd);
}
}
}
return 0; } int get_listen_fd(){
int listen_fd;
struct sockaddr_in serv_addr; listen_fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT); bind(listen_fd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
set_non_blocking(listen_fd);
listen(listen_fd, 10); return listen_fd;
} void set_non_blocking(int fd){
int s;
s = fcntl(fd, F_GETFL, 0);
s |= O_NONBLOCK;
fcntl(fd, F_SETFL, s);
} void do_use_fd(int fd){
int done = 0;
for(;;){
int count;
char buf[BUFFER_SIZE]; count = read(fd, buf, sizeof(buf));
if(count == 0){
done = 1;
break;
}
else if(count == -1){
if(errno != EAGAIN){
perror("read");
done = 1;
}
break;
}
else{
write(STDOUT_FILENO, buf, count);
}
}
if(done){
printf("Closed connection on descriptor %d\n", fd);
close(fd);
}
}

示例说明

  1. listen socket 在本例中被设为边缘触发,且是非阻塞的,那么在返回epoll_wait()等待前必须将所有到达的新连接全部处理了,直到EAGAIN被触发;
  2. 本例中的do_use_fd()也是类似的,边缘触发、非阻塞,返回等待前必须保证所有到达数据被处理;
  3. 一个描述符被关闭,将导致epoll自动将该描述符对应的事件移除。不过要注意dup2()fork()等对描述符形成的复制。只有所有引用某个文件的描述符都被关闭了,epoll才会将相应的事件移除。(详见epoll的man page,Q6)

总结

几种I/O模型、阻塞/非阻塞常常是在服务器编程选型时候需要考虑的总点,本篇是相关基础知识的一个总结。熟悉相关的API和用法也是在编程时候提高效率,避免踩坑的必要条件。

UNP学习总结(二)的更多相关文章

  1. crawler4j 学习(二)

    crawler4j 学习(二) 实现控制器类以制定抓取的种子(seed).中间数据存储的文件夹.并发线程的数目: public class Controller { public static voi ...

  2. 从零开始学习jQuery (二) 万能的选择器

    本系列文章导航 从零开始学习jQuery (二) 万能的选择器 一.摘要 本章讲解jQuery最重要的选择器部分的知识. 有了jQuery的选择器我们几乎可以获取页面上任意的一个或一组对象, 可以明显 ...

  3. Android Animation学习(二) ApiDemos解析:基本Animators使用

    Android Animation学习(二) ApiDemos解析:基本Animatiors使用 Animator类提供了创建动画的基本结构,但是一般使用的是它的子类: ValueAnimator.O ...

  4. AspectJ基础学习之二搭建环境(转载)

    AspectJ基础学习之二搭建环境(转载) 一.下载Aspectj以及AJDT 上一章已经列出了他的官方网站,自己上去download吧.AJDT是一个eclipse插件,开发aspectj必装,他可 ...

  5. WPF的Binding学习笔记(二)

    原文: http://www.cnblogs.com/pasoraku/archive/2012/10/25/2738428.htmlWPF的Binding学习笔记(二) 上次学了点点Binding的 ...

  6. AJax 学习笔记二(onreadystatechange的作用)

    AJax 学习笔记二(onreadystatechange的作用) 当发送一个请求后,客户端无法确定什么时候会完成这个请求,所以需要用事件机制来捕获请求的状态XMLHttpRequest对象提供了on ...

  7. MyBatis学习系列二——增删改查

    目录 MyBatis学习系列一之环境搭建 MyBatis学习系列二——增删改查 MyBatis学习系列三——结合Spring 数据库的经典操作:增删改查. 在这一章我们主要说明一下简单的查询和增删改, ...

  8. MyBatis学习 之 二、SQL语句映射文件(2)增删改查、参数、缓存

    目录(?)[-] 二SQL语句映射文件2增删改查参数缓存 select insert updatedelete sql parameters 基本类型参数 Java实体类型参数 Map参数 多参数的实 ...

  9. MyBatis学习 之 二、SQL语句映射文件(1)resultMap

    目录(?)[-] 二SQL语句映射文件1resultMap resultMap idresult constructor association联合 使用select实现联合 使用resultMap实 ...

  10. UML学习(二)-----类图

    UML学习(二)-----类图 http://www.cnblogs.com/silent2012/archive/2011/09/07/2169946.html http://www.cnblogs ...

随机推荐

  1. Concat层解析

    Concat层的作用就是将两个及以上的特征图按照在channel或num维度上进行拼接,并没有eltwise层的运算操作,举个例子,如果说是在channel维度上进行拼接conv_9和deconv_9 ...

  2. centos6.4下配置nginx服务器更改根目录

    安装完nginx服务器后发现nginx的根目录在/usr/share/nginx/html/下,但是对于部署文件来说,在该目录下是不太习惯的,我就尝试着更改nginx访问的根目录 #  vi /etc ...

  3. D. GCD Counting(树上dp)

    题目链接:http://codeforces.com/contest/1101/problem/D 题目大意:给你n个点,每个点都有权值,然后给你边的关系,问你树上的最大距离.(这里的最大距离指的是这 ...

  4. MVC中检测到有潜在危险的 Request.Form 值

    在做mvc项目时,当使用xhedit or.ueditor编辑器时,点击提交时,编辑器中的内容会带有html标签提交给服务器,这时就是会报错,出现如下内容: “/”应用程序中的服务器错误. 从客户端( ...

  5. ETL利器Kettle实战应用解析系列二

    本系列文章主要索引如下: 一.ETL利器Kettle实战应用解析系列一[Kettle使用介绍] 二.ETL利器Kettle实战应用解析系列二 [应用场景和实战DEMO下载] 三.ETL利器Kettle ...

  6. go语言 documentation

    Documentation文档   The Go programming language is an open source project to make programmers more pro ...

  7. 获取SQL Server的版本信息

    微软 SQL Server 版本号 产品名称 发行日期 主版本号 正式版 SP1 SP2 SP3 SP4 SQL Server 2016 2016.06.01 13.00.1601.5 13.00.1 ...

  8. SQLAlchemy-对象关系教程ORM-一对多(外键),一对一,多对多

    一:一对多 表示一对多的关系时,在子表类中通过 foreign key (外键)引用父表类,然后,在父表类中通过 relationship() 方法来引用子表的类. 在一对多的关系中建立双向的关系,这 ...

  9. Extjs6设置Store、Ajax、form的请求方式(GET、POST)

    Extjs6 设置Store.Ajax.form的请求方式(GET.POST) Ajax请求和Form的submit方法设置请求方式和原来一样,使用method : 'POST'设置 // 表单提交 ...

  10. C++ 实现memcpy和strcpy

    /** * @Method: Memcpy * @Access: public * @Return: void * * @Param : dst - 目的起始地址 * @Param : src - 源 ...