IO/Linux部分

1、I/O多路复用

首先需要明确的是,Linux有五类 io 模型:阻塞 IO模型,非阻塞 IO 模型, IO复用模型,信号驱动IO模型,异步IO模型

1.阻塞I/O模型:最简单的一种IO模型,简单理解就是死等,即进程或线程一直等待条件,不满足则一直等待
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

2.非阻塞I/O模型:应用进程与内核交互,目的未到达之前会直接返回,然后不断轮询,不停的去问内核数据是否准备好?如果发现准备好了,就把数据拷贝到用户空间中。在两次发送请求的时间段,进程可以先做别的事情。
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。耗费:往返车站6次,路上6小时,其他时间做了好多事。

3.I/O复用模型:多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间
①.select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:打电话
②.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:无需打电话

4.信号驱动I/O模型:应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝到用户空间中。
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:无需打电话

信号驱动,内核是在数据准备好之后通知进程,然后进程再进行数据拷贝,我们可以认为数据准备阶段是异步的,但是,数据拷贝操作是同步的,所以,整个IO过程也不能认为是异步的。

5.异步I/O模型:应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:无需打电话

五种IO模型对比

 

回到IO多路复用上来,I/O多路复用的机制是:让单个线程(这里的复用就是指的复用同一个线程)通过监视多个文件描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作。如下图所示:

                     

有很多连接进来,epoll(select/poll)会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

IO多路复用又被称为事件驱动,即 reactor 模式。

多线程的方式存在一个很大的弊端,需要CPU上下文的切换,所以转回用单线程的方式处理大量的访问连接(即多路IO复用)

select函数介绍

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval*timeout);
  1. nfds 参数指定被监听的文件描述符的总数。它通常被设置为 select 监听的所有文件描述符的最大值加1,因为文件描述符是从 0 开始计数的。
  2. readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。这3个参数是fd_set结构指针类型,是一个bitmap,用来表征哪一个文件描述符是被监听的,默认是有1024位,需要被监听的置为1,不需要监听的置为0。

  1. 上面例子中,首先创建了fds存放了有5个文件描述符,可以接受5个客户端的连接;接着使用 void FD_ZERO(fd_set *fdset);函数将 rset集合清空,然后利用void FD_SET(int fd, fd_set *fdset);函数,将 fds中的5个文件描述符循环写入rset中。
  2. 程序运行在用户态,但select函数会将用户态中的 bitmap(即rset)拷贝到内核态,由内核态判断是否有数据来(效率比用户态更高);如果没有数据,内核态就会一直在这判断,整个程序会呈阻塞状态,即select函数是一个阻塞函数,如果没有任何数据到来,那么程序一直会阻塞在select这一行;有数据的时候,内核会做两件事情,第一,会将有数据的fd置位(将fd表示为有数据来了)(注意:这里的fd置位中的FD其实是 rset对应的那一位,而不是真正的fds中的元素),第二,select会返回(不再阻塞了,程序执行到下面一行)。
  3. 接下来会遍历FD,并使用 int FD_ISSET(int fd, fd_set *fdset); 检查fds中的文件描述符是否可以读写(被置位了),被置位的数据,会将其读出来并进行处理。因为可能同时几个fd都有数据,所以需要遍历5个fd,看看有没有数据

select的缺点

  1. 每个fd_set结构体最多只能标识1024个描述符
  2. rset不可重用,每次调用select的时候,都需要重新初始化并赋值readset结构体,将需要监听的描述符对应的bit置为1,而不能直接使用readset,因为这个时候readset已经被内核改变了。
  3. 每次调用select时,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  4. select在返回的时候并不知道哪些数据就位,需要遍历所有的fd。

加深理解:

fd_set的四个关联的api

void FD_ZERO(fd_set *fdset) //清空fdset,将所有bit置为0
void FD_SET(int fd, fd_set *fdset) //将fd对应的bit置为1
void FD_CLR(int fd, fd_set *fdset) //将fd对应的bit置为0
void FD_ISSET(int fd, fd_set *fdset) //判断fd对应的bit是否为1,也就是fd是否就绪

理解select模型的关键在于理解fd_set,fd_set中的每一bit可以对应一个文件描述符fd。

  1. 执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
  2. 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
  3. 若再加入fd=2,fd=1,则set变为0001,0011
  4. 执行select(6,&set,0,0,0)阻塞等待
  5. 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空

 

poll函数介绍

第一个参数是指向一个结构体数组第一个元素的指针。每个数组元素都是一个pollfd结构

struct pollfd {
    int fd;                    // poll的文件描述符
    short int events;          // poll关心的事件类型
    short int revents;         // 发生的事件类型
  };

revents为经过poll调用之后返回的事件类型,在调用poll的时候,一般会传入一个pollfd的结构体数组,数组的元素个数表示监控的描述符个数,所以pollfd相对于select,没有最大1024个描述符的限制。

  1. poll解决了select的两个缺点,首先是pollfd这样的数组没有1024的限制。
  2. 每次内核都是置位revents字段,而不是对整个操作,所以我们恢复一下revents即可,所以pollfd可以重用。

epoll函数介绍

epoll 使用一组函数来完成任务,而不是单个函数。epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用 epoll_create函数来创建:

int epoll_create (int __size);

epoll_create函数创建一个epoll实例并返回,该实例可以用于监控__size个文件描述符

 

int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);

该函数用来向epoll中注册事件函数,其中__epfd为epoll_create返回的epoll实例,__op表示要进行的操作,__fd为要进行监控的文件描述符,__event要监控的事件。

 

int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

epoll_wait类似与select中的select函数、poll中的poll函数,等待内核返回监听描述符的事件产生,

 

  • 每次调用select或者poll,都需要将监听的fd_set或者pollfd发送给内核态,如果需要监听大量的文件描述符,这样的效率是很低下的
  • 在内核态中,每次需要对传入的文件描述符进行轮询,查询是否有对应的事件产生。

epoll的高效在于将这些分开,首先epoll不是在每次调用epoll_wait的时候,将描述符传送给内核,而是在epoll_ctl的时候传送描述符给内核,当调用epoll_wait的收,不用每次都接收

不像select和poll使用一个单独的API函数,在epoll中,使用epoll_create创建一个epoll实例,然后当调用epoll_ctl新增监听描述符的时候,这个时候才将用户态的描述符发送到内核态,因为epoll_wait调用的频率肯定要比epoll_create的频率要高,所以当epoll_wait的时候无需传送任何描述符到用户态;

 

2、文件描述符

文件描述符是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。程序刚刚启动时,第一个打开的文件是0,第二个是1,以此类推。也可以理解为文件的身份ID。

 

3、Linux Top命令

Linux中的top命令就像是Windows中的任务管理器。它会以列表的形式展示出系统的当前状态以及进程信息,并且定时刷新。

直接执行top命令,就会看到如下界面:

第一行:概况;第二行:进程计数(Tasks);第三行:CPU使用率(%Cpu(s));

第四、五行:物理内存和交换空间(Mem/Swap);以下所有行:进程详细信息

在top命令的界面中,可以输入一些指令实现交互性的操作,下面列出一些比较常用的交互性操作。

  • CTRL+L:刷新整个屏幕,重新开始显示。
  • h:显示帮助。
  • q:退出top程序。
  • 空格:立即刷新信息
  • k:杀掉进程。输入k之后,会提示用户输入PID及要发送哪种信号。

 

4、epoll 和 select之间的区别?

  1. epoll不需要每次调用的时候都在用户态和内核态拷贝文件描述符集合,而select需要。
  2. epoll在内核态通过红黑树和回调的方式处理文件描述符,返回时只需要遍历就绪链表即可,而select通过数组的方式处理,每次需要遍历整个数组判断哪些文件描述符就绪。
  3. epoll使用maxevents参数指定最多监听的文件描述符的个数,最多可以到达系统允许打开的最大文件描述 符个数,而select允许监听的最大文件描述符数量有限制。
  4. epoll可以工作在高效的ET模式,而select只能工作在相对低效的LT模式。

 

5、epoll需要在用户态和内核态拷贝数据么?

答:在注册监听事件时从用户态将数据传入内核态;当返回时需要将就绪队列的内容拷贝到用户空间。

 

6、epoll的实现知道么?在内核当中是什么样的数据结构进行存储,每个操作的时间复杂度是多少?

答:在内核当中是以红黑树的方式组织监听的事件,查询开销是O(logn)。采用回调的方式检测就绪事件,时间复杂的为O(1);

 

7.epoll的水平触发和边沿触发有什么区别?

答:水平触发(Level Trigger):是eopll的默认工作模式,当epoll_wait检测到某文件描述符上有事件发生,并通知应用程序之后,应用程序可以不立即处理该事件,当下次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,直到该事件被应用程序处理。

       边缘触发(Edge Trigger): 当epoll_wait检测到某文件描述符上有事件发生,并通知应用程序之后,应用程序必须立即处理该事件。如果应用程序不处理,下次调用epoll_wait时,epoll_wait不会再次通知此事件。

       ET模式很大程度上减少了epoll事件被重复触发的次数,因此效率比LT模式下高。

 

 

 

 

 

 

 

5.epoll使用的是回调的方式将就绪文件描述符插入就绪链表,返回时用户只需要O(1)的时间开销寻找就绪事 件,而select采用轮询的方式,需要O(n)的时间开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潇湘夜雨~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值