码迷,mamicode.com
首页 > 系统相关 > 详细

Linux IO

时间:2015-11-29 13:24:10      阅读:190      评论:0      收藏:0      [点我收藏+]

标签:

 

1. I/O概述

1.1 文件类型

    UNIX将系统所有的内容都视为文件,其中文件类型包括如下几种:

  • 普通文件(regular file):这是最常用的文件类型,对于这种数据是文件还是二进制数据,对于UNIX内核而言并无差别。
  • 目录文件(directory file):这种文件包含了其它文件的名字以及指向与这些文件有关信息的指针。
  • 块特殊文件(block special file):这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
  • 字符特殊文件(character special file):这种 类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
  • FIFO这种类型的文件用于进程间通信,有时也成为命名管道(named pipe)。
  • 套接字(socket):这种类型的文件用于进程间的网络通信。桃姐也可用于在一台宿主机上进程之间的非网络通信。
  • 符号连接(symbolic link):这种类型的文件指向另一个文件。

1.2 I/O模型

    所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call)库函数是指为了迎合程序员使用,而编制的通用库函数,虽然这些函数可能会调用一个或多个内核的系统滴啊用,但是它们并不是内核的入口点。例如,printf函数会调用write(系统调用)输出一个字符串。

    从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。但是需要注意的是:我们可以替换库函数,而通常不能替换系统调用。

    在UNIX下可用如下的几种I/O模型:

  • 阻塞式I/O模型
  • 非阻塞式I/O模型
  • I/O复用:
  • 信号驱动I/O(基于套接字的异步I/O机制)
  • 异步I/O
  • 存储映射I/O

2. 阻塞式I/O模型

 

    阻塞式模型I/O是指当调用此类函数时,就会发生阻塞,直至函数返回如下图所示:

技术分享

    阻塞式模型又可以分为两种模型:不带缓冲的函数和带缓冲的函数(标准IO)。

2.1 不带缓冲的函数

 

    不带缓冲是指每个read和write都调用内核中的一个系统调用。其中有:open、read、write、lseek以及close。这些不带缓冲的I/O函数不是ISO C的组成部分,而是POSIX.1和Single UNIX Specification的组成部分。

2.2 带缓冲的函数

    标准I/O库是一类带缓冲的函数,其是ISO C的标准,标准I/O库是对上述的不带缓冲的系统调用函数进行封装和调用,其不属于UNIX中的系统函数,但标准I/O不仅在UNIX中能得到使用,在其它系统也能使用。

(一)缓冲

    标准I/O提供了3种类型的缓冲:

     1) 全缓冲:    即只在填满缓冲区后才进行实际I/O操作。

    对于驻留在磁盘上的文件通常是全缓冲的

     2) 行缓冲:是指当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。

    这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。当涉及一个 终端时(如标准输入和标准输出),通常使用行缓冲

     3) 不带缓冲:标准I/O库不对字符进行缓冲存储,即马上进行I/O操作。

    例如标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符立即输出。其中标准错误流stderr通常是不带缓冲的。

小结:标准错误时不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。

(二)标准I/O函数

    标准I/O函数也是先打开(open)对流文件,然后进行读或写,其中一旦打开了流,则可进行3中不同类型的非格式化I/O操作:

  1. 每次一个字符的I/O:一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理了所有缓冲。
  2. 每次一行的I/O:使用fgetsfputs可以一次读写一行,每行都以一个换行符终止。
  3. 直接I/Ofreadfwrite是每次读写某种数量的对象。 

3. 非阻塞式I/O模型

3.1 阻塞的原因

    系统调用分成两类:"低速"系统调用和其它。但低速的系统调用可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些文件类型(如读管道、终端设备和网络设备)的数据不存在,读操作可能会使调用者永远阻塞;
  • 如果数据不能被相同的文件类型立即接受(如管道中不空间、网络流控制)、写操作可能会使调用者永远阻塞;
  • 对已经加上强制性记录锁的文件进程读写
  • 在某种条件下打开某些文件类型可能会阻塞;
  • 某些ioctl操作
  • 某些进程间通信函数    

3.2 定义

    非阻塞I/O是指我们发出的open、read和write等I/O操作,并使这些操作不会永远阻塞。如果这样的操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。如下图所示:

技术分享

    前三次调用recvfrom时没有数据可返回,因此内核立即返回一个EWOLDBLOCK错误,第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。

3.3 使用方式

    对于给定的描述符(文件描述符),有两种为其指定费阻塞I/O的方法:

  1. 如果调用open获得描述符,则可指定O_NONBLOCK标志。
  2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志

    非阻塞式I/O可以应用到管道、FIFO、套接字、终端、伪终端以及其他一些类型的设备上。其中的文件描述符可以是由open函数的返回值,也可以是网络编程的socket值,如《UNP》343对TCP设置为非阻塞的方式:

int val,sockfd; 
    sockfd = socket(…)//打开socket 
    val = fcntl(sockfd, F_GETFL, 0); //获取文件状态标志 
    fcntl(sockfd, F_SETFL, val | O_NONBLOCK); //在原来的状态标志中添加非阻塞模型 

 

4. I/O多路复用

 

    I/O多路复用是指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作主要由select,poll和epoll来实现这种I/O多路复用的机制。但select,poll,epoll本质上都是同步I/O即会阻塞等待,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

 

    其中需先了解两种通知模式

  • 水平触发通知:如果文件描述符上可以非阻塞地执行I/O系统调用,此时认为它已经就绪;
  • 边缘触发通知:如果文件描述符自上次状态检查依赖有了新的I/O活动(比如新的输入),此时需要触发通知。

    如下表总结了I/O多路复用、信号驱动I/O以及epoll所采用的通知模型:

I/O模式

水平触发

边缘触发

select(),poll()

l

 

信号驱动I/O

 

l

epoll()

l

l

 

4.1 select函数

 

    select函数关注的问题是:在指定的时间内所关心的描述符集是否准备就绪

4.1.1 函数原型

int select(int maxfd, fd_set *readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * tvptr)
                                                      返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
?    maxfd:最大文件描述符编号值加1;
?    readfds 、writefds、exceptfds:分别指向所关心的描述符集的指针;
?    tvptr:指定愿意等待的时间长度。

4.1.2 注意问题

  • readfds等是值结果参数,会被函数修改;
  • 要注意计算maxfd,其是最大文件描述符编号值加1
  • tvptr如果为NULL表示阻塞等;如果tvptr指向的时间为0,表示非阻塞;否则表示select的超时时间;
  • select返回-1表示错误,返回0表示超时时间到没有监听到的事件发生,返回正数表示监听到的所有事件数(包括可读,可写,异常);
  • Linux的实现中select返回时会将tvptr修改为剩余时间,所以重复使用tvptr需要注意。

4.1.3 select的缺点

  • 由于描述符集合set的限制,每个set最多只能监听FD_SETSIZE(在Linux上是1024)个句柄(不同机器可能不一样);
  • 返回的可读集合是个fd_set类型,需要对所有的监听读句柄进行FD_ISSET的一一测试来判断是否可读;
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

4.2 poll函数

    poll的功能和select类似,只是修改了select的函数原型,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。

4.2.1 函数原型

    pollfd结构

struct pollfd { 
    int fd;     /* file descriptor to check, or <0 to ignore */ 
    short events;     /* events of interest on fd */ 
    short revents; /* events that occurred on fd */ 
}; 

 

    poll函数原型为

int poll(struct pollfd fdarrays[], nfds_t nfds, int timeout); 
                                                                 返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1

 

4.2.2 注意问题

  • nfds表示监听的fdarrays的长度,如果fdarrays [i].fd < 0,则poll忽略这样的pollfd
  • timeout是以ms为单位的超时时间;若为-1,表示永远等下去;0表示立即返回不等待;
  • poll在处理流设备时能提供额外的信息;
  • 使用poll不需要显式地监听异常事件,使用poll如果pollfd异常,则内核会设置reventsPOLLERR位;

4.2.3 和select相比,poll的优点是

  • 不再局限于FD_SETSIZE个监听描述符,只要能打开的描述符,都可以监听;
  • timeout参数不会被函数修改,分辨率较低些(但实际上没有影响);
  • 监听描述符集合不再是值结果参数,而是event表示监听事件,revents表示触发的事件;
  • poll的效率比select稍高(poll只遍历输入的监听数组中的描述符,如果数组中的fd<0,则poll忽略fd,当监听的描述符离散时效率稍高于select,比如监听0和1000两个句柄,则poll只需要遍历两个描述符,而select需要遍历1001个描述符;当监听描述符连续时,poll和select效率相当,底层实现也是一致的)。

4.2.4 poll缺点

    poll和select共同的问题是性能较差:遍历所有的文件描述符,当监听描述符个数增加时,监听效率降低,并且select和poll每次都要在用户态和内核态拷贝监听的描述符参数。

4.3 epoll函数

    epoll是Linux所独有的,所以epoll的移植性没有select和poll好,但epoll既支持水平模式,又支持触发模式

epoll API的核心数据结构称作epoll实例,它和一个打开的文件描述符相关联。通过这个描述符实现如下的目的:

  • 记录了进程中声明过的感兴趣列表——interest list(兴趣列表,输入内核);
  • 维护了处于I/O就绪态的文件描述符列表——ready list(就绪列表,输出进程)。

4.3.1 epoll操作函数

epoll API由一下3个系统调用组成。

  • epoll_create创建一个epoll实例,返回代表该实例的文件描述符。
  • epoll_ctl操作epoll描述符实例相关联的兴趣列表。
  • epoll_wait返回epoll实例相关联的就绪列表中的成员。

1) epoll_create函数创建epoll实例

 

int epoll-create(int size);                                               
                 成功返回描述符,错误返回-1

          参数size指定了内部数据结构的划分初始大小,而不是最大上限(从Linux2.6.8版就忽略该值)。

 

2) epoll_ctl函数修改epoll兴趣列表

int epoll_ctl(int epfd, int op, struct epoll_event* ev) 
                                                            成功返回0,错误返回-1

    参数fd指定感兴趣列表中的哪一个文件描述符的设定。op值可以是EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL分表对epoll兴趣列表进行增加、修改和删除操作。ev是指向epoll_event的指针,结构体的定义如下:

 struct epoll_event{ 
        unit32_t events; 
        epoll_data_t data; 
    } 

 

    其中的data是如下的一个联合体类型:

typdef union epoll_data { 
        void *ptr; 
        int fd; 
        unit32_t u32; 
        unit64_t u64; 
}epoll_data_t; 

 

    其中ev为文件描述符fd所做的设置如下:

  1. events是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合。
  2.     data是一个联合体,当描述符fd稍后成为就绪态时,联合体的成员可用来指定传回给调用进程的信息

 

3) epoll_wait函数事件等待

    系统调用epoll_wait()返回epoll实例中处于就绪状态的文件描述符信息。单个epoll_wait()调用能返回多个就绪态文件描述符的信息。

  int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout) 
                                                               返回准备就绪的文件描述符,0超时,-1出错

    evlist所指向的结构体数组中返回的就绪文件描述符信息,maxevents是指定的evlist数组的大小。

    在数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。

    timeout用来确定epoll_wait()的阻塞行为,有如下几种。

  • 等于-1:一直阻塞,直到有兴趣列表有事件发生,所捕获到一个信号为止;
  • 等于0:执行一次非阻塞式的检查;
  • 大于0:至多阻塞timeout秒。

4.3.2 epoll的优点

epoll解决了select和poll的几个性能上的缺陷:

  • 不限制监听的描述符个数(poll也是),只受进程打开描述符总数的限制;
  • 监听性能不随着监听描述符数的增加而增加,是O(1)的,不再是轮询描述符来探测事件,而是由描述符主动上报事件;
  • 使用共享内存的方式,不在用户和内核之间反复传递监听的描述符信息;
  • 返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发。
  • epoll显著提高性能的前提是:监听大量描述符,并且每次触发事件的描述符文件非常少。

5. 信号驱动I/O

5.1 定义

    信号驱动I/O是指请求数据的进程可以利用信号,向内核声明一个信号处理例程,让内核在描述符就绪时发送SIGIO信号(默认情况下)通知进程;而进程在向内核声明后,能够处理其他的任务,当I/O操作可执行时通过接受信号来获得通知。如下图所示:

技术分享

5.2 步骤

要使用信号驱动I/O,程序需要按照如下步骤来执行:

  1. 为内核发送的通知信号安装(通过sigaction函数)一个信号处理例程。默认情况下,这个通知信号为SIGIO.
  2. 设定文件描述符的属主,即当文件描述符上可执行I/O时,接收到通知信号的进程或进程组。通常让调用进程成为属主,并通过fcntl()F_SETOWN操作完成属主的设定:

    fcntl(fd, F_SETOWN, pid);

  3. 通过设定O_NONBLOCK标志使能非阻塞I/O
  4. 通过打开O_ASYNC标志使能信号驱动I/O。可以与上一步合并为一个操作。

    flags= fcntl(fd, F_GETFL);

    fcntl(fd, F_SETFL, flags |ASYNC | NONBLOCK);

  5. 调用进程现在可以执行其他的任务了。当I/O操作就绪时,内核为进程发送一个信号,然后调用在第1步中安装好的信号处理例程。
  6. 信号驱动I/O提供的是边缘触发通知。这表示一旦进程被通知I/O就绪,它就应该尽可能多地执行I/O(如尽可能多地读取字节)。

6. 异步IO

    从历史上信号驱动I/O有时也称为异步I/O,但是,如今的异步I/O(POSIX AIO规范)机制是进程请求内核执行一次I/O操作,内核启动该操作之后立刻将控制权还给调用进程,并且当内核在整个操作完成(包括将数据从内核复制到进程的缓冲区)或错误发生时,该进程得到通知如下图所示:

技术分享

 

    其中信号驱动I/O与异步I/O的区别是:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作(即何时就绪);而异步I/O模型是通知我们I/O操作何时完成

7. 存储映射I/O

7.1 定义

    存储映射I/O    能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中读取数据时,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不适用read和write的情况下执行I/O。

    映射分为两种类型:

  • 文件映射是指将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了;
  • 匿名映射一个匿名映射没有对应的文件。相反,这种映射的分页会被初始化为0

    并且进程之间可以共享存储映射区域,所以又可以将映射的区域分为私有映射共享映射

  • 私有映射:在映射内容上发生的变更对其它进程不可见,对于文件映射来讲,变更将不会再底层文件上进行。
  • 共享映射:在映射内容上发生的变更对所有共享同一个映射的其它进程都可将,对于文件映射来讲,变更将会发生在底层的文件上。

 

7.2 使用

 

    为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的

void *mmap(void* addr, size_t len, int prot, int floag, int fd, off_t off); 
                                                        返回值:若成功,返回映射区的起始地址;若出错,返回MAP_FAILED 
  • addr:用于指定映射存储去的起始地址。通常将其设置为0,这表示有系统选择该映射区的 起始地址。
  • fd:指定要被映射文件的描述符
  • len:映射的字节数;
  • off:是要映射字节在文件中的起始偏移量;
  • prot:指定了映射存储区的保护要求(可读、可写、可执行和不可访问);

当进程终止时,会自动解除存储映射区的映射,货值直接调用munmap函数也可以解除映射区

int munmap(void* addr, size_t len); 
                                                                   返回值:若成功,返回0;若出错,返回-1

 

Linux IO

标签:

原文地址:http://www.cnblogs.com/hlwfirst/p/5004611.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!