译自: https://medium.com/@copyconst...
Unix中I/O的基本组成元素是字节序列。大多数程序应用于字节流或I/O流。
进程经过描述符引用I/O流,也被称做文件描述符。管道、文件、POSIX IPC's(消息队列,信号量,共享内存),事件队列等都是经过文件描述符引用I/O流。数组
描述符建立:网络
描述符释放:数据结构
当进程forks时,全部描述符都会复制到子进程中。若是任意描述符被标记为close on exec,那么当子进程execs以前,父进程forks以后,这些描述符将关闭而且在子进程中再也不可用。socket
使用描述符经过read、 write命令调用的数据转换函数
每一个描述符都指向内核中的File entry的数据结构。file entry为每一个描述符维度了一个file offset。系统调用命令open建立file entry.spa
由fork建立的描述符被父子进程共享,在file entry中引用同一个offset。dup/dup2的系统调用与此相似。3d
#include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(char \*argv\[\]) { int fd = open("abc.txt", O\_WRONLY | O\_CREAT | O\_TRUNC, 0666); fork(); write(fd, "xyz", 3); printf("%ld\\n", lseek(fd, 0, SEEK\_CUR)); close(fd); return 0; }
运行结果指针
3 6
由于多个描述符可能引用同一个file entry, file entry为每一个描述符维护了一个file offset。read和write操做从这个file offset开始,而且在数据转换以后file offset也将更新。offset决定了下次read write操做的位置。当进程终止时,内核将回收全部该进程所持有的描述符,若是此进程是引用file entry的最后一个进程,内核将回收整个file entry。日志
每一个file entry包含:code
稍微解释下,全部的描述符都对外提供了一套通用的API操做,包含读、写、修改描述符模式、截断描述符、ioctl操做、polling等。
针对不一样类型的文件,这些操做都有所不一样,而且有不一样的实现。对sockets的读操做与对pipes的读操做就有所不一样,即便它们高层次的API是同样的。open命令并不在此列,由于不一样类型的文件的open操做差别很是大。可是一旦file entry由open建立,剩下的操做均可以使用同一套通用的API
大多数的网络通信使用sockets。sockets由描述符引用,做为传输的终点。两个进程能够建立两个sockets,经过链接这两个sockets创建可靠的字节流传输。一旦链接创建,描述符可使用file offsets进行读写。内核能够将一个进程的输出重定向到另外一台机器的另外一个进程。对于字节流链接,统一使用read write命令读写,但对于不一样类型的消息(好比网络数据包)使用不一样的系统命令处理。
默认状况下,在没有数据可用时,经过描述符read将阻塞。write和send也是如此。多数描述符的操做都是如此,可是磁盘文件除外,由于写磁盘并非直接写,而是经过内核的buffer cache。只有当open磁盘文件时使用O_SYNC标识才会同步写磁盘。
任何描述符(pipes, FIFOs, sockets, terminals, pseudo-terminals等)均可以设置为非阻塞模式。当一个描述符设置为非阻塞模式时,对此描述符的I/O调用都将当即返回,即便此请求并不能立刻完成(请求完成期间将使进程阻塞)。返回值分为下列状况:
经过设置非延迟标识O_NONBLOCK将描述符设置为非阻塞模式。这个标识也被叫作“open-file”状态标识。
当进程经过描述符执行I/O操做时不被阻塞,称为描述符就绪。描述符就绪与操做是否会传输数据无关,而只与I/O操做是否能够无阻塞执行相关。
当有I/O事件发生时描述符进行就绪状态,例如新输入的到达、socket链接完成或者当TCP将列队中的数据传输后,socket的发送buffer出现可用容量时。
有两种方式能够判断一个描述符是否进入就绪状态——edge triggered和level triggered
能够把level triggered看做是拉模式(pull或poll模式)。为了判断一个描述符是否就绪,进程尝试执行非阻塞的I/O操做。进程能够执行任意次这样的操做。这为随后的I/O操做提供了更多灵活性。好比,一个描述符进入就绪状态,进程能够读取全部可用数据,也能够不执行任何I/O操做,或者不读取buffer中的全部数据。
下面举例来看下
在t0时间,进程尝试使用非阻塞描述符进行I/O操做。若是I/O操做阻塞,系统调用返回error。
在t1时刻,进程再一次执行I/O,假设此次操做也阻塞并返回error。
在t2时刻,进程又执行了I/O,假设也阻塞或返回error。
假设到了t3时刻,进程拉取描述符的状态而且描述符就绪。进程能够执行整个I/O操做(例如读取socket上全部可用数据)
假设t4时刻,进程拉取描述符状态但描述符并无就绪,此次调用将再次阻塞或返回error。
t5时刻,描述符就绪,进程只执行了部分I/O操做(例如只读取一半可用数据)
t6时刻,描述符就绪,进程什么I/O操做也没执行
当描述符就绪时,进程将收到一个通知(一般是描述符上有新事件发生)。能够把这种模式看做是push模式,这个描述符就绪的通知是被push给进程的。注意,push模式仅通知进程描述符已就绪,而不会通知其余信息,好比有多少数据已到达socket的buffer中。
所以,经过这种方式进程只能获取到不完整的数据,因此进程须要继续进行操做。当每次获得通知时,进程尝试进行最多的I/O操做,若是不这样作,进程不得不等到下一次获得通知时才能获取数据,即便在下一次通知到来前仍有部分数据可用。
下面举例说明
在t2时刻,进程获得描述符就绪的通知
可用的字节流存储在buffer中,假设有1024个字节可读。
假设进程只读取了其中的500个字节
这意味着在t3 t4 t5时刻,buffer中仍然有524个字节可以使进程无阻塞地读取。可是由于只有在它获得下次通知时才会执行I/O操做,这524个字节的数据在这期间将一直留在buffer中。
假设进程在t6时刻接到下次通知,buffer中又有1024个字节可用。此时buffer中可用的数据为1548个字节——524字节是上次没读的,1024是新到达的。
假设进程此次读取了1024字节。
这意味着在此次I/O操做结束后仍有524字节的数据留在buffer中,直到一次通知到来进程才能读取到。
当一个描述符在通知来到时若是尝试执行全部I/O操做,可能形成其余描述符“饥饿”。即便使用level triggered,一次大量的write或send也可能致使阻塞。
上面咱们只讨论了一个进程只处理一个描述符的状况。一般进程处理多个描述符。一个常见的场景是一个应用程序须要打印日志,同时接收socket链接而且和其余服务创建RPC链接。
有如下几种多路复用I/O方式:
将全部描述符都设置为非阻塞模式
进程尝试对描述符执行I/O操做,检查是否有任意I/O操做返回error。
内核在描述符上执行I/O操做,返回error或部分输出或者是所有结果。
频繁检查:若是进程频繁尝试执行I/O操做,进程不得不持续地重复检查描述符是否就绪的操做。在tight循环中这样的busy-waiting可能会耗尽CPU周期。
不频繁检查:若是这样的操做执行不频繁,可能使进程对于有效的I/O事件长时间得不到响应。
对于输出描述符(好比write)的操做并不老是阻塞的。在这种场景下,能够首先尝试执行I/O操做,若是返回error再回退到polling。当使用edge-triggered通知方式时也可使用这种方式,此时描述符设置为非阻塞模式,进程一旦获得一个I/O事件的通知,进程能够重复执行I/O操做直到系统调用被阻塞(EAGAIN or EWOULDBLOCK)。
当任意描述符上可执行I/O操做时,内核将发送通知给进程。
进程等待任何描述符就绪的信号。
跟踪描述符列表,当任意描述符就绪时给进程发送信号通知。
捕获信号的开销较大,当大量I/O操做时使用信号驱动I/O方式并不现实。
一般在一些“特例条件”下使用,此时处理信号的开销低于不断使用select/poll/epoll或kevent的polling操做。一个“特例条件”的场景是socket上的带外(out-of-band)数据的到达。总之不经常使用。