<<Linux高性能服务器编程>>(IO复用等内容)2019-07-10

Linux 简要基础知识

  • 用户空间/内核空间
    操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间,内核功能模块运行在内核空间,而应用程序运行在用户空间.
  • 缓存I/O
    缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O.在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间.

1.I/O复用

I/O复用使得程序能同时监听多个文件描述符,可以有效提高程序的性能,但本身是阻塞的,所以当多个文件描述符同时就绪时,如果不采取额外措施,将只能按顺序依次处理其中的每一个文件描述符,如果要实现高并发,则只能通过多线程或者多进程来实现.
\color{blue}{I/O多路转接技术}

  • 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中.
  • 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回;其中该函数是阻塞函数,而且对文件描述符的检测操作是由内核完成的.

1.select(轮询):

相关函数:

#include <sys/select.h>
int select (int nfds, fd_set* readfds, fd_set* writedfds, fd_set* exceptfds, struct timeval* timeout);
//如:
fd_set reads;
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
      ret = select(maxfd+1, &reads, NULL, NULL, NULL);
}

---参数:
nfds:要检测的文件描述中最大的fd+1;
readfds,writedfds,exceptfds:分别指向读集合,写集合和异常事件的描述符集合;
timeout:用来设置select的超时时间,当设置为NULL时表示永久阻塞,或者进行设置如:

struct timeval a;
 a.tv_sec = 10;
 a.tv_usec = 0;

文件描述符操作函数:

- 全部清空
○ void FD_ZERO(fd_set *set);
- 从集合中删除某一项
○ void FD_CLR(int fd, fd_set *set);
- 将某个文件描述符添加到集合
○ void FD_SET(int fd, fd_set *set);
- 判断某个文件描述符是否在集合中
○ int FD_ISSET(int fd, fd_set *set);

select的优缺点:

  • 优点:跨平台
  • 缺点:
    1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
    2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大.
    3.底层通过数组的数据结构实现,且无法突破1024个文件描述符的限制.

2.poll(轮询):

相关函数:

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);//成功时返回就绪文件描述符的总数
//如:
for(int i=0; i<1024; ++i)
    {
        allfd[i].fd = -1;
    }
    allfd[0].fd = lfd;
    allfd[0].events = POLLIN;
while(1)
{
      int ret = poll(allfd, max_index+1, -1); 
}

---参数:
fds:是一种poll类型结构体数组, 指定文件描述符上发生的可读可写与异常事件;

struct pollfd {
int fd;/* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */ - 内核给的反馈
};

nfds:数组的最大长度, 数组中最后一个使用的元素下标+1,每次系统内核都会轮询检测更新;
timeout:用来设置select的超时时间,当设置为-1时表示永久阻塞,0时会立即返回,>0时为等待的时长.
poll相比于select的优势在于poll可以突破文件描述符最大值1024的限制,且poll底层的实现采用的是内部链表数据结构.

3.epoll:

epoll是linux内核特有的复用函数,他会把用户关心的文件描述符的事件放在内核里的一个事件表上,无需像select和poll那样每次调用都要重复传入文件描述符集和事件集.但epoll需要一个额外的文件描述符,来唯一标示内核中的这个事件表,底层使用红黑树来完成.
相关函数:

#include <sys/epoll.h>
int epoll_create(int size);//创建epoll句柄,相当于创建一个根
//或者int epoll_create1(EPOLL_CLOEXEC);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//将accept后的文件描述符栓到树上
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//在超时时间内等待一组文件描述符上的事件,成功时返回就绪的文件描述符个数,
//而poll与select会返回整个文件描述符数组,用户代码需要遍历数组以找到哪些文件描述符上有活动的IO事件

---参数:
epoll_ctl函数:
epfd:是要操作的epoll_create产生的句柄;
op:为要指定的操作:

  • EPOLL_CTL_ADD 注册新的fd到epfd中
  • EPOLL_CTL_MOD 修改已注册的fd的监听事件
  • EPOLL_CTL_DEL 从epfd中删除一个fd
    fd:为要监听的文件描述符;
    events:表示要监听的事件,如EPOLLIN(可读事件),EPOLLET(使用ET工作模式).
    epoll_wait函数(作用是当检测到事件时,将所有就绪的事件从内核事件表中复制到它的第二个参数events所指的数组中,只用于就绪事件,通知内核fd文件I/O检测):
    epfd:是要操作的epoll_create产生的句柄;
    events: 表示从内核得到的就绪事件集合;
    maxevents: 告诉内核events的大小,最多监听多少个事件;
    timeout:设置超时事件.

4.epoll的工作模式

1.水平触发模式(默认的模式)

  • 只要fd对应的缓冲区有数据,epoll_wait就会返回;
  • 返回次数与发送数据的次数没有关系.
  • 相当于一个效率较高的poll.

2.边沿触发模式

  • fd默认的阻塞属性;
  • 客户端给server发数据,发一次数据server的epoll_wait就返回一次,不在乎数据是否读完,在ET模式下,读事件触发,一次性把数据要是读不完,之后,就再不会触发ET时间了,这样会使套接字一直阻塞下去,读不到新数据;要是设置为非阻塞了,那我们在处理读时间处就应该设置循环,意思就是事件尽管触发一次,但读的次数由我来决定,我要读到套接字返回EAGAIN为止。这就是使用epoll的情况时,套接字设置为非阻塞的原因!
    epoll的使用ET的实例"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;

    // 设置边沿触发
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
        printf("================== epoll_wait =============\n");

        // 遍历all数组中的前ret个元素
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 设置文件cfd为非阻塞模式
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                // 设置边沿触发
                temp.events = EPOLLIN | EPOLLET;
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
                
                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %d\n",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    ntohs(client_addr.sin_port));
                
            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) 
                {
                    continue;
                }

                // 读数据
                char buf[5] = {0};
                int len;
                // 循环读数据
                while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
                {
                    // 数据打印到终端
                    write(STDOUT_FILENO, buf, len);
                    // 发送给客户端
                    send(fd, buf, len, 0);
                }
                if(len == 0)
                {
                    printf("客户端断开了连接\n");
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1)
                {
                    if(errno == EAGAIN)
                    {
                        printf("缓冲区数据已经读完\n");
                    }
                    else
                    {
                        printf("recv error----\n");
                        exit(1);
                    }
                }
            }
        }
    }

    close(lfd);
    return 0;
}

epoll使用LT模式仿muduo的实现的epoll(更规范,考虑的东西更多):

#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
typedef std::vector<struct epoll_event> EventList;

int main(void)
{
    signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号
    signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号,僵尸进程直接交给init进程处理

     //空闲fd
   // int idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);

    //创建套接字
    int lfd = socket(AF_INET,SOCK_STREAM | SOCK_NONBLOCK ,0);
    assert( lfd >= 0);
    cout << lfd << endl;
    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8888);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    //bind绑定
    int ret2 = bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    assert(ret2 != -1);

    listen(lfd,36);
    cout << "start accept......" << endl;
    
    vector<int> clients;
    int epfd = epoll_create(5);

    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;
    
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

    eventlist events(16);
    struct sockaddr_in client_addr;
    socklen_t cli_len;
    int connfd;
    int ret;
    while(1)
    {
        ret = epoll_wait(epfd,&*events.begin(),static_cast<int>(events.size()),-1);
        assert(ret != -1);
//  cout << ret << endl;
        if(ret == 0)        //什么事没有
            continue;
        if((size_t)ret == events.size())     //扩展
            events.resize(events.size()*2);

        for(int i = 0;i<ret;++i)
        {
            if(events[i].data.fd == lfd)
            {   
                cli_len = sizeof(client_addr);
                connfd = ::accept4(lfd,(struct sockaddr*)&client_addr,&cli_len,SOCK_NONBLOCK | SOCK_CLOEXEC);
//空闲fd,防止描述符已满
//                int connfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len);
//                if(connfd == -1)
//                {
//                    if(errno == EMFILE)
//                    {
//过程:获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;
//随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空
//闲文件,把“坑”填上,以备再次出现这种情况时使用
//                        close(idlefd);
//                        idlefd = accept(lfd,NULL,NULL);
//                        close(idlefd);

//                        idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);
//                        continue;
//                    }
//                    else
//                    {
//                        perror("accept error");
//                        exit(1);
////              cout << "jjj" << endl;
//                    }
//                }
                char ip[64] = {0};
                cout << "New client IP=" << inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ip,sizeof(ip)) << ",port = "
                    << ntohs(client_addr.sin_port) << endl;
                clients.push_back(connfd);
                //将新得到的connfd挂载到树上
                struct epoll_event temp;
                temp.data.fd = connfd;
                temp.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&temp);
            }
         
            else if (events[i].events & EPOLLIN)
            { 
               
                connfd = events[i].data.fd;
                if(connfd < 0)
                    continue;
//                char buf[1024]={0};
           
                char buf[1024] = {0};
                int len = recv(connfd, buf, sizeof(buf),0);
                if(len == -1)
                {
                    if(errno == EAGAIN)
                    {
                        cout << "读完" << endl;
                    }
                    else
                    {
                        perror("read error...\n");
                        exit(1);
                    }
                }
                if(len == 0)
                {
                    cout << "客户端断开了连接" << endl;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
                    close(connfd);
                    //remove将connfd指定的fd移动到vector的末尾
                    clients.erase(std::remove(clients.begin(),clients.end(),connfd),
                            clients.end());
                    continue;

                }
                cout << buf;
                //回射回去
                write(connfd,buf,strlen(buf));
            }
        }
    }
    close(lfd);
    return 0;
}

三种I/O复用方式的比较:

\color{blue}{实现原理}
select:本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
(1) 单个进程可监视的fd数量被限制.
(2) 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大.
(3) 对socket进行扫描时是线性扫描.
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd.这个过程经历了多次无谓的遍历.
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义.
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd.
epoll:在前面说到的复制问题上,epoll使用mmap,共享内存减少复制开销。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知.
\color{blue}{消息传递方式}
select:内核需要将消息传递到用户空间,都需要内核拷贝动作.
poll:同select.
epoll:epoll通过内核和用户空间共享一块内存来实现的.

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

推荐阅读更多精彩内容

  • 本文摘抄自linux基础编程 IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设...
    VD2012阅读 1,017评论 0 2
  • 2018-11-06 这一块操作系统主要分为两个部分,一个部分是书本上操作系统的知识,还有一部门是linux的相关...
    zuoerfeng阅读 2,198评论 0 1
  • 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。所...
    Daniel521阅读 1,369评论 0 6
  • I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O...
    飞扬code阅读 2,157评论 0 6
  • 必备的理论基础 1.操作系统作用: 隐藏丑陋复杂的硬件接口,提供良好的抽象接口。 管理调度进程,并将多个进程对硬件...
    drfung阅读 3,517评论 0 5