【socket 要点回顾】I/ O 多路复用

【socket 要点回顾】I/ O 多路复用

为什么要使用I/ O 多路复用?

  1. 当一个进程有很多个I/O 需要处理的时候,1号I/O 可能没有数据可能,此时由于I/O 是阻塞读,整个进程就阻塞到从1号I/O 读数据的这个地方。什么其他事情都无法处理,即使,此时,如果有其他的I/O 有数据可以读了。这很明显是很低效的方式,那么你可能会想,我开n个线程,每个线程处理一个I/O读的事件,那么不就可以避免上述问题了吗?
  2. 确是这样的,但是呢,如果一个I/O流进来,我们就开启一个线程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个线程一一对应处理这些I/O流(——这就是传统意义下的多进程并发处理)。思考一下,一百万个线程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力

I/O 复用如何实现呢?

也就是说,我们希望应用需要同时在多个文件描述符上阻塞,并且在其中一个或多个文件描述可以读写,或出错的情况收到通知即被唤醒。

I/O 复用如何设计呢?

首先,既然要设计,我们就先忽略底层如何实现,专心于如何设计数据结构和输入输出来完成这个事情。

  1. 需要有个数据结构来保存多个文件描述符
  2. 需要有几个可以将一个文件描述符 添加,删除到该数据结构中的方法
  3. 需要有个方法,在唤醒后,来判断某个文件描述符是否是可读可写的
  4. 需要有个核心的方法,该方法接受这个数据结构,并会当没有文件可以读可写时阻塞,并且在某个或多个文件描述符可读可写后被唤醒
  5. 同时需要设置超时时间,来避免应用一直等等下去。

既然如此,我们看一下linux 下的三种实现方式 ,select ,poll,epoll

select

       /* According to POSIX.1-2001, POSIX.1-2008 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

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

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

      struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };

啊哈,看起来和我们所设想的实现方式蛮相似的

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

// nfds:NFD应该设置为三个集合中编号最高的文件描述符的值 加上1。

// readfds,writefds,exceptfds:指定了我们让内核测试读、写和异常条件的描述字

// fd_set:为一个存放文件描述符的信息的结构体,可以通过下面的宏进行设置。


void FD_ZERO(fd_set *fdset);
//清空集合
void FD_SET(int fd, fd_set *fdset);
//将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);
//将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);
// 检查集合中指定的文件描述符是否可以读写

注意 : nfds:nfds 应该设置为三个集合中编号最高的文件描述符的值 加上1

在这里注意一下linux select 的实现方式

  1. fd_set 实现方式是一个位数组。
  2. 因为fd_set 是一个位数组,为了使用FD_ISSET 必须用额外一个数组,来存储所有待监听的文件描述符。
  3. 当select 调用成功后,每个集合都会被修改成只包含I/O 就绪的文件描述符集合。
  4. 其中的时间的结构体中的内容会被修改,而且把值改为剩余时间。
## EXAMPLE
       #include <stdio.h>
       #include <stdlib.h>
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int
       main(void)
       {
           fd_set rfds;
           struct timeval tv;
           int retval;

           /* Watch stdin (fd 0) to see when it has input. */

           FD_ZERO(&rfds);
           FD_SET(0, &rfds);

           /* Wait up to five seconds. */

           tv.tv_sec = 5;
           tv.tv_usec = 0;

           retval = select(1, &rfds, NULL, NULL, &tv);
           /* Don't rely on the value of tv now! */

           if (retval == -1)
               perror("select()");
           else if (retval)
               printf("Data is available now.\n");
               /* FD_ISSET(0, &rfds) will be true. */
           else
               printf("No data within five seconds.\n");

           exit(EXIT_SUCCESS);
       }

小提一下,可以用select 替换sleep 来做到更通用的,精确的睡眠阻塞。

poll

看一下另一个实现方式,有种面向对象的感觉了。

       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

       // The timeout argument specifies the number of milliseconds

很简洁,一个结构体,一个核心函数。封装了一下,这里抽象了一个事件的结构体。

设置事件就是给events 赋值,判断事件就看revents的值

man poll 就OK了

在正式述说epoll 前先说一下select 和 poll 的优缺点。

  1. 由于select 使用的是位数组,在文件描述符很大的时候,效率会低一些。
  2. 其次,select 需要计算最大的文件描述符的值。
  3. 但是呢 ,select 的时间精度更高一些。
  4. 而且呢 ,select 可移植性好一些。

select 和 poll 有两点不太好的地方

  1. 每次调用都需要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。
  2. 每次为了获得可读可写的文件描述符都需要遍历存储了所有监听的文件描述符的表,非常低效。

epoll

那么epoll 是如何解决这个问题的呢?

首先我们分析一下这个过程,添加一个文件描述符是一个低频的操作,然而监听这个操作是一个高频的事件。那么,为何不把 注册监听和实际监听分离出来。这样,整个过程变成了,

  1. 创建一个上下文。
  2. 添加,删除或修改一个文件描述符到上下文中去。
  3. 进行事件等待。

同时,epoll 会返回一个数值,代表有几个事件就绪,并返回所有事件成立的数组。

epoll 的实现里,他将添加删除修改,封装成了一个函数,通过一个额外的一个控制位来控制具体的操作。

那么以下就一个epoll 实现I/ O 复用 的一个框架。

           #define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

           epollfd = epoll_create1(0);
           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }

           for (;;) {
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }

               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) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 必备的理论基础 1.操作系统作用: 隐藏丑陋复杂的硬件接口,提供良好的抽象接口。 管理调度进程,并将多个进程对硬件...
    drfung阅读 3,525评论 0 5
  • 本文讨论的背景是Linux环境下的network IO。 一、 概念说明 在进行解释之前,首先要说明几个概念: 用...
    faunjoe阅读 4,353评论 1 15
  • IO模型介绍 为了更好地了解IO模型,我们需要事先回顾下:同步、异步、阻塞、非阻塞 同步(synchronous)...
    可笑的黑耀斑阅读 1,162评论 0 2
  • 一、概述 I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。 I/O复用虽然能同时监听多个文...
    saviochen阅读 1,037评论 0 4
  • 同步、异步、阻塞、非阻塞 同步 & 异步 同步与异步是针对多个事件(线程/进程)来说的。 如果事件A需要等待事件B...
    rainybowe阅读 2,875评论 0 9