翻译自:https://medium.com/@copyconst...Unix中I/O的基本元素是字节序列。大多数程序使用字节流或I/O流。进程通过描述符(也称为文件描述符)引用I/O流。管道、文件、POSIXIPC(消息队列、信号量、共享内存)、事件队列等都通过文件描述符引用I/O流。创建和释放描述符描述符创建:通过系统命令调用(open、pipe、socket等)创建;从父进程继承。描述符释放:进程退出系统调用closeexec上标记为close的描述符在execClose-on-exec之后被释放当一个进程fork时,所有的描述符都被复制到子进程中。如果任何描述符在exec上被标记为关闭,那么当子进程执行时,在父进程fork之后,这些描述符将被关闭并且在子进程中不再可用。使用描述符转换读写命令调用的数据FileEntry每个描述符指向内核中Fileentry的数据结构。文件条目为每个描述符指定一个文件偏移量。系统调用打开命令创建文件条目。Fork/Dup和文件入口fork创建的描述符为父子进程共享,文件入口引用相同的偏移量。dup/dup2的系统调用是相似的。#include#include#include#includeintmain(char\*argv\[\]){intfd=open("abc.txt",O\_WRONLY|O\_CREAT|O\_TRUNC,0666);叉();写(fd,“xyz”,3);printf("%ld\\n",lseek(fd,0,SEEK\_CUR));关闭(fd);返回0;}运行结果为36Offset-per-descriptor因为多个描述符可能引用同一个文件入口,所以文件入口为每个描述符维护一个文件偏移量。读写操作从这个文件偏移量开始,数据转换后文件偏移量也会更新。offset决定了下一次读写操作的位置。当进程终止时,内核将回收该进程持有的所有描述符。如果这个进程是引用文件入口的最后一个进程,内核将回收整个文件入口。分析文件条目每个文件条目包含:类型函数指针数组。这个函数指针数组将描述符上的通用操作转换为特定文件类型的实现。稍微解释一下,所有的描述符都提供了一组通用的API操作,包括读、写、修改描述符模式、截断描述符、ioctl操作、轮询等,这些操作是不同的,针对不同类型的文件有不同的实现。从套接字读取与从管道读取不同,尽管它们的高级API是相同的。打开命令这里就不一一列举了,因为不同类型文件的打开操作有很大区别。但是一旦文件条目被打开创建,其余的操作就可以使用同一套通用API。大多数网络通信使用套接字。套接字被描述符引用为传输的目的地。两个进程可以创建两个套接字,通过连接两个套接字建立可靠的字节流传输。建立连接后,可以使用文件偏移量读取和写入描述符。内核可以将一个进程的输出重定向到另一台机器上的另一个进程。对于字节流连接,统一使用readwrite命令进行读写,但是使用不同的系统命令来处理不同类型的报文(比如网络数据包)。非阻塞描述符默认情况下,当没有数据可用时,读取描述符将阻塞。写入和发送也是如此。大多数描述符操作都是如此,除了磁盘文件,因为写入磁盘不是直接写入,而是通过内核的缓冲区缓存。只有在打开磁盘文件时使用O_SYNC标志,才会同步写入磁盘。任何描述符(管道、FIFO、套接字、终端、伪终端等)都可以设置为非阻塞模式。当一个描述符被设置为非阻塞模式时,所有对该描述符的I/O调用都会立即返回,即使请求不能立即完成(请求完成期间进程将被阻塞)。返回值分为以下几种情况:错误:操作根本无法完成部分计数:输入或输出可以部分完成整个结果:I/O操作可以完全完成描述符设置为通过设置非延迟标志O_NONBLOCK来实现非阻塞模式。该标志也称为“打开文件”状态标志。描述符就绪当进程通过描述符进行I/O操作时,没有被阻塞,称为描述符就绪。描述符就绪与操作是否会传输数据无关,而只与I/O操作是否可以无阻塞执行有关。当I/O事件发生时,描述符进入就绪状态,例如新输入的到达、套接字连接的完成,或者当TCP传输排队数据时套接字的发送缓冲区可用时。判断一个描述符是否就绪有两种方式——边沿触发和电平触发LevelTriggered可以看做是一种拉动模式(pullorpollmode)。为了确定描述符是否就绪,进程会尝试执行非阻塞I/O操作。一个进程可以多次执行此操作。这为后续I/O操作提供了更大的灵活性。例如,一个描述符进入就绪状态,进程可以读取所有可用数据,或者不执行任何I/O操作,或者不读取缓冲区中的所有数据。我们举个例子,在时间t0,一个进程试图使用一个非阻塞描述符来进行I/O操作。如果I/O操作阻塞,则系统调用返回错误。在时间t1,进程再次执行I/O,假设此操作也阻塞并返回错误。在时间t2,进程再次执行I/O,假设它也阻塞或返回错误。假设在时间t3,进程拉取描述符的状态,描述符就绪。该进程可以执行整个I/O操作(例如读取套接字上的所有可用数据)。假设在时间t4,进程拉取描述符状态但是描述符还没有准备好,这个调用会再次阻塞或者返回错误。在t5时刻,描述符就绪,进程只执行一些I/O操作(例如,只读取了一半的可用数据)。在t6时刻,描述符准备就绪,进程不执行任何I/O操作。EdgeTriggered当描述符就绪时,进程会收到一个通知(通常是描述符上的新事件)。这种模式可以看作是推送模式,将描述符准备好的通知推送给进程。注意push模式只通知进程描述符就绪,不通知其他信息,比如有多少数据到达了socket缓冲区。因此,进程只能通过这种方式得到不完整的数据,所以进程需要继续运行。进程在每次收到通知时都会尝试做最多的I/O操作,如果没有,则进程必须等到下一次通知才能获取数据,即使在下一次通知之前某些数据仍然可用。下面的例子说明了在时间t2,进程被通知描述符准备就绪并且可用的字节流存储在缓冲区中,假设有1024个可读字节。假设进程只读取了500个字节,这意味着在t3t4t5时刻,缓冲区中仍有524个字节可供进程读取而不会阻塞。但是因为I/O操作只有在下次通知时才会执行,所以这524字节的数据在这段时间内会一直留在缓冲区中。假设进程在时间t6收到下一个通知,缓冲区中还有1024个字节可用。此时缓冲区中可用数据为1548字节——上次未读取524字节,新到1024字节。假设进程这次读取了1024个字节。这意味着在这个I/O操作结束后,缓冲区中还剩下524字节的数据,并且在通知到达之前进程无法读取它。当一个描述符在通知到达时尝试执行所有I/O操作时,它可能会使其他描述符饿死。即使触发了级别,大量的写入或发送也会导致阻塞。多路复用I/O上面我们只讨论了一个进程只处理一个描述符的情况。通常一个进程处理多个描述符。一个常见的场景是应用程序需要打印日志、接收套接字连接以及与其他服务建立RPC连接。多路复用I/O有几种方式:非阻塞I/O(描述符本身被标记为非阻塞,操作可能会部分完成)信号驱动I/O(当I/O状态变化过程)轮询I/O(通过select或poll系统调用,两者都提供了一个级别触发的描述符就绪通知机制)BSD机制的内核事件轮询(使用kevent系统调用)多个非阻塞I/O多路复用I/O描述符将所有描述符设置为非阻塞模式。该进程尝试对描述符执行I/O操作,并检查是否有任何I/O操作返回错误。内核内核对描述符执行I/O操作,返回错误或部分或全部结果。缺点Frequentchecking:如果进程频繁尝试执行I/O操作,则进程不得不不断重复检查描述符是否就绪的操作。这种在紧密循环中的忙碌等待会耗尽CPU周期。不经常检查:如果不经常执行这样的操作,可能会使进程长时间不响应有效的I/O事件。何时使用对输出描述符的操作(如写)并不总是阻塞的。在这种情况下,您可以尝试先执行I/O操作,如果返回错误,然后回退到轮询。当使用边沿触发通知方式时也可以使用该方式。此时,描述符被设置为非阻塞模式。一旦进程收到I/O事件通知,进程可以重复I/O操作,直到系统调用被阻塞(EAGAIN或EWOULDBLOCK)。信号驱动I/O的多路复用I/O描述符当I/O操作在任何描述符上可用时,内核会向进程发送通知。进程等待描述符准备就绪的任何信号。内核跟踪描述符列表并在任何描述符准备就绪时向进程发出信号。缺点是捕获信号的开销较大,在进行大量I/O操作时使用信号驱动的I/O方式不太现实。何时使用通常在某些“特殊情况”下使用,当处理信号的开销低于不断使用select/poll/epoll或kevent的轮询操作时。“特殊情况”场景是带外数据到达套接字。总之不常用。用于轮询I/O的多路复用I/O描述符