本文是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. 浅谈iOS与社交化网络

    CHENYILONG Blog 社交化网络 技术博客http://www.cnblogs.com/ChenYilong/ 新浪微博http://weibo.com/luohanchenyilong  ...

  2. node的导入导出

    node的每一个文件,都是一个域,那么里面所有的变量都不允许被外界引用,除非导出.要使用外界的变量,也必须使用导入的方式来导入.import 文件路径. css可以直接使用import +文件路径导入 ...

  3. C# 图片和Base64之间的转换

    public static Bitmap GetImageFromBase64String(string strBase) { try { MemoryStream stream = new Memo ...

  4. 使用转义防御XSS

    使用转义防御XSS 在输出的时候防御XSS即对用户输入进行转义,XSS的问题本质上还是代码注入,HTML或者javascript的代码注入,即混淆了用户输入的数据和代码.而解决这个问题,就需要根据用户 ...

  5. Sicily 1211. 商人的宣传

    题目链接:http://soj.me/1211 Description Bruce是K国的商人,他在A州成立了自己的公司,这次他的公司生产出了一批性能很好的产品,准备宣传活动开始后的第L天到达B州进行 ...

  6. PE结构详解

    1 基本概念 下表描述了贯穿于本文中的一些概念: 名称 描述 地址 是“虚拟地址”而不是“物理地址”.为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支.避开错误的内存位置等 ...

  7. 【黑客免杀攻防】读书笔记15 - 源码免杀、C++壳的编写

    1.源码免杀 1.1 定位产生特征的源码 定位文件特征 1.根据MyCCL的特征码定位工具,定位出有特征的地址 2.根据VS的反汇编窗口,输入有特征的地址得到特征地址与源码的关系 3.插入Messag ...

  8. 如何使用vs2012单步调试uGUI(unity3d 5.3f4)

    下载uGUI源代码 uGUI源代码地址:https://bitbucket.org/Unity-Technologies/ui 下载代码工具:tortoisehg-3.6.2-x64.msi http ...

  9. unity 欧拉旋转

    欧拉旋转   在文章开头关于欧拉旋转的细节没有解释的太清楚,而又有不少人询问相关问题,我尽量把自己的理解写到这里,如有不对还望指出.     欧拉旋转是怎么运作的     欧拉旋转是我们最容易理解的一 ...

  10. MySQL字符集编码相关

    Windows 10家庭中文版,MySQL  5.7.20,2018-05-07 Part.1 查找数据库的字符集编码 查看MySQL字符集编码:status命令 使用命令行登录MySQL服务器,然后 ...