【APUE笔记】第十二章 高级I/O

1.非阻塞I/O

  系统调用分成两类:低速系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用:
(1)如果数据并不存在,则读文件可能会使调用者永远阻塞(例如读管道,终端设备和网络设备)。
(2)如果数据不能立即被接受,则写这些同样的文件也会使调用者永远阻塞。
(3)在某些条件发生之前,打开文件会被阻塞(例如打开一个终端设备可能需等到与之连接的调制解调器应答;又例如若以只写方式打开FIFO,那么在没有其他进程已用读方式打开该FIFO时也要等待)。
(4)对已经加上强制性记录锁的文件进行读、写。
(5)某些ioctl操作。
(6)某些进程间通信函数。

  非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。

  对于一个给定的描述符有两种方法对其指定非阻塞I/O:
(1)如果是调用open以获得该描述符,则可指定O_NONBLOCK标志。
(2)对于已经打开的一个描述符,则可调用fcntl打开O_NONBLOCK文件状态标志。

2.记录锁

  记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。

2.1.fcntl记录锁
#include <sys/types.h>
#include <unistd.h>
#include <fcnt1.h>
int fcnt1(int filedes,int cmd,.../* struct flock *flockptr* / ) ;
返回:若成功则依赖于cmd,若出错则为- 1

  对于记录锁,cmd是FGETLK、FSETLK或FSETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。

struct flock {
      short l_type;    /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */
      short l_whence;  /* 加锁的起始位置:SEEK_SET, SEEK_CUR, SEEK_END */
      off_t l_start;   /* 加锁的起始偏移,相对于l_whence */
      off_t l_len;     /* 上锁的字节数,如果为0,表示从偏移处一直到文件的末尾*/
      pid_t l_pid;     /* 已经占用锁的PID(只对F_GETLK 命令有效) */
};

flock结构说明:
(1)所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)。
(2)要加锁或解锁的区域的起始地址,由l_start和l_whence两者决定。l_stat是相对位移量(字节),l_whence则决定了相对位移量的起点。这与lseek函数中最后两个参数类似。
(3)区域的长度,由l_len表示。

关于加锁和解锁区域的说明还要注意下列各点:
(1)该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置。
(2)如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。
(3)为了锁整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,l_len说明为0。

  共享读锁(I_type为L_RDLCK)和独占写锁(L_WRLCK)的基本规则:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上的写锁则只能由一个进程独用。更进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。具体如下表格:

读锁写锁
无锁可以可以
一把或多把读锁可以拒绝
一把写锁拒绝拒绝

注:加读锁时,该描述符必须是读打开;加写锁时,该描述符必须是写打开。

死锁:
  如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能性。检测到死锁时,内核必须选择一个进程收到出错返回。

2.2.锁的隐含继承和释放

三条规则:
(1)锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
(2)由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。
(3)在执行exec后,新程序可以继承原执行程序的锁。

2.3.建议性锁和强制性锁

  考虑数据库存取例程序。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数存取数据库的任何进程集为合作进程( cooperating process)。如果这些函数是唯一的用来存取数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件有写许可权的任何其他进程写数据库文件。不使用协同一致的方法(数据库存取例程库)来存取数据库的进程是一个非合作进程。

  强制性锁机制中,内核对每一个open、read和write都要检查调用进程对正在存取的文件是否违背了某一把锁的作用。

  如果一个进程试图读、写一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了读、写锁,此时会发生什么呢?对这一问题的回答取决于三方面的因素:
(1)操作类型(read或write)
(2)其他进程保有的锁的类型(读锁或写锁)
(3)有关描述符是阻塞还是非阻塞的。
  下表列出了这8种可能性:
from APUE
  EAGAIN:在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。(本段来自https://www.cnblogs.com/big-xuyue/p/3905597.html)

3.流

3.1.流简介

  流是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法。
  流在用户进程和设备驱动程序之间提供了一条全双工通路。流无需和实际硬件设备直接对话,流也可以用作为伪设备驱动程序。
  在流首之下可以压入处理模块。这可以用ioctl实现。
  任一个数的处理模块可以压入流。我们使用术语“压入”,是因为每一新模块总是插到流首之下,而将以前压入的模块下压。(这类似于后进、先出的栈。)
一个简单流:
from APUE
具有处理模块的流:
from APUE

3.2.流消息

  流的所有输入和输出都基于消息。流首和用户进程使用read、write、getmsg、getpmsg、putmsg和putpmsg交换消息。在流首、各处理模块和设备驱动程序之间,消息可以顺流而下,也可以逆流而上。
  在用户进程和流首之间,消息由下列几部分组成:消息类型、可选择的控制信息以及可选择的数据。
为write、putmsg和putpmsg产生的流消息的类型:
from APUE

3.3.putmsg和putpmsg函数

  putmsg和putpmsg函数用于将流消息(控制信息或数据,或两者)写至流中。这两个函数的区别是后者允许对消息指定一个优先波段。

#include <stropts.h>
int putmsg(int filedes,const struct strbuf *ctlptr,
             const struct strbuf *dataptr,int flag) ;
int putpmsg(int filedes,const struct strbuf *ctlptr,
             const struct strbuf dataptr,int band,int flag) ;
两个函数返回:若成功则为0,若出错则为- 1

  对流也可以使用write函数,它等效于不带任何控制信息,flag为0的putmsg。这两个函数可以产生三种不同优先级的消息:普通、优先波段和高优先级。
3.4.getmsg和getpmsg函数

#include <stropts.h>
int getmsg(int filedes,struct strbuf *ctlptr,
             struct strbuf *dataptr,int * flagptr) ;
int getpmsg(int filedes,struct strbuf * ctlptr,
            struct strbuf *dataptr, int *bandptr, int *flagptr) ;
两个函数返回:若成功则为非负值,若出错则为- 1

  注意,flagptr和bandptr是指向整型的指针。在调用之前,这两个指针所指向的整型单元中应设置成所希望的消息类型,在返回时,此整型量设置为所读到的消息的类型。
  如果flagptr指向的整型单元的值是0,则getmsg返回流首读队列中的下一个消息。如果下一个消息是高优先权消息,则在返回时, flagptr所指向的整型单元设置为RS_HIPRI。如果希望只接收高优先权消息,则在调用getmsg之前必须将flagptr所指向的整型单元设置为RS_HIPRI。
  getpmsg使用了一个不同的常数集。并且它使用bandptr指明特定的优先波段。

4.多路转接(多路复用)

  多路复用函数有select、poll、epoll。举个例子,假如你在宿舍楼下等你女神下楼,宿管大妈有三种:
(1)select大妈:每一个女生下楼,select大妈都不知道这个是不是你女神,她需要一个一个地询问,并且select大妈能力有限,最多一次帮你监视1024个女生。
(2)poll大妈:不限制盯着女生的数量。只要是经过宿舍楼下的女生,都会帮你去问是不是你女神。
(3)epoll大妈:不限制盯着女生的数量。并且不需要一个一个地询问。epoll大妈会为每个进宿舍楼的女生脸上贴一个大字条,上面写着女生的名字,只要女生下楼了,epoll大妈就知道这个是不是你女神了,然后再通知你。

  假如有一个需求:我们需要在一个程序里要查看按键是否按下,如果按键按下那么蜂鸣器响;同时还要从串口读数据进行处理;同时也要处理网络上来的数据。如果按照阻塞的模式来写代码:

if( read(fd_keypad, buf, sizeof(buf) > 0 )//阻塞1
{
	turn_buzzer(on);
}
if( read(fd_serialport, buf, sizeof(buf)) > 0 )//阻塞2
{
	process_serialport_data(buf);
}
if( read(fd_socket, buf, sizeof(buf)) > 0 )//阻塞3
{
	process_socket_data(buf);
}

  从上面的伪代码中可以看到,如果按键此时没按下(即数据没有准备好),read调用就不会返回,即使现在串口或者网络有数据传来也无法处理。如果有一种方法能同时监听按键、串口、网络的事件,一旦事件发生就通知大家,并告诉是谁的事件、什么事件就好了。这种实现方式叫多路复用。

4.1.select多路复用
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h> 
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *except, struct timeval *timeout);
返回:准备就绪的描述符数,若超时则为0,若出错则为- 1

  select监视并等待多个文件描述符的属性发生变化,它的监视分为3类,分别是readfds(文件描述符有数据到来可读)、writefds(文件描述符可写)、和exceptfds(文件描述符异常)。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时(timeout指定等待时间)发生函数才返回。当select函数返回后,可以通过遍历fdset,来找到究竟是哪些文件描述符就绪。

  第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1。Linux内核从0开始到max_fd-1扫描文件描述符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么Linux内核实际也要监测0-7,此时真正带测试的文件描述符是0~10总共11个,及max(8,9,10)+1,所以第一个参数是所有要监听的文件描述符中最大的+1。

  中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL。

  最后一个参数是设置select的超时时间,如果设置为NULL则为永不超时。

注意:待测试的文件描述集总是从0,1,2,…开始的。所以,假如你要监测的描述符为8,9,10,那么系统实际也要监测0,1,2,3,4,5,6,7,此时真正待测试的描述符的个数为11个,也就是max(8,9,10)+1

  在Linux内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,这也意味着select所用到的FD_SET是有限的,也正是这个原因select()默认只能同时处理1024个客户端的连接请求:
/linux/posix_types.h:
#define __FD_SETSIZE 1024

使用select多路复用实现网络socket服务器多路并发的流程图:


  基于select的I/O复用模型可以用单个进程为多个客户端服务,这样可以减少创建线程的或进程所需要的CPU时间片或内存资源的开销;此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点。当然它也有几个主要的缺点:
1.每次调用select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd,这时如果客户端fd很多时会导致系统开销很大。
2.单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()、修改宏定义甚至重新编译内核等方式来提升这一限制,但是这样也会造成效率的降低。
3.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行I/O操作,那么之后每次select调用还是会将这些文件描述符通知进程。

4.2.poll多路复用

  select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V中引入的。poll()的机制与select()类似,与select()在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll()没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll()和select()同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

#include <stropts.h>
#include <poll.h>

struct pollfd
{
	int fd;
	short events;//等待的事件
	short revents;//实际发生了的事件
};

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

 第一个参数用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。

 第二个参数nfds指定数组中监听的元素个数。

 第三个参数timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

4.3.epoll多路复用

  在linux没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等I/O多路复用的方法来实现并发服务程序。自Linux2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术,在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地越来越有限,风头已经被epoll占尽。

  相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但是其他的缺点依然存在。拿select模型为例,假设我们的服务器需要支持100万的并发连接,在__FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此。基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

  epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用I/O接口select/pol的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核I/O事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存I/O状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

  LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行I/O操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

  ET(edge-triggered)是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

  ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。

  由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几干的并发连接。

  epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:
1.调用epoll_create()建立一个epoll对象(在epol文件系统中为这个句柄对象分配资源)
2.调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3.调用epoll_wait收集发生的事件的连接

4.3.1.创建epoll实例:epoll_creat()
#include <sys/epoll.h>
int epoll_create(int size);

  系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。从Linux2.6.8版以来,size参数被忽略不用。

  作为函数返回值,epoll_create()返回了代表新创建的epoll实例的文件描述符。这个文件描述符在其他几个epoll系统调用中用来表示epoll实例。当这个文件描述符不再需要时,应该通过close()来关闭。当所有与epoll实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。该系统调用执行的任务同epoll_create()一样,但是去掉了无用的参数size,并增加了一个可用来修改系统调用行为的flags参数。目前只支持一个flag标志:EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭标志。

4.3.2.修改epoll的兴趣列表:epoll_ctl()
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);

  系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。若成功返回0,若出错返回-1。

参数:
  第一个参数epfd是epoll_create()的返回值;
  第二个参数op用来指定需要执行的操作,它可以是:
(1)EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已经存在的文件描述符,epoll_ctl()将出现EEXIST错误。
(2)EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。
(3)EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表中移除。
  第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
  第四个参数ev是指向结构体epoll_even的指针,结构体的定义如下:

typedef union epoll_data
{
	void	*ptr;//pointer to user-defined data
	int		fd;
	uint32_t	u32;
	uint64_t	u64;
}epoll_data_t;

struct epoll_event
{
	uint32_t	events;//epoll events(bit mask)
	epoll_data_t	data;/user data
}

参数ev为文件描述符fd所做的设置(epoll_event)如下:
(1)events字段是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合;
(2)data字段是一个联合体,当描述符fd稍后成为就绪态时,联合的成员可用来指定传回给调用进程的信息。

4.3.3.事件等待:epoll_wait()
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

  系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。调用成功后epoll wait()返回数组evlist中的元素个数,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0,出错时返回-1并在errno中设定错误码以表示错误原因。

参数:
  第一个参数epfd是epoll_create()的返回值;
第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;
第三个参数maxevents指定所evlist数组里包含的元素个数;
第四个参数timeout用来确定epoll_wait()的阻塞行为,有如下几种:
(1)如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
(2)如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
(3)如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。

  数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。注意,data字段是唯一可获知同这个事件相关的文件描述符的途径。因此,当我们调用epol_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体。

  当我们调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下所示:

默认情况下,一旦通过epoll_ctl()的EPOLL_CTL_ADD操作将文件描述符添加到epoll实例的兴趣列表中后,它会保持激活状态(即之后对epoll_wait()的调用会在描述符处于就绪态时通知我们)直到我们显示地通过epoll_ctl()的EPOLL_CTL_DEL操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()的ev.events中指定EPOLLONESHOT标志。如果指定了这个标志,那么在下一个epoll_wait()调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的epoll_wait()调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后用过调用epoll_ctl()的EPOLL_CTL_MOD操作重新激活对这个文件描述符的检查。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值